Compare commits

...

21 commits

Author SHA1 Message Date
Suven-p
95889b2116
Merge 7be040472a into 6899603eb7 2024-11-09 15:30:21 +00:00
Suven-p
7be040472a Bugfix: Preserve query params when navigating to same path 2024-11-01 16:39:31 +05:45
Suven-p
49c20b1840 Refactor: Remove checks for null 2024-11-01 16:38:47 +05:45
Suven-p
1a70dac690 Fix filters active 2024-11-01 16:25:24 +05:45
Suven-p
6618ba68e5
Merge branch 'master' into feat-3672-use_query_params_for_filter 2024-10-30 06:02:39 +05:45
Suven-p
36a6f19f3e
Merge branch 'master' into feat-3672-use_query_params_for_filter 2024-10-28 20:40:12 +05:45
Suven-p
d552975d6c Display toast on invalid query params 2024-10-28 07:21:28 +05:45
Suven-p
baf0a1bdb9 Refactor 2024-10-28 06:05:35 +05:45
Suven-p
dfb1b97780 Use default value to remove unused filters from query params 2024-10-28 06:01:35 +05:45
Suven-p
177363ac60 Revert "Delete query params if empty"
This reverts commit 2e18a19a9e.
2024-10-28 05:50:50 +05:45
Suven-p
1b379690dd
Merge branch 'master' into feat-3672-use_query_params_for_filter 2024-10-27 20:04:08 +05:45
Suven-p
db1a980e23 Modify only required parts of url 2024-10-27 20:03:43 +05:45
Suven-p
d9c626d4cc Fix linting errors 2024-10-27 19:59:16 +05:45
Suven-p
2e18a19a9e Delete query params if empty 2024-10-27 19:55:21 +05:45
Suven-p
b68cdf70d6 Preserve query params on route change 2024-10-27 19:48:01 +05:45
Suven-p
c6a9a78175 Merge branch 'feat-3672-use_query_params_for_filter' of https://github.com/Suven-p/uptime-kuma into feat-3672-use_query_params_for_filter 2024-10-27 08:47:41 +05:45
Suven-p
958fc99ea9 Add type for updateFilter param 2024-10-27 08:47:04 +05:45
Suven-p
f549eaec20
Add type for filterState
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-27 08:45:22 +05:45
Suven-p
f8ef326743
Merge branch 'master' into feat-3672-use_query_params_for_filter 2024-10-27 07:52:54 +05:45
Suven-p
9c50d709f1 Clarify comment for conditional rendering of Tag 2024-10-26 11:34:27 +05:45
Suven-p
6832c791c4 Convert filters and search text into query params 2024-10-26 11:09:04 +05:45
4 changed files with 138 additions and 54 deletions

View file

@ -89,17 +89,11 @@ export default {
}, },
data() { data() {
return { return {
searchText: "",
selectMode: false, selectMode: false,
selectAll: false, selectAll: false,
disableSelectAllWatcher: false, disableSelectAllWatcher: false,
selectedMonitors: {}, selectedMonitors: {},
windowTop: 0, windowTop: 0,
filterState: {
status: null,
active: null,
tags: null,
}
}; };
}, },
computed: { computed: {
@ -164,12 +158,66 @@ export default {
return Object.keys(this.selectedMonitors).length; return Object.keys(this.selectedMonitors).length;
}, },
/**
* Returns applied filters based on query params.
* @returns {{ status: number[], active: any[], tags: number[] }} The current filter state.
*/
filterState() {
// Since query params are always strings, convert them to the correct type
let status = this.$route.query["status"] || [];
if (!Array.isArray(status)) {
status = [ status ];
}
status = status.map(Number);
// Casting to boolean does not work here as Boolean("false") === true
let active = this.$route.query["active"] || [];
if (!Array.isArray(active)) {
active = [ active ];
}
active = active.map(val => {
if (val === "true") {
return true;
}
if (val === "false") {
return false;
}
return val;
});
let tags = this.$route.query["tags"] || [];
if (!Array.isArray(tags)) {
tags = [ tags ];
}
tags = tags.map(Number);
return {
status,
active,
tags,
};
},
searchText: {
get() {
return this.$route.query.searchText || "";
},
set(value) {
let newQuery = { ...this.$route.query };
if (value === "") {
delete newQuery.searchText;
} else {
newQuery.searchText = value;
}
this.$router.replace({
query: newQuery,
});
}
},
/** /**
* Determines if any filters are active. * Determines if any filters are active.
* @returns {boolean} True if any filter is active, false otherwise. * @returns {boolean} True if any filter is active, false otherwise.
*/ */
filtersActive() { filtersActive() {
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== ""; return this.filterState.status.length > 0 || this.filterState.active.length > 0 || this.filterState.tags.length > 0 || this.searchText !== "";
} }
}, },
watch: { watch: {
@ -239,11 +287,16 @@ export default {
}, },
/** /**
* Update the MonitorList Filter * Update the MonitorList Filter
* @param {object} newFilter Object with new filter * @param {{ status: number[], active: any[], tags: number[] }} newFilter Object with new filter
* @returns {void} * @returns {void}
*/ */
updateFilter(newFilter) { updateFilter(newFilter) {
this.filterState = newFilter; this.$router.replace({
query: {
...this.$route.query,
...newFilter,
},
});
}, },
/** /**
* Deselect a monitor * Deselect a monitor
@ -333,7 +386,7 @@ export default {
// filter by status // filter by status
let statusMatch = true; let statusMatch = true;
if (this.filterState.status != null && this.filterState.status.length > 0) { if (this.filterState.status.length > 0) {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) { if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status; monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
} }
@ -342,13 +395,13 @@ export default {
// filter by active // filter by active
let activeMatch = true; let activeMatch = true;
if (this.filterState.active != null && this.filterState.active.length > 0) { if (this.filterState.active.length > 0) {
activeMatch = this.filterState.active.includes(monitor.active); activeMatch = this.filterState.active.includes(monitor.active);
} }
// filter by tags // filter by tags
let tagsMatch = true; let tagsMatch = true;
if (this.filterState.tags != null && this.filterState.tags.length > 0) { if (this.filterState.tags.length > 0) {
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags .filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
.length > 0; .length > 0;

View file

@ -14,10 +14,10 @@
<font-awesome-icon v-if="numFiltersActive > 0" icon="times" /> <font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
</button> </button>
<MonitorListFilterDropdown <MonitorListFilterDropdown
:filterActive="filterState.status?.length > 0" :filterActive="filterState.status.length > 0"
> >
<template #status> <template #status>
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" /> <Status v-if="filterState.status.length === 1" :status="filterState.status[0]" />
<span v-else> <span v-else>
{{ $t('Status') }} {{ $t('Status') }}
</span> </span>
@ -29,7 +29,7 @@
<Status :status="1" /> <Status :status="1" />
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.up }} {{ $root.stats.up }}
<span v-if="filterState.status?.includes(1)" class="px-1 filter-active"> <span v-if="filterState.status.includes(1)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -42,7 +42,7 @@
<Status :status="0" /> <Status :status="0" />
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.down }} {{ $root.stats.down }}
<span v-if="filterState.status?.includes(0)" class="px-1 filter-active"> <span v-if="filterState.status.includes(0)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -55,7 +55,7 @@
<Status :status="2" /> <Status :status="2" />
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.pending }} {{ $root.stats.pending }}
<span v-if="filterState.status?.includes(2)" class="px-1 filter-active"> <span v-if="filterState.status.includes(2)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -68,7 +68,7 @@
<Status :status="3" /> <Status :status="3" />
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.maintenance }} {{ $root.stats.maintenance }}
<span v-if="filterState.status?.includes(3)" class="px-1 filter-active"> <span v-if="filterState.status.includes(3)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -77,11 +77,12 @@
</li> </li>
</template> </template>
</MonitorListFilterDropdown> </MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.active?.length > 0"> <MonitorListFilterDropdown :filterActive="filterState.active.length > 0">
<template #status> <template #status>
<span v-if="filterState.active?.length === 1"> <span v-if="filterState.active.length === 1">
<span v-if="filterState.active[0]">{{ $t("Running") }}</span> <span v-if="filterState.active[0] === true">{{ $t("Running") }}</span>
<span v-else>{{ $t("filterActivePaused") }}</span> <span v-else-if="filterState.active[0] === false">{{ $t("filterActivePaused") }}</span>
<span v-else>{{ $t("Unknown") }}</span>
</span> </span>
<span v-else> <span v-else>
{{ $t("filterActive") }} {{ $t("filterActive") }}
@ -94,7 +95,7 @@
<span>{{ $t("Running") }}</span> <span>{{ $t("Running") }}</span>
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.active }} {{ $root.stats.active }}
<span v-if="filterState.active?.includes(true)" class="px-1 filter-active"> <span v-if="filterState.active.includes(true)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -107,7 +108,7 @@
<span>{{ $t("filterActivePaused") }}</span> <span>{{ $t("filterActivePaused") }}</span>
<span class="ps-3"> <span class="ps-3">
{{ $root.stats.pause }} {{ $root.stats.pause }}
<span v-if="filterState.active?.includes(false)" class="px-1 filter-active"> <span v-if="filterState.active.includes(false)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -116,10 +117,11 @@
</li> </li>
</template> </template>
</MonitorListFilterDropdown> </MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0"> <MonitorListFilterDropdown :filterActive="filterState.tags.length > 0">
<template #status> <template #status>
<!-- Prevent rendering Tag component if tagsList has not been fetched or filterState contains invalid tag -->
<Tag <Tag
v-if="filterState.tags?.length === 1" v-if="filterState.tags.length === 1 && tagsList.find(tag => tag.id === filterState.tags[0])"
:item="tagsList.find(tag => tag.id === filterState.tags[0])" :item="tagsList.find(tag => tag.id === filterState.tags[0])"
:size="'sm'" :size="'sm'"
/> />
@ -134,7 +136,7 @@
<span><Tag :item="tag" :size="'sm'" /></span> <span><Tag :item="tag" :size="'sm'" /></span>
<span class="ps-3"> <span class="ps-3">
{{ getTaggedMonitorCount(tag) }} {{ getTaggedMonitorCount(tag) }}
<span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active"> <span v-if="filterState.tags.includes(tag.id)" class="px-1 filter-active">
<font-awesome-icon icon="check" /> <font-awesome-icon icon="check" />
</span> </span>
</span> </span>
@ -179,16 +181,37 @@ export default {
let num = 0; let num = 0;
Object.values(this.filterState).forEach(item => { Object.values(this.filterState).forEach(item => {
if (item != null && item.length > 0) { if (item.length > 0) {
num += 1; num += 1;
} }
}); });
return num; return num;
},
/**
* Returns an array of invalid filters assuming tagsList has been fetched
* @returns {Array} Array of invalid filters
*/
invalidFilters() {
const filters = [];
if (!this.filterState.status.every((val) => val >= 0 && val <= 3)) {
filters.push(this.$t("Status"));
}
if (!this.filterState.active.every((val) => val === true || val === false)) {
filters.push(this.$t("Active"));
}
if (!this.filterState.tags.every((val) => this.tagsList.find(tag => tag.id === val))) {
filters.push(this.$t("Tags"));
}
return filters;
} }
}, },
mounted() { mounted() {
this.getExistingTags(); this.getExistingTags(() => {
if (this.invalidFilters.length > 0) {
this.$root.toastError(this.$t("InvalidFilters", [ this.invalidFilters.join(", ") ]));
}
});
}, },
methods: { methods: {
toggleStatusFilter(status) { toggleStatusFilter(status) {
@ -196,14 +219,10 @@ export default {
...this.filterState ...this.filterState
}; };
if (newFilter.status == null) { if (newFilter.status.includes(status)) {
newFilter.status = [ status ]; newFilter.status = newFilter.status.filter(item => item !== status);
} else { } else {
if (newFilter.status.includes(status)) { newFilter.status.push(status);
newFilter.status = newFilter.status.filter(item => item !== status);
} else {
newFilter.status.push(status);
}
} }
this.$emit("updateFilter", newFilter); this.$emit("updateFilter", newFilter);
}, },
@ -212,14 +231,10 @@ export default {
...this.filterState ...this.filterState
}; };
if (newFilter.active == null) { if (newFilter.active.includes(active)) {
newFilter.active = [ active ]; newFilter.active = newFilter.active.filter(item => item !== active);
} else { } else {
if (newFilter.active.includes(active)) { newFilter.active.push(active);
newFilter.active = newFilter.active.filter(item => item !== active);
} else {
newFilter.active.push(active);
}
} }
this.$emit("updateFilter", newFilter); this.$emit("updateFilter", newFilter);
}, },
@ -228,27 +243,24 @@ export default {
...this.filterState ...this.filterState
}; };
if (newFilter.tags == null) { if (newFilter.tags.includes(tag.id)) {
newFilter.tags = [ tag.id ]; newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
} else { } else {
if (newFilter.tags.includes(tag.id)) { newFilter.tags.push(tag.id);
newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
} else {
newFilter.tags.push(tag.id);
}
} }
this.$emit("updateFilter", newFilter); this.$emit("updateFilter", newFilter);
}, },
clearFilters() { clearFilters() {
this.$emit("updateFilter", { this.$emit("updateFilter", {
status: null, status: [],
}); });
}, },
getExistingTags() { getExistingTags(callback) {
this.$root.getSocket().emit("getTags", (res) => { this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) { if (res.ok) {
this.tagsList = res.tags; this.tagsList = res.tags;
} }
callback();
}); });
}, },
getTaggedMonitorCount(tag) { getTaggedMonitorCount(tag) {

View file

@ -1051,5 +1051,6 @@
"RabbitMQ Password": "RabbitMQ Password", "RabbitMQ Password": "RabbitMQ Password",
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key", "SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas" "Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
"InvalidFilters": "The following filters are invalid: {0}"
} }

View file

@ -192,8 +192,26 @@ const routes = [
}, },
]; ];
export const router = createRouter({ const router = createRouter({
linkActiveClass: "active", linkActiveClass: "active",
history: createWebHistory(), history: createWebHistory(),
routes, routes,
}); });
router.beforeEach((to, from) => {
// If the path is same, either the query or has has changed so avoid changing query params
// Without this check, modifying any query params will be blocked
// Check if redirectedFrom is defined to check if this function has already been run
// Without this check, the router will be stuck in an infinite loop
if (to.fullPath !== from.fullPath && !to.redirectedFrom) {
return {
...to,
query: {
...to.query,
...from.query,
},
};
}
});
export { router };