Tab + search the System Settings page to cut scrolling

System Settings had grown to one long scroll (Integrations, Email, Audit,
Authentication, Asset Identifiers, Global Search, PC Type Mapping). Now a
vertical tab nav shows one section at a time, plus a search box that filters the
tabs by label/keywords (and shows every matching section while searching). Type
"toner" -> Integrations, "fqdn" -> Asset Identifiers, etc.

Frontend-only, behavior of each section unchanged. Build + naming green, verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 22:16:39 -04:00
parent 398353ff79
commit 6f4c161c0f

View File

@@ -3,9 +3,26 @@
<h1>System Settings</h1>
</div>
<div class="settings-search">
<input v-model="settingsSearch" type="text" class="form-control"
placeholder="Search settings (zabbix, smtp, toner, fqdn...)" />
</div>
<div class="settings-layout">
<nav class="settings-tabs">
<button
v-for="tab in visibleTabs"
:key="tab.key"
class="settings-tab"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>{{ tab.label }}</button>
<p v-if="!visibleTabs.length" class="settings-tab-empty">No matching settings</p>
</nav>
<div class="settings-sections">
<!-- Integrations Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('integrations')">
<h2 class="section-title">Integrations</h2>
<div class="setting-group">
@@ -63,7 +80,7 @@
</div>
<!-- Email Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('email')">
<h2 class="section-title">Email / SMTP</h2>
<div class="setting-group">
@@ -215,7 +232,7 @@
</div>
<!-- Audit Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('audit')">
<h2 class="section-title">Audit & Logging</h2>
<div class="setting-group">
@@ -247,7 +264,7 @@
</div>
<!-- Authentication Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('auth')">
<h2 class="section-title">Authentication</h2>
<div class="setting-group">
@@ -377,7 +394,7 @@
</div>
<!-- Asset Identifiers Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('identifiers')">
<h2 class="section-title">Asset Identifiers</h2>
<div class="setting-group">
@@ -416,7 +433,7 @@
</div>
<!-- Global Search Section -->
<div class="section-card">
<div class="section-card" v-show="isVisible('search')">
<h2 class="section-title">Global Search</h2>
<div class="setting-group">
@@ -442,7 +459,7 @@
</div>
<!-- Collector PC Type Mapping Section -->
<div class="section-card" v-if="pcTypeMappings.length">
<div class="section-card" v-show="isVisible('pctype')" v-if="pcTypeMappings.length">
<h2 class="section-title">Collector PC Type Mapping</h2>
<div class="setting-group">
@@ -476,16 +493,52 @@
</div>
</div>
</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="success" class="success-message">{{ success }}</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { settingsApi, computersApi } from '../../api'
import { setIdentifierFlag } from '../../composables/identifierSettings'
// Section tabs - one section visible at a time to avoid a long scroll. The
// search box filters tabs (and, while searching, shows every matching section).
const SETTINGS_TABS = [
{ key: 'integrations', label: 'Integrations', keywords: 'zabbix toner supply printer monitoring api integration' },
{ key: 'email', label: 'Email / SMTP', keywords: 'email smtp mail notifications alerts tls from recipients' },
{ key: 'audit', label: 'Audit & Logging', keywords: 'audit log retention history' },
{ key: 'auth', label: 'Authentication', keywords: 'auth saml sso login users idp' },
{ key: 'identifiers', label: 'Asset Identifiers', keywords: 'identifier gauge lab maintenance fqdn hostname asset' },
{ key: 'search', label: 'Global Search', keywords: 'search results domains' },
{ key: 'pctype', label: 'PC Type Mapping', keywords: 'pc type mapping collector enrollment shopfloor computer type' },
]
const settingsSearch = ref('')
const activeTab = ref('integrations')
const visibleTabs = computed(() => {
const term = settingsSearch.value.trim().toLowerCase()
if (!term) return SETTINGS_TABS
return SETTINGS_TABS.filter(t =>
t.label.toLowerCase().includes(term) || t.keywords.includes(term))
})
function isVisible(key) {
if (settingsSearch.value.trim()) {
return visibleTabs.value.some(t => t.key === key)
}
return activeTab.value === key
}
// Keep the active tab valid as the search narrows the list.
watch(visibleTabs, (tabs) => {
if (tabs.length && !tabs.some(t => t.key === activeTab.value)) {
activeTab.value = tabs[0].key
}
})
const settings = reactive({
// Zabbix
zabbix_enabled: false,
@@ -786,10 +839,65 @@ onMounted(loadSettings)
</script>
<style scoped>
.settings-search {
margin-bottom: 1rem;
}
.settings-search input {
width: 100%;
max-width: 420px;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-card);
color: var(--text);
}
.settings-layout {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.settings-tabs {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 180px;
position: sticky;
top: 1rem;
}
.settings-tab {
text-align: left;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text);
cursor: pointer;
font-size: 0.95rem;
}
.settings-tab:hover { background: var(--bg); }
.settings-tab.active {
background: var(--primary);
color: #fff;
}
.settings-tab-empty {
color: var(--text-light);
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
}
.settings-sections {
display: flex;
flex-direction: column;
gap: 1.5rem;
flex: 1;
min-width: 0;
}
@media (max-width: 700px) {
.settings-layout { flex-direction: column; }
.settings-tabs { flex-direction: row; flex-wrap: wrap; position: static; min-width: 0; }
}
.section-card {