Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Support URL parameters to change Dashboard filters #4496

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
80 changes: 63 additions & 17 deletions src/components/MonitorList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</div>
</div>
<div class="header-filter">
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
<MonitorListFilter @update-filter="updateFilter" />
</div>

<!-- Selection Controls -->
Expand Down Expand Up @@ -95,11 +95,6 @@ export default {
disableSelectAllWatcher: false,
selectedMonitors: {},
windowTop: 0,
filterState: {
status: null,
active: null,
tags: null,
}
};
},
computed: {
Expand Down Expand Up @@ -169,7 +164,7 @@ export default {
* @returns {boolean} True if any filter is active, false otherwise.
*/
filtersActive() {
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
return this.$router.currentRoute.value.query?.status != null || this.$router.currentRoute.value.query?.active != null || this.$router.currentRoute.value.query?.tags != null || this.searchText !== "";
}
},
watch: {
Expand Down Expand Up @@ -204,8 +199,46 @@ export default {
}
},
},
mounted() {
async mounted() {
NihadBadalov marked this conversation as resolved.
Show resolved Hide resolved
window.addEventListener("scroll", this.onScroll);

const queryParams = this.$router.currentRoute.value.query;
const statusParams = queryParams?.["status"];
const activeParams = queryParams?.["active"];
const tagParams = queryParams?.["tags"];

const tags = await (() => {
return new Promise((resolve) => {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
resolve(res.tags);
}
});
});
})();

const fetchedTagIDs = tagParams
? tagParams
.split(",")
.map(identifier => {
const tagID = parseInt(identifier, 10);
if (isNaN(tagID)) {
return;
}
return tags.find(t => t.tag_id === tagID)?.id ?? 1;
})
.filter(tagID => tagID !== 0)
: undefined;

this.updateFilter({
status: statusParams ? statusParams.split(",").map(
status => status.trim()
) : queryParams?.["status"],
active: activeParams ? activeParams.split(",").map(
active => active.trim()
) : queryParams?.["active"],
tags: tagParams ? fetchedTagIDs : queryParams?.["tags"],
});
},
beforeUnmount() {
window.removeEventListener("scroll", this.onScroll);
Expand Down Expand Up @@ -243,7 +276,20 @@ export default {
* @returns {void}
*/
updateFilter(newFilter) {
this.filterState = newFilter;
const newQuery = { ...this.$router.currentRoute.value.query };

for (const [ key, value ] of Object.entries(newFilter)) {
if (!value
|| (value instanceof Array && value.length === 0)) {
delete newQuery[key];
continue;
}

newQuery[key] = value instanceof Array
? value.length > 0 ? value.join(",") : null
: value;
}
this.$router.push({ query: newQuery });
},
/**
* Deselect a monitor
Expand Down Expand Up @@ -333,25 +379,25 @@ export default {

// filter by status
let statusMatch = true;
if (this.filterState.status != null && this.filterState.status.length > 0) {
if (this.$router.currentRoute.value.query?.status != null && this.$router.currentRoute.value.query?.status.length > 0) {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
statusMatch = this.filterState.status.includes(monitor.status);
statusMatch = this.$router.currentRoute.value.query?.status.includes(monitor.status);
}

// filter by active
let activeMatch = true;
if (this.filterState.active != null && this.filterState.active.length > 0) {
activeMatch = this.filterState.active.includes(monitor.active);
if (this.$router.currentRoute.value.query?.active != null && this.$router.currentRoute.value.query?.active.length > 0) {
activeMatch = this.$router.currentRoute.value.query?.active.includes(monitor.active);
}

// filter by tags
let tagsMatch = true;
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
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
.length > 0;
const tagsInURL = this.$router.currentRoute.value.query?.tags?.split(",") || [];
if (this.$router.currentRoute.value.query?.tags != null && this.$router.currentRoute.value.query?.tags.length > 0) {
const monitorTagIds = monitor.tags.map(tag => tag.tag_id);
tagsMatch = tagsInURL.map(Number).some(tagId => monitorTagIds.includes(tagId));
}

return searchTextMatch && statusMatch && activeMatch && tagsMatch;
Expand Down
128 changes: 71 additions & 57 deletions src/components/MonitorListFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
<font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
</button>
<MonitorListFilterDropdown
:filterActive="filterState.status?.length > 0"
:filterActive="$router.currentRoute.value.query?.status?.length > 0"
>
<template #status>
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
<Status v-if="$router.currentRoute.value.query?.status?.length === 1" :status="$router.currentRoute.value.query?.status[0]" />
<span v-else>
{{ $t('Status') }}
</span>
Expand All @@ -29,7 +29,10 @@
<Status :status="1" />
<span class="ps-3">
{{ $root.stats.up }}
<span v-if="filterState.status?.includes(1)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.status?.includes('1')"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -42,7 +45,10 @@
<Status :status="0" />
<span class="ps-3">
{{ $root.stats.down }}
<span v-if="filterState.status?.includes(0)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.status?.includes('0')"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -55,7 +61,10 @@
<Status :status="2" />
<span class="ps-3">
{{ $root.stats.pending }}
<span v-if="filterState.status?.includes(2)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.status?.includes('2')"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -68,7 +77,10 @@
<Status :status="3" />
<span class="ps-3">
{{ $root.stats.maintenance }}
<span v-if="filterState.status?.includes(3)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.status?.includes('3')"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -77,10 +89,10 @@
</li>
</template>
</MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.active?.length > 0">
<MonitorListFilterDropdown :filterActive="$router.currentRoute.value.query?.active?.length > 0">
<template #status>
<span v-if="filterState.active?.length === 1">
<span v-if="filterState.active[0]">{{ $t("Running") }}</span>
<span v-if="$router.currentRoute.value.query?.active?.length === 1">
<span v-if="$router.currentRoute.value.query?.active[0]">{{ $t("Running") }}</span>
<span v-else>{{ $t("filterActivePaused") }}</span>
</span>
<span v-else>
Expand All @@ -94,7 +106,10 @@
<span>{{ $t("Running") }}</span>
<span class="ps-3">
{{ $root.stats.active }}
<span v-if="filterState.active?.includes(true)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.active?.includes(true)"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -107,7 +122,10 @@
<span>{{ $t("filterActivePaused") }}</span>
<span class="ps-3">
{{ $root.stats.pause }}
<span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.active?.includes(false)"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand All @@ -116,12 +134,11 @@
</li>
</template>
</MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0">
<MonitorListFilterDropdown :filterActive="$router.currentRoute.value.query?.tags?.length > 0">
<template #status>
<Tag
v-if="filterState.tags?.length === 1"
:item="tagsList.find(tag => tag.id === filterState.tags[0])"
:size="'sm'"
v-if="$router.currentRoute.value.query?.tags?.split?.(',')?.length === 1 && tagsList.find(tag => tag.id === +$router.currentRoute.value.query?.tags?.split?.(',')?.[0])"
:item="tagsList.find(tag => tag.id === +$router.currentRoute.value.query?.tags?.split?.(',')?.[0])" :size="'sm'"
/>
<span v-else>
{{ $t('Tags') }}
Expand All @@ -131,10 +148,15 @@
<li v-for="tag in tagsList" :key="tag.id">
<div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)">
<div class="d-flex align-items-center justify-content-between">
<span><Tag :item="tag" :size="'sm'" /></span>
<span>
<Tag :item="tag" :size="'sm'" />
</span>
<span class="ps-3">
{{ getTaggedMonitorCount(tag) }}
<span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active">
<span
v-if="$router.currentRoute.value.query?.tags?.split(',').includes(''+tag.id)"
class="px-1 filter-active"
>
<font-awesome-icon icon="check" />
</span>
</span>
Expand Down Expand Up @@ -162,12 +184,6 @@ export default {
Status,
Tag,
},
props: {
filterState: {
type: Object,
required: true,
}
},
emits: [ "updateFilter" ],
data() {
return {
Expand All @@ -176,72 +192,70 @@ export default {
},
computed: {
numFiltersActive() {
let num = 0;

Object.values(this.filterState).forEach(item => {
if (item != null && item.length > 0) {
num += 1;
}
});

return num;
return this.$router.currentRoute.value.query.status?.length > 0 ? 1 : 0 +
this.$router.currentRoute.value.query.active?.length > 0 ? 1 : 0 +
this.$router.currentRoute.value.query.tags?.length > 0 ? 1 : 0;
}
},
mounted() {
this.getExistingTags();
},
methods: {
getActiveFilters: function () {
const filters = Object.fromEntries(
NihadBadalov marked this conversation as resolved.
Show resolved Hide resolved
Array.from(Object.entries(this.$router.currentRoute.value.query ?? {}))
);

return {
status: filters["status"] ? filters["status"].split(",") : [],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you try to store the query as a comma separated string.
I think there is a simpler way, as vue-router seems to allow storing lists ⇒ should be able to retrieve it
https://stackoverflow.com/questions/50692081/vue-js-router-query-array

Is there a reason for this approach I am not seeing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know that existed. Very nice, will rewrite that part for tags.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CommanderStorm Do you want me to rewrite that for every filter? The current approach works well, is it not fine?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please consider using the tools provided via the vue router instead of inventing a new way of storing lists.
This should significantly clean up the code ^^

active: filters["active"] ? filters["active"].split(",") : [],
tags: filters["tags"] ? filters["tags"].split(",") : [],
};
},
toggleStatusFilter(status) {
let newFilter = {
...this.filterState
...this.getActiveFilters(),
};

if (newFilter.status == null) {
newFilter.status = [ status ];
if (newFilter.status.includes("" + status)) {
newFilter.status = newFilter.status.filter(item => item !== "" + status);
} else {
if (newFilter.status.includes(status)) {
newFilter.status = newFilter.status.filter(item => item !== status);
} else {
newFilter.status.push(status);
}
newFilter.status.push(status);
}

this.$emit("updateFilter", newFilter);
},
toggleActiveFilter(active) {
let newFilter = {
...this.filterState
...this.getActiveFilters(),
};

if (newFilter.active == null) {
newFilter.active = [ active ];
if (newFilter.active.includes("" + active)) {
newFilter.active = newFilter.active.filter(item => item !== "" + active);
} else {
if (newFilter.active.includes(active)) {
newFilter.active = newFilter.active.filter(item => item !== active);
} else {
newFilter.active.push(active);
}
newFilter.active.push(active);
}

this.$emit("updateFilter", newFilter);
},
toggleTagFilter(tag) {
let newFilter = {
...this.filterState
...this.getActiveFilters(),
};

if (newFilter.tags == null) {
newFilter.tags = [ tag.id ];
if (newFilter.tags.includes("" + tag.id)) {
newFilter.tags = newFilter.tags.filter(item => item !== "" + tag.id);
} else {
if (newFilter.tags.includes(tag.id)) {
newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
} else {
newFilter.tags.push(tag.id);
}
newFilter.tags.push(tag.id);
}

this.$emit("updateFilter", newFilter);
},
clearFilters() {
this.$emit("updateFilter", {
status: null,
status: undefined,
active: undefined,
tags: undefined,
});
},
getExistingTags() {
Expand Down