Skip to content

Commit 4aae51d

Browse files
author
Algis Dumbris
committed
fix(050): sidebar count badges + clickable Tools stat cards + server-link affordance
- Add Servers/Secrets count badges to sidebar WORKSPACE section for visual parity with the existing Tools badge. Server count is reactive via the status SSE; secret count fetched on mount. - Secret badge uses total_secrets from /secrets/config (same source as the Secrets page) instead of /secrets/refs, which over-counts every config reference including ${env:...} placeholders. - Tools summary cards (Total/Enabled/Disabled/Pending) are now clickable filter toggles with ring highlight, mirroring the Servers page. - Tools table server name uses link-primary (always visible as a link) instead of link-hover, matching the modal's affordance.
1 parent 2580985 commit 4aae51d

2 files changed

Lines changed: 88 additions & 9 deletions

File tree

frontend/src/components/SidebarNav.vue

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@
211211
>
212212
<IconServers class="w-5 h-5 shrink-0" />
213213
<span v-show="!collapsed">Servers</span>
214+
<span
215+
v-if="!collapsed && serverCount > 0"
216+
class="badge badge-sm badge-ghost ml-auto tabular-nums"
217+
>{{ serverCount }}</span>
214218
</router-link>
215219
</li>
216220
<li>
@@ -237,6 +241,10 @@
237241
>
238242
<IconSecrets class="w-5 h-5 shrink-0" />
239243
<span v-show="!collapsed">Secrets</span>
244+
<span
245+
v-if="!collapsed && secretCount > 0"
246+
class="badge badge-sm badge-ghost ml-auto tabular-nums"
247+
>{{ secretCount }}</span>
240248
</router-link>
241249
</li>
242250
<!-- Sub-item: Agent Tokens nested under Secrets.
@@ -451,6 +459,7 @@ onMounted(() => {
451459
if (!authStore.isTeamsEdition) {
452460
void onboardingStore.fetchState()
453461
void fetchToolCount()
462+
void fetchSecretCount()
454463
}
455464
})
456465
@@ -562,6 +571,28 @@ async function fetchToolCount() {
562571
}
563572
}
564573
574+
// Sidebar badge parity with Tools: show total server + secret counts so the
575+
// WORKSPACE section reads consistently. Server count is already reactive via
576+
// the status SSE; secrets need a one-shot fetch on mount.
577+
const serverCount = computed(() => systemStore.upstreamStats.total_servers ?? 0)
578+
const secretCount = ref(0)
579+
580+
async function fetchSecretCount() {
581+
try {
582+
// Use the same source as the Secrets page (total_secrets from
583+
// /secrets/config) so the badge matches what the user sees there.
584+
// /secrets/refs is NOT equivalent — it counts every config reference
585+
// including ${env:...} placeholders (TERM_SESSION_ID etc.), not stored
586+
// keyring secrets.
587+
const resp = await api.getConfigSecrets()
588+
if (resp.success && resp.data) {
589+
secretCount.value = resp.data.total_secrets ?? 0
590+
}
591+
} catch {
592+
// Silently ignore — badge is non-critical
593+
}
594+
}
595+
565596
// Server edition menus (unchanged behavior)
566597
const teamsUserMenu = [
567598
{ name: 'My Servers', path: '/my/servers' },

frontend/src/views/Tools.vue

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,44 @@
2020

2121
<!-- Summary Stat Cards -->
2222
<div v-if="stats" class="stats shadow bg-base-100 w-full">
23-
<div class="stat" data-test="stat-total">
23+
<button
24+
type="button"
25+
:class="['stat text-left transition-colors cursor-pointer hover:bg-base-200/60', activeStatCard === 'total' ? 'bg-base-200 ring-2 ring-inset ring-primary/40' : '']"
26+
data-test="stat-total"
27+
@click="selectStatCard('total')"
28+
>
2429
<div class="stat-title">Total</div>
2530
<div class="stat-value text-2xl">{{ stats.total }}</div>
26-
</div>
27-
<div class="stat" data-test="stat-enabled">
31+
</button>
32+
<button
33+
type="button"
34+
:class="['stat text-left transition-colors cursor-pointer hover:bg-base-200/60', activeStatCard === 'enabled' ? 'bg-base-200 ring-2 ring-inset ring-primary/40' : '']"
35+
data-test="stat-enabled"
36+
@click="selectStatCard('enabled')"
37+
>
2838
<div class="stat-title">Enabled</div>
2939
<div class="stat-value text-2xl text-success">{{ stats.enabled }}</div>
30-
</div>
31-
<div class="stat" data-test="stat-disabled">
40+
</button>
41+
<button
42+
type="button"
43+
:class="['stat text-left transition-colors cursor-pointer hover:bg-base-200/60', activeStatCard === 'disabled' ? 'bg-base-200 ring-2 ring-inset ring-primary/40' : '']"
44+
data-test="stat-disabled"
45+
@click="selectStatCard('disabled')"
46+
>
3247
<div class="stat-title">Disabled</div>
3348
<div class="stat-value text-2xl text-warning">{{ stats.disabled }}</div>
34-
</div>
35-
<div class="stat" data-test="stat-pending">
49+
</button>
50+
<button
51+
type="button"
52+
:class="['stat text-left transition-colors cursor-pointer hover:bg-base-200/60', activeStatCard === 'pending' ? 'bg-base-200 ring-2 ring-inset ring-primary/40' : '']"
53+
data-test="stat-pending"
54+
@click="selectStatCard('pending')"
55+
>
3656
<div class="stat-title">Pending Approval</div>
3757
<div class="stat-value text-2xl" :class="stats.pending_approval > 0 ? 'text-error' : ''">
3858
{{ stats.pending_approval }}
3959
</div>
40-
</div>
60+
</button>
4161
</div>
4262

4363
<!-- Partial-error banner -->
@@ -280,7 +300,7 @@
280300
<td>
281301
<router-link
282302
:to="`/servers/${tool.server_name}`"
283-
class="link link-hover text-sm font-medium"
303+
class="link link-primary text-sm font-medium"
284304
@click.stop
285305
>
286306
{{ tool.server_name }}
@@ -557,6 +577,34 @@ const hasActiveFilters = computed(() =>
557577
!!searchQuery.value || !!filterServer.value || !!filterStatus.value || !!filterRisk.value || !!filterApproval.value
558578
)
559579
580+
// Clickable stat cards (parity with Servers page): each card drives the
581+
// status/approval filter and toggles off when its active card is clicked again.
582+
type StatCard = 'total' | 'enabled' | 'disabled' | 'pending'
583+
584+
const activeStatCard = computed<StatCard>(() => {
585+
if (filterApproval.value === 'pending') return 'pending'
586+
if (filterStatus.value === 'enabled') return 'enabled'
587+
if (filterStatus.value === 'disabled') return 'disabled'
588+
if (!filterStatus.value && !filterApproval.value) return 'total'
589+
return 'total'
590+
})
591+
592+
function selectStatCard(card: StatCard) {
593+
// Toggle: re-clicking the active card resets to the unfiltered "total" view.
594+
if (card === 'total' || activeStatCard.value === card) {
595+
filterStatus.value = ''
596+
filterApproval.value = ''
597+
return
598+
}
599+
if (card === 'pending') {
600+
filterStatus.value = ''
601+
filterApproval.value = 'pending'
602+
} else {
603+
filterApproval.value = ''
604+
filterStatus.value = card // 'enabled' | 'disabled'
605+
}
606+
}
607+
560608
// ---- Computed: risk derivation ----
561609
function getRisk(tool: GlobalTool): 'read' | 'write' | 'destructive' {
562610
if (tool.annotations?.destructiveHint) return 'destructive'

0 commit comments

Comments
 (0)