mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-04-20 07:36:05 -04:00
Adjust group sorting display style and support sharing sorting state with URL
This commit is contained in:
parent
2d67631cb2
commit
8d7a595c9f
@ -10,9 +10,70 @@
|
||||
<div class="mb-5" data-testid="group">
|
||||
<!-- Group Title -->
|
||||
<h2 class="group-title">
|
||||
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
|
||||
<div class="title-section">
|
||||
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
|
||||
</div>
|
||||
|
||||
<div v-if="group.element && group.element.monitorList && group.element.monitorList.length > 1" class="sort-dropdown">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle sort-button" type="button" :id="'sortDropdown' + group.index" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ $t('Sort By') }}
|
||||
<span v-if="group.element.sortKey" class="ms-1">
|
||||
<span class="sort-direction-indicator">
|
||||
{{ getSortDirectionSymbol(group.element) }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end sort-menu" :aria-labelledby="'sortDropdown' + group.index">
|
||||
<li>
|
||||
<a class="dropdown-item sort-item" href="#" @click.prevent="setSort(group.element, 'status')">
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t('Status') }}</span>
|
||||
<span v-if="getSortKey(group.element) === 'status'" class="sort-indicators">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span class="sort-direction-indicator">{{ getSortDirectionSymbol(group.element) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item sort-item" href="#" @click.prevent="setSort(group.element, 'name')">
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t('Name') }}</span>
|
||||
<span v-if="getSortKey(group.element) === 'name'" class="sort-indicators">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span class="sort-direction-indicator">{{ getSortDirectionSymbol(group.element) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item sort-item" href="#" @click.prevent="setSort(group.element, 'uptime')">
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t('Uptime') }}</span>
|
||||
<span v-if="getSortKey(group.element) === 'uptime'" class="sort-indicators">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span class="sort-direction-indicator">{{ getSortDirectionSymbol(group.element) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="showCertificateExpiry">
|
||||
<a class="dropdown-item sort-item" href="#" @click.prevent="setSort(group.element, 'cert')">
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t('Cert Exp.') }}</span>
|
||||
<span v-if="getSortKey(group.element) === 'cert'" class="sort-indicators">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span class="sort-direction-indicator">{{ getSortDirectionSymbol(group.element) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
@ -117,7 +178,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -126,9 +186,239 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Initialize sort settings
|
||||
this.initializeSortSettings();
|
||||
},
|
||||
mounted() {
|
||||
// Load sort settings from URL
|
||||
this.loadSortSettingsFromURL();
|
||||
// Listen for URL changes
|
||||
window.addEventListener('popstate', this.handlePopState);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Remove URL change listener
|
||||
window.removeEventListener('popstate', this.handlePopState);
|
||||
},
|
||||
watch: {
|
||||
// Watch for changes in heartbeat list, reapply sorting
|
||||
'$root.heartbeatList': {
|
||||
handler() {
|
||||
if (this.$root && this.$root.publicGroupList) {
|
||||
this.$root.publicGroupList.forEach(group => {
|
||||
if (group) {
|
||||
this.applySort(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
// Watch for changes in uptime list, reapply sorting
|
||||
'$root.uptimeList': {
|
||||
handler() {
|
||||
if (this.$root && this.$root.publicGroupList) {
|
||||
this.$root.publicGroupList.forEach(group => {
|
||||
if (group) {
|
||||
this.applySort(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Initialize group sort settings
|
||||
*/
|
||||
initializeSortSettings() {
|
||||
if (this.$root.publicGroupList) {
|
||||
this.$root.publicGroupList.forEach(group => {
|
||||
if (group) {
|
||||
// Try to read saved sort settings from localStorage
|
||||
const savedSettings = this.getSavedSortSettings(group);
|
||||
|
||||
if (savedSettings) {
|
||||
// Apply saved settings
|
||||
group.sortKey = savedSettings.key;
|
||||
group.sortDirection = savedSettings.direction;
|
||||
} else {
|
||||
// Use default settings
|
||||
if (group.sortKey === undefined) {
|
||||
group.sortKey = 'status';
|
||||
}
|
||||
if (group.sortDirection === undefined) {
|
||||
group.sortDirection = 'desc';
|
||||
}
|
||||
}
|
||||
// Apply initial sorting
|
||||
this.applySort(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Watch for new groups being added and initialize their sort state
|
||||
if (this.$root) {
|
||||
this.$root.$watch('publicGroupList', (newGroups) => {
|
||||
if (newGroups) {
|
||||
newGroups.forEach(group => {
|
||||
if (group && group.sortKey === undefined) {
|
||||
const savedSettings = this.getSavedSortSettings(group);
|
||||
|
||||
if (savedSettings) {
|
||||
group.sortKey = savedSettings.key;
|
||||
group.sortDirection = savedSettings.direction;
|
||||
} else {
|
||||
group.sortKey = 'status';
|
||||
group.sortDirection = 'desc';
|
||||
}
|
||||
|
||||
this.applySort(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, { deep: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get saved sort settings from localStorage
|
||||
* @param {object} group object
|
||||
* @returns {object|null} saved sorting settings
|
||||
*/
|
||||
getSavedSortSettings(group) {
|
||||
try {
|
||||
const groupId = this.getGroupIdentifier(group);
|
||||
const slug = this.$root.statusPage ? this.$root.statusPage.slug : 'default';
|
||||
const storageKey = `uptime-kuma-sort-${slug}-${groupId}`;
|
||||
|
||||
const savedSettings = localStorage.getItem(storageKey);
|
||||
if (savedSettings) {
|
||||
return JSON.parse(savedSettings);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Cannot read sort settings', error);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get sort key for a group
|
||||
* @param {object} group object
|
||||
* @returns {string} sort key
|
||||
*/
|
||||
getSortKey(group) {
|
||||
return group.sortKey || 'status';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get sort direction symbol
|
||||
* @param {object} group object
|
||||
* @returns {string} sort direction symbol
|
||||
*/
|
||||
getSortDirectionSymbol(group) {
|
||||
return (group.sortDirection === 'asc') ? '↑' : '↓';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set group sort key and direction, then apply sorting
|
||||
* @param {object} group object
|
||||
* @param {string} sort key ('status', 'name', 'uptime', 'cert')
|
||||
*/
|
||||
setSort(group, key) {
|
||||
if (group.sortKey === key) {
|
||||
group.sortDirection = group.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
group.sortKey = key;
|
||||
group.sortDirection = (key === 'status') ? 'desc' : 'asc';
|
||||
}
|
||||
try {
|
||||
const groupId = this.getGroupIdentifier(group);
|
||||
const slug = this.$root.statusPage ? this.$root.statusPage.slug : 'default';
|
||||
const storageKey = `uptime-kuma-sort-${slug}-${groupId}`;
|
||||
|
||||
const sortSettings = {
|
||||
key: group.sortKey,
|
||||
direction: group.sortDirection
|
||||
};
|
||||
localStorage.setItem(storageKey, JSON.stringify(sortSettings));
|
||||
} catch (error) {
|
||||
console.error('Cannot save sort settings', error);
|
||||
}
|
||||
|
||||
this.applySort(group);
|
||||
|
||||
this.updateURLSortParams();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply sorting logic directly to the group's monitorList (in-place)
|
||||
* @param {object} group object containing monitorList
|
||||
*/
|
||||
applySort(group) {
|
||||
if (!group || !group.monitorList || !Array.isArray(group.monitorList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortKey = group.sortKey || 'status';
|
||||
const sortDirection = group.sortDirection || 'desc';
|
||||
|
||||
group.monitorList.sort((a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
|
||||
let comparison = 0;
|
||||
let valueA, valueB;
|
||||
|
||||
if (sortKey === 'status') {
|
||||
// Sort by status
|
||||
const getStatusPriority = (monitor) => {
|
||||
if (!monitor || !monitor.id) return 4;
|
||||
|
||||
const hbList = this.$root.heartbeatList || {};
|
||||
const hbArr = hbList[monitor.id];
|
||||
if (hbArr && hbArr.length > 0) {
|
||||
const lastStatus = hbArr.at(-1).status;
|
||||
if (lastStatus === 0) return 0; // Down
|
||||
if (lastStatus === 1) return 1; // Up
|
||||
if (lastStatus === 2) return 2; // Pending
|
||||
if (lastStatus === 3) return 3; // Maintenance
|
||||
}
|
||||
return 4; // Unknown/No data
|
||||
};
|
||||
valueA = getStatusPriority(a);
|
||||
valueB = getStatusPriority(b);
|
||||
} else if (sortKey === 'name') {
|
||||
// Sort alphabetically by name
|
||||
valueA = a.name ? a.name.toLowerCase() : '';
|
||||
valueB = b.name ? b.name.toLowerCase() : '';
|
||||
} else if (sortKey === 'uptime') {
|
||||
// Sort by uptime
|
||||
const uptimeList = this.$root.uptimeList || {};
|
||||
const uptimeA = a.id ? parseFloat(uptimeList[`${a.id}_24`]) || 0 : 0;
|
||||
const uptimeB = b.id ? parseFloat(uptimeList[`${b.id}_24`]) || 0 : 0;
|
||||
valueA = uptimeA;
|
||||
valueB = uptimeB;
|
||||
} else if (sortKey === 'cert') {
|
||||
// Sort by certificate expiry time
|
||||
valueA = a.validCert && a.certExpiryDaysRemaining ? a.certExpiryDaysRemaining : -1;
|
||||
valueB = b.validCert && b.certExpiryDaysRemaining ? b.certExpiryDaysRemaining : -1;
|
||||
}
|
||||
|
||||
if (valueA < valueB) {
|
||||
comparison = -1;
|
||||
} else if (valueA > valueB) {
|
||||
comparison = 1;
|
||||
}
|
||||
|
||||
// Special handling for status sorting
|
||||
if (sortKey === 'status') {
|
||||
return sortDirection === 'desc' ? (comparison * -1) : comparison;
|
||||
} else {
|
||||
return sortDirection === 'asc' ? comparison : (comparison * -1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the specified group
|
||||
* @param {number} index Index of group to remove
|
||||
@ -193,6 +483,89 @@ export default {
|
||||
}
|
||||
return "#DC2626";
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle browser back/forward button events
|
||||
*/
|
||||
handlePopState() {
|
||||
this.loadSortSettingsFromURL();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load sort settings from URL
|
||||
*/
|
||||
loadSortSettingsFromURL() {
|
||||
if (!this.$root.publicGroupList) return;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Iterate through all groups, look for sort parameters in URL
|
||||
this.$root.publicGroupList.forEach(group => {
|
||||
if (!group) return;
|
||||
|
||||
const groupId = this.getGroupIdentifier(group);
|
||||
const sortParam = urlParams.get(`sort_${groupId}`);
|
||||
|
||||
if (sortParam) {
|
||||
const [key, direction] = sortParam.split('_');
|
||||
if (key && ['status', 'name', 'uptime', 'cert'].includes(key) &&
|
||||
direction && ['asc', 'desc'].includes(direction)) {
|
||||
group.sortKey = key;
|
||||
group.sortDirection = direction;
|
||||
this.applySort(group);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update sort parameters in URL
|
||||
*/
|
||||
updateURLSortParams() {
|
||||
if (!this.$root.publicGroupList) return;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// First clear all sort_ parameters
|
||||
const paramsToRemove = [];
|
||||
for (const [key] of urlParams.entries()) {
|
||||
if (key.startsWith('sort_')) {
|
||||
paramsToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
paramsToRemove.forEach(key => {
|
||||
urlParams.delete(key);
|
||||
});
|
||||
|
||||
// Add current sort parameters
|
||||
this.$root.publicGroupList.forEach(group => {
|
||||
if (!group || !group.sortKey) return;
|
||||
|
||||
const groupId = this.getGroupIdentifier(group);
|
||||
urlParams.set(`sort_${groupId}`, `${group.sortKey}_${group.sortDirection}`);
|
||||
});
|
||||
|
||||
// Update URL without refreshing the page
|
||||
const newUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`;
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get unique identifier for a group
|
||||
* @param {object} group object
|
||||
* @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;
|
||||
}
|
||||
// Fallback to ID or index
|
||||
return group.id ? `group${group.id}` : `group${this.$root.publicGroupList.indexOf(group)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -254,16 +627,78 @@ export default {
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sort-button {
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.7rem;
|
||||
}
|
||||
|
||||
.sort-menu {
|
||||
min-width: auto;
|
||||
width: auto;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
.sort-item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sort-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sort-direction-indicator {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
padding: 13px 0 10px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.sort-dropdown {
|
||||
margin-left: 0;
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bg-maintenance {
|
||||
|
Loading…
x
Reference in New Issue
Block a user