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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user