Skip to content

Commit a1df953

Browse files
committed
fix(ui): live-refresh dashboard stat tiles on feature-set / client / server changes
The Home dashboard loaded its FeatureSets / Clients / Servers counts once (on mount + Space switch + gateway/server-status events) but ignored feature-set-changed and client-changed. So a FeatureSet composed by an MCP client via mcpmux_manage_feature_set — or a newly authenticated app — left the tile counts stale until a Space switch or reload. Subscribe loadStats to feature-set-changed, client-changed, and server-changed (memoized via useCallback so the subscription is stable per Space). The FeatureSets *page* already live-refreshed via its own feature-set-changed listener; this closes the dashboard gap. Adds HomePageStats.test.tsx: the FeatureSets tile count updates from 1 → 2 when a feature-set-changed event arrives. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent f44f8af commit a1df953

2 files changed

Lines changed: 93 additions & 6 deletions

File tree

apps/desktop/src/features/home/HomePage.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
* row of stat tiles that double as navigation — every tile is a button into
1010
* the page that manages what it counts.
1111
*/
12-
import { useEffect, useState } from 'react';
12+
import { useEffect, useState, useCallback } from 'react';
1313
import { Server, Wrench, Monitor, Globe, ArrowUpRight, Compass, ArrowRight } from 'lucide-react';
1414
import type { LucideIcon } from 'lucide-react';
1515
import { PageHeader } from '@mcpmux/ui';
1616
import { ConnectionCard } from '@/components/ConnectionCard';
17-
import { useGatewayEvents, useServerStatusEvents } from '@/hooks/useDomainEvents';
17+
import { useGatewayEvents, useServerStatusEvents, useDomainEvents } from '@/hooks/useDomainEvents';
1818
import { useViewSpace, useNavigateTo } from '@/stores';
1919
import type { NavItem } from '@/stores/types';
2020
import { spaceAccentColor } from '@/lib/spaceAccent';
@@ -162,7 +162,7 @@ export function HomePage() {
162162
const [statsLoaded, setStatsLoaded] = useState(false);
163163
const viewSpace = useViewSpace();
164164

165-
const loadStats = async () => {
165+
const loadStats = useCallback(async () => {
166166
try {
167167
const [clients, featureSets, gateway, installedServers] = await Promise.all([
168168
import('@/lib/api/clients').then((m) => m.listClients()),
@@ -182,13 +182,12 @@ export function HomePage() {
182182
} catch (e) {
183183
console.error('Failed to load home stats:', e);
184184
}
185-
};
185+
}, [viewSpace?.id]);
186186

187187
// Load on mount and when the viewed Space changes.
188188
useEffect(() => {
189189
loadStats();
190-
// eslint-disable-next-line react-hooks/exhaustive-deps
191-
}, [viewSpace?.id]);
190+
}, [loadStats]);
192191

193192
// Keep `Tools: X/Y` honest across gateway start/stop and backend churn.
194193
// ConnectionCard owns the actual running/URL UI.
@@ -206,6 +205,20 @@ export function HomePage() {
206205
}
207206
});
208207

208+
// Keep the FeatureSets + Clients tiles live when those change anywhere —
209+
// e.g. an MCP client composing a FeatureSet via `mcpmux_manage_feature_set`,
210+
// or a new app authenticating. Without this the counts go stale until a
211+
// Space switch or reload.
212+
const { subscribe } = useDomainEvents();
213+
useEffect(() => {
214+
const unsubs = [
215+
subscribe('feature-set-changed', () => void loadStats()),
216+
subscribe('client-changed', () => void loadStats()),
217+
subscribe('server-changed', () => void loadStats()),
218+
];
219+
return () => unsubs.forEach((u) => u());
220+
}, [subscribe, loadStats]);
221+
209222
return (
210223
<div className="space-y-6">
211224
<PageHeader
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Dashboard stat tiles must reflect changes made anywhere — including a
3+
* FeatureSet composed by an MCP client via `mcpmux_manage_feature_set`, which
4+
* arrives as a `feature-set-changed` domain event. Guards the fix for
5+
* "created a FeatureSet via the tool but the count didn't update live".
6+
*/
7+
8+
import { describe, it, expect, vi, beforeEach } from 'vitest';
9+
import { render, screen, act, waitFor } from '@testing-library/react';
10+
11+
const { handlers, mockListClients, mockListFS, mockGatewayStatus, mockListInstalled } = vi.hoisted(
12+
() => ({
13+
handlers: new Map<string, ((e: { payload: unknown }) => void)[]>(),
14+
mockListClients: vi.fn(),
15+
mockListFS: vi.fn(),
16+
mockGatewayStatus: vi.fn(),
17+
mockListInstalled: vi.fn(),
18+
})
19+
);
20+
21+
vi.mock('@tauri-apps/api/event', () => ({
22+
listen: vi.fn((name: string, cb: (e: { payload: unknown }) => void) => {
23+
const arr = handlers.get(name) ?? [];
24+
arr.push(cb);
25+
handlers.set(name, arr);
26+
return Promise.resolve(() => {});
27+
}),
28+
}));
29+
vi.mock('@/lib/api/clients', () => ({ listClients: mockListClients }));
30+
vi.mock('@/lib/api/featureSets', () => ({
31+
listFeatureSetsBySpace: mockListFS,
32+
listFeatureSets: mockListFS,
33+
}));
34+
vi.mock('@/lib/api/gateway', () => ({ getGatewayStatus: mockGatewayStatus }));
35+
vi.mock('@/lib/api/registry', () => ({ listInstalledServers: mockListInstalled }));
36+
vi.mock('@/stores', () => ({
37+
useViewSpace: () => ({ id: 'space-1', name: 'My Space' }),
38+
useNavigateTo: () => () => {},
39+
}));
40+
vi.mock('@/components/ConnectionCard', () => ({ ConnectionCard: () => null }));
41+
42+
import { HomePage } from '@/features/home/HomePage';
43+
44+
function emit(channel: string, payload: unknown) {
45+
act(() => {
46+
handlers.get(channel)?.forEach((cb) => cb({ payload }));
47+
});
48+
}
49+
50+
describe('HomePage dashboard stats', () => {
51+
beforeEach(() => {
52+
handlers.clear();
53+
mockListClients.mockReset().mockResolvedValue([]);
54+
mockGatewayStatus.mockReset().mockResolvedValue({ connected_backends: 0 });
55+
mockListInstalled.mockReset().mockResolvedValue([{}]); // 1 server → skip onboarding strip
56+
mockListFS.mockReset();
57+
});
58+
59+
it('refreshes the FeatureSets count when a feature-set-changed event arrives', async () => {
60+
// First load → 1 FeatureSet; after the event → 2.
61+
mockListFS.mockResolvedValueOnce([{}]).mockResolvedValue([{}, {}]);
62+
63+
render(<HomePage />);
64+
await waitFor(() =>
65+
expect(screen.getByTestId('stat-featuresets-value')).toHaveTextContent('1')
66+
);
67+
68+
emit('feature-set-changed', { action: 'created' });
69+
70+
await waitFor(() =>
71+
expect(screen.getByTestId('stat-featuresets-value')).toHaveTextContent('2')
72+
);
73+
});
74+
});

0 commit comments

Comments
 (0)