Skip to content

Commit 6215633

Browse files
committed
feat(desktop): add server count summary and smarter hover tooltips
Show installed/connected/disabled/error counts beside My Servers, move filter clear-all outside the popover, and flip HoverTooltip placement from viewport space. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent b641acd commit 6215633

6 files changed

Lines changed: 303 additions & 52 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { HoverTooltip } from '@mcpmux/ui';
2+
import {
3+
describeServerCountSummary,
4+
formatServerCountSummary,
5+
type ServerCountSummary,
6+
} from './servers-page.helpers';
7+
8+
interface ServersCountSummaryProps {
9+
summary: ServerCountSummary;
10+
}
11+
12+
/**
13+
* Inline installed-server counts beside the My Servers title, with hover breakdown.
14+
*/
15+
export function ServersCountSummary({ summary }: ServersCountSummaryProps) {
16+
if (summary.installed === 0) {
17+
return null;
18+
}
19+
20+
return (
21+
<HoverTooltip
22+
title="Installed servers"
23+
lines={describeServerCountSummary(summary)}
24+
data-testid="servers-count-tooltip"
25+
className="flex-shrink min-w-0"
26+
>
27+
<p
28+
className="text-sm text-[rgb(var(--muted))] truncate cursor-default"
29+
data-testid="servers-count-summary"
30+
>
31+
{formatServerCountSummary(summary)}
32+
</p>
33+
</HoverTooltip>
34+
);
35+
}

apps/desktop/src/features/servers/ServersFiltersPopover.tsx

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -49,34 +49,46 @@ export function ServersFiltersPopover({
4949
data-testid="servers-filters-tooltip"
5050
className="flex-shrink-0"
5151
>
52-
<DropdownMenu open={open} onOpenChange={setOpen}>
53-
<DropdownMenuTrigger data-testid="servers-filters-trigger">
52+
<div className="inline-flex items-center gap-2">
53+
{activeCount > 0 && (
5454
<Button
55-
variant="secondary"
55+
variant="ghost"
5656
size="md"
5757
type="button"
58-
className={
59-
activeCount > 0
60-
? 'bg-[rgb(var(--primary))]/10 text-[rgb(var(--primary))] border-[rgb(var(--primary))]/40'
61-
: undefined
62-
}
58+
onClick={onClearAllFilters}
59+
data-testid="servers-filters-clear-all"
6360
>
64-
<SlidersHorizontal className="h-4 w-4" />
65-
Filters
66-
{activeCount > 0 && (
67-
<span
68-
className="min-w-[1.25rem] px-1.5 py-0.5 text-xs font-semibold rounded-full bg-[rgb(var(--primary))] text-[rgb(var(--primary-foreground))]"
69-
data-testid="servers-filters-count"
70-
>
71-
{activeCount}
72-
</span>
73-
)}
74-
<ChevronDown
75-
className={`h-4 w-4 text-[rgb(var(--muted))] transition-transform ${open ? 'rotate-180' : ''}`}
76-
/>
61+
Clear all
7762
</Button>
78-
</DropdownMenuTrigger>
79-
<DropdownMenuContent align="end" className="w-72 p-4 space-y-4" data-testid="servers-filters-popover">
63+
)}
64+
<DropdownMenu open={open} onOpenChange={setOpen}>
65+
<DropdownMenuTrigger data-testid="servers-filters-trigger">
66+
<Button
67+
variant="secondary"
68+
size="md"
69+
type="button"
70+
className={
71+
activeCount > 0
72+
? 'bg-[rgb(var(--primary))]/10 text-[rgb(var(--primary))] border-[rgb(var(--primary))]/40'
73+
: undefined
74+
}
75+
>
76+
<SlidersHorizontal className="h-4 w-4" />
77+
Filters
78+
{activeCount > 0 && (
79+
<span
80+
className="min-w-[1.25rem] px-1.5 py-0.5 text-xs font-semibold rounded-full bg-[rgb(var(--primary))] text-[rgb(var(--primary-foreground))]"
81+
data-testid="servers-filters-count"
82+
>
83+
{activeCount}
84+
</span>
85+
)}
86+
<ChevronDown
87+
className={`h-4 w-4 text-[rgb(var(--muted))] transition-transform ${open ? 'rotate-180' : ''}`}
88+
/>
89+
</Button>
90+
</DropdownMenuTrigger>
91+
<DropdownMenuContent align="end" className="w-72 p-4 space-y-4" data-testid="servers-filters-popover">
8092
<div className="space-y-2">
8193
<p className="text-xs font-medium text-[rgb(var(--muted))]">Transport</p>
8294
<div className="flex flex-wrap gap-2">
@@ -121,24 +133,9 @@ export function ServersFiltersPopover({
121133
Combine status filters (e.g. Connected + Error). All = no status filter.
122134
</p>
123135
</div>
124-
125-
{activeCount > 0 && (
126-
<Button
127-
variant="ghost"
128-
size="sm"
129-
type="button"
130-
className="w-full"
131-
onClick={() => {
132-
onClearAllFilters();
133-
setOpen(false);
134-
}}
135-
data-testid="servers-filters-clear-all"
136-
>
137-
Clear all filters
138-
</Button>
139-
)}
140-
</DropdownMenuContent>
141-
</DropdownMenu>
136+
</DropdownMenuContent>
137+
</DropdownMenu>
138+
</div>
142139
</HoverTooltip>
143140
);
144141
}

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import { ServerActionMenu } from './ServerActionMenu';
2828
import { CloneAccountModal } from './CloneAccountModal';
2929
import { AddServerMenu } from './AddServerMenu';
3030
import { ServersFiltersPopover } from './ServersFiltersPopover';
31+
import { ServersCountSummary } from './ServersCountSummary';
3132
import { UninstallSourceWithClonesDialog } from './UninstallSourceWithClonesDialog';
3233
import type { ServerViewModel, ServerDefinition, InstalledServerState, InputDefinition } from '../../types/registry';
3334
import type { ServerFeature } from '@/lib/api/serverFeatures';
3435
import { listServerFeatures, listServerFeaturesByServer } from '@/lib/api/serverFeatures';
3536
import {
37+
computeServerCountSummary,
3638
groupFeaturesByServerId,
3739
serverMatchesFilters,
3840
type ServerActionKey,
@@ -574,6 +576,9 @@ export function ServersPage() {
574576

575577
const expandableServerCount = installedServers.filter(isServerExpandable).length;
576578
const hasExpandedServers = expandedServers.size > 0;
579+
const serverCountSummary = computeServerCountSummary(installedServers, (server) =>
580+
getServerAction(server)
581+
);
577582

578583
/** Installed servers matching transport, status, and search filters. */
579584
const filteredServers = installedServers.filter((server) =>
@@ -1112,8 +1117,13 @@ export function ServersPage() {
11121117
{/* Header */}
11131118
<div className="space-y-4">
11141119
<div className="flex items-center justify-between gap-4">
1115-
<div className="flex-shrink-0">
1116-
<h1 className="text-2xl font-bold" data-testid="servers-title">My Servers</h1>
1120+
<div className="flex-shrink min-w-0">
1121+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
1122+
<h1 className="text-2xl font-bold" data-testid="servers-title">
1123+
My Servers
1124+
</h1>
1125+
<ServersCountSummary summary={serverCountSummary} />
1126+
</div>
11171127
<p className="text-sm text-[rgb(var(--muted))]">
11181128
Manage your installed MCP servers
11191129
</p>

apps/desktop/src/features/servers/servers-page.helpers.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,76 @@ export function countActiveServerFilters(
144144
/**
145145
* Human-readable lines describing the currently applied server list filters.
146146
*/
147+
/** Per-status counts for the My Servers header summary. */
148+
export type ServerCountSummary = {
149+
installed: number;
150+
connected: number;
151+
disabled: number;
152+
error: number;
153+
needsSetup: number;
154+
};
155+
156+
/**
157+
* Aggregate installed-server counts by status bucket (same buckets as status filters).
158+
*/
159+
export function computeServerCountSummary(
160+
servers: ServerViewModel[],
161+
getAction: (server: ServerViewModel) => ServerActionKey
162+
): ServerCountSummary {
163+
const summary: ServerCountSummary = {
164+
installed: servers.length,
165+
connected: 0,
166+
disabled: 0,
167+
error: 0,
168+
needsSetup: 0,
169+
};
170+
171+
for (const server of servers) {
172+
switch (statusKeyFromAction(getAction(server))) {
173+
case 'connected':
174+
summary.connected += 1;
175+
break;
176+
case 'disabled':
177+
summary.disabled += 1;
178+
break;
179+
case 'error':
180+
summary.error += 1;
181+
break;
182+
case 'needs_setup':
183+
summary.needsSetup += 1;
184+
break;
185+
}
186+
}
187+
188+
return summary;
189+
}
190+
191+
/** Compact inline summary next to the My Servers title. */
192+
export function formatServerCountSummary(summary: ServerCountSummary): string {
193+
return [
194+
`${summary.installed} installed`,
195+
`${summary.connected} connected`,
196+
`${summary.disabled} disabled`,
197+
`${summary.error} error`,
198+
].join(', ');
199+
}
200+
201+
/** Tooltip lines for the server count hover panel. */
202+
export function describeServerCountSummary(summary: ServerCountSummary): string[] {
203+
const lines = [
204+
`${summary.installed} installed`,
205+
`${summary.connected} connected`,
206+
`${summary.disabled} disabled`,
207+
`${summary.error} error`,
208+
];
209+
210+
if (summary.needsSetup > 0) {
211+
lines.push(`${summary.needsSetup} needs setup`);
212+
}
213+
214+
return lines;
215+
}
216+
147217
export function describeAppliedServerFilters(
148218
transportFilter: TransportFilter,
149219
activeStatusFilters: ReadonlySet<StatusFilterKey>

0 commit comments

Comments
 (0)