mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-08-07 10:14:59 +00:00
feat: add collapsible groups to status page (#7154)
Automatically close stale issues / stale (push) Has been cancelled
Auto Test / auto-test (20, macos-latest) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-22.04) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-22.04-arm) (push) Has been cancelled
Auto Test / auto-test (20, windows-latest) (push) Has been cancelled
Auto Test / auto-test (24, macos-latest) (push) Has been cancelled
Auto Test / auto-test (24, ubuntu-22.04) (push) Has been cancelled
Auto Test / auto-test (24, ubuntu-22.04-arm) (push) Has been cancelled
Auto Test / auto-test (24, windows-latest) (push) Has been cancelled
Auto Test / auto-test (25, ubuntu-22.04) (push) Has been cancelled
Auto Test / armv7-simple-test (20) (push) Has been cancelled
Auto Test / armv7-simple-test (22) (push) Has been cancelled
Auto Test / check-linters (push) Has been cancelled
Auto Test / e2e-test (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / zizmor (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
validate / json-yaml-validate (push) Has been cancelled
validate / validate (push) Has been cancelled
NPM Update / npm-update (push) Has been cancelled
Nightly Release / release-nightly (push) Has been cancelled
Automatically close stale issues / stale (push) Has been cancelled
Auto Test / auto-test (20, macos-latest) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-22.04) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-22.04-arm) (push) Has been cancelled
Auto Test / auto-test (20, windows-latest) (push) Has been cancelled
Auto Test / auto-test (24, macos-latest) (push) Has been cancelled
Auto Test / auto-test (24, ubuntu-22.04) (push) Has been cancelled
Auto Test / auto-test (24, ubuntu-22.04-arm) (push) Has been cancelled
Auto Test / auto-test (24, windows-latest) (push) Has been cancelled
Auto Test / auto-test (25, ubuntu-22.04) (push) Has been cancelled
Auto Test / armv7-simple-test (20) (push) Has been cancelled
Auto Test / armv7-simple-test (22) (push) Has been cancelled
Auto Test / check-linters (push) Has been cancelled
Auto Test / e2e-test (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / zizmor (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
validate / json-yaml-validate (push) Has been cancelled
validate / validate (push) Has been cancelled
NPM Update / npm-update (push) Has been cancelled
Nightly Release / release-nightly (push) Has been cancelled
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -17,11 +17,20 @@
|
||||
class="action remove me-3"
|
||||
@click="removeGroup(group.index)"
|
||||
/>
|
||||
<span class="collapse-toggle" @click="toggleGroup(group.element)">
|
||||
<font-awesome-icon
|
||||
icon="chevron-down"
|
||||
class="chevron me-2"
|
||||
:class="{ collapsed: isGroupCollapsed(group.element) }"
|
||||
/>
|
||||
</span>
|
||||
<Editable
|
||||
v-model="group.element.name"
|
||||
:contenteditable="editMode"
|
||||
tag="span"
|
||||
:class="{ 'collapse-toggle': !editMode }"
|
||||
data-testid="group-name"
|
||||
@click="!editMode && toggleGroup(group.element)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,97 +42,101 @@
|
||||
/>
|
||||
</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
|
||||
{{ $t("No Monitors") }}
|
||||
</div>
|
||||
<transition name="slide-fade-up">
|
||||
<div v-if="!isGroupCollapsed(group.element)" class="shadow-box monitor-list mt-4 position-relative">
|
||||
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
|
||||
{{ $t("No Monitors") }}
|
||||
</div>
|
||||
|
||||
<!-- Monitor List -->
|
||||
<!-- animation is not working, no idea why -->
|
||||
<Draggable
|
||||
v-model="group.element.monitorList"
|
||||
class="monitor-list"
|
||||
group="same-group"
|
||||
:disabled="!editMode"
|
||||
:animation="100"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="monitor">
|
||||
<div class="item" data-testid="monitor">
|
||||
<div class="row">
|
||||
<div class="col-9 col-xl-6 small-padding">
|
||||
<div class="info">
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="arrows-alt-v"
|
||||
class="action drag me-3"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="times"
|
||||
class="action remove me-3"
|
||||
@click="removeMonitor(group.index, monitor.index)"
|
||||
/>
|
||||
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="cog"
|
||||
class="action me-3 ms-0"
|
||||
:class="{ 'link-active': true, 'btn-link': true }"
|
||||
data-testid="monitor-settings"
|
||||
@click="$refs.monitorSettingDialog.show(group, monitor)"
|
||||
/>
|
||||
<Status
|
||||
v-if="showOnlyLastHeartbeat"
|
||||
:status="statusOfLastHeartbeat(monitor.element.id)"
|
||||
/>
|
||||
<Uptime v-else :monitor="monitor.element" type="24" :pill="true" />
|
||||
<a
|
||||
v-if="showLink(monitor)"
|
||||
:href="monitor.element.url"
|
||||
class="item-name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="monitor-name"
|
||||
>
|
||||
{{ monitor.element.name }}
|
||||
</a>
|
||||
<p v-else class="item-name" data-testid="monitor-name">
|
||||
{{ monitor.element.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="extra-info">
|
||||
<div
|
||||
v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining"
|
||||
>
|
||||
<Tag
|
||||
:item="{
|
||||
name: $t('Cert Exp.'),
|
||||
value: formattedCertExpiryMessage(monitor),
|
||||
color: certExpiryColor(monitor),
|
||||
}"
|
||||
:size="'sm'"
|
||||
<!-- Monitor List -->
|
||||
<!-- animation is not working, no idea why -->
|
||||
<Draggable
|
||||
v-model="group.element.monitorList"
|
||||
class="monitor-list"
|
||||
group="same-group"
|
||||
:disabled="!editMode"
|
||||
:animation="100"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="monitor">
|
||||
<div class="item" data-testid="monitor">
|
||||
<div class="row">
|
||||
<div class="col-9 col-xl-6 small-padding">
|
||||
<div class="info">
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="arrows-alt-v"
|
||||
class="action drag me-3"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="times"
|
||||
class="action remove me-3"
|
||||
@click="removeMonitor(group.index, monitor.index)"
|
||||
/>
|
||||
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
icon="cog"
|
||||
class="action me-3 ms-0"
|
||||
:class="{ 'link-active': true, 'btn-link': true }"
|
||||
data-testid="monitor-settings"
|
||||
@click="$refs.monitorSettingDialog.show(group, monitor)"
|
||||
/>
|
||||
<Status
|
||||
v-if="showOnlyLastHeartbeat"
|
||||
:status="statusOfLastHeartbeat(monitor.element.id)"
|
||||
/>
|
||||
<Uptime v-else :monitor="monitor.element" type="24" :pill="true" />
|
||||
<a
|
||||
v-if="showLink(monitor)"
|
||||
:href="monitor.element.url"
|
||||
class="item-name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="monitor-name"
|
||||
>
|
||||
{{ monitor.element.name }}
|
||||
</a>
|
||||
<p v-else class="item-name" data-testid="monitor-name">
|
||||
{{ monitor.element.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="showTags">
|
||||
<Tag
|
||||
v-for="tag in monitor.element.tags"
|
||||
:key="tag"
|
||||
:item="tag"
|
||||
:size="'sm'"
|
||||
data-testid="monitor-tag"
|
||||
/>
|
||||
<div class="extra-info">
|
||||
<div
|
||||
v-if="
|
||||
showCertificateExpiry && monitor.element.certExpiryDaysRemaining
|
||||
"
|
||||
>
|
||||
<Tag
|
||||
:item="{
|
||||
name: $t('Cert Exp.'),
|
||||
value: formattedCertExpiryMessage(monitor),
|
||||
color: certExpiryColor(monitor),
|
||||
}"
|
||||
:size="'sm'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showTags">
|
||||
<Tag
|
||||
v-for="tag in monitor.element.tags"
|
||||
:key="tag"
|
||||
:item="tag"
|
||||
:size="'sm'"
|
||||
data-testid="monitor-tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-xl-6">
|
||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-xl-6">
|
||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
@@ -183,6 +196,59 @@ export default {
|
||||
// Sorting is now handled by GroupSortDropdown component
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Toggle collapsed state for a group
|
||||
* @param {object} group Group to toggle
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleGroup(group) {
|
||||
if (!this.$router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = this.getGroupIdentifier(group);
|
||||
const collapsed = this.getCollapsedList();
|
||||
const index = collapsed.indexOf(groupId);
|
||||
|
||||
if (index >= 0) {
|
||||
collapsed.splice(index, 1);
|
||||
} else {
|
||||
collapsed.push(groupId);
|
||||
}
|
||||
|
||||
const query = { ...this.$route.query };
|
||||
if (collapsed.length > 0) {
|
||||
query.collapse = collapsed;
|
||||
} else {
|
||||
delete query.collapse;
|
||||
}
|
||||
|
||||
this.$router.push({ query }).catch(() => {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a group is collapsed
|
||||
* @param {object} group Group to check
|
||||
* @returns {boolean} Whether the group is collapsed
|
||||
*/
|
||||
isGroupCollapsed(group) {
|
||||
return this.getCollapsedList().includes(this.getGroupIdentifier(group));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of collapsed group identifiers from the query param.
|
||||
* Vue Router normalises repeated params (?collapse=1&collapse=2) into an array.
|
||||
* @returns {string[]} Collapsed group identifiers
|
||||
*/
|
||||
getCollapsedList() {
|
||||
const raw = this.$route.query.collapse;
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
// Normalise to array: a single query param is a string, repeated params are already an array
|
||||
return [].concat(raw);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the specified group
|
||||
* @param {number} index Index of group to remove
|
||||
@@ -278,14 +344,10 @@ export default {
|
||||
* @returns {string} group identifier
|
||||
*/
|
||||
getGroupIdentifier(group) {
|
||||
// Use the name directly if available
|
||||
if (group.name) {
|
||||
// Only remove spaces and use encodeURIComponent for URL safety
|
||||
const cleanName = group.name.replace(/\s+/g, "");
|
||||
return cleanName;
|
||||
if (group.id !== undefined && group.id !== null) {
|
||||
return group.id.toString();
|
||||
}
|
||||
// Fallback to ID or index
|
||||
return group.id ? `group${group.id}` : `group${this.$root.publicGroupList.indexOf(group)}`;
|
||||
return `group${this.$root.publicGroupList.indexOf(group)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -363,6 +425,21 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 0.8em;
|
||||
color: #bbb;
|
||||
transition: all 0.2s $easing-in;
|
||||
|
||||
&.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
padding: 13px 0 10px;
|
||||
|
||||
Reference in New Issue
Block a user