|
8 | 8 | Manage mcpproxy settings. Changes save instantly; a badge marks fields that need a restart. |
9 | 9 | </p> |
10 | 10 | </div> |
11 | | - <button class="btn btn-sm btn-outline" :disabled="loading" @click="loadConfig" data-test="settings-reload"> |
12 | | - <span v-if="loading" class="loading loading-spinner loading-xs"></span> |
13 | | - <span v-else>Reload</span> |
14 | | - </button> |
| 11 | + <div class="flex items-center gap-2"> |
| 12 | + <label class="input input-bordered input-sm flex items-center gap-2 w-64"> |
| 13 | + <span class="opacity-50">🔍</span> |
| 14 | + <input v-model="search" type="text" class="grow" placeholder="Search all settings…" data-test="settings-search" /> |
| 15 | + <button v-if="search" class="opacity-50 hover:opacity-100" @click="search = ''" title="Clear">✕</button> |
| 16 | + </label> |
| 17 | + <button class="btn btn-sm btn-outline" :disabled="loading" @click="loadConfig" data-test="settings-reload"> |
| 18 | + <span v-if="loading" class="loading loading-spinner loading-xs"></span> |
| 19 | + <span v-else>Reload</span> |
| 20 | + </button> |
| 21 | + </div> |
| 22 | + </div> |
| 23 | + |
| 24 | + <!-- Search results (across all sections) --> |
| 25 | + <div v-if="loaded && search.trim()" class="card bg-base-100 shadow-md" data-test="settings-search-results"> |
| 26 | + <div class="card-body"> |
| 27 | + <h2 class="card-title text-lg">🔍 Search results <span class="text-sm font-normal text-base-content/60">({{ filteredFields.length }})</span></h2> |
| 28 | + <SettingsSection section-id="search" :fields="filteredFields" :working="state.working" :original="state.original" /> |
| 29 | + </div> |
15 | 30 | </div> |
16 | 31 |
|
17 | 32 | <!-- Tabs --> |
18 | | - <div role="tablist" class="tabs tabs-bordered" data-test="settings-tabs"> |
| 33 | + <div v-show="!search.trim()" role="tablist" class="tabs tabs-bordered" data-test="settings-tabs"> |
19 | 34 | <button |
20 | 35 | v-for="t in tabs" |
21 | 36 | :key="t.id" |
|
33 | 48 | <span>{{ loadError }}</span> |
34 | 49 | </div> |
35 | 50 |
|
36 | | - <template v-else-if="loaded"> |
| 51 | + <template v-else-if="loaded && !search.trim()"> |
37 | 52 | <!-- Security & Access --> |
38 | 53 | <div v-show="activeTab === 'security'" class="card bg-base-100 shadow-md"> |
39 | 54 | <div class="card-body"> |
40 | 55 | <h2 class="card-title text-lg">🔒 Security & Access</h2> |
41 | 56 | <p class="text-sm text-base-content/60 mb-2">The settings that most affect how exposed and protected your instance is.</p> |
| 57 | + <!-- posture summary --> |
| 58 | + <div class="flex flex-wrap gap-2 mb-4" data-test="settings-posture"> |
| 59 | + <span |
| 60 | + v-for="p in posture" |
| 61 | + :key="p.label" |
| 62 | + class="badge gap-1" |
| 63 | + :class="p.good ? 'badge-success badge-outline' : 'badge-warning'" |
| 64 | + :title="p.good ? 'OK' : 'Review this'" |
| 65 | + > |
| 66 | + {{ p.label }}: {{ p.on ? 'on' : 'off' }} |
| 67 | + </span> |
| 68 | + </div> |
42 | 69 | <SettingsSection section-id="security" :fields="securityFields" :working="state.working" :original="state.original" /> |
43 | 70 | </div> |
44 | 71 | </div> |
@@ -154,6 +181,41 @@ const activeTab = ref<string>('security') |
154 | 181 | const state = reactive<{ working: any; original: any }>({ working: {}, original: {} }) |
155 | 182 | const hasTeams = computed(() => state.working && state.working.teams != null) |
156 | 183 |
|
| 184 | +// cross-section search: type to find any setting across all tabs |
| 185 | +const search = ref('') |
| 186 | +const allFields = computed<SettingField[]>(() => [ |
| 187 | + ...securityFields, |
| 188 | + ...generalFields, |
| 189 | + ...advancedAccordions.flatMap((a) => a.fields), |
| 190 | + ...(hasTeams.value ? teamsFields : []), |
| 191 | +]) |
| 192 | +const filteredFields = computed<SettingField[]>(() => { |
| 193 | + const q = search.value.trim().toLowerCase() |
| 194 | + if (!q) return [] |
| 195 | + return allFields.value.filter( |
| 196 | + (f) => |
| 197 | + f.label.toLowerCase().includes(q) || |
| 198 | + f.key.toLowerCase().includes(q) || |
| 199 | + (f.help?.toLowerCase().includes(q) ?? false) |
| 200 | + ) |
| 201 | +}) |
| 202 | +
|
| 203 | +// at-a-glance security posture (computed from the working config) |
| 204 | +const posture = computed(() => { |
| 205 | + const w: any = state.working || {} |
| 206 | + const sdd = w.sensitive_data_detection?.enabled !== false |
| 207 | + const quarantine = w.quarantine_enabled !== false // default-on |
| 208 | + return [ |
| 209 | + { label: 'Quarantine', on: quarantine, good: quarantine }, |
| 210 | + { label: 'MCP auth', on: !!w.require_mcp_auth, good: !!w.require_mcp_auth }, |
| 211 | + { label: 'Docker isolation', on: !!(w.docker_isolation && w.docker_isolation.enabled), good: !!(w.docker_isolation && w.docker_isolation.enabled) }, |
| 212 | + { label: 'Secret scan', on: sdd, good: sdd }, |
| 213 | + { label: 'Code exec', on: !!w.enable_code_execution, good: !w.enable_code_execution }, |
| 214 | + { label: 'Read-only', on: !!w.read_only_mode, good: true }, |
| 215 | + { label: 'Reveal headers', on: !!w.reveal_secret_headers, good: !w.reveal_secret_headers }, |
| 216 | + ] |
| 217 | +}) |
| 218 | +
|
157 | 219 | const tabs = computed(() => { |
158 | 220 | const base = [ |
159 | 221 | { id: 'security', label: 'Security & Access', icon: '🔒' }, |
|
0 commit comments