mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:38:39 +08:00

This commit updates the display of totals and table rows for reports in the admin interface. Currently we show abbreviated numbers for totals e.g. 2.1M which is not helpful when you need accurate data. We are also not adding locale-specific number separators so the row numbers are hard to read e.g. 246999 instead of 246,999. This commit fixes both issues to improve the UX of reports without having to export them. **Before (totals)**  **After (totals)**  **Before (rows)**  **After (rows)** 
187 lines
4.8 KiB
JavaScript
187 lines
4.8 KiB
JavaScript
import Component from "@ember/component";
|
|
import { action } from "@ember/object";
|
|
import { alias } from "@ember/object/computed";
|
|
import { classNameBindings, classNames } from "@ember-decorators/component";
|
|
import discourseComputed from "discourse/lib/decorators";
|
|
import { makeArray } from "discourse/lib/helpers";
|
|
|
|
const PAGES_LIMIT = 8;
|
|
|
|
@classNameBindings("sortable", "twoColumns")
|
|
@classNames("admin-report-table")
|
|
export default class AdminReportTable extends Component {
|
|
sortable = false;
|
|
sortDirection = 1;
|
|
|
|
@alias("options.perPage") perPage;
|
|
|
|
page = 0;
|
|
|
|
@discourseComputed("model.computedLabels.length")
|
|
twoColumns(labelsLength) {
|
|
return labelsLength === 2;
|
|
}
|
|
|
|
@discourseComputed(
|
|
"totalsForSample",
|
|
"options.total",
|
|
"model.dates_filtering"
|
|
)
|
|
showTotalForSample(totalsForSample, total, datesFiltering) {
|
|
// check if we have at least one cell which contains a value
|
|
const sum = totalsForSample
|
|
.map((t) => t.value)
|
|
.compact()
|
|
.reduce((s, v) => s + v, 0);
|
|
|
|
return sum >= 1 && total && datesFiltering;
|
|
}
|
|
|
|
@discourseComputed("model.total", "options.total", "twoColumns")
|
|
showTotal(reportTotal, total, twoColumns) {
|
|
return reportTotal && total && twoColumns;
|
|
}
|
|
|
|
@discourseComputed(
|
|
"model.{average,data}",
|
|
"totalsForSample.1.value",
|
|
"twoColumns"
|
|
)
|
|
showAverage(model, sampleTotalValue, hasTwoColumns) {
|
|
return (
|
|
model.average &&
|
|
model.data.length > 0 &&
|
|
sampleTotalValue &&
|
|
hasTwoColumns
|
|
);
|
|
}
|
|
|
|
@discourseComputed("totalsForSample.1.value", "model.data.length")
|
|
averageForSample(totals, count) {
|
|
const averageLabel = this.model.computedLabels.at(-1);
|
|
return averageLabel.compute({ y: (totals / count).toFixed(0) })
|
|
.formattedValue;
|
|
}
|
|
|
|
@discourseComputed("model.data.length")
|
|
showSortingUI(dataLength) {
|
|
return dataLength >= 5;
|
|
}
|
|
|
|
@discourseComputed("totalsForSampleRow", "model.computedLabels")
|
|
totalsForSample(row, labels) {
|
|
return labels.map((label) => {
|
|
const computedLabel = label.compute(row);
|
|
computedLabel.type = label.type;
|
|
computedLabel.property = label.mainProperty;
|
|
return computedLabel;
|
|
});
|
|
}
|
|
|
|
@discourseComputed("model.total", "model.computedLabels")
|
|
formattedTotal(total, labels) {
|
|
const totalLabel = labels.at(-1);
|
|
return totalLabel.compute({ y: total }).formattedValue;
|
|
}
|
|
|
|
@discourseComputed("model.data", "model.computedLabels")
|
|
totalsForSampleRow(rows, labels) {
|
|
if (!rows || !rows.length) {
|
|
return {};
|
|
}
|
|
|
|
let totalsRow = {};
|
|
|
|
labels.forEach((label) => {
|
|
const reducer = (sum, row) => {
|
|
const computedLabel = label.compute(row);
|
|
const value = computedLabel.value;
|
|
|
|
if (!["seconds", "number", "percent"].includes(label.type)) {
|
|
return;
|
|
} else {
|
|
return sum + Math.round(value || 0);
|
|
}
|
|
};
|
|
|
|
const total = rows.reduce(reducer, 0);
|
|
totalsRow[label.mainProperty] =
|
|
label.type === "percent" ? Math.round(total / rows.length) : total;
|
|
});
|
|
|
|
return totalsRow;
|
|
}
|
|
|
|
@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
|
|
sortedData(sortLabel, sortDirection, data) {
|
|
data = makeArray(data);
|
|
|
|
if (sortLabel) {
|
|
const compare = (label, direction) => {
|
|
return (a, b) => {
|
|
const aValue = label.compute(a, { useSortProperty: true }).value;
|
|
const bValue = label.compute(b, { useSortProperty: true }).value;
|
|
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
|
return result * direction;
|
|
};
|
|
};
|
|
|
|
return data.sort(compare(sortLabel, sortDirection));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
@discourseComputed("sortedData.[]", "perPage", "page")
|
|
paginatedData(data, perPage, page) {
|
|
if (perPage < data.length) {
|
|
const start = perPage * page;
|
|
return data.slice(start, start + perPage);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
@discourseComputed("model.data", "perPage", "page")
|
|
pages(data, perPage, page) {
|
|
if (!data || data.length <= perPage) {
|
|
return [];
|
|
}
|
|
|
|
const pagesIndexes = [];
|
|
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
|
|
pagesIndexes.push(i);
|
|
}
|
|
|
|
let pages = pagesIndexes.map((v) => {
|
|
return {
|
|
page: v + 1,
|
|
index: v,
|
|
class: v === page ? "is-current" : null,
|
|
};
|
|
});
|
|
|
|
if (pages.length > PAGES_LIMIT) {
|
|
const before = Math.max(0, page - PAGES_LIMIT / 2);
|
|
const after = Math.max(PAGES_LIMIT, page + PAGES_LIMIT / 2);
|
|
pages = pages.slice(before, after);
|
|
}
|
|
|
|
return pages;
|
|
}
|
|
|
|
@action
|
|
changePage(page) {
|
|
this.set("page", page);
|
|
}
|
|
|
|
@action
|
|
sortByLabel(label) {
|
|
if (this.sortLabel === label) {
|
|
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
|
|
} else {
|
|
this.set("sortLabel", label);
|
|
}
|
|
}
|
|
}
|