Skip to content

Commit 4179da0

Browse files
committed
fix(webui): v0.36.0 registry/tools/add-server UX feedback
Repositories: - R4: remove the prominent provenance banner; surface registry name on each server card (badge-ghost, non-colliding data-test) for multi-registry results - R6: card header wraps long ids ([overflow-wrap:anywhere], min-w-0) so the registry badge stays readable instead of being struck through - R2: replace colorful NPM/Remote badges with neutral transport labels derived from install_cmd/url (remote, stdio:npm, stdio:python, stdio:docker, stdio) - R3: add a Transport filter (All/Remote/Stdio) to the search form Tools: - T1: Pending Approval stat counts pending+changed but the Approval=Pending filter matched only pending -> empty table. Clicking the card now applies an 'Awaiting approval' filter (pending OR changed) so count == rows. Add Server modal: - A1: drop DaisyUI .label (justify-between) for explicit left-aligned flex so Options toggle labels sit beside the toggle instead of centered - A2: constrain Options tooltips to wrap (before:w-56) so tooltip-right stays inside the modal frame instead of being clipped Verified live with Playwright (5/5 scenarios green). Unit suite 101/101. Refs MCP-991 (MCP-992/993/994/995/998/999/1000)
1 parent cdf1a0a commit 4179da0

5 files changed

Lines changed: 99 additions & 89 deletions

File tree

frontend/src/components/AddServerModal.vue

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
2-
<dialog :open="show" class="modal">
3-
<div class="modal-box max-w-3xl">
2+
<dialog :open="show" class="modal" data-test="add-server-modal">
3+
<div class="modal-box max-w-3xl" data-test="add-server-modal-box">
44
<h3 class="font-bold text-lg mb-4">Add New Server</h3>
55

66
<!-- Tab Selection -->
@@ -166,17 +166,17 @@
166166
<!-- Toggles Section -->
167167
<div class="divider mt-6">Options</div>
168168

169-
<div class="space-y-3">
169+
<div class="space-y-3" data-test="addserver-options">
170170
<!-- Enabled -->
171171
<div class="form-control">
172-
<label class="label cursor-pointer justify-start space-x-3">
172+
<label class="flex items-center gap-3 cursor-pointer py-1">
173173
<input
174174
type="checkbox"
175175
v-model="formData.enabled"
176176
class="toggle toggle-primary"
177177
/>
178178
<span class="label-text font-semibold">Enabled</span>
179-
<div class="tooltip tooltip-right" data-tip="Start this server immediately after adding">
179+
<div class="tooltip tooltip-right before:whitespace-normal before:w-56 before:max-w-[14rem]" data-tip="Start this server immediately after adding">
180180
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
181181
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
182182
</svg>
@@ -186,14 +186,14 @@
186186

187187
<!-- Quarantined -->
188188
<div class="form-control">
189-
<label class="label cursor-pointer justify-start space-x-3">
189+
<label class="flex items-center gap-3 cursor-pointer py-1">
190190
<input
191191
type="checkbox"
192192
v-model="formData.quarantined"
193193
class="toggle toggle-warning"
194194
/>
195195
<span class="label-text font-semibold">Quarantined</span>
196-
<div class="tooltip tooltip-right" data-tip="Prevent tool execution until security review is complete. Recommended for new servers.">
196+
<div class="tooltip tooltip-right before:whitespace-normal before:w-56 before:max-w-[14rem]" data-tip="Prevent tool execution until security review is complete. Recommended for new servers.">
197197
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
198198
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
199199
</svg>
@@ -203,15 +203,15 @@
203203

204204
<!-- Isolated (Docker) -->
205205
<div class="form-control">
206-
<label class="label cursor-pointer justify-start space-x-3">
206+
<label class="flex items-center gap-3 cursor-pointer py-1">
207207
<input
208208
type="checkbox"
209209
v-model="formData.isolated"
210210
class="toggle toggle-info"
211211
:disabled="formData.type !== 'stdio'"
212212
/>
213213
<span class="label-text font-semibold">Docker Isolation</span>
214-
<div class="tooltip tooltip-right" data-tip="Run stdio server in isolated Docker container for enhanced security (stdio only)">
214+
<div class="tooltip tooltip-right before:whitespace-normal before:w-56 before:max-w-[14rem]" data-tip="Run stdio server in isolated Docker container for enhanced security (stdio only)">
215215
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
216216
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
217217
</svg>
@@ -221,15 +221,15 @@
221221

222222
<!-- Idle on Inactivity -->
223223
<div class="form-control">
224-
<label class="label cursor-pointer justify-start space-x-3">
224+
<label class="flex items-center gap-3 cursor-pointer py-1">
225225
<input
226226
type="checkbox"
227227
v-model="formData.idleOnInactivity"
228228
class="toggle toggle-success"
229229
disabled
230230
/>
231231
<span class="label-text font-semibold opacity-50">Idle on Inactivity</span>
232-
<div class="tooltip tooltip-right" data-tip="Future feature: Automatically stop server after period of inactivity to save resources">
232+
<div class="tooltip tooltip-right before:whitespace-normal before:w-56 before:max-w-[14rem]" data-tip="Future feature: Automatically stop server after period of inactivity to save resources">
233233
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
234234
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
235235
</svg>

frontend/src/components/TopHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</div>
3535

3636
<!-- Add Server Button -->
37-
<button @click="showAddServerModal = true" class="btn btn-primary">
37+
<button @click="showAddServerModal = true" class="btn btn-primary" data-test="header-add-server">
3838
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3939
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
4040
</svg>

frontend/src/views/Repositories.vue

Lines changed: 67 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@
5757
/>
5858
</div>
5959

60+
<!-- Transport Filter (R3) -->
61+
<div class="form-control">
62+
<label class="label">
63+
<span class="label-text font-semibold">Transport</span>
64+
</label>
65+
<select
66+
v-model="transportFilter"
67+
class="select select-bordered"
68+
data-test="registry-transport-filter"
69+
>
70+
<option value="all">All</option>
71+
<option value="remote">Remote</option>
72+
<option value="stdio">Stdio</option>
73+
</select>
74+
</div>
75+
6076
<!-- Search Button -->
6177
<div class="form-control sm:self-end">
6278
<button
@@ -70,45 +86,6 @@
7086
</button>
7187
</div>
7288
</div>
73-
74-
<!-- Registry Info + provenance/trust (MCP-866/MCP-867) -->
75-
<div
76-
v-if="selectedRegistryInfo"
77-
class="alert mt-4"
78-
:class="isCustomRegistry(selectedRegistryInfo) ? 'alert-warning' : 'alert-info'"
79-
:data-test="`registry-info-${selectedRegistryInfo.id}`"
80-
>
81-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
83-
</svg>
84-
<div>
85-
<p class="font-semibold flex items-center gap-2 flex-wrap">
86-
{{ selectedRegistryInfo.name }}
87-
<span
88-
v-if="isCustomRegistry(selectedRegistryInfo)"
89-
class="badge badge-warning badge-sm gap-1"
90-
data-test="registry-provenance-badge-custom"
91-
>
92-
Third-party · unverified
93-
</span>
94-
<span
95-
v-else
96-
class="badge badge-success badge-sm gap-1"
97-
data-test="registry-provenance-badge-official"
98-
>
99-
Official · trusted
100-
</span>
101-
</p>
102-
<p class="text-sm">{{ selectedRegistryInfo.description }}</p>
103-
<p
104-
v-if="isCustomRegistry(selectedRegistryInfo)"
105-
class="text-xs mt-1 opacity-90"
106-
data-test="registry-custom-quarantine-note"
107-
>
108-
Servers added from this third-party registry are always quarantined and cannot skip security review.
109-
</p>
110-
</div>
111-
</div>
11289
</div>
11390
</div>
11491

@@ -133,7 +110,9 @@
133110
<!-- Server Results -->
134111
<div v-else-if="servers.length > 0" class="space-y-4">
135112
<div class="flex justify-between items-center">
136-
<p class="text-sm text-base-content/70">Found {{ servers.length }} server(s)</p>
113+
<p class="text-sm text-base-content/70" data-test="registry-results-count">
114+
Found {{ filteredServers.length }} server(s)<span v-if="transportFilter !== 'all'"> of {{ servers.length }}</span>
115+
</p>
137116
</div>
138117

139118
<!-- Server Cards with Smooth Transitions -->
@@ -142,40 +121,39 @@
142121
tag="div"
143122
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
144123
>
145-
<div v-for="server in servers" :key="server.id" :data-test="`registry-server-${server.id}`" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
124+
<div v-for="server in filteredServers" :key="server.id" :data-test="`registry-server-${server.id}`" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
146125
<div class="card-body">
147-
<div class="flex justify-between items-start">
148-
<h3 class="card-title text-lg">{{ server.name }}</h3>
149-
<div class="badge badge-outline badge-sm">{{ server.registry }}</div>
126+
<div class="flex justify-between items-start gap-2">
127+
<h3 class="card-title text-lg min-w-0 [overflow-wrap:anywhere]">{{ server.name }}</h3>
128+
<div
129+
v-if="server.registry"
130+
class="badge badge-ghost badge-sm shrink-0 whitespace-nowrap font-normal"
131+
:data-test="`registry-source-${server.id}`"
132+
:title="`From registry: ${server.registry}`"
133+
>
134+
{{ server.registry }}
135+
</div>
150136
</div>
151137

152138
<p class="text-sm text-base-content/70 line-clamp-3">
153139
{{ server.description }}
154140
</p>
155141

156-
<!-- Repository Info Badges -->
142+
<!-- Transport + requirements (neutral, non-colorful tags — R2) -->
157143
<div class="flex flex-wrap gap-2 mt-2">
158-
<div v-if="server.repository_info?.npm?.exists" class="badge badge-success badge-sm">
159-
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 24 24">
160-
<path d="M0 0h24v24H0z" fill="none"/>
161-
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
162-
</svg>
163-
NPM
164-
</div>
165-
<div v-if="server.url" class="badge badge-info badge-sm">
166-
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 24 24">
167-
<path d="M0 0h24v24H0z" fill="none"/>
168-
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
169-
</svg>
170-
Remote
144+
<div
145+
class="badge badge-outline badge-sm font-mono"
146+
:data-test="`registry-transport-${server.id}`"
147+
>
148+
{{ serverTransport(server) }}
171149
</div>
172150
<div
173151
v-if="server.required_inputs && server.required_inputs.length > 0"
174-
class="badge badge-warning badge-sm"
152+
class="badge badge-outline badge-sm"
175153
:data-test="`registry-requires-input-${server.id}`"
176154
:title="`Requires: ${server.required_inputs.map(i => i.name).join(', ')}`"
177155
>
178-
Requires input
156+
requires input
179157
</div>
180158
</div>
181159

@@ -480,11 +458,6 @@ const showThirdPartyWarning = ref(false)
480458
481459
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
482460
483-
// Computed
484-
const selectedRegistryInfo = computed(() => {
485-
return registries.value.find(r => r.id === selectedRegistry.value)
486-
})
487-
488461
// A registry is "custom/unverified" (third-party) when its provenance says so,
489462
// or — defensively — when trusted is explicitly false. Anything else (including
490463
// older payloads without the field) is treated as official/trusted.
@@ -493,6 +466,35 @@ function isCustomRegistry(registry?: Registry | null): boolean {
493466
return registry.provenance === REGISTRY_PROVENANCE_CUSTOM || registry.trusted === false
494467
}
495468
469+
// Transport classification (R2) + filter (R3). Derived purely from the
470+
// install command / url already returned by the registry search API:
471+
// url set, no install cmd -> remote
472+
// npx / npm / node -> stdio:npm
473+
// uvx / uv / pip / python -> stdio:python
474+
// docker -> stdio:docker
475+
// anything else with install cmd -> stdio
476+
const transportFilter = ref<'all' | 'remote' | 'stdio'>('all')
477+
478+
function serverTransport(server: RepositoryServer): string {
479+
const cmd = (server.install_cmd || '').trim().toLowerCase()
480+
if (cmd) {
481+
if (cmd.startsWith('docker')) return 'stdio:docker'
482+
if (cmd.startsWith('npx') || /(^|\s)(npm|node)(\s|$)/.test(cmd)) return 'stdio:npm'
483+
if (cmd.startsWith('uvx') || cmd.startsWith('uv ') || /(^|\s)(pipx?|python3?)(\s|$)/.test(cmd)) return 'stdio:python'
484+
return 'stdio'
485+
}
486+
if (server.url) return 'remote'
487+
return 'stdio'
488+
}
489+
490+
const filteredServers = computed(() => {
491+
if (transportFilter.value === 'all') return servers.value
492+
return servers.value.filter(s => {
493+
const t = serverTransport(s)
494+
return transportFilter.value === 'remote' ? t === 'remote' : t.startsWith('stdio')
495+
})
496+
})
497+
496498
const showPrompt = computed(() => promptServer.value !== null)
497499
498500
// Add is blocked until every prompted input has a non-empty value.

frontend/src/views/Tools.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
</label>
139139
<select v-model="filterApproval" class="select select-bordered select-sm" data-test="filter-approval">
140140
<option value="">All</option>
141+
<option value="awaiting">Awaiting approval</option>
141142
<option value="approved">Approved</option>
142143
<option value="pending">Pending</option>
143144
<option value="changed">Changed</option>
@@ -582,7 +583,7 @@ const hasActiveFilters = computed(() =>
582583
type StatCard = 'total' | 'enabled' | 'disabled' | 'pending'
583584
584585
const activeStatCard = computed<StatCard>(() => {
585-
if (filterApproval.value === 'pending') return 'pending'
586+
if (filterApproval.value === 'awaiting' || filterApproval.value === 'pending') return 'pending'
586587
if (filterStatus.value === 'enabled') return 'enabled'
587588
if (filterStatus.value === 'disabled') return 'disabled'
588589
if (!filterStatus.value && !filterApproval.value) return 'total'
@@ -598,7 +599,7 @@ function selectStatCard(card: StatCard) {
598599
}
599600
if (card === 'pending') {
600601
filterStatus.value = ''
601-
filterApproval.value = 'pending'
602+
filterApproval.value = 'awaiting'
602603
} else {
603604
filterApproval.value = ''
604605
filterStatus.value = card // 'enabled' | 'disabled'
@@ -659,7 +660,11 @@ const filteredTools = computed(() => {
659660
tools = tools.filter(t => getRisk(t) === filterRisk.value)
660661
}
661662
662-
if (filterApproval.value) {
663+
if (filterApproval.value === 'awaiting') {
664+
// "Awaiting approval" mirrors the Pending Approval stat, which counts both
665+
// brand-new (pending) and rug-pull (changed) tools.
666+
tools = tools.filter(t => t.approval_status === 'pending' || t.approval_status === 'changed')
667+
} else if (filterApproval.value) {
663668
tools = tools.filter(t => t.approval_status === filterApproval.value)
664669
}
665670

frontend/tests/unit/repositories-add-registry.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,23 @@ describe('Repositories — add registry + provenance + third-party warning', ()
6868
expect(wrapper.find('[data-test="registry-add-source-button"]').exists()).toBe(true)
6969
})
7070

71-
it('flags a custom registry as third-party/unverified and an official one as trusted', async () => {
71+
it('flags a custom registry as unverified in the selector and an official one without that suffix', async () => {
72+
// R4 (v0.36.0 feedback): the prominent provenance banner was removed in
73+
// favour of surfacing trust inline. The selector option text carries the
74+
// "— unverified" suffix for third-party registries; server cards carry the
75+
// registry name. No big alert block any more.
7276
const wrapper = mountView()
7377
await flushPromises()
7478

75-
const select = wrapper.find('[data-test="registry-select"]')
76-
await select.setValue('acme')
77-
await flushPromises()
78-
expect(wrapper.find('[data-test="registry-provenance-badge-custom"]').exists()).toBe(true)
79-
expect(wrapper.find('[data-test="registry-custom-quarantine-note"]').exists()).toBe(true)
79+
const options = wrapper.findAll('[data-test="registry-select"] option')
80+
const acme = options.find(o => o.attributes('value') === 'acme')
81+
const official = options.find(o => o.attributes('value') === 'official')
82+
expect(acme?.text()).toContain('unverified')
83+
expect(official?.text()).not.toContain('unverified')
8084

81-
await select.setValue('official')
82-
await flushPromises()
83-
expect(wrapper.find('[data-test="registry-provenance-badge-official"]').exists()).toBe(true)
85+
// The old prominent banner / quarantine-note block is gone.
8486
expect(wrapper.find('[data-test="registry-provenance-badge-custom"]').exists()).toBe(false)
87+
expect(wrapper.find('[data-test="registry-custom-quarantine-note"]').exists()).toBe(false)
8588
})
8689

8790
it('shows the one-time third-party warning before the first add and does NOT call the API yet', async () => {

0 commit comments

Comments
 (0)