From 4b230bf741add5da10ba61bfe9ddd5764e5d6742 Mon Sep 17 00:00:00 2001 From: abishek Date: Tue, 23 Sep 2025 11:19:18 +0530 Subject: [PATCH] refactor(groups): decouple group filter from live status API and created a seprate group API --- .../Controllers/GroupController.cs | 65 +++++++++++++++++++ .../Models/StatusDtos.cs | 1 + ThingConnect.Pulse.Server/Program.cs | 3 + .../Services/Group/GroupService.cs | 48 ++++++++++++++ .../Services/Group/IGroupService.cs | 10 +++ .../obj/Debug/package.g.props | 22 +++---- .../src/components/status/EndpointFilters.tsx | 1 - .../src/components/status/StatusTable.tsx | 2 +- .../src/hooks/useGroupsQuery.tsx | 13 ++++ .../src/pages/Dashboard.tsx | 17 ++--- 10 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 ThingConnect.Pulse.Server/Controllers/GroupController.cs create mode 100644 ThingConnect.Pulse.Server/Services/Group/GroupService.cs create mode 100644 ThingConnect.Pulse.Server/Services/Group/IGroupService.cs create mode 100644 thingconnect.pulse.client/src/hooks/useGroupsQuery.tsx diff --git a/ThingConnect.Pulse.Server/Controllers/GroupController.cs b/ThingConnect.Pulse.Server/Controllers/GroupController.cs new file mode 100644 index 0000000..ef6852f --- /dev/null +++ b/ThingConnect.Pulse.Server/Controllers/GroupController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ThingConnect.Pulse.Server.Models; +using ThingConnect.Pulse.Server.Services; + +namespace ThingConnect.Pulse.Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] // Only authenticated users can access +public sealed class GroupsController : ControllerBase +{ + private readonly IGroupService _groupService; + private readonly ILogger _logger; + + public GroupsController(IGroupService groupService, ILogger logger) + { + _groupService = groupService; + _logger = logger; + } + + /// + /// Get all master groups. + /// + /// List of groups + [HttpGet] + public async Task>> GetGroupsAsync() + { + try + { + var groups = await _groupService.GetAllGroupsAsync(); + return Ok(groups); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching groups"); + return StatusCode(500, new { message = "Failed to retrieve groups", error = ex.Message }); + } + } + + /// + /// Get a single group by ID. + /// + /// Group ID + /// Group details + [HttpGet("{id}")] + public async Task> GetGroupByIdAsync(string id) + { + try + { + var group = await _groupService.GetGroupByIdAsync(id); + if (group == null) + { + return NotFound(new { message = "Group not found" }); + } + + return Ok(group); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching group with ID {GroupId}", id); + return StatusCode(500, new { message = "Failed to retrieve group", error = ex.Message }); + } + } +} diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index 11df3f8..2b18afe 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -37,6 +37,7 @@ public sealed class GroupDto public string Name { get; set; } = default!; public string? ParentId { get; set; } public string? Color { get; set; } + public int? SortOrder { get; set; } } public sealed class PageMetaDto diff --git a/ThingConnect.Pulse.Server/Program.cs b/ThingConnect.Pulse.Server/Program.cs index 75aac7d..48afe5e 100644 --- a/ThingConnect.Pulse.Server/Program.cs +++ b/ThingConnect.Pulse.Server/Program.cs @@ -173,6 +173,9 @@ public static async Task Main(string[] args) builder.Services.AddSingleton(provider => provider.GetRequiredService()); builder.Services.AddHostedService(provider => provider.GetRequiredService()); + // Add group service + builder.Services.AddScoped(); + // Add CORS builder.Services.AddCors(options => { diff --git a/ThingConnect.Pulse.Server/Services/Group/GroupService.cs b/ThingConnect.Pulse.Server/Services/Group/GroupService.cs new file mode 100644 index 0000000..75aea35 --- /dev/null +++ b/ThingConnect.Pulse.Server/Services/Group/GroupService.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using ThingConnect.Pulse.Server.Data; +using ThingConnect.Pulse.Server.Models; + +namespace ThingConnect.Pulse.Server.Services +{ + public sealed class GroupService : IGroupService + { + private readonly PulseDbContext _context; + + public GroupService(PulseDbContext context) + { + _context = context; + } + + public async Task> GetAllGroupsAsync() + { + return await _context.Groups + .Select(g => new GroupDto + { + Id = g.Id, + Name = g.Name, + ParentId = g.ParentId, + Color = g.Color, + SortOrder = g.SortOrder + }) + .OrderBy(g => g.SortOrder) + .ToListAsync(); + } + + public async Task GetGroupByIdAsync(string groupId) + { + var group = await _context.Groups + .FirstOrDefaultAsync(g => g.Id == groupId); + + if (group == null) return null; + + return new GroupDto + { + Id = group.Id, + Name = group.Name, + ParentId = group.ParentId, + Color = group.Color, + SortOrder = group.SortOrder + }; + } + } +} diff --git a/ThingConnect.Pulse.Server/Services/Group/IGroupService.cs b/ThingConnect.Pulse.Server/Services/Group/IGroupService.cs new file mode 100644 index 0000000..7938856 --- /dev/null +++ b/ThingConnect.Pulse.Server/Services/Group/IGroupService.cs @@ -0,0 +1,10 @@ +using ThingConnect.Pulse.Server.Models; + +namespace ThingConnect.Pulse.Server.Services +{ + public interface IGroupService + { + Task> GetAllGroupsAsync(); + Task GetGroupByIdAsync(string groupId); + } +} diff --git a/thingconnect.pulse.client/obj/Debug/package.g.props b/thingconnect.pulse.client/obj/Debug/package.g.props index b0a9162..c1004d2 100644 --- a/thingconnect.pulse.client/obj/Debug/package.g.props +++ b/thingconnect.pulse.client/obj/Debug/package.g.props @@ -36,31 +36,31 @@ ^7.62.0 ^5.5.0 ^7.8.1 - ^4.0.17 + ^4.1.9 ^3.24.0 ^9.33.0 ^5.83.1 ^5.85.5 ^4.17.20 ^3.7.1 - ^22 + ^24 ^2.0.5 - ^19.1.10 - ^19.1.7 - ^3.11.0 - ^9.33.0 + ^19.1.13 + ^19.1.9 + ^4.1.0 + ^9.35.0 ^10.1.8 - ^1.52.3 + ^1.53.1 ^5.2.0 ^0.4.20 - ^1.52.3 + ^1.53.1 ^16.3.0 ^9.1.7 ^16.1.4 ^3.6.2 - ~5.8.3 - ^8.39.1 - ^7.1.2 + ~5.9.2 + ^8.44.0 + ^7.1.6 ^5.1.4 [ "eslint --fix", diff --git a/thingconnect.pulse.client/src/components/status/EndpointFilters.tsx b/thingconnect.pulse.client/src/components/status/EndpointFilters.tsx index 42b366c..c7df081 100644 --- a/thingconnect.pulse.client/src/components/status/EndpointFilters.tsx +++ b/thingconnect.pulse.client/src/components/status/EndpointFilters.tsx @@ -86,7 +86,6 @@ export function EndpointFilters({ items={groups.map(g => ({ label: g.name, value: g.id }))} selectedValue={selectedGroup ? selectedGroup : ''} onChange={handleGroupChange} - isLoading={false} /> {/* Search Input */} diff --git a/thingconnect.pulse.client/src/components/status/StatusTable.tsx b/thingconnect.pulse.client/src/components/status/StatusTable.tsx index 5cd1a38..a1e9303 100644 --- a/thingconnect.pulse.client/src/components/status/StatusTable.tsx +++ b/thingconnect.pulse.client/src/components/status/StatusTable.tsx @@ -51,7 +51,7 @@ export function StatusTable({ items, isLoading }: StatusTableProps) { void navigate(`/endpoints/${id}`); }; - console.log('isLoading in StatusTable:', items); + return ( diff --git a/thingconnect.pulse.client/src/hooks/useGroupsQuery.tsx b/thingconnect.pulse.client/src/hooks/useGroupsQuery.tsx new file mode 100644 index 0000000..627527d --- /dev/null +++ b/thingconnect.pulse.client/src/hooks/useGroupsQuery.tsx @@ -0,0 +1,13 @@ +import type { Group } from '@/api/types'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +export const useGroupsQuery = () => { + return useQuery({ + queryKey: ['groups'], + queryFn: async () => { + const response = await axios.get('/api/groups'); + return response.data; + }, + }); +}; diff --git a/thingconnect.pulse.client/src/pages/Dashboard.tsx b/thingconnect.pulse.client/src/pages/Dashboard.tsx index c1d3950..67bf58a 100644 --- a/thingconnect.pulse.client/src/pages/Dashboard.tsx +++ b/thingconnect.pulse.client/src/pages/Dashboard.tsx @@ -9,6 +9,7 @@ import type { LiveStatusParams } from '@/api/types'; import type { LiveStatusItem } from '@/api/types'; import { EndpointFilters } from '@/components/status/EndpointFilters'; import { EndpointAccordion } from '@/components/status/EndpointAccordion'; +import { useGroupsQuery } from '@/hooks/useGroupsQuery'; type GroupedEndpoints = | LiveStatusItem[] @@ -19,6 +20,7 @@ export default function Dashboard() { const analytics = useAnalytics(); const [filters, setFilters] = useState({}); const [selectedGroup, setSelectedGroup] = useState(undefined); + const { data: groupsData = [] } = useGroupsQuery(); const [searchTerm, setSearchTerm] = useState(''); @@ -166,20 +168,9 @@ export default function Dashboard() { return Object.keys(finalResult).length > 0 ? finalResult : filteredItems; }, [data?.items, groupByOptions, searchTerm, filters.group]); - // Extract unique groups for filter dropdown const groups = useMemo(() => { - if (!data?.items) return []; - const groupMap = new Map(); - - data.items.forEach(item => { - const g = item.endpoint.group; - if (g?.id) { - groupMap.set(g.id, { id: g.id, name: g.name }); - } - }); - - return Array.from(groupMap.values()).sort((a, b) => a.name.localeCompare(b.name)); - }, [data?.items]); + return groupsData.sort((a, b) => a.name.localeCompare(b.name)); + }, [groupsData]); // Count status totals const statusCounts = useMemo(() => {