Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions ThingConnect.Pulse.Server/Controllers/GroupController.cs
Original file line number Diff line number Diff line change
@@ -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<GroupsController> _logger;

public GroupsController(IGroupService groupService, ILogger<GroupsController> logger)
{
_groupService = groupService;
_logger = logger;
}

/// <summary>
/// Get all master groups.
/// </summary>
/// <returns>List of groups</returns>
[HttpGet]
public async Task<ActionResult<List<GroupDto>>> 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 });
}
}

/// <summary>
/// Get a single group by ID.
/// </summary>
/// <param name="id">Group ID</param>
/// <returns>Group details</returns>
[HttpGet("{id}")]
public async Task<ActionResult<GroupDto>> 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 });
}
}
}
1 change: 1 addition & 0 deletions ThingConnect.Pulse.Server/Models/StatusDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ThingConnect.Pulse.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ public static async Task Main(string[] args)
builder.Services.AddSingleton<INotificationService>(provider => provider.GetRequiredService<NotificationBackgroundService>());
builder.Services.AddHostedService<NotificationBackgroundService>(provider => provider.GetRequiredService<NotificationBackgroundService>());

// Add group service
builder.Services.AddScoped<IGroupService, GroupService>();

// Add CORS
builder.Services.AddCors(options =>
{
Expand Down
48 changes: 48 additions & 0 deletions ThingConnect.Pulse.Server/Services/Group/GroupService.cs
Original file line number Diff line number Diff line change
@@ -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<List<GroupDto>> 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<GroupDto?> 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
};
}
}
}
10 changes: 10 additions & 0 deletions ThingConnect.Pulse.Server/Services/Group/IGroupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ThingConnect.Pulse.Server.Models;

namespace ThingConnect.Pulse.Server.Services
{
public interface IGroupService
{
Task<List<GroupDto>> GetAllGroupsAsync();
Task<GroupDto?> GetGroupByIdAsync(string groupId);
}
}
22 changes: 11 additions & 11 deletions thingconnect.pulse.client/obj/Debug/package.g.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,31 @@
<PackageJsonDependenciesReactHookForm Condition="$(PackageJsonDependenciesReactHookForm) == ''">^7.62.0</PackageJsonDependenciesReactHookForm>
<PackageJsonDependenciesReactIcons Condition="$(PackageJsonDependenciesReactIcons) == ''">^5.5.0</PackageJsonDependenciesReactIcons>
<PackageJsonDependenciesReactRouterDom Condition="$(PackageJsonDependenciesReactRouterDom) == ''">^7.8.1</PackageJsonDependenciesReactRouterDom>
<PackageJsonDependenciesZod Condition="$(PackageJsonDependenciesZod) == ''">^4.0.17</PackageJsonDependenciesZod>
<PackageJsonDependenciesZod Condition="$(PackageJsonDependenciesZod) == ''">^4.1.9</PackageJsonDependenciesZod>
<PackageJsonDevdependenciesChakraUiCli Condition="$(PackageJsonDevdependenciesChakraUiCli) == ''">^3.24.0</PackageJsonDevdependenciesChakraUiCli>
<PackageJsonDevdependenciesEslintJs Condition="$(PackageJsonDevdependenciesEslintJs) == ''">^9.33.0</PackageJsonDevdependenciesEslintJs>
<PackageJsonDevdependenciesTanstackEslintPluginQuery Condition="$(PackageJsonDevdependenciesTanstackEslintPluginQuery) == ''">^5.83.1</PackageJsonDevdependenciesTanstackEslintPluginQuery>
<PackageJsonDevdependenciesTanstackReactQueryDevtools Condition="$(PackageJsonDevdependenciesTanstackReactQueryDevtools) == ''">^5.85.5</PackageJsonDevdependenciesTanstackReactQueryDevtools>
<PackageJsonDevdependenciesTypesLodash Condition="$(PackageJsonDevdependenciesTypesLodash) == ''">^4.17.20</PackageJsonDevdependenciesTypesLodash>
<PackageJsonDevdependenciesTypesLuxon Condition="$(PackageJsonDevdependenciesTypesLuxon) == ''">^3.7.1</PackageJsonDevdependenciesTypesLuxon>
<PackageJsonDevdependenciesTypesNode Condition="$(PackageJsonDevdependenciesTypesNode) == ''">^22</PackageJsonDevdependenciesTypesNode>
<PackageJsonDevdependenciesTypesNode Condition="$(PackageJsonDevdependenciesTypesNode) == ''">^24</PackageJsonDevdependenciesTypesNode>
<PackageJsonDevdependenciesTypesNumeral Condition="$(PackageJsonDevdependenciesTypesNumeral) == ''">^2.0.5</PackageJsonDevdependenciesTypesNumeral>
<PackageJsonDevdependenciesTypesReact Condition="$(PackageJsonDevdependenciesTypesReact) == ''">^19.1.10</PackageJsonDevdependenciesTypesReact>
<PackageJsonDevdependenciesTypesReactDom Condition="$(PackageJsonDevdependenciesTypesReactDom) == ''">^19.1.7</PackageJsonDevdependenciesTypesReactDom>
<PackageJsonDevdependenciesVitejsPluginReactSwc Condition="$(PackageJsonDevdependenciesVitejsPluginReactSwc) == ''">^3.11.0</PackageJsonDevdependenciesVitejsPluginReactSwc>
<PackageJsonDevdependenciesEslint Condition="$(PackageJsonDevdependenciesEslint) == ''">^9.33.0</PackageJsonDevdependenciesEslint>
<PackageJsonDevdependenciesTypesReact Condition="$(PackageJsonDevdependenciesTypesReact) == ''">^19.1.13</PackageJsonDevdependenciesTypesReact>
<PackageJsonDevdependenciesTypesReactDom Condition="$(PackageJsonDevdependenciesTypesReactDom) == ''">^19.1.9</PackageJsonDevdependenciesTypesReactDom>
<PackageJsonDevdependenciesVitejsPluginReactSwc Condition="$(PackageJsonDevdependenciesVitejsPluginReactSwc) == ''">^4.1.0</PackageJsonDevdependenciesVitejsPluginReactSwc>
<PackageJsonDevdependenciesEslint Condition="$(PackageJsonDevdependenciesEslint) == ''">^9.35.0</PackageJsonDevdependenciesEslint>
<PackageJsonDevdependenciesEslintConfigPrettier Condition="$(PackageJsonDevdependenciesEslintConfigPrettier) == ''">^10.1.8</PackageJsonDevdependenciesEslintConfigPrettier>
<PackageJsonDevdependenciesEslintPluginReactDom Condition="$(PackageJsonDevdependenciesEslintPluginReactDom) == ''">^1.52.3</PackageJsonDevdependenciesEslintPluginReactDom>
<PackageJsonDevdependenciesEslintPluginReactDom Condition="$(PackageJsonDevdependenciesEslintPluginReactDom) == ''">^1.53.1</PackageJsonDevdependenciesEslintPluginReactDom>
<PackageJsonDevdependenciesEslintPluginReactHooks Condition="$(PackageJsonDevdependenciesEslintPluginReactHooks) == ''">^5.2.0</PackageJsonDevdependenciesEslintPluginReactHooks>
<PackageJsonDevdependenciesEslintPluginReactRefresh Condition="$(PackageJsonDevdependenciesEslintPluginReactRefresh) == ''">^0.4.20</PackageJsonDevdependenciesEslintPluginReactRefresh>
<PackageJsonDevdependenciesEslintPluginReactX Condition="$(PackageJsonDevdependenciesEslintPluginReactX) == ''">^1.52.3</PackageJsonDevdependenciesEslintPluginReactX>
<PackageJsonDevdependenciesEslintPluginReactX Condition="$(PackageJsonDevdependenciesEslintPluginReactX) == ''">^1.53.1</PackageJsonDevdependenciesEslintPluginReactX>
<PackageJsonDevdependenciesGlobals Condition="$(PackageJsonDevdependenciesGlobals) == ''">^16.3.0</PackageJsonDevdependenciesGlobals>
<PackageJsonDevdependenciesHusky Condition="$(PackageJsonDevdependenciesHusky) == ''">^9.1.7</PackageJsonDevdependenciesHusky>
<PackageJsonDevdependenciesLintStaged Condition="$(PackageJsonDevdependenciesLintStaged) == ''">^16.1.4</PackageJsonDevdependenciesLintStaged>
<PackageJsonDevdependenciesPrettier Condition="$(PackageJsonDevdependenciesPrettier) == ''">^3.6.2</PackageJsonDevdependenciesPrettier>
<PackageJsonDevdependenciesTypescript Condition="$(PackageJsonDevdependenciesTypescript) == ''">~5.8.3</PackageJsonDevdependenciesTypescript>
<PackageJsonDevdependenciesTypescriptEslint Condition="$(PackageJsonDevdependenciesTypescriptEslint) == ''">^8.39.1</PackageJsonDevdependenciesTypescriptEslint>
<PackageJsonDevdependenciesVite Condition="$(PackageJsonDevdependenciesVite) == ''">^7.1.2</PackageJsonDevdependenciesVite>
<PackageJsonDevdependenciesTypescript Condition="$(PackageJsonDevdependenciesTypescript) == ''">~5.9.2</PackageJsonDevdependenciesTypescript>
<PackageJsonDevdependenciesTypescriptEslint Condition="$(PackageJsonDevdependenciesTypescriptEslint) == ''">^8.44.0</PackageJsonDevdependenciesTypescriptEslint>
<PackageJsonDevdependenciesVite Condition="$(PackageJsonDevdependenciesVite) == ''">^7.1.6</PackageJsonDevdependenciesVite>
<PackageJsonDevdependenciesViteTsconfigPaths Condition="$(PackageJsonDevdependenciesViteTsconfigPaths) == ''">^5.1.4</PackageJsonDevdependenciesViteTsconfigPaths>
<PackageJsonLintStagedTsTsx Condition="$(PackageJsonLintStagedTsTsx) == ''">[
"eslint --fix",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export function EndpointFilters({
items={groups.map(g => ({ label: g.name, value: g.id }))}
selectedValue={selectedGroup ? selectedGroup : ''}
onChange={handleGroupChange}
isLoading={false}
/>
</Box>
{/* Search Input */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function StatusTable({ items, isLoading }: StatusTableProps) {

void navigate(`/endpoints/${id}`);
};
console.log('isLoading in StatusTable:', items);

return (
<Box borderRadius='md' overflow='hidden'>
<Table.Root size='md' borderWidth={0} width='full'>
Expand Down
13 changes: 13 additions & 0 deletions thingconnect.pulse.client/src/hooks/useGroupsQuery.tsx
Original file line number Diff line number Diff line change
@@ -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<Group[], Error>({
queryKey: ['groups'],
queryFn: async () => {
const response = await axios.get('/api/groups');
return response.data;
},
});
};
17 changes: 4 additions & 13 deletions thingconnect.pulse.client/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -19,6 +20,7 @@ export default function Dashboard() {
const analytics = useAnalytics();
const [filters, setFilters] = useState<LiveStatusParams>({});
const [selectedGroup, setSelectedGroup] = useState<string | undefined>(undefined);
const { data: groupsData = [] } = useGroupsQuery();

const [searchTerm, setSearchTerm] = useState('');

Expand Down Expand Up @@ -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<string, { id: string; name: string }>();

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(() => {
Expand Down
Loading