Added support for ordering stats

This commit is contained in:
Fabio Manganiello 2025-04-08 02:06:09 +02:00
parent 46e94c4a31
commit d9e891a821
Signed by: blacklight
GPG key ID: D90FBA7F76362774
3 changed files with 101 additions and 15 deletions
frontend/src/views
src

View file

@ -20,12 +20,24 @@
<table class="table" v-if="stats.length"> <table class="table" v-if="stats.length">
<thead> <thead>
<tr> <tr>
<th class="key" v-for="attr in Object.keys(stats[0].key)" :key="attr"> <th class="key"
{{ displayName(attr) }} @click="onColumnHeadClick(attr)"
v-for="attr in Object.keys(stats[0].key)"
:key="attr">
<font-awesome-icon v-if="orderBy === attr"
:icon="['fas', order === 'asc' ? 'sort-up' : 'sort-down']" />
<font-awesome-icon icon="fas fa-sort" v-else />
&nbsp;{{ displayName(attr) }}
</th>
<th :class="column.className"
@click="onColumnHeadClick(columnName)"
v-for="column, columnName in columns"
:key="columnName">
<font-awesome-icon v-if="orderBy === columnName"
:icon="['fas', order === 'asc' ? 'sort-up' : 'sort-down']" />
<font-awesome-icon icon="fas fa-sort" v-else />
&nbsp;{{ column.displayName }}
</th> </th>
<th class="count"># of records</th>
<th class="date">First record</th>
<th class="date">Last record</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -104,6 +116,20 @@ export default {
data() { data() {
return { return {
columns: {
count: {
displayName: '# of records',
className: 'count',
},
startDate: {
displayName: 'First record',
className: 'date',
},
endDate: {
displayName: 'Last record',
className: 'date',
},
},
loading: false, loading: false,
metrics: { metrics: {
country: true, country: true,
@ -112,6 +138,8 @@ export default {
postalCode: false, postalCode: false,
description: false, description: false,
}, },
order: 'desc',
orderBy: 'count',
showSelectForm: false, showSelectForm: false,
stats: [] as LocationStats[], stats: [] as LocationStats[],
} }
@ -122,11 +150,34 @@ export default {
return new StatsRequest({ return new StatsRequest({
// @ts-ignore // @ts-ignore
userId: this.$root.user.id, userId: this.$root.user.id,
order: this.order,
orderBy: this.orderBy,
groupBy: Object.entries(this.metrics) groupBy: Object.entries(this.metrics)
.filter(([_, enabled]) => enabled) .filter(([_, enabled]) => enabled)
.map(([metric]) => metric), .map(([metric]) => metric),
}) })
}, },
urlQuery() {
return Object.entries(this.query)
.map(([key, value]) => {
if (key === 'userId') {
return ''
}
if (key === 'groupBy') {
return `${key}=${(value as string[]).sort().join(',')}`;
}
if (key === 'order') {
value = (value as string)?.toLowerCase();
}
return `${key}=${encodeURIComponent(value as any)}`
})
.filter((param: string) => param.length)
.join('&');
},
}, },
methods: { methods: {
@ -149,16 +200,14 @@ export default {
return `/#${key}&order=${opts.ascending ? 'asc' : 'desc'}` return `/#${key}&order=${opts.ascending ? 'asc' : 'desc'}`
}, },
setURLQuery() { async setURLQuery() {
const enabledMetrics = Object.entries(this.metrics)
.filter(([_, enabled]) => enabled)
.map(([metric]) => metric);
window.history.replaceState( window.history.replaceState(
window.history.state, window.history.state,
'', '',
`${window.location.pathname}#groupBy=${enabledMetrics.sort().join(',')}`, `${window.location.pathname}#${this.urlQuery}`,
); );
await this.refresh();
}, },
setState() { setState() {
@ -183,6 +232,9 @@ export default {
} }
} }
this.orderBy = params.get('orderBy') || 'count';
this.order = params.get('order') || 'desc';
this.metrics = metrics as { this.metrics = metrics as {
country: boolean, country: boolean,
locality: boolean, locality: boolean,
@ -208,6 +260,15 @@ export default {
this.closeForm(); this.closeForm();
}, },
onColumnHeadClick(attr: string) {
if (this.orderBy === attr) {
this.order = this.order === 'asc' ? 'desc' : 'asc';
} else {
this.orderBy = attr;
this.order = attr === 'count' ? 'desc' : 'asc';
}
},
displayValue(key: string, value: any) { displayValue(key: string, value: any) {
if (key === 'country') { if (key === 'country') {
return this.displayCountry(value); return this.displayCountry(value);
@ -242,16 +303,26 @@ export default {
query: { query: {
handler() { handler() {
this.setURLQuery(); this.setURLQuery();
this.refresh();
}, },
deep: true, deep: true,
}, },
orderBy: {
handler() {
this.setURLQuery();
},
},
order: {
handler() {
this.setURLQuery();
},
},
}, },
async created() { async created() {
this.setState(); this.setState();
this.setURLQuery(); await this.setURLQuery();
await this.refresh();
}, },
} }
</script> </script>
@ -303,6 +374,18 @@ export default {
} }
table { table {
thead {
th {
margin: 0 auto;
cursor: pointer;
&:hover {
background-color: var(--color-accent);
color: var(--color-background);
}
}
}
tbody { tbody {
tr { tr {
td { td {

View file

@ -24,7 +24,7 @@ class Stats {
.map((d) => d.dataValues.id) .map((d) => d.dataValues.id)
}, },
group: groupBy, group: groupBy,
order: [[Sequelize.fn('COUNT', Sequelize.col($db.locationTableColumns.id)), req.order]], order: [[req.orderBy, req.order]],
}) })
).map(({dataValues: data}: any) => ).map(({dataValues: data}: any) =>
new LocationStats({ new LocationStats({

View file

@ -4,11 +4,13 @@ type GroupBy = 'device' | 'country' | 'locality' | 'postalCode' | 'description';
class StatsRequest { class StatsRequest {
userId: number; userId: number;
groupBy: GroupBy[]; groupBy: GroupBy[];
orderBy: string = 'count';
order: Order = 'DESC'; order: Order = 'DESC';
constructor(req: { constructor(req: {
userId: number; userId: number;
groupBy: string[] | string; groupBy: string[] | string;
orderBy?: string;
order?: string; order?: string;
}) { }) {
this.userId = req.userId; this.userId = req.userId;
@ -18,6 +20,7 @@ class StatsRequest {
req.groupBy req.groupBy
) as GroupBy[]; ) as GroupBy[];
this.orderBy = req.orderBy || this.orderBy;
this.order = (req.order || this.order).toUpperCase() as Order; this.order = (req.order || this.order).toUpperCase() as Order;
} }
} }