feat(dashboard): add expand/collapse all groups button (#6743)

Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Dorian Grasset 2026-01-18 11:49:45 +01:00 committed by GitHub
parent a0d73aba1a
commit bf9b734f6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 7 deletions

View File

@ -35,7 +35,13 @@
:aria-label="selectAll ? $t('deselectAllMonitorsAria') : $t('selectAllMonitorsAria')"
/>
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
<MonitorListFilter
:filterState="filterState"
:allCollapsed="allGroupsCollapsed"
:hasGroups="groupMonitors.length >= 2"
@update-filter="updateFilter"
@toggle-collapse-all="toggleCollapseAll"
/>
</div>
</div>
@ -100,8 +106,8 @@
</div>
<MonitorListItem
v-for="(item, index) in sortedMonitorList"
:key="index"
v-for="item in sortedMonitorList"
:key="`${item.id}-${collapseKey}`"
:monitor="item"
:isSelectMode="selectMode"
:isSelected="isSelected"
@ -154,6 +160,7 @@ export default {
active: null,
tags: null,
},
collapseKey: 0,
};
},
computed: {
@ -229,6 +236,38 @@ export default {
this.searchText !== ""
);
},
/**
* Gets all group monitors at root level that have children
* @returns {Array} Array of group monitors with children
*/
groupMonitors() {
const monitors = Object.values(this.$root.monitorList);
return monitors.filter(
(m) => m.type === "group" && m.parent === null && monitors.some((child) => child.parent === m.id)
);
},
/**
* Determines if all groups are collapsed.
* Note: collapseKey is included to force re-computation when toggleCollapseAll()
* updates localStorage, since Vue cannot detect localStorage changes.
* @returns {boolean} True if all groups are collapsed
*/
allGroupsCollapsed() {
// collapseKey forces this computed to re-evaluate after localStorage updates
if (this.collapseKey < 0 || this.groupMonitors.length === 0) {
return true;
}
const storage = window.localStorage.getItem("monitorCollapsed");
if (storage === null) {
return true; // Default is collapsed
}
const storageObject = JSON.parse(storage);
return this.groupMonitors.every((group) => storageObject[`monitor_${group.id}`] !== false);
},
},
watch: {
searchText() {
@ -303,6 +342,26 @@ export default {
updateFilter(newFilter) {
this.filterState = newFilter;
},
/**
* Toggle collapse state for all group monitors
* @returns {void}
*/
toggleCollapseAll() {
const shouldCollapse = !this.allGroupsCollapsed;
let storageObject = {};
const storage = window.localStorage.getItem("monitorCollapsed");
if (storage !== null) {
storageObject = JSON.parse(storage);
}
this.groupMonitors.forEach((group) => {
storageObject[`monitor_${group.id}`] = shouldCollapse;
});
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
this.collapseKey++;
},
/**
* Deselect a monitor
* @param {number} id ID of monitor
@ -731,6 +790,7 @@ export default {
.search-input {
width: 100%;
padding-right: 30px;
transition: none !important;
}
.monitor-item {

View File

@ -137,6 +137,15 @@
</li>
</template>
</MonitorListFilterDropdown>
<button
v-if="hasGroups"
type="button"
class="btn btn-outline-normal btn-collapse-all"
:title="allCollapsed ? $t('Expand All Groups') : $t('Collapse All Groups')"
@click="$emit('toggle-collapse-all')"
>
<font-awesome-icon :icon="allCollapsed ? 'folder' : 'folder-open'" fixed-width />
</button>
</template>
<script>
@ -155,8 +164,16 @@ export default {
type: Object,
required: true,
},
allCollapsed: {
type: Boolean,
default: true,
},
hasGroups: {
type: Boolean,
default: false,
},
},
emits: ["updateFilter"],
emits: ["updateFilter", "toggle-collapse-all"],
data() {
return {
tagsList: [],
@ -322,4 +339,8 @@ export default {
}
}
}
.btn-collapse-all {
transition: none !important;
}
</style>

View File

@ -53,6 +53,8 @@ import {
faInfoCircle,
faClone,
faCertificate,
faFolder,
faFolderOpen,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@ -103,6 +105,8 @@ library.add(
faInfoCircle,
faClone,
faCertificate,
faFolder,
faFolderOpen,
);
export { FontAwesomeIcon };

View File

@ -1368,5 +1368,7 @@
"Expected TLS Alert": "Expected TLS Alert",
"None (Successful Connection)": "None (Successful Connection)",
"expectedTlsAlertDescription": "Select the TLS alert you expect the server to return. Use {code} to verify mTLS endpoints reject connections without client certificates. See {link} for details.",
"TLS Alert Spec": "RFC 8446"
"TLS Alert Spec": "RFC 8446",
"Expand All Groups": "Expand All Groups",
"Collapse All Groups": "Collapse All Groups"
}

View File

@ -1,7 +1,7 @@
<template>
<div class="container-fluid">
<div class="row">
<div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
<div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4 ps-0">
<div>
<router-link to="/add" class="btn btn-primary mb-3">
<font-awesome-icon icon="plus" />

View File

@ -5,7 +5,7 @@
{{ $t("Quick Stats") }}
</h1>
<div class="shadow-box big-padding text-center mb-4">
<div class="shadow-box big-padding text-center mb-3">
<div class="row">
<div class="col">
<h3>{{ $t("Up") }}</h3>