feat: collapsible groups in IP addressing view

Groups are collapsed by default. Click a header to toggle it; chevron
rotates to indicate state. "Expand all" / "Collapse all" buttons in the
page header; each is disabled when the action is already complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:16:16 +02:00
parent 6b4de6f792
commit 66aeb04cb5
2 changed files with 75 additions and 3 deletions
+69 -3
View File
@@ -10,6 +10,10 @@
<div class="page-header">
<h1>{{ t('ipAddressing') }}</h1>
<div v-if="!allHaveNoIp" class="header-actions">
<button class="btn-toggle-all" @click="expandAll" :disabled="allExpanded">{{ t('expandAll') }}</button>
<button class="btn-toggle-all" @click="collapseAll" :disabled="allCollapsed">{{ t('collapseAll') }}</button>
</div>
</div>
<div v-if="allHaveNoIp" class="empty">
@@ -18,7 +22,10 @@
<div v-else class="groups">
<div v-for="group in groups" :key="group.key" class="group">
<div class="group-header" :style="{ borderLeftColor: group.color }">
<div class="group-header" :style="{ borderLeftColor: group.color }" @click="toggleGroup(group.key)">
<svg class="chevron" :class="{ open: expanded.has(group.key) }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
<span class="badge-vlan" :style="{ background: group.color }">
{{ group.badge }}
</span>
@@ -27,7 +34,7 @@
<span class="group-count">{{ group.rows.length }}</span>
</div>
<div class="table-wrap">
<div v-show="expanded.has(group.key)" class="table-wrap">
<table>
<thead>
<tr>
@@ -65,7 +72,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { t } from '../i18n.js'
import { detectOs } from '../brandIcons.js'
import DeviceFormModal from './DeviceFormModal.vue'
@@ -77,6 +84,24 @@ const props = defineProps({
const emit = defineEmits(['refresh'])
const editingDevice = ref(null)
const expanded = ref(new Set())
function toggleGroup(key) {
const s = new Set(expanded.value)
s.has(key) ? s.delete(key) : s.add(key)
expanded.value = s
}
function expandAll() {
expanded.value = new Set(groups.value.map(g => g.key))
}
function collapseAll() {
expanded.value = new Set()
}
const allExpanded = computed(() => groups.value.length > 0 && groups.value.every(g => expanded.value.has(g.key)))
const allCollapsed = computed(() => groups.value.every(g => !expanded.value.has(g.key)))
function ipToNum(ip) {
if (!ip) return 0
@@ -162,6 +187,7 @@ const allHaveNoIp = computed(() => groups.value.length === 0)
.page-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
@@ -171,6 +197,32 @@ const allHaveNoIp = computed(() => groups.value.length === 0)
color: var(--text-primary);
}
.header-actions {
margin-left: auto;
display: flex;
gap: 6px;
}
.btn-toggle-all {
padding: 4px 12px;
font-size: 12px;
font-weight: 500;
border: 1.5px solid var(--border);
border-radius: 6px;
background: var(--bg-chip);
color: var(--text-secondary);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-toggle-all:hover:not(:disabled) {
background: var(--border);
color: var(--text-primary);
}
.btn-toggle-all:disabled {
opacity: 0.35;
cursor: default;
}
.empty {
color: var(--text-muted);
font-size: 14px;
@@ -200,6 +252,20 @@ const allHaveNoIp = computed(() => groups.value.length === 0)
border-bottom: 1px solid var(--border);
border-left: 4px solid transparent;
background: var(--bg-thead);
cursor: pointer;
user-select: none;
}
.group-header:hover {
background: var(--bg-card-hover);
}
.chevron {
color: var(--text-muted);
flex-shrink: 0;
transition: transform 0.18s ease;
}
.chevron.open {
transform: rotate(90deg);
}
.badge-vlan {
+6
View File
@@ -19,6 +19,8 @@ const LANGS = {
colOs: 'OS',
fieldOs: "Système d'exploitation",
noIpConfigured: 'Aucune adresse IP configurée.',
expandAll: 'Tout déplier',
collapseAll: 'Tout replier',
discovery: '🔍 Découverte auto',
statsNetwork: 'Réseau',
statsNetworks: 'Réseaux',
@@ -199,6 +201,8 @@ const LANGS = {
colOs: 'OS',
fieldOs: 'Operating system',
noIpConfigured: 'No IP address configured.',
expandAll: 'Expand all',
collapseAll: 'Collapse all',
discovery: '🔍 Auto discovery',
statsNetwork: 'Network',
statsNetworks: 'Networks',
@@ -373,6 +377,8 @@ const LANGS = {
colOs: 'SO',
fieldOs: 'Sistema operativo',
noIpConfigured: 'No hay ninguna dirección IP configurada.',
expandAll: 'Expandir todo',
collapseAll: 'Contraer todo',
discovery: '🔍 Descubrimiento auto',
statsNetwork: 'Red',
statsNetworks: 'Redes',