FEATURE: part 2 of dashboard improvements
- moderation tab - sorting/pagination - improved third party reports support - trending charts - better perf - many fixes - refactoring - new reports Co-Authored-By: Simon Cossar <scossar@users.noreply.github.com>
This commit is contained in:
parent
4e09206061
commit
1a78e12f4e
|
@ -0,0 +1,117 @@
|
|||
import { number } from "discourse/lib/formatter";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["admin-report-chart"],
|
||||
limit: 8,
|
||||
primaryColor: "rgb(0,136,204)",
|
||||
total: 0,
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._resetChart();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
const $chartCanvas = this.$(".chart-canvas");
|
||||
if (!$chartCanvas || !$chartCanvas.length) return;
|
||||
|
||||
const context = $chartCanvas[0].getContext("2d");
|
||||
const model = this.get("model");
|
||||
const chartData = Ember.makeArray(
|
||||
model.get("chartData") || model.get("data")
|
||||
);
|
||||
const prevChartData = Ember.makeArray(
|
||||
model.get("prevChartData") || model.get("prev_data")
|
||||
);
|
||||
|
||||
const labels = chartData.map(d => d.x);
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data: chartData.map(d => Math.round(parseFloat(d.y))),
|
||||
backgroundColor: prevChartData.length
|
||||
? "transparent"
|
||||
: "rgba(200,220,240,0.3)",
|
||||
borderColor: this.get("primaryColor")
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (prevChartData.length) {
|
||||
data.datasets.push({
|
||||
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
|
||||
borderColor: this.get("primaryColor"),
|
||||
borderDash: [5, 5],
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0
|
||||
});
|
||||
}
|
||||
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
this._resetChart();
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: tooltipItem =>
|
||||
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: { callback: label => number(label) }
|
||||
}
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
gridLines: { display: false },
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_resetChart() {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export default Ember.Component.extend({
|
||||
classNames: ["admin-report-inline-table"]
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "th",
|
||||
classNames: ["admin-report-table-header"],
|
||||
classNameBindings: ["label.property", "isCurrentSort"],
|
||||
attributeBindings: ["label.title:title"],
|
||||
|
||||
@computed("currentSortLabel.sort_property", "label.sort_property")
|
||||
isCurrentSort(currentSortField, labelSortField) {
|
||||
return currentSortField === labelSortField;
|
||||
},
|
||||
|
||||
@computed("currentSortDirection")
|
||||
sortIcon(currentSortDirection) {
|
||||
return currentSortDirection === 1 ? "caret-up" : "caret-down";
|
||||
}
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "tr",
|
||||
classNames: ["admin-report-table-row"],
|
||||
|
||||
@computed("data", "labels")
|
||||
cells(row, labels) {
|
||||
return labels.map(label => {
|
||||
return label.compute(row);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,147 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
|
||||
import { isNumeric } from "discourse/lib/utilities";
|
||||
|
||||
const PAGES_LIMIT = 8;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: ["sortable", "twoColumns"],
|
||||
classNames: ["admin-report-table"],
|
||||
sortable: false,
|
||||
sortDirection: 1,
|
||||
perPage: Ember.computed.alias("options.perPage"),
|
||||
page: 0,
|
||||
|
||||
didRender() {
|
||||
this._super(...arguments);
|
||||
|
||||
unregisterTooltip($(".text[data-tooltip]"));
|
||||
registerTooltip($(".text[data-tooltip]"));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
unregisterTooltip($(".text[data-tooltip]"));
|
||||
},
|
||||
|
||||
@computed("model.computedLabels.length")
|
||||
twoColumns(labelsLength) {
|
||||
return labelsLength === 2;
|
||||
},
|
||||
|
||||
@computed("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;
|
||||
},
|
||||
|
||||
@computed("model.total", "options.total", "twoColumns")
|
||||
showTotal(reportTotal, total, twoColumns) {
|
||||
return reportTotal && total && twoColumns;
|
||||
},
|
||||
|
||||
@computed("model.data.length")
|
||||
showSortingUI(dataLength) {
|
||||
return dataLength >= 5;
|
||||
},
|
||||
|
||||
@computed("totalsForSampleRow", "model.computedLabels")
|
||||
totalsForSample(row, labels) {
|
||||
return labels.map(label => label.compute(row));
|
||||
},
|
||||
|
||||
@computed("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 (computedLabel.type === "link" || (value && !isNumeric(value))) {
|
||||
return undefined;
|
||||
} else {
|
||||
return sum + value;
|
||||
}
|
||||
};
|
||||
|
||||
totalsRow[label.property] = rows.reduce(reducer, 0);
|
||||
});
|
||||
|
||||
return totalsRow;
|
||||
},
|
||||
|
||||
@computed("sortLabel", "sortDirection", "model.data.[]")
|
||||
sortedData(sortLabel, sortDirection, data) {
|
||||
data = Ember.makeArray(data);
|
||||
|
||||
if (sortLabel) {
|
||||
const compare = (label, direction) => {
|
||||
return (a, b) => {
|
||||
let aValue = label.compute(a).value;
|
||||
let bValue = label.compute(b).value;
|
||||
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
return result * direction;
|
||||
};
|
||||
};
|
||||
|
||||
return data.sort(compare(sortLabel, sortDirection));
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@computed("sortedData.[]", "perPage", "page")
|
||||
paginatedData(data, perPage, page) {
|
||||
if (perPage < data.length) {
|
||||
const start = perPage * page;
|
||||
return data.slice(start, start + perPage);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@computed("model.data", "perPage", "page")
|
||||
pages(data, perPage, page) {
|
||||
if (!data || data.length <= perPage) return [];
|
||||
|
||||
let pages = [...Array(Math.ceil(data.length / perPage)).keys()].map(v => {
|
||||
return {
|
||||
page: v + 1,
|
||||
index: v,
|
||||
class: v === page ? "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;
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePage(page) {
|
||||
this.set("page", page);
|
||||
},
|
||||
|
||||
sortByLabel(label) {
|
||||
if (this.get("sortLabel") === label) {
|
||||
this.set("sortDirection", this.get("sortDirection") === 1 ? -1 : 1);
|
||||
} else {
|
||||
this.set("sortLabel", label);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,366 @@
|
|||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Report from "admin/models/report";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
|
||||
|
||||
const TABLE_OPTIONS = {
|
||||
perPage: 8,
|
||||
total: true,
|
||||
limit: 20
|
||||
};
|
||||
|
||||
const CHART_OPTIONS = {};
|
||||
|
||||
function collapseWeekly(data, average) {
|
||||
let aggregate = [];
|
||||
let bucket, i;
|
||||
let offset = data.length % 7;
|
||||
for (i = offset; i < data.length; i++) {
|
||||
if (bucket && i % 7 === offset) {
|
||||
if (average) {
|
||||
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
||||
}
|
||||
aggregate.push(bucket);
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
bucket = bucket || { x: data[i].x, y: 0 };
|
||||
bucket.y += data[i].y;
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [
|
||||
"isEnabled",
|
||||
"isLoading",
|
||||
"dasherizedDataSourceName",
|
||||
"currentMode"
|
||||
],
|
||||
classNames: ["admin-report"],
|
||||
isEnabled: true,
|
||||
disabledLabel: "admin.dashboard.disabled",
|
||||
isLoading: false,
|
||||
dataSourceName: null,
|
||||
report: null,
|
||||
model: null,
|
||||
reportOptions: null,
|
||||
forcedModes: null,
|
||||
showAllReportsLink: false,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
showTrend: false,
|
||||
showHeader: true,
|
||||
showTitle: true,
|
||||
showFilteringUI: false,
|
||||
showCategoryOptions: Ember.computed.alias("model.category_filtering"),
|
||||
showDatesOptions: Ember.computed.alias("model.dates_filtering"),
|
||||
showGroupOptions: Ember.computed.alias("model.group_filtering"),
|
||||
showExport: Ember.computed.not("model.onlyTable"),
|
||||
hasFilteringActions: Ember.computed.or(
|
||||
"showCategoryOptions",
|
||||
"showDatesOptions",
|
||||
"showGroupOptions",
|
||||
"showExport"
|
||||
),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._reports = [];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.get("report")) {
|
||||
this._renderReport(
|
||||
this.get("report"),
|
||||
this.get("forcedModes"),
|
||||
this.get("currentMode")
|
||||
);
|
||||
} else if (this.get("dataSourceName")) {
|
||||
this._fetchReport().finally(() => this._computeReport());
|
||||
}
|
||||
},
|
||||
|
||||
didRender() {
|
||||
this._super(...arguments);
|
||||
|
||||
unregisterTooltip($(".info[data-tooltip]"));
|
||||
registerTooltip($(".info[data-tooltip]"));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
unregisterTooltip($(".info[data-tooltip]"));
|
||||
},
|
||||
|
||||
showTimeoutError: Ember.computed.alias("model.timeout"),
|
||||
|
||||
@computed("dataSourceName", "model.type")
|
||||
dasherizedDataSourceName(dataSourceName, type) {
|
||||
return (dataSourceName || type || "undefined").replace(/_/g, "-");
|
||||
},
|
||||
|
||||
@computed("dataSourceName", "model.type")
|
||||
dataSource(dataSourceName, type) {
|
||||
dataSourceName = dataSourceName || type;
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
},
|
||||
|
||||
@computed("displayedModes.length")
|
||||
showModes(displayedModesLength) {
|
||||
return displayedModesLength > 1;
|
||||
},
|
||||
|
||||
@computed("currentMode", "model.modes", "forcedModes")
|
||||
displayedModes(currentMode, reportModes, forcedModes) {
|
||||
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
||||
|
||||
return Ember.makeArray(modes).map(mode => {
|
||||
const base = `mode-button ${mode}`;
|
||||
const cssClass = currentMode === mode ? `${base} current` : base;
|
||||
|
||||
return {
|
||||
mode,
|
||||
cssClass,
|
||||
icon: mode === "table" ? "table" : "signal"
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@computed()
|
||||
categoryOptions() {
|
||||
const arr = [{ name: I18n.t("category.all"), value: "all" }];
|
||||
return arr.concat(
|
||||
Discourse.Site.currentProp("sortedCategories").map(i => {
|
||||
return { name: i.get("name"), value: i.get("id") };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed()
|
||||
groupOptions() {
|
||||
const arr = [
|
||||
{ name: I18n.t("admin.dashboard.reports.groups"), value: "all" }
|
||||
];
|
||||
return arr.concat(
|
||||
this.site.groups.map(i => {
|
||||
return { name: i["name"], value: i["id"] };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed("currentMode")
|
||||
modeComponent(currentMode) {
|
||||
return `admin-report-${currentMode}`;
|
||||
},
|
||||
|
||||
actions: {
|
||||
exportCsv() {
|
||||
exportEntity("report", {
|
||||
name: this.get("model.type"),
|
||||
start_date: this.get("startDate"),
|
||||
end_date: this.get("endDate"),
|
||||
category_id:
|
||||
this.get("categoryId") === "all" ? undefined : this.get("categoryId"),
|
||||
group_id:
|
||||
this.get("groupId") === "all" ? undefined : this.get("groupId")
|
||||
}).then(outputExportResult);
|
||||
},
|
||||
|
||||
changeMode(mode) {
|
||||
this.set("currentMode", mode);
|
||||
}
|
||||
},
|
||||
|
||||
_computeReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._reports || !this._reports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// on a slow network _fetchReport could be called multiple times between
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
let filteredReports = this._reports.uniqBy("report_key");
|
||||
let report;
|
||||
|
||||
const sort = r => {
|
||||
if (r.length > 1) {
|
||||
return r.findBy("type", this.get("dataSourceName"));
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
let startDate = this.get("startDate");
|
||||
let endDate = this.get("endDate");
|
||||
|
||||
startDate =
|
||||
startDate && typeof startDate.isValid === "function"
|
||||
? startDate.format("YYYYMMDD")
|
||||
: startDate;
|
||||
endDate =
|
||||
startDate && typeof endDate.isValid === "function"
|
||||
? endDate.format("YYYYMMDD")
|
||||
: endDate;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
report = sort(filteredReports)[0];
|
||||
} else {
|
||||
let reportKey = `reports:${this.get("dataSourceName")}`;
|
||||
|
||||
if (this.get("categoryId") && this.get("categoryId") !== "all") {
|
||||
reportKey += `:${this.get("categoryId")}`;
|
||||
} else {
|
||||
reportKey += `:`;
|
||||
}
|
||||
|
||||
reportKey += `:${startDate.replace(/-/g, "")}`;
|
||||
reportKey += `:${endDate.replace(/-/g, "")}`;
|
||||
|
||||
if (this.get("groupId") && this.get("groupId") !== "all") {
|
||||
reportKey += `:${this.get("groupId")}`;
|
||||
} else {
|
||||
reportKey += `:`;
|
||||
}
|
||||
|
||||
reportKey += `:`;
|
||||
|
||||
report = sort(
|
||||
filteredReports.filter(r => r.report_key.includes(reportKey))
|
||||
)[0];
|
||||
|
||||
if (!report) {
|
||||
console.log(
|
||||
"failed to find a report to render",
|
||||
`expected key: ${reportKey}`,
|
||||
`existing keys: ${filteredReports.map(f => f.report_key)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._renderReport(
|
||||
report,
|
||||
this.get("forcedModes"),
|
||||
this.get("currentMode")
|
||||
);
|
||||
},
|
||||
|
||||
_renderReport(report, forcedModes, currentMode) {
|
||||
const modes = forcedModes ? forcedModes.split(",") : report.modes;
|
||||
currentMode = currentMode || modes[0];
|
||||
|
||||
this.setProperties({
|
||||
model: report,
|
||||
currentMode,
|
||||
options: this._buildOptions(currentMode)
|
||||
});
|
||||
},
|
||||
|
||||
_fetchReport() {
|
||||
this._super();
|
||||
|
||||
this.set("isLoading", true);
|
||||
|
||||
let payload = this._buildPayload(["prev_period"]);
|
||||
|
||||
return ajax(this.get("dataSource"), payload)
|
||||
.then(response => {
|
||||
if (response && response.report) {
|
||||
this._reports.push(this._loadReport(response.report));
|
||||
} else {
|
||||
console.log("failed loading", this.get("dataSource"));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
||||
this.set("isLoading", false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_buildPayload(facets) {
|
||||
let payload = { data: { cache: true, facets } };
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = moment(
|
||||
this.get("startDate"),
|
||||
"YYYY-MM-DD"
|
||||
).format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = moment(this.get("endDate"), "YYYY-MM-DD").format(
|
||||
"YYYY-MM-DD[T]HH:mm:ss.SSSZZ"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("groupId") && this.get("groupId") !== "all") {
|
||||
payload.data.group_id = this.get("groupId");
|
||||
}
|
||||
|
||||
if (this.get("categoryId") && this.get("categoryId") !== "all") {
|
||||
payload.data.category_id = this.get("categoryId");
|
||||
}
|
||||
|
||||
if (this.get("reportOptions.table.limit")) {
|
||||
payload.data.limit = this.get("reportOptions.table.limit");
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
_buildOptions(mode) {
|
||||
if (mode === "table") {
|
||||
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
||||
return Ember.Object.create(
|
||||
_.assign(tableOptions, this.get("reportOptions.table") || {})
|
||||
);
|
||||
} else {
|
||||
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
||||
return Ember.Object.create(
|
||||
_.assign(chartOptions, this.get("reportOptions.chart") || {})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_loadReport(jsonReport) {
|
||||
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
||||
|
||||
if (jsonReport.chartData && jsonReport.chartData.length > 40) {
|
||||
jsonReport.chartData = collapseWeekly(
|
||||
jsonReport.chartData,
|
||||
jsonReport.average
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonReport.prev_data) {
|
||||
Report.fillMissingDates(jsonReport, {
|
||||
filledField: "prevChartData",
|
||||
dataField: "prev_data",
|
||||
starDate: jsonReport.prev_start_date,
|
||||
endDate: jsonReport.prev_end_date
|
||||
});
|
||||
|
||||
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
|
||||
jsonReport.prevChartData = collapseWeekly(
|
||||
jsonReport.prevChartData,
|
||||
jsonReport.average
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Report.create(jsonReport);
|
||||
}
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed("model.sortedData")
|
||||
totalForPeriod(data) {
|
||||
const values = data.map(d => d.y);
|
||||
return values.reduce((sum, v) => sum + v);
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["dashboard-inline-table"],
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["total", "prev30Days"]);
|
||||
|
||||
return Ember.RSVP.Promise.all(
|
||||
this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload).then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,176 +0,0 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
import Report from "admin/models/report";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
|
||||
|
||||
function collapseWeekly(data, average) {
|
||||
let aggregate = [];
|
||||
let bucket, i;
|
||||
let offset = data.length % 7;
|
||||
for (i = offset; i < data.length; i++) {
|
||||
if (bucket && i % 7 === offset) {
|
||||
if (average) {
|
||||
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
||||
}
|
||||
aggregate.push(bucket);
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
bucket = bucket || { x: data[i].x, y: 0 };
|
||||
bucket.y += data[i].y;
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["chart", "dashboard-mini-chart"],
|
||||
total: 0,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
this._colorsPool = ["rgb(0,136,204)", "rgb(235,83,148)"];
|
||||
},
|
||||
|
||||
didRender() {
|
||||
this._super();
|
||||
registerTooltip($(this.element).find("[data-tooltip]"));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
unregisterTooltip($(this.element).find("[data-tooltip]"));
|
||||
},
|
||||
|
||||
pickColorAtIndex(index) {
|
||||
return this._colorsPool[index] || this._colorsPool[0];
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["prev_period"]);
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
return Ember.RSVP.Promise.all(
|
||||
this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload).then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
loadReport(report, previousReport) {
|
||||
Report.fillMissingDates(report);
|
||||
|
||||
if (report.data && report.data.length > 40) {
|
||||
report.data = collapseWeekly(report.data, report.average);
|
||||
}
|
||||
|
||||
if (previousReport && previousReport.color.length) {
|
||||
report.color = previousReport.color;
|
||||
} else {
|
||||
const dataSourceNameIndex = this.get("dataSourceNames")
|
||||
.split(",")
|
||||
.indexOf(report.type);
|
||||
report.color = this.pickColorAtIndex(dataSourceNameIndex);
|
||||
}
|
||||
|
||||
return Report.create(report);
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
this._super();
|
||||
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
const $chartCanvas = this.$(".chart-canvas");
|
||||
if (!$chartCanvas.length) return;
|
||||
const context = $chartCanvas[0].getContext("2d");
|
||||
|
||||
const reportsForPeriod = this.get("reportsForPeriod");
|
||||
|
||||
const labels = Ember.makeArray(
|
||||
reportsForPeriod.get("firstObject.data")
|
||||
).map(d => d.x);
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: reportsForPeriod.map(report => {
|
||||
return {
|
||||
data: Ember.makeArray(report.data).map(d =>
|
||||
Math.round(parseFloat(d.y))
|
||||
),
|
||||
backgroundColor: "rgba(200,220,240,0.3)",
|
||||
borderColor: report.color
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
}
|
||||
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: context =>
|
||||
moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: { callback: label => number(label) }
|
||||
}
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
gridLines: { display: false },
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["dashboard-table"],
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["total", "prev30Days"]);
|
||||
|
||||
return Ember.RSVP.Promise.all(
|
||||
this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload).then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
import { setting } from "discourse/lib/computed";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||
import Report from "admin/models/report";
|
||||
import PeriodComputationMixin from "admin/mixins/period-computation";
|
||||
|
||||
export default Ember.Controller.extend(PeriodComputationMixin, {
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller("exception"),
|
||||
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||
logSearchQueriesEnabled: setting("log_search_queries"),
|
||||
lastBackupTakenAt: Ember.computed.alias(
|
||||
"model.attributes.last_backup_taken_at"
|
||||
),
|
||||
shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"),
|
||||
|
||||
@computed
|
||||
topReferredTopicsTopions() {
|
||||
return { table: { total: false, limit: 8 } };
|
||||
},
|
||||
|
||||
@computed
|
||||
trendingSearchOptions() {
|
||||
return { table: { total: false, limit: 8 } };
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
topReferredTopicsReport(reports) {
|
||||
return reports.find(x => x.type === "top_referred_topics");
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
trendingSearchReport(reports) {
|
||||
return reports.find(x => x.type === "trending_search");
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
usersByTypeReport(reports) {
|
||||
return reports.find(x => x.type === "users_by_type");
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
usersByTrustLevelReport(reports) {
|
||||
return reports.find(x => x.type === "users_by_trust_level");
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
activityMetricsReports(reports) {
|
||||
return reports.filter(report => {
|
||||
return [
|
||||
"page_view_total_reqs",
|
||||
"visits",
|
||||
"time_to_first_response",
|
||||
"likes",
|
||||
"flags",
|
||||
"user_to_user_private_messages_with_replies"
|
||||
].includes(report.type);
|
||||
});
|
||||
},
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
if (
|
||||
!this.get("dashboardFetchedAt") ||
|
||||
moment()
|
||||
.subtract(30, "minutes")
|
||||
.toDate() > this.get("dashboardFetchedAt")
|
||||
) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
AdminDashboardNext.fetchGeneral()
|
||||
.then(adminDashboardNextModel => {
|
||||
this.setProperties({
|
||||
dashboardFetchedAt: new Date(),
|
||||
model: adminDashboardNextModel,
|
||||
reports: adminDashboardNextModel.reports.map(x => Report.create(x))
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
this.replaceRoute("exception");
|
||||
})
|
||||
.finally(() => this.set("isLoading", false));
|
||||
}
|
||||
},
|
||||
|
||||
@computed("model.attributes.updated_at")
|
||||
updatedTimestamp(updatedAt) {
|
||||
return moment(updatedAt).format("LLL");
|
||||
},
|
||||
|
||||
@computed("lastBackupTakenAt")
|
||||
backupTimestamp(lastBackupTakenAt) {
|
||||
return moment(lastBackupTakenAt).format("LLL");
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return Discourse.getURL(`/admin/dashboard/general?period=${period}`);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Report from "admin/models/report";
|
||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||
import PeriodComputationMixin from "admin/mixins/period-computation";
|
||||
|
||||
export default Ember.Controller.extend(PeriodComputationMixin, {
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller("exception"),
|
||||
|
||||
@computed
|
||||
flagsStatusOptions() {
|
||||
return {
|
||||
table: {
|
||||
total: false,
|
||||
perPage: 10
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
flagsStatusReport(reports) {
|
||||
return reports.find(x => x.type === "flags_status");
|
||||
},
|
||||
|
||||
@computed("reports.[]")
|
||||
postEditsReport(reports) {
|
||||
return reports.find(x => x.type === "post_edits");
|
||||
},
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
if (
|
||||
!this.get("dashboardFetchedAt") ||
|
||||
moment()
|
||||
.subtract(30, "minutes")
|
||||
.toDate() > this.get("dashboardFetchedAt")
|
||||
) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
AdminDashboardNext.fetchModeration()
|
||||
.then(model => {
|
||||
const reports = model.reports.map(x => Report.create(x));
|
||||
this.setProperties({
|
||||
dashboardFetchedAt: new Date(),
|
||||
model,
|
||||
reports
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
this.replaceRoute("exception");
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`);
|
||||
}
|
||||
});
|
|
@ -1,34 +1,38 @@
|
|||
import { setting } from "discourse/lib/computed";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||
import Report from "admin/models/report";
|
||||
import VersionCheck from "admin/models/version-check";
|
||||
|
||||
const PROBLEMS_CHECK_MINUTES = 1;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["period"],
|
||||
period: "monthly",
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller("exception"),
|
||||
showVersionChecks: setting("version_checks"),
|
||||
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||
lastBackupTakenAt: Ember.computed.alias(
|
||||
"model.attributes.last_backup_taken_at"
|
||||
),
|
||||
logSearchQueriesEnabled: setting("log_search_queries"),
|
||||
availablePeriods: ["yearly", "quarterly", "monthly", "weekly"],
|
||||
shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"),
|
||||
|
||||
@computed("problems.length")
|
||||
foundProblems(problemsLength) {
|
||||
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
||||
},
|
||||
|
||||
fetchProblems() {
|
||||
if (this.get("isLoadingProblems")) return;
|
||||
|
||||
if (
|
||||
!this.get("problemsFetchedAt") ||
|
||||
moment()
|
||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
||||
.toDate() > this.get("problemsFetchedAt")
|
||||
) {
|
||||
this._loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
|
||||
if (this.get("isLoading") || !versionChecks) return;
|
||||
|
||||
if (
|
||||
!this.get("dashboardFetchedAt") ||
|
||||
|
@ -38,22 +42,17 @@ export default Ember.Controller.extend({
|
|||
) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
AdminDashboardNext.fetch()
|
||||
.then(model => {
|
||||
let properties = {
|
||||
dashboardFetchedAt: new Date()
|
||||
};
|
||||
|
||||
AdminDashboardNext.find()
|
||||
.then(adminDashboardNextModel => {
|
||||
if (versionChecks) {
|
||||
this.set(
|
||||
"versionCheck",
|
||||
VersionCheck.create(adminDashboardNextModel.version_check)
|
||||
);
|
||||
properties.versionCheck = VersionCheck.create(model.version_check);
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
dashboardFetchedAt: new Date(),
|
||||
model: adminDashboardNextModel,
|
||||
reports: adminDashboardNextModel.reports.map(x => Report.create(x))
|
||||
});
|
||||
this.setProperties(properties);
|
||||
})
|
||||
.catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
|
@ -63,27 +62,17 @@ export default Ember.Controller.extend({
|
|||
this.set("isLoading", false);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!this.get("problemsFetchedAt") ||
|
||||
moment()
|
||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
||||
.toDate() > this.get("problemsFetchedAt")
|
||||
) {
|
||||
this.loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
loadProblems() {
|
||||
this.set("loadingProblems", true);
|
||||
this.set("problemsFetchedAt", new Date());
|
||||
_loadProblems() {
|
||||
this.setProperties({
|
||||
loadingProblems: true,
|
||||
problemsFetchedAt: new Date()
|
||||
});
|
||||
|
||||
AdminDashboardNext.fetchProblems()
|
||||
.then(d => {
|
||||
this.set("problems", d.problems);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loadingProblems", false);
|
||||
});
|
||||
.then(model => this.set("problems", model.problems))
|
||||
.finally(() => this.set("loadingProblems", false));
|
||||
},
|
||||
|
||||
@computed("problemsFetchedAt")
|
||||
|
@ -93,69 +82,9 @@ export default Ember.Controller.extend({
|
|||
.format("LLL");
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.endOf("day")
|
||||
.subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day")
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
@computed("model.attributes.updated_at")
|
||||
updatedTimestamp(updatedAt) {
|
||||
return moment(updatedAt).format("LLL");
|
||||
},
|
||||
|
||||
@computed("lastBackupTakenAt")
|
||||
backupTimestamp(lastBackupTakenAt) {
|
||||
return moment(lastBackupTakenAt).format("LLL");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
},
|
||||
refreshProblems() {
|
||||
this.loadProblems();
|
||||
this._loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return Discourse.getURL(`/admin?period=${period}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,108 +1,36 @@
|
|||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import Report from "admin/models/report";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
||||
viewMode: "graph",
|
||||
viewingTable: Em.computed.equal("viewMode", "table"),
|
||||
viewingGraph: Em.computed.equal("viewMode", "graph"),
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
queryParams: ["start_date", "end_date", "category_id", "group_id"],
|
||||
categoryId: null,
|
||||
groupId: null,
|
||||
refreshing: false,
|
||||
|
||||
@computed()
|
||||
categoryOptions() {
|
||||
const arr = [{ name: I18n.t("category.all"), value: "all" }];
|
||||
return arr.concat(
|
||||
Discourse.Site.currentProp("sortedCategories").map(i => {
|
||||
return { name: i.get("name"), value: i.get("id") };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed()
|
||||
groupOptions() {
|
||||
const arr = [
|
||||
{ name: I18n.t("admin.dashboard.reports.groups"), value: "all" }
|
||||
];
|
||||
return arr.concat(
|
||||
this.site.groups.map(i => {
|
||||
return { name: i["name"], value: i["id"] };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed("model.type")
|
||||
showCategoryOptions(modelType) {
|
||||
return [
|
||||
"topics",
|
||||
"posts",
|
||||
"time_to_first_response_total",
|
||||
"topics_with_no_response",
|
||||
"flags",
|
||||
"likes",
|
||||
"bookmarks"
|
||||
].includes(modelType);
|
||||
},
|
||||
reportOptions(type) {
|
||||
let options = { table: { perPage: 50, limit: 50 } };
|
||||
|
||||
@computed("model.type")
|
||||
showGroupOptions(modelType) {
|
||||
return (
|
||||
modelType === "visits" ||
|
||||
modelType === "signups" ||
|
||||
modelType === "profile_views"
|
||||
);
|
||||
if (type === "top_referred_topics") {
|
||||
options.table.limit = 10;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
actions: {
|
||||
refreshReport() {
|
||||
var q;
|
||||
this.set("refreshing", true);
|
||||
|
||||
this.setProperties({
|
||||
start_date: this.get("startDate"),
|
||||
end_date: this.get("endDate"),
|
||||
category_id: this.get("categoryId")
|
||||
});
|
||||
|
||||
if (this.get("groupId")) {
|
||||
this.set("group_id", this.get("groupId"));
|
||||
}
|
||||
|
||||
q = Report.find(
|
||||
this.get("model.type"),
|
||||
this.get("startDate"),
|
||||
this.get("endDate"),
|
||||
this.get("categoryId"),
|
||||
this.get("groupId")
|
||||
);
|
||||
q.then(m => this.set("model", m)).finally(() =>
|
||||
this.set("refreshing", false)
|
||||
);
|
||||
onSelectStartDate(startDate) {
|
||||
this.set("start_date", startDate);
|
||||
},
|
||||
|
||||
viewAsTable() {
|
||||
this.set("viewMode", "table");
|
||||
onSelectCategory(categoryId) {
|
||||
this.set("category_id", categoryId);
|
||||
},
|
||||
|
||||
viewAsGraph() {
|
||||
this.set("viewMode", "graph");
|
||||
onSelectGroup(groupId) {
|
||||
this.set("group_id", groupId);
|
||||
},
|
||||
|
||||
exportCsv() {
|
||||
exportEntity("report", {
|
||||
name: this.get("model.type"),
|
||||
start_date: this.get("startDate"),
|
||||
end_date: this.get("endDate"),
|
||||
category_id:
|
||||
this.get("categoryId") === "all" ? undefined : this.get("categoryId"),
|
||||
group_id:
|
||||
this.get("groupId") === "all" ? undefined : this.get("groupId")
|
||||
}).then(outputExportResult);
|
||||
onSelectEndDate(endDate) {
|
||||
this.set("end_date", endDate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: ["isLoading", "dataSourceNames"],
|
||||
reports: null,
|
||||
isLoading: false,
|
||||
dataSourceNames: "",
|
||||
title: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.set("reports", []);
|
||||
},
|
||||
|
||||
@computed("dataSourceNames")
|
||||
dataSources(dataSourceNames) {
|
||||
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
|
||||
},
|
||||
|
||||
buildPayload(facets) {
|
||||
let payload = { data: { cache: true, facets } };
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").format(
|
||||
"YYYY-MM-DD[T]HH:mm:ss.SSSZZ"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = this.get("endDate").format(
|
||||
"YYYY-MM-DD[T]HH:mm:ss.SSSZZ"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("limit")) {
|
||||
payload.data.limit = this.get("limit");
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
@computed("reports.[]", "startDate", "endDate", "dataSourceNames")
|
||||
reportsForPeriod(reports, startDate, endDate, dataSourceNames) {
|
||||
// on a slow network fetchReport could be called multiple times between
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
reports = reports.uniqBy("report_key");
|
||||
|
||||
const sort = r => {
|
||||
if (r.length > 1) {
|
||||
return dataSourceNames.split(",").map(name => r.findBy("type", name));
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return sort(reports);
|
||||
}
|
||||
|
||||
return sort(
|
||||
reports.filter(report => {
|
||||
return (
|
||||
report.report_key.includes(startDate.format("YYYYMMDD")) &&
|
||||
report.report_key.includes(endDate.format("YYYYMMDD"))
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport().finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport().finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
this.set(
|
||||
"title",
|
||||
this.get("reportsForPeriod")
|
||||
.map(r => r.title)
|
||||
.join(", ")
|
||||
);
|
||||
this.set("isLoading", false);
|
||||
},
|
||||
|
||||
loadReport(jsonReport) {
|
||||
return Report.create(jsonReport);
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this.set("reports", []);
|
||||
this.set("isLoading", true);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
queryParams: ["period"],
|
||||
|
||||
period: "monthly",
|
||||
|
||||
availablePeriods: ["yearly", "quarterly", "monthly", "weekly"],
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.endOf("day")
|
||||
.subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day")
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,29 +1,41 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
const ATTRIBUTES = ["disk_space", "updated_at", "last_backup_taken_at"];
|
||||
const GENERAL_ATTRIBUTES = ["disk_space", "updated_at", "last_backup_taken_at"];
|
||||
|
||||
const AdminDashboardNext = Discourse.Model.extend({});
|
||||
|
||||
AdminDashboardNext.reopenClass({
|
||||
/**
|
||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||
has expired and the server must collect the data again.
|
||||
|
||||
@method find
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
find() {
|
||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||
var model = AdminDashboardNext.create();
|
||||
|
||||
model.set("reports", json.reports);
|
||||
fetch() {
|
||||
return ajax("/admin/dashboard-next.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
model.set("version_check", json.version_check);
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchModeration() {
|
||||
return ajax("/admin/dashboard/moderation.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
model.setProperties({
|
||||
reports: json.reports,
|
||||
loaded: true
|
||||
});
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchGeneral() {
|
||||
return ajax("/admin/dashboard/general.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
|
||||
const attributes = {};
|
||||
ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||
model.set("attributes", attributes);
|
||||
GENERAL_ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||
|
||||
model.set("loaded", true);
|
||||
model.setProperties({
|
||||
reports: json.reports,
|
||||
attributes,
|
||||
loaded: true
|
||||
});
|
||||
|
||||
return model;
|
||||
});
|
||||
|
|
|
@ -1,22 +1,105 @@
|
|||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fillMissingDates } from "discourse/lib/utilities";
|
||||
import { fillMissingDates, isNumeric } from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import { number, durationTiny } from "discourse/lib/formatter";
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("labels")
|
||||
computedLabels(labels) {
|
||||
return labels.map(label => {
|
||||
const type = label.type;
|
||||
const properties = label.properties;
|
||||
const property = properties[0];
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
sort_property: label.sort_property || property,
|
||||
property,
|
||||
compute: row => {
|
||||
let value = row[property];
|
||||
let escapedValue = escapeExpression(value);
|
||||
let tooltip;
|
||||
let base = { property, value, type };
|
||||
|
||||
if (value === null || typeof value === "undefined") {
|
||||
return _.assign(base, {
|
||||
value: null,
|
||||
formatedValue: "-",
|
||||
type: "undefined"
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "seconds") {
|
||||
return _.assign(base, {
|
||||
formatedValue: escapeExpression(durationTiny(value))
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "link") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `<a href="${escapeExpression(
|
||||
row[properties[1]]
|
||||
)}">${escapedValue}</a>`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "percent") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `${escapedValue}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "number" || isNumeric(value))
|
||||
return _.assign(base, {
|
||||
type: "number",
|
||||
formatedValue: number(value)
|
||||
});
|
||||
|
||||
if (type !== "string" && type !== "text") {
|
||||
const date = moment(value, "YYYY-MM-DD");
|
||||
if (type === "date" || date.isValid()) {
|
||||
return _.assign(base, {
|
||||
type: "date",
|
||||
formatedValue: date.format("LL")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "text") tooltip = escapedValue;
|
||||
|
||||
return _.assign(base, {
|
||||
tooltip,
|
||||
type: type || "string",
|
||||
formatedValue: escapedValue
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@computed("modes")
|
||||
onlyTable(modes) {
|
||||
return modes.length === 1 && modes[0] === "table";
|
||||
},
|
||||
|
||||
@computed("type", "start_date", "end_date")
|
||||
reportUrl(type, start_date, end_date) {
|
||||
start_date = moment(start_date)
|
||||
start_date = moment
|
||||
.utc(start_date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
end_date = moment(end_date)
|
||||
|
||||
end_date = moment
|
||||
.utc(end_date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
|
||||
return Discourse.getURL(
|
||||
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
||||
);
|
||||
|
@ -142,29 +225,6 @@ const Report = Discourse.Model.extend({
|
|||
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("type")
|
||||
icon(type) {
|
||||
if (type.indexOf("message") > -1) {
|
||||
return "envelope";
|
||||
}
|
||||
switch (type) {
|
||||
case "page_view_total_reqs":
|
||||
return "file";
|
||||
case "visits":
|
||||
return "user";
|
||||
case "time_to_first_response":
|
||||
return "reply";
|
||||
case "flags":
|
||||
return "flag";
|
||||
case "likes":
|
||||
return "heart";
|
||||
case "bookmarks":
|
||||
return "bookmark";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("type")
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
|
@ -290,18 +350,23 @@ const Report = Discourse.Model.extend({
|
|||
});
|
||||
|
||||
Report.reopenClass({
|
||||
fillMissingDates(report) {
|
||||
if (_.isArray(report.data)) {
|
||||
fillMissingDates(report, options = {}) {
|
||||
const dataField = options.dataField || "data";
|
||||
const filledField = options.filledField || "data";
|
||||
const startDate = options.startDate || "start_date";
|
||||
const endDate = options.endDate || "end_date";
|
||||
|
||||
if (_.isArray(report[dataField])) {
|
||||
const startDateFormatted = moment
|
||||
.utc(report.start_date)
|
||||
.utc(report[startDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
const endDateFormatted = moment
|
||||
.utc(report.end_date)
|
||||
.utc(report[endDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
report.data = fillMissingDates(
|
||||
report.data,
|
||||
report[filledField] = fillMissingDates(
|
||||
JSON.parse(JSON.stringify(report[dataField])),
|
||||
startDateFormatted,
|
||||
endDateFormatted
|
||||
);
|
||||
|
@ -317,8 +382,12 @@ Report.reopenClass({
|
|||
group_id: groupId
|
||||
}
|
||||
}).then(json => {
|
||||
// Add zero values for missing dates
|
||||
Report.fillMissingDates(json.report);
|
||||
// don’t fill for large multi column tables
|
||||
// which are not date based
|
||||
const modes = json.report.modes;
|
||||
if (modes.length !== 1 && modes[0] !== "table") {
|
||||
Report.fillMissingDates(json.report);
|
||||
}
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next-general").fetchDashboard();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next-moderation").fetchDashboard();
|
||||
}
|
||||
});
|
|
@ -2,7 +2,22 @@ import { scrollTop } from "discourse/mixins/scroll-top";
|
|||
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next").fetchProblems();
|
||||
this.controllerFor("admin-dashboard-next").fetchDashboard();
|
||||
scrollTop();
|
||||
},
|
||||
|
||||
afterModel(model, transition) {
|
||||
if (transition.targetName === "admin.dashboardNext.index") {
|
||||
this.transitionTo("admin.dashboardNext.general");
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
if (transition.targetName === "admin.dashboardNext.index") {
|
||||
this.transitionTo("admin.dashboardNext.general");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,35 +1,18 @@
|
|||
import Report from "admin/models/report";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: {
|
||||
mode: {},
|
||||
start_date: {},
|
||||
end_date: {},
|
||||
category_id: {},
|
||||
group_id: {}
|
||||
},
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
|
||||
model(params) {
|
||||
return Report.find(
|
||||
params.type,
|
||||
params["start_date"],
|
||||
params["end_date"],
|
||||
params["category_id"],
|
||||
params["group_id"]
|
||||
);
|
||||
},
|
||||
if (!controller.get("start_date")) {
|
||||
controller.set(
|
||||
"start_date",
|
||||
moment()
|
||||
.subtract("30", "day")
|
||||
.format("YYYY-MM-DD")
|
||||
);
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
categoryId: model.get("category_id") || "all",
|
||||
groupId: model.get("group_id"),
|
||||
startDate: moment(model.get("start_date"))
|
||||
.utc()
|
||||
.format("YYYY-MM-DD"),
|
||||
endDate: moment(model.get("end_date"))
|
||||
.utc()
|
||||
.format("YYYY-MM-DD")
|
||||
});
|
||||
if (!controller.get("end_date")) {
|
||||
controller.set("end_date", moment().format("YYYY-MM-DD"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
export default function() {
|
||||
this.route("admin", { resetNamespace: true }, function() {
|
||||
this.route("dashboard", { path: "/dashboard-old" });
|
||||
this.route("dashboardNext", { path: "/" });
|
||||
|
||||
this.route("dashboardNext", { path: "/" }, function() {
|
||||
this.route("general", { path: "/dashboard/general" });
|
||||
this.route("moderation", { path: "/dashboard/moderation" });
|
||||
});
|
||||
|
||||
this.route(
|
||||
"adminSiteSettings",
|
||||
{ path: "/site_settings", resetNamespace: true },
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div class="chart-canvas-container">
|
||||
<canvas class="chart-canvas"></canvas>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
<div class="table-container">
|
||||
{{#each model.data as |data|}}
|
||||
<a class="table-cell user-{{data.key}}" href="{{data.url}}">
|
||||
<span class="label">
|
||||
{{#if data.icon}}
|
||||
{{d-icon data.icon}}
|
||||
{{/if}}
|
||||
{{data.x}}
|
||||
</span>
|
||||
<span class="value">
|
||||
{{number data.y}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
{{#if showSortingUI}}
|
||||
{{d-button action=sortByLabel icon=sortIcon class="sort-button"}}
|
||||
{{/if}}
|
||||
|
||||
<span>{{label.title}}</span>
|
|
@ -0,0 +1,5 @@
|
|||
{{#each cells as |cell|}}
|
||||
<td class="{{cell.type}} {{cell.property}}" title="{{cell.tooltip}}">
|
||||
{{{cell.formatedValue}}}
|
||||
</td>
|
||||
{{/each}}
|
|
@ -0,0 +1,60 @@
|
|||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{#if model.computedLabels}}
|
||||
{{#each model.computedLabels as |label index|}}
|
||||
{{admin-report-table-header
|
||||
showSortingUI=showSortingUI
|
||||
currentSortDirection=sortDirection
|
||||
currentSortLabel=sortLabel
|
||||
label=label
|
||||
sortByLabel=(action "sortByLabel" label)}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each model.data as |data|}}
|
||||
<th>{{data.x}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each paginatedData as |data|}}
|
||||
{{admin-report-table-row data=data labels=model.computedLabels}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{#if showTotalForSample}}
|
||||
<small>{{i18n 'admin.dashboard.reports.totals_for_sample'}}</small>
|
||||
<table class="totals-sample-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#each totalsForSample as |total|}}
|
||||
<td>{{total.formatedValue}}</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
{{#if showTotal}}
|
||||
<small>{{i18n 'admin.dashboard.reports.total'}}</small>
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>{{number model.total}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
<div class="pagination">
|
||||
{{#each pages as |pageState|}}
|
||||
{{d-button
|
||||
translatedLabel=pageState.page
|
||||
action="changePage"
|
||||
actionParam=pageState.index
|
||||
class=pageState.class}}
|
||||
{{/each}}
|
||||
</div>
|
|
@ -0,0 +1,154 @@
|
|||
{{#if isEnabled}}
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{#if showTimeoutError}}
|
||||
<div class="alert alert-error">
|
||||
{{i18n "admin.dashboard.timeout_error"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showHeader}}
|
||||
<div class="report-header">
|
||||
{{#if showTitle}}
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#if showAllReportsLink}}
|
||||
{{#link-to "adminReports" class="all-report-link"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
<span class="separator">|</span>
|
||||
{{/if}}
|
||||
|
||||
<a href="{{model.reportUrl}}" class="report-link">
|
||||
{{model.title}}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{{#if model.description}}
|
||||
<span class="info" data-tooltip="{{model.description}}">
|
||||
{{d-icon "question-circle"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showTrend}}
|
||||
{{#if model.prev_period}}
|
||||
<div class="trend {{model.trend}}">
|
||||
<span class="trend-value" title="{{model.trendTitle}}">
|
||||
{{#if model.average}}
|
||||
{{number model.currentAverage}}{{#if model.percent}}%{{/if}}
|
||||
{{else}}
|
||||
{{number model.currentTotal noTitle="true"}}{{#if model.percent}}%{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if model.trendIcon}}
|
||||
{{d-icon model.trendIcon class="trend-icon"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showModes}}
|
||||
<ul class="mode-switch">
|
||||
{{#each displayedModes as |displayedMode|}}
|
||||
<li class="mode">
|
||||
{{d-button
|
||||
action="changeMode"
|
||||
actionParam=displayedMode.mode
|
||||
class=displayedMode.cssClass
|
||||
icon=displayedMode.icon}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="report-body">
|
||||
{{#unless showTimeoutError}}
|
||||
{{#if currentMode}}
|
||||
{{component modeComponent model=model options=options}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showFilteringUI}}
|
||||
{{#if hasFilteringActions}}
|
||||
<div class="report-filters">
|
||||
{{#if showDatesOptions}}
|
||||
<div class="filtering-control">
|
||||
<span class="filtering-label">
|
||||
{{i18n 'admin.dashboard.reports.start_date'}}
|
||||
</span>
|
||||
|
||||
<div class="filtering-input">
|
||||
{{date-picker-past
|
||||
value=startDate
|
||||
defaultDate=startDate
|
||||
onSelect=onSelectStartDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filtering-control">
|
||||
<span class="filtering-label">
|
||||
{{i18n 'admin.dashboard.reports.end_date'}}
|
||||
</span>
|
||||
|
||||
<div class="filtering-input">
|
||||
{{date-picker-past
|
||||
value=endDate
|
||||
defaultDate=endDate
|
||||
onSelect=onSelectEndDate}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showCategoryOptions}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{combo-box
|
||||
onSelect=onSelectCategory
|
||||
filterable=true
|
||||
valueAttribute="value"
|
||||
content=categoryOptions
|
||||
castInteger=true
|
||||
value=categoryId}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showGroupOptions}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{combo-box
|
||||
onSelect=onSelectGroup
|
||||
castInteger=true
|
||||
filterable=true
|
||||
valueAttribute="value"
|
||||
content=groupOptions
|
||||
value=groupId}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showExport}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{d-button class="export-btn" action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-report dataSourceName=model.relatedReport.type}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-section}}
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{{i18n disabledLabel}}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,37 +0,0 @@
|
|||
{{#if model.sortedData}}
|
||||
<table class="table report {{model.type}}">
|
||||
<tr>
|
||||
<th>{{model.xaxis}}</th>
|
||||
<th>{{model.yaxis}}</th>
|
||||
</tr>
|
||||
|
||||
{{#each model.sortedData as |row|}}
|
||||
<tr>
|
||||
<td class="x-value">{{row.x}}</td>
|
||||
<td>
|
||||
{{row.y}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
<tr class="total-for-period">
|
||||
<td class="x-value">
|
||||
{{i18n 'admin.dashboard.reports.total_for_period'}}
|
||||
</td>
|
||||
<td>
|
||||
{{totalForPeriod}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{#if model.total}}
|
||||
<tr class="total">
|
||||
<td class="x-value">
|
||||
{{i18n 'admin.dashboard.reports.total'}}
|
||||
</td>
|
||||
<td>
|
||||
{{model.total}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
{{/if}}
|
|
@ -1,27 +0,0 @@
|
|||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="table-container">
|
||||
{{#unless hasBlock}}
|
||||
{{#each report.data as |data|}}
|
||||
<a class="table-cell user-{{data.key}}" href="{{data.url}}">
|
||||
<span class="label">
|
||||
{{#if data.icon}}
|
||||
{{d-icon data.icon}}
|
||||
{{/if}}
|
||||
{{data.x}}
|
||||
</span>
|
||||
<span class="value">
|
||||
{{number data.y}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{yield (hash report=report)}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/conditional-loading-section}}
|
|
@ -1,33 +0,0 @@
|
|||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="status">
|
||||
<h4 class="title">
|
||||
<a href="{{report.reportUrl}}">
|
||||
{{report.title}}
|
||||
</a>
|
||||
|
||||
<span class="info" data-tooltip="{{report.description}}">
|
||||
{{d-icon "question-circle"}}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="trend {{report.trend}}">
|
||||
<span class="trend-value" title="{{report.trendTitle}}">
|
||||
{{#if report.average}}
|
||||
{{number report.currentAverage}}{{#if report.percent}}%{{/if}}
|
||||
{{else}}
|
||||
{{number report.currentTotal noTitle="true"}}{{#if report.percent}}%{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if report.trendIcon}}
|
||||
{{d-icon report.trendIcon class="trend-icon"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div class="chart-canvas-container">
|
||||
<canvas class="chart-canvas"></canvas>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
|
@ -1,36 +0,0 @@
|
|||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{{#if report.labels}}
|
||||
{{#each report.labels as |label|}}
|
||||
<th>{{label}}</th>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each report.data as |data|}}
|
||||
<th>{{data.x}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#unless hasBlock}}
|
||||
{{#each report.data as |data|}}
|
||||
<tr>
|
||||
<td>{{number data.y}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{yield (hash report=report)}}
|
||||
{{/unless}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/conditional-loading-section}}
|
|
@ -3,179 +3,26 @@
|
|||
{{#if showVersionChecks}}
|
||||
<div class="section-top">
|
||||
<div class="version-checks">
|
||||
{{partial 'admin/templates/version-checks'}}
|
||||
{{partial "admin/templates/version-checks"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{partial 'admin/templates/dashboard-problems'}}
|
||||
{{partial "admin/templates/dashboard-problems"}}
|
||||
|
||||
<div class="community-health section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
{{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="signups"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="topics"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="posts"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="dau_by_mau"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="daily_engaged_users"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="new_contributors"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
<div class="dashboard-table activity-metrics">
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
|
||||
<div class="table-title">
|
||||
<h3>{{i18n "admin.dashboard.activity_metrics"}}</h3>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each reports as |report|}}
|
||||
{{admin-report-counts report=report allTime=false}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
<ul class="navigation">
|
||||
<li class="navigation-item general">
|
||||
{{#link-to "admin.dashboardNext.general" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.general_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="navigation-item moderation">
|
||||
{{#link-to "admin.dashboardNext.moderation" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.moderation_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="user-metrics">
|
||||
{{dashboard-inline-table dataSourceNames="users_by_type" lastRefreshedAt=lastRefreshedAt}}
|
||||
{{outlet}}
|
||||
|
||||
{{dashboard-inline-table dataSourceNames="users_by_trust_level" lastRefreshedAt=lastRefreshedAt}}
|
||||
</div>
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
|
||||
<div class="misc">
|
||||
{{#if shouldDisplayDurability}}
|
||||
<div class="durability">
|
||||
{{#if currentUser.admin}}
|
||||
<div class="backups">
|
||||
<h3 class="durability-title">
|
||||
<a href="/admin/backups">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
|
||||
</h3>
|
||||
<p>
|
||||
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="uploads">
|
||||
<h3 class="durability-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<p>
|
||||
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="last-dashboard-update">
|
||||
<div>
|
||||
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
|
||||
<p>{{updatedTimestamp}}</p>
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
|
||||
</p>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
<div class="top-referred-topics">
|
||||
{{#dashboard-table
|
||||
dataSourceNames="top_referred_topics"
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
limit=8
|
||||
as |context|}}
|
||||
{{#each context.report.data as |data|}}
|
||||
<tr>
|
||||
<td class='left'>
|
||||
<a href="{{data.topic_url}}">
|
||||
{{data.topic_title}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{data.num_clicks}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/dashboard-table}}
|
||||
</div>
|
||||
|
||||
<div class="trending-search">
|
||||
{{#dashboard-table
|
||||
limit=8
|
||||
dataSourceNames="trending_search"
|
||||
isEnabled=logSearchQueriesEnabled
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"
|
||||
startDate=lastWeek
|
||||
endDate=endDate as |context|}}
|
||||
{{#each context.report.data as |data|}}
|
||||
<tr>
|
||||
<td class='left'>
|
||||
{{data.term}}
|
||||
</td>
|
||||
<td>
|
||||
{{number data.unique_searches}}
|
||||
</td>
|
||||
<td>
|
||||
{{data.ctr}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/dashboard-table}}
|
||||
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{plugin-outlet name="admin-dashboard-bottom"}}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
{{plugin-outlet name="admin-dashboard-general-top"}}
|
||||
|
||||
<div class="community-health section">
|
||||
<div class="period-section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
{{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{admin-report
|
||||
dataSourceName="signups"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="topics"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="posts"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="dau_by_mau"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="daily_engaged_users"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="new_contributors"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
<div class="admin-report activity-metrics">
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
|
||||
<div class="report-header">
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#link-to "adminReports" class="report-link"}}
|
||||
{{i18n "admin.dashboard.activity_metrics"}}
|
||||
{{/link-to}}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-body">
|
||||
<div class="admin-report-table">
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="admin-report-table-header"></th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.today'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.yesterday'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_7_days'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_30_days'}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each activityMetricsReports as |report|}}
|
||||
{{admin-report-counts report=report allTime=false class="admin-report-table-row"}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
|
||||
<div class="user-metrics">
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTypeReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTrustLevelReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
|
||||
<div class="misc">
|
||||
|
||||
{{#if shouldDisplayDurability}}
|
||||
<div class="durability">
|
||||
{{#if currentUser.admin}}
|
||||
<div class="backups">
|
||||
<h3 class="durability-title">
|
||||
<a href="/admin/backups">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
|
||||
</h3>
|
||||
<p>
|
||||
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="uploads">
|
||||
<h3 class="durability-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<p>
|
||||
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="last-dashboard-update">
|
||||
<div>
|
||||
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
|
||||
<p>{{updatedTimestamp}}</p>
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
|
||||
</p>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
{{admin-report
|
||||
report=topReferredTopicsReport
|
||||
reportOptions=topReferredTopicsTopions}}
|
||||
|
||||
{{admin-report
|
||||
reportOptions=trendingSearchOptions
|
||||
report=trendingSearchReport
|
||||
isEnabled=logSearchQueriesEnabled
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="admin-dashboard-general-bottom"}}
|
||||
{{/conditional-loading-spinner}}
|
|
@ -0,0 +1,40 @@
|
|||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
<div class="sections">
|
||||
{{plugin-outlet name="admin-dashboard-moderation-top"}}
|
||||
|
||||
<div class="moderators-activity section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.moderators_activity"}}</h2>
|
||||
{{period-chooser
|
||||
period=period
|
||||
action="changePeriod"
|
||||
content=availablePeriods
|
||||
fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
{{admin-report
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
showHeader=false
|
||||
dataSourceName="moderators_activity"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-section">
|
||||
{{admin-report
|
||||
report=flagsStatusReport
|
||||
startDate=lastWeek
|
||||
reportOptions=flagsStatusOptions
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
report=postEditsReport
|
||||
startDate=lastWeek
|
||||
endDate=endDate}}
|
||||
|
||||
{{plugin-outlet name="admin-dashboard-moderation-bottom"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/conditional-loading-spinner}}
|
|
@ -1,60 +1,16 @@
|
|||
<h3>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
|
||||
|
|
||||
|
||||
{{model.title}}
|
||||
</h3>
|
||||
|
||||
{{#if model.description}}
|
||||
<p>{{model.description}}</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="report-container">
|
||||
<div class="visualization">
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
<div class='view-options'>
|
||||
{{#if viewingTable}}
|
||||
{{i18n 'admin.dashboard.reports.view_table'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if viewingGraph}}
|
||||
{{i18n 'admin.dashboard.reports.view_graph'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if viewingGraph}}
|
||||
{{admin-graph model=model}}
|
||||
{{else}}
|
||||
{{admin-table-report model=model}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-table-report model=model.relatedReport}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
|
||||
</span>
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
|
||||
</span>
|
||||
{{#if showCategoryOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{/if}}
|
||||
{{#if showGroupOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
|
||||
{{/if}}
|
||||
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
|
||||
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
{{admin-report
|
||||
showAllReportsLink=true
|
||||
dataSourceName=model.type
|
||||
categoryId=category_id
|
||||
groupId=group_id
|
||||
reportOptions=reportOptions
|
||||
startDate=start_date
|
||||
endDate=end_date
|
||||
showFilteringUI=true
|
||||
onSelectCategory=(action "onSelectCategory")
|
||||
onSelectStartDate=(action "onSelectStartDate")
|
||||
onSelectEndDate=(action "onSelectEndDate")}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,8 @@ export default DatePicker.extend({
|
|||
|
||||
_opts() {
|
||||
return {
|
||||
defaultDate: new Date(this.get("defaultDate")) || new Date(),
|
||||
defaultDate:
|
||||
moment(this.get("defaultDate"), "YYYY-MM-DD").toDate() || new Date(),
|
||||
setDefaultDate: !!this.get("defaultDate"),
|
||||
maxDate: new Date()
|
||||
};
|
||||
|
|
|
@ -29,13 +29,17 @@ export default Ember.Component.extend({
|
|||
weekdays: moment.weekdays(),
|
||||
weekdaysShort: moment.weekdaysShort()
|
||||
},
|
||||
onSelect: date =>
|
||||
this.set(
|
||||
"value",
|
||||
moment(date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD")
|
||||
)
|
||||
onSelect: date => {
|
||||
const formattedDate = moment(date).format("YYYY-MM-DD");
|
||||
|
||||
if (this.attrs.onSelect) {
|
||||
this.attrs.onSelect(formattedDate);
|
||||
}
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
|
||||
this.set("value", formattedDate);
|
||||
}
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||
|
|
|
@ -591,6 +591,10 @@ export function clipboardData(e, canUpload) {
|
|||
return { clipboard, types, canUpload, canPasteHtml };
|
||||
}
|
||||
|
||||
export function isNumeric(input) {
|
||||
return !isNaN(parseFloat(input)) && isFinite(input);
|
||||
}
|
||||
|
||||
export function fillMissingDates(data, startDate, endDate) {
|
||||
const startMoment = moment(startDate, "YYYY-MM-DD");
|
||||
const endMoment = moment(endDate, "YYYY-MM-DD");
|
||||
|
|
|
@ -447,7 +447,7 @@ export default Ember.Component.extend(
|
|||
if (get(this.actions, actionName)) {
|
||||
run.next(() => this.send(actionName, ...params));
|
||||
} else if (this.get(actionName)) {
|
||||
run.next(() => this.get(actionName)());
|
||||
run.next(() => this.get(actionName)(...params));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -932,4 +932,8 @@ table#user-badges {
|
|||
@import "common/admin/backups";
|
||||
@import "common/admin/plugins";
|
||||
@import "common/admin/admin_reports";
|
||||
@import "common/admin/admin_report";
|
||||
@import "common/admin/admin_report_chart";
|
||||
@import "common/admin/admin_report_table";
|
||||
@import "common/admin/admin_report_inline_table";
|
||||
@import "common/admin/dashboard_previous";
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
.admin-report {
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.report-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: $font-up-1;
|
||||
.separator {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.report-link {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.separator + .report-link {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
cursor: pointer;
|
||||
margin-left: 0.25em;
|
||||
color: $primary-low-mid;
|
||||
|
||||
&:hover {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trend {
|
||||
align-items: center;
|
||||
|
||||
&.trending-down,
|
||||
&.high-trending-down {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&.trending-up,
|
||||
&.high-trending-up {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.no-change {
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
font-size: $font-up-1;
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
font-size: $font-up-1;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
.mode {
|
||||
display: inline;
|
||||
|
||||
.mode-button.current {
|
||||
color: $tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.admin-report-table,
|
||||
.admin-report-chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.report-filters {
|
||||
margin-left: 1em;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.filtering-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.filtering-label {
|
||||
}
|
||||
.filtering-input {
|
||||
width: 100%;
|
||||
|
||||
.date-picker-wrapper,
|
||||
.combo-box,
|
||||
.export-btn {
|
||||
width: 100%;
|
||||
|
||||
.date-picker {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-filters:only-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
.admin-report-chart {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.admin-report-inline-table {
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
.admin-report-table {
|
||||
@media screen and (max-width: 650px) {
|
||||
table {
|
||||
tbody tr td {
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5em;
|
||||
|
||||
button {
|
||||
margin-left: 0.5em;
|
||||
|
||||
&.current {
|
||||
color: $tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.two-columns {
|
||||
.report-table tbody tr td:first-child,
|
||||
.report-table thead tr th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
table-layout: auto;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
border: 1px solid $primary-low;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
tbody {
|
||||
border: none;
|
||||
|
||||
tr {
|
||||
td {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
border: 1px solid $primary-low;
|
||||
|
||||
.admin-report-table-header {
|
||||
.sort-button {
|
||||
outline: none;
|
||||
background: none;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.is-current-sort {
|
||||
.d-icon {
|
||||
color: $tertiary;
|
||||
}
|
||||
|
||||
.sort-button:hover {
|
||||
color: $primary-medium;
|
||||
background: $primary-low;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-current-sort) .sort-button {
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
color: $primary-medium;
|
||||
background: $primary-low;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
th {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,16 @@
|
|||
.admin-reports {
|
||||
h3 {
|
||||
border-bottom: 1px solid $primary-low;
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
.admin-report {
|
||||
width: 100%;
|
||||
|
||||
.report-header {
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
border-bottom: 1px solid $primary-low;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report-chart .chart-canvas-container .chart-canvas {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.reports-list {
|
||||
|
|
|
@ -10,6 +10,40 @@
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
.navigation-item {
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.navigation-link {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin active-navigation-item {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border: 1px solid $primary-low;
|
||||
border-bottom: 10px solid $secondary;
|
||||
}
|
||||
|
||||
&.moderation .navigation-item.moderation {
|
||||
@include active-navigation-item;
|
||||
}
|
||||
|
||||
&.general .navigation-item.general {
|
||||
@include active-navigation-item;
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -177,7 +211,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.community-health {
|
||||
.section {
|
||||
.period-chooser .period-chooser-header {
|
||||
.selected-name,
|
||||
.d-icon {
|
||||
|
@ -203,105 +237,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dashboard-mini-chart {
|
||||
.status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0 0.45em 0 0;
|
||||
|
||||
.title {
|
||||
font-size: $font-up-1;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.info {
|
||||
cursor: pointer;
|
||||
margin-left: 0.25em;
|
||||
color: $primary-low-mid;
|
||||
|
||||
&:hover {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trend {
|
||||
align-items: center;
|
||||
|
||||
&.trending-down,
|
||||
&.high-trending-down {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&.trending-up,
|
||||
&.high-trending-up {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.no-change {
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
font-size: $font-up-1;
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
font-size: $font-up-1;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conditional-loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.loading-container.visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 1em 0;
|
||||
a,
|
||||
a:visited {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
.admin-report-table {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.is-disabled {
|
||||
|
@ -320,92 +256,14 @@
|
|||
&.is-loading {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
border: 1px solid $primary-low;
|
||||
|
||||
thead {
|
||||
border: 1px solid $primary-low;
|
||||
tr {
|
||||
th {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
border-top: none;
|
||||
tr {
|
||||
td:first-child {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
td.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.value {
|
||||
text-align: right;
|
||||
padding: 8px 21px 8px 8px; // accounting for negative right caret margin
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px 12px 8px;
|
||||
}
|
||||
i {
|
||||
margin-right: -12px; // align on caret
|
||||
@media screen and (max-width: 650px) {
|
||||
margin-right: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-up,
|
||||
&.trending-up {
|
||||
i {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
&.high-trending-down,
|
||||
&.trending-down {
|
||||
i {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-left: -5%;
|
||||
margin: 2em 0 0.75em -5%; // Negative margin allows for a margin when in 2-columns,
|
||||
flex-direction: column;
|
||||
|
||||
.dashboard-inline-table {
|
||||
// and "hides" margin when the item spans 100% width
|
||||
flex: 1 0 auto;
|
||||
|
@ -468,23 +326,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dashboard-inline-table {
|
||||
margin-left: 5%;
|
||||
margin-bottom: 1.25em;
|
||||
|
||||
.table-title {
|
||||
border-bottom: 1px solid $primary-low;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table.activity-metrics {
|
||||
.admin-report.activity-metrics {
|
||||
table {
|
||||
@media screen and (min-width: 400px) {
|
||||
table-layout: auto;
|
||||
|
@ -505,6 +347,60 @@
|
|||
padding: 8px 0 8px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td {
|
||||
text-overflow: unset;
|
||||
overflow: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
td.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.value {
|
||||
text-align: right;
|
||||
padding: 8px 21px 8px 8px; // accounting for negative right caret margin
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px 12px 8px;
|
||||
}
|
||||
i {
|
||||
margin-right: -12px; // align on caret
|
||||
@media screen and (max-width: 650px) {
|
||||
margin-right: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-up,
|
||||
&.trending-up {
|
||||
i {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
&.high-trending-down,
|
||||
&.trending-down {
|
||||
i {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -547,3 +443,81 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.community-health.section {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.admin-report.activity-metrics {
|
||||
table {
|
||||
table-layout: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.users-by-type {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.admin-report.users-by-type,
|
||||
.admin-report.users-by-trust-level {
|
||||
margin-bottom: 1em;
|
||||
flex: 1;
|
||||
.report-header {
|
||||
border-bottom: 1px solid $primary-medium;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.moderators-activity {
|
||||
tbody tr td.username,
|
||||
thead tr th.username {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.trending-search {
|
||||
tbody tr td.term,
|
||||
thead tr th.term {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.top-traffic-sources {
|
||||
tbody tr td.domain,
|
||||
thead tr th.domain {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-next.moderation {
|
||||
.admin-dashboard-moderation-top {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 1em;
|
||||
}
|
||||
|
||||
.main-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 1em;
|
||||
|
||||
> * {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.admin-dashboard-moderation-bottom-outlet {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.flags-status {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.admin-report.post-edits {
|
||||
grid-column: span 6;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,35 @@ require 'disk_space'
|
|||
|
||||
class Admin::DashboardNextController < Admin::AdminController
|
||||
def index
|
||||
dashboard_data = AdminDashboardNextData.fetch_cached_stats
|
||||
dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks?
|
||||
dashboard_data[:disk_space] = DiskSpace.cached_stats if SiteSetting.enable_backups
|
||||
render json: dashboard_data
|
||||
data = AdminDashboardNextIndexData.fetch_cached_stats
|
||||
|
||||
if SiteSetting.version_checks?
|
||||
data.merge!(version_check: DiscourseUpdates.check_version.as_json)
|
||||
end
|
||||
|
||||
render json: data
|
||||
end
|
||||
|
||||
def moderation
|
||||
render json: AdminDashboardNextModerationData.fetch_cached_stats
|
||||
end
|
||||
|
||||
def general
|
||||
data = AdminDashboardNextGeneralData.fetch_cached_stats
|
||||
|
||||
if SiteSetting.enable_backups
|
||||
data[:last_backup_taken_at] = last_backup_taken_at
|
||||
data[:disk_space] = DiskSpace.cached_stats
|
||||
end
|
||||
|
||||
render json: data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def last_backup_taken_at
|
||||
if last_backup = Backup.all.last
|
||||
File.ctime(last_backup.path).utc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
require_dependency 'admin_dashboard_data'
|
||||
require_dependency 'admin_dashboard_next_index_data'
|
||||
require_dependency 'admin_dashboard_next_general_data'
|
||||
require_dependency 'admin_dashboard_next_moderation_data'
|
||||
require_dependency 'group'
|
||||
require_dependency 'group_message'
|
||||
|
||||
|
@ -15,7 +18,9 @@ module Jobs
|
|||
end
|
||||
|
||||
# TODO: decide if we want to keep caching this every 30 minutes
|
||||
AdminDashboardNextData.refresh_stats
|
||||
AdminDashboardNextIndexData.refresh_stats
|
||||
AdminDashboardNextGeneralData.refresh_stats
|
||||
AdminDashboardNextModerationData.refresh_stats
|
||||
AdminDashboardData.refresh_stats
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,51 +1,31 @@
|
|||
class AdminDashboardNextData
|
||||
include StatsCacheable
|
||||
|
||||
REPORTS = %w{
|
||||
page_view_total_reqs
|
||||
visits
|
||||
time_to_first_response
|
||||
likes
|
||||
flags
|
||||
user_to_user_private_messages_with_replies
|
||||
}
|
||||
|
||||
def initialize(opts = {})
|
||||
@opts = opts
|
||||
end
|
||||
|
||||
def self.fetch_stats
|
||||
AdminDashboardNextData.new.as_json
|
||||
self.class.new.as_json
|
||||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'dash-next-stats'
|
||||
def self.fetch_stats
|
||||
new.as_json
|
||||
end
|
||||
|
||||
def get_json
|
||||
{}
|
||||
end
|
||||
|
||||
def as_json(_options = nil)
|
||||
@json ||= get_json
|
||||
end
|
||||
|
||||
def get_json
|
||||
json = {
|
||||
reports: AdminDashboardNextData.reports(REPORTS),
|
||||
updated_at: Time.zone.now.as_json
|
||||
}
|
||||
|
||||
if SiteSetting.enable_backups
|
||||
json[:last_backup_taken_at] = last_backup_taken_at
|
||||
end
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
def last_backup_taken_at
|
||||
if last_backup = Backup.all.last
|
||||
File.ctime(last_backup.path).utc
|
||||
end
|
||||
end
|
||||
|
||||
def self.reports(source)
|
||||
source.map { |type| Report.find(type).as_json }
|
||||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'dashboard-next-data'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
class AdminDashboardNextGeneralData < AdminDashboardNextData
|
||||
def reports
|
||||
@reports ||= %w{
|
||||
page_view_total_reqs
|
||||
visits
|
||||
time_to_first_response
|
||||
likes
|
||||
flags
|
||||
user_to_user_private_messages_with_replies
|
||||
top_referred_topics
|
||||
users_by_type
|
||||
users_by_trust_level
|
||||
trending_search
|
||||
}
|
||||
end
|
||||
|
||||
def get_json
|
||||
{
|
||||
reports: self.class.reports(reports),
|
||||
updated_at: Time.zone.now.as_json
|
||||
}
|
||||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'general-dashboard-data'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
class AdminDashboardNextIndexData < AdminDashboardNextData
|
||||
def get_json
|
||||
{
|
||||
updated_at: Time.zone.now.as_json
|
||||
}
|
||||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'index-dashboard-data'
|
||||
end
|
||||
|
||||
# TODO: problems should be loaded from this model
|
||||
# and not from a separate model/route
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
class AdminDashboardNextModerationData < AdminDashboardNextData
|
||||
def reports
|
||||
@reports ||= %w{
|
||||
flags_status
|
||||
post_edits
|
||||
}
|
||||
end
|
||||
|
||||
def get_json
|
||||
{
|
||||
reports: self.class.reports(reports),
|
||||
updated_at: Time.zone.now.as_json
|
||||
}
|
||||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'moderation-dashboard-data'
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
class IncomingLinksReport
|
||||
|
||||
attr_accessor :type, :data, :y_titles, :start_date, :limit
|
||||
attr_accessor :type, :data, :y_titles, :start_date, :end_date, :limit
|
||||
|
||||
def initialize(type)
|
||||
@type = type
|
||||
|
@ -15,7 +15,8 @@ class IncomingLinksReport
|
|||
xaxis: I18n.t("reports.#{self.type}.xaxis"),
|
||||
ytitles: self.y_titles,
|
||||
data: self.data,
|
||||
start_date: start_date
|
||||
start_date: start_date,
|
||||
end_date: end_date
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -27,6 +28,7 @@ class IncomingLinksReport
|
|||
report = IncomingLinksReport.new(type)
|
||||
|
||||
report.start_date = _opts[:start_date] || 30.days.ago
|
||||
report.end_date = _opts[:end_date] || Time.now.end_of_day
|
||||
report.limit = _opts[:limit].to_i if _opts[:limit]
|
||||
|
||||
send(report_method, report)
|
||||
|
@ -38,8 +40,8 @@ class IncomingLinksReport
|
|||
report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks")
|
||||
report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics")
|
||||
|
||||
num_clicks = link_count_per_user(start_date: report.start_date)
|
||||
num_topics = topic_count_per_user(start_date: report.start_date)
|
||||
num_clicks = link_count_per_user(start_date: report.start_date, end_date: report.end_date)
|
||||
num_topics = topic_count_per_user(start_date: report.start_date, end_date: report.end_date)
|
||||
user_id_lookup = User.where(username: num_clicks.keys).select(:id, :username).inject({}) { |sum, v| sum[v.username] = v.id; sum; }
|
||||
report.data = []
|
||||
num_clicks.each_key do |username|
|
||||
|
@ -48,19 +50,19 @@ class IncomingLinksReport
|
|||
report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10]
|
||||
end
|
||||
|
||||
def self.per_user(start_date:)
|
||||
def self.per_user(start_date:, end_date:)
|
||||
@per_user_query ||= public_incoming_links
|
||||
.where('incoming_links.created_at > ? AND incoming_links.user_id IS NOT NULL', start_date)
|
||||
.where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND incoming_links.user_id IS NOT NULL', start_date, end_date)
|
||||
.joins(:user)
|
||||
.group('users.username')
|
||||
end
|
||||
|
||||
def self.link_count_per_user(start_date:)
|
||||
per_user(start_date: start_date).count
|
||||
def self.link_count_per_user(start_date:, end_date:)
|
||||
per_user(start_date: start_date, end_date: end_date).count
|
||||
end
|
||||
|
||||
def self.topic_count_per_user(start_date:)
|
||||
per_user(start_date: start_date).joins(:post).count("DISTINCT posts.topic_id")
|
||||
def self.topic_count_per_user(start_date:, end_date:)
|
||||
per_user(start_date: start_date, end_date: end_date).joins(:post).count("DISTINCT posts.topic_id")
|
||||
end
|
||||
|
||||
# Return top 10 domains that brought traffic to the site within the last 30 days
|
||||
|
@ -69,7 +71,7 @@ class IncomingLinksReport
|
|||
report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics")
|
||||
report.y_titles[:num_users] = I18n.t("reports.#{report.type}.num_users")
|
||||
|
||||
num_clicks = link_count_per_domain(start_date: report.start_date)
|
||||
num_clicks = link_count_per_domain(start_date: report.start_date, end_date: report.end_date)
|
||||
num_topics = topic_count_per_domain(num_clicks.keys)
|
||||
report.data = []
|
||||
num_clicks.each_key do |domain|
|
||||
|
@ -78,9 +80,9 @@ class IncomingLinksReport
|
|||
report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10]
|
||||
end
|
||||
|
||||
def self.link_count_per_domain(limit: 10, start_date:)
|
||||
def self.link_count_per_domain(limit: 10, start_date:, end_date:)
|
||||
public_incoming_links
|
||||
.where('incoming_links.created_at > ?', start_date)
|
||||
.where('incoming_links.created_at > ? AND incoming_links.created_at < ?', start_date, end_date)
|
||||
.joins(incoming_referer: :incoming_domain)
|
||||
.group('incoming_domains.name')
|
||||
.order('count_all DESC')
|
||||
|
@ -102,7 +104,7 @@ class IncomingLinksReport
|
|||
|
||||
def self.report_top_referred_topics(report)
|
||||
report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks")
|
||||
num_clicks = link_count_per_topic(start_date: report.start_date)
|
||||
num_clicks = link_count_per_topic(start_date: report.start_date, end_date: report.end_date)
|
||||
num_clicks = num_clicks.to_a.sort_by { |x| x[1] }.last(report.limit || 10).reverse
|
||||
report.data = []
|
||||
topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] })
|
||||
|
@ -115,9 +117,9 @@ class IncomingLinksReport
|
|||
report.data
|
||||
end
|
||||
|
||||
def self.link_count_per_topic(start_date:)
|
||||
def self.link_count_per_topic(start_date:, end_date:)
|
||||
public_incoming_links
|
||||
.where('incoming_links.created_at > ? AND topic_id IS NOT NULL', start_date)
|
||||
.where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND topic_id IS NOT NULL', start_date, end_date)
|
||||
.group('topic_id')
|
||||
.count
|
||||
end
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
require_dependency 'topic_subtype'
|
||||
|
||||
class Report
|
||||
|
||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||
:end_date, :category_id, :group_id, :labels, :async,
|
||||
:prev_period, :facets, :limit, :processing, :average, :percent,
|
||||
:higher_is_better
|
||||
:higher_is_better, :icon, :modes, :category_filtering,
|
||||
:group_filtering, :prev_data, :prev_start_date, :prev_end_date,
|
||||
:dates_filtering, :timeout
|
||||
|
||||
def self.default_days
|
||||
30
|
||||
|
@ -15,9 +16,15 @@ class Report
|
|||
@type = type
|
||||
@start_date ||= Report.default_days.days.ago.beginning_of_day
|
||||
@end_date ||= Time.zone.now.end_of_day
|
||||
@prev_end_date = @start_date
|
||||
@average = false
|
||||
@percent = false
|
||||
@higher_is_better = true
|
||||
@category_filtering = false
|
||||
@group_filtering = false
|
||||
@modes = [:table, :chart]
|
||||
@prev_data = nil
|
||||
@dates_filtering = true
|
||||
end
|
||||
|
||||
def self.cache_key(report)
|
||||
|
@ -39,6 +46,28 @@ class Report
|
|||
end
|
||||
end
|
||||
|
||||
def self.wrap_slow_query(timeout = 20000)
|
||||
begin
|
||||
ActiveRecord::Base.connection.transaction do
|
||||
# Set a statement timeout so we can't tie up the server
|
||||
DB.exec "SET LOCAL statement_timeout = #{timeout}"
|
||||
yield
|
||||
end
|
||||
rescue ActiveRecord::QueryCanceled
|
||||
return :timeout
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def prev_start_date
|
||||
self.start_date - (self.end_date - self.start_date)
|
||||
end
|
||||
|
||||
def prev_end_date
|
||||
self.start_date
|
||||
end
|
||||
|
||||
def as_json(options = nil)
|
||||
description = I18n.t("reports.#{type}.description", default: "")
|
||||
|
||||
|
@ -51,18 +80,29 @@ class Report
|
|||
data: data,
|
||||
start_date: start_date&.iso8601,
|
||||
end_date: end_date&.iso8601,
|
||||
prev_data: self.prev_data,
|
||||
prev_start_date: prev_start_date&.iso8601,
|
||||
prev_end_date: prev_end_date&.iso8601,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
prev30Days: self.prev30Days,
|
||||
dates_filtering: self.dates_filtering,
|
||||
report_key: Report.cache_key(self),
|
||||
labels: labels,
|
||||
labels: labels || [
|
||||
{ type: :date, properties: [:x], title: I18n.t("reports.default.labels.day") },
|
||||
{ type: :number, properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
],
|
||||
processing: self.processing,
|
||||
average: self.average,
|
||||
percent: self.percent,
|
||||
higher_is_better: self.higher_is_better
|
||||
higher_is_better: self.higher_is_better,
|
||||
category_filtering: self.category_filtering,
|
||||
group_filtering: self.group_filtering,
|
||||
modes: self.modes
|
||||
}.tap do |json|
|
||||
json[:total] = total if total
|
||||
json[:prev_period] = prev_period if prev_period
|
||||
json[:timeout] = self.timeout if self.timeout
|
||||
json[:total] = self.total if self.total
|
||||
json[:prev_period] = self.prev_period if self.prev_period
|
||||
json[:prev30Days] = self.prev30Days if self.prev30Days
|
||||
json[:limit] = self.limit if self.limit
|
||||
|
||||
|
@ -105,6 +145,8 @@ class Report
|
|||
end
|
||||
|
||||
def self.find(type, opts = nil)
|
||||
clear_cache
|
||||
|
||||
report = _get(type, opts)
|
||||
report_method = :"report_#{type}"
|
||||
|
||||
|
@ -129,6 +171,10 @@ class Report
|
|||
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
||||
end
|
||||
|
||||
if filter == :page_view_total
|
||||
report.icon = 'file'
|
||||
end
|
||||
|
||||
report.data = []
|
||||
data.where('date >= ? AND date <= ?', report.start_date, report.end_date)
|
||||
.order(date: :asc)
|
||||
|
@ -147,6 +193,9 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_visits(report)
|
||||
report.group_filtering = true
|
||||
report.icon = 'user'
|
||||
|
||||
basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id
|
||||
|
||||
add_counts report, UserVisit, 'visited_at'
|
||||
|
@ -159,13 +208,16 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_signups(report)
|
||||
report.group_filtering = true
|
||||
|
||||
if report.group_id
|
||||
basic_report_about report, User.real, :count_by_signup_date, report.start_date, report.end_date, report.group_id
|
||||
add_counts report, User.real, 'users.created_at'
|
||||
else
|
||||
|
||||
report_about report, User.real, :count_by_signup_date
|
||||
end
|
||||
|
||||
add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date
|
||||
end
|
||||
|
||||
def self.report_new_contributors(report)
|
||||
|
@ -183,8 +235,9 @@ class Report
|
|||
end
|
||||
|
||||
if report.facets.include?(:prev_period)
|
||||
prev_period_data = User.real.count_by_first_post(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
prev_period_data = User.real.count_by_first_post(report.prev_start_date, report.prev_end_date)
|
||||
report.prev_period = prev_period_data.sum { |k, v| v }
|
||||
report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } }
|
||||
end
|
||||
|
||||
data.each do |key, value|
|
||||
|
@ -209,7 +262,7 @@ class Report
|
|||
end
|
||||
|
||||
if report.facets.include?(:prev_period)
|
||||
prev_data = UserAction.count_daily_engaged_users(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
prev_data = UserAction.count_daily_engaged_users(report.prev_start_date, report.prev_end_date)
|
||||
|
||||
prev = prev_data.sum { |k, v| v }
|
||||
if prev > 0
|
||||
|
@ -252,7 +305,7 @@ class Report
|
|||
end
|
||||
|
||||
if report.facets.include?(:prev_period)
|
||||
report.prev_period = dau_avg.call(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
report.prev_period = dau_avg.call(report.prev_start_date, report.prev_end_date)
|
||||
end
|
||||
|
||||
if report.facets.include?(:prev30Days)
|
||||
|
@ -261,6 +314,7 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_profile_views(report)
|
||||
report.group_filtering = true
|
||||
start_date = report.start_date
|
||||
end_date = report.end_date
|
||||
basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id
|
||||
|
@ -270,6 +324,7 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_topics(report)
|
||||
report.category_filtering = true
|
||||
basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id
|
||||
countable = Topic.listable_topics
|
||||
countable = countable.where(category_id: report.category_id) if report.category_id
|
||||
|
@ -277,6 +332,8 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_posts(report)
|
||||
report.modes = [:table, :chart]
|
||||
report.category_filtering = true
|
||||
basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, report.category_id
|
||||
countable = Post.public_posts.where(post_type: Post.types[:regular])
|
||||
countable = countable.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
|
||||
|
@ -284,6 +341,8 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_time_to_first_response(report)
|
||||
report.category_filtering = true
|
||||
report.icon = 'reply'
|
||||
report.higher_is_better = false
|
||||
report.data = []
|
||||
Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r|
|
||||
|
@ -294,6 +353,7 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_topics_with_no_response(report)
|
||||
report.category_filtering = true
|
||||
report.data = []
|
||||
Topic.with_no_response_per_day(report.start_date, report.end_date, report.category_id).each do |r|
|
||||
report.data << { x: r["date"], y: r["count"].to_i }
|
||||
|
@ -319,12 +379,21 @@ class Report
|
|||
end
|
||||
end
|
||||
|
||||
def self.add_prev_data(report, subject_class, report_method, *args)
|
||||
if report.modes.include?(:chart) && report.facets.include?(:prev_period)
|
||||
prev_data = subject_class.send(report_method, *args)
|
||||
report.prev_data = prev_data.map { |k, v| { x: k, y: v } }
|
||||
end
|
||||
end
|
||||
|
||||
def self.add_counts(report, subject_class, query_column = 'created_at')
|
||||
if report.facets.include?(:prev_period)
|
||||
report.prev_period = subject_class
|
||||
prev_data = subject_class
|
||||
.where("#{query_column} >= ? and #{query_column} < ?",
|
||||
(report.start_date - (report.end_date - report.start_date)),
|
||||
report.start_date).count
|
||||
report.prev_start_date,
|
||||
report.prev_end_date)
|
||||
|
||||
report.prev_period = prev_data.count
|
||||
end
|
||||
|
||||
if report.facets.include?(:total)
|
||||
|
@ -342,6 +411,15 @@ class Report
|
|||
def self.report_users_by_trust_level(report)
|
||||
report.data = []
|
||||
|
||||
report.modes = [:table]
|
||||
|
||||
report.dates_filtering = false
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:key], title: I18n.t("reports.users_by_trust_level.labels.level") },
|
||||
{ properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
]
|
||||
|
||||
User.real.group('trust_level').count.sort.each do |level, count|
|
||||
key = TrustLevel.levels[level.to_i]
|
||||
url = Proc.new { |k| "/admin/users/list/#{k}" }
|
||||
|
@ -351,6 +429,8 @@ class Report
|
|||
|
||||
# Post action counts:
|
||||
def self.report_flags(report)
|
||||
report.category_filtering = true
|
||||
report.icon = 'flag'
|
||||
report.higher_is_better = false
|
||||
|
||||
basic_report_about report, PostAction, :flag_count_by_date, report.start_date, report.end_date, report.category_id
|
||||
|
@ -360,10 +440,14 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_likes(report)
|
||||
report.category_filtering = true
|
||||
report.icon = 'heart'
|
||||
post_action_report report, PostActionType.types[:like]
|
||||
end
|
||||
|
||||
def self.report_bookmarks(report)
|
||||
report.category_filtering = true
|
||||
report.icon = 'bookmark'
|
||||
post_action_report report, PostActionType.types[:bookmark]
|
||||
end
|
||||
|
||||
|
@ -380,47 +464,68 @@ class Report
|
|||
# Private messages counts:
|
||||
|
||||
def self.private_messages_report(report, topic_subtype)
|
||||
report.icon = 'envelope'
|
||||
basic_report_about report, Topic, :private_message_topics_count_per_day, report.start_date, report.end_date, topic_subtype
|
||||
add_counts report, Topic.private_messages.with_subtype(topic_subtype), 'topics.created_at'
|
||||
end
|
||||
|
||||
def self.report_user_to_user_private_messages(report)
|
||||
report.icon = 'envelope'
|
||||
private_messages_report report, TopicSubtype.user_to_user
|
||||
end
|
||||
|
||||
def self.report_user_to_user_private_messages_with_replies(report)
|
||||
report.icon = 'envelope'
|
||||
topic_subtype = TopicSubtype.user_to_user
|
||||
basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype
|
||||
add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at'
|
||||
end
|
||||
|
||||
def self.report_system_private_messages(report)
|
||||
report.icon = 'envelope'
|
||||
private_messages_report report, TopicSubtype.system_message
|
||||
end
|
||||
|
||||
def self.report_moderator_warning_private_messages(report)
|
||||
report.icon = 'envelope'
|
||||
private_messages_report report, TopicSubtype.moderator_warning
|
||||
end
|
||||
|
||||
def self.report_notify_moderators_private_messages(report)
|
||||
report.icon = 'envelope'
|
||||
private_messages_report report, TopicSubtype.notify_moderators
|
||||
end
|
||||
|
||||
def self.report_notify_user_private_messages(report)
|
||||
report.icon = 'envelope'
|
||||
private_messages_report report, TopicSubtype.notify_user
|
||||
end
|
||||
|
||||
def self.report_web_crawlers(report)
|
||||
report.labels = [
|
||||
{ type: :string, properties: [:user_agent], title: I18n.t("reports.web_crawlers.labels.user_agent") },
|
||||
{ properties: [:count], title: I18n.t("reports.web_crawlers.labels.page_views") }
|
||||
]
|
||||
report.modes = [:table]
|
||||
report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date)
|
||||
.limit(200)
|
||||
.order('sum_count DESC')
|
||||
.group(:user_agent).sum(:count)
|
||||
.map { |ua, count| { x: ua, y: count } }
|
||||
.map { |ua, count| { user_agent: ua, count: count } }
|
||||
end
|
||||
|
||||
def self.report_users_by_type(report)
|
||||
report.data = []
|
||||
|
||||
report.modes = [:table]
|
||||
|
||||
report.dates_filtering = false
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:x], title: I18n.t("reports.users_by_type.labels.type") },
|
||||
{ properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
]
|
||||
|
||||
label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") }
|
||||
url = Proc.new { |key| "/admin/users/list/#{key}" }
|
||||
|
||||
|
@ -438,15 +543,49 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_top_referred_topics(report)
|
||||
report.labels = [I18n.t("reports.top_referred_topics.xaxis"),
|
||||
I18n.t("reports.top_referred_topics.num_clicks")]
|
||||
result = IncomingLinksReport.find(:top_referred_topics, start_date: 7.days.ago, limit: report.limit)
|
||||
report.data = result.data
|
||||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:topic_title, :topic_url], title: I18n.t("reports.top_referred_topics.labels.topic") },
|
||||
{ properties: [:num_clicks], title: I18n.t("reports.top_referred_topics.labels.num_clicks") }
|
||||
]
|
||||
|
||||
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
|
||||
result = nil
|
||||
report.timeout = wrap_slow_query do
|
||||
result = IncomingLinksReport.find(:top_referred_topics, options)
|
||||
report.data = result.data
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_top_traffic_sources(report)
|
||||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:domain], title: I18n.t("reports.top_traffic_sources.labels.domain") },
|
||||
{ properties: [:num_clicks], title: I18n.t("reports.top_traffic_sources.labels.num_clicks") },
|
||||
{ properties: [:num_topics], title: I18n.t("reports.top_traffic_sources.labels.num_topics") }
|
||||
]
|
||||
|
||||
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
|
||||
result = nil
|
||||
report.timeout = wrap_slow_query do
|
||||
result = IncomingLinksReport.find(:top_traffic_sources, options)
|
||||
report.data = result.data
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_trending_search(report)
|
||||
report.labels = [
|
||||
{ properties: [:term], title: I18n.t("reports.trending_search.labels.term") },
|
||||
{ properties: [:unique_searches], title: I18n.t("reports.trending_search.labels.searches") },
|
||||
{ type: :percent, properties: [:ctr], title: I18n.t("reports.trending_search.labels.click_through") }
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
||||
report.modes = [:table]
|
||||
|
||||
select_sql = <<~SQL
|
||||
lower(term) term,
|
||||
COUNT(*) AS searches,
|
||||
|
@ -463,10 +602,6 @@ class Report
|
|||
.order('unique_searches DESC, click_through ASC, term ASC')
|
||||
.limit(report.limit || 20).to_a
|
||||
|
||||
report.labels = [:term, :searches, :click_through].map { |key|
|
||||
I18n.t("reports.trending_search.labels.#{key}")
|
||||
}
|
||||
|
||||
trends.each do |trend|
|
||||
ctr =
|
||||
if trend.click_through == 0 || trend.searches == 0
|
||||
|
@ -478,8 +613,300 @@ class Report
|
|||
report.data << {
|
||||
term: trend.term,
|
||||
unique_searches: trend.unique_searches,
|
||||
ctr: (ctr * 100).ceil(1).to_s + "%"
|
||||
ctr: (ctr * 100).ceil(1)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_moderators_activity(report)
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:username, :user_url], title: I18n.t("reports.moderators_activity.labels.moderator") },
|
||||
{ properties: [:flag_count], title: I18n.t("reports.moderators_activity.labels.flag_count") },
|
||||
{ type: :seconds, properties: [:time_read], title: I18n.t("reports.moderators_activity.labels.time_read") },
|
||||
{ properties: [:topic_count], title: I18n.t("reports.moderators_activity.labels.topic_count") },
|
||||
{ properties: [:post_count], title: I18n.t("reports.moderators_activity.labels.post_count") }
|
||||
]
|
||||
|
||||
report.modes = [:table]
|
||||
|
||||
report.data = []
|
||||
mod_data = {}
|
||||
|
||||
User.real.where(moderator: true).find_each do |u|
|
||||
mod_data[u.id] = {
|
||||
user_id: u.id,
|
||||
username: u.username,
|
||||
user_url: "/admin/users/#{u.id}/#{u.username}"
|
||||
}
|
||||
end
|
||||
|
||||
time_read_query = <<~SQL
|
||||
SELECT SUM(uv.time_read) AS time_read,
|
||||
uv.user_id
|
||||
FROM user_visits uv
|
||||
JOIN users u
|
||||
ON u.id = uv.user_id
|
||||
WHERE u.moderator = 'true'
|
||||
AND u.id > 0
|
||||
AND uv.visited_at >= '#{report.start_date}'
|
||||
AND uv.visited_at <= '#{report.end_date}'
|
||||
GROUP BY uv.user_id
|
||||
SQL
|
||||
|
||||
flag_count_query = <<~SQL
|
||||
WITH period_actions AS (
|
||||
SELECT agreed_by_id,
|
||||
disagreed_by_id
|
||||
FROM post_actions
|
||||
WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(',')})
|
||||
AND created_at >= '#{report.start_date}'
|
||||
AND created_at <= '#{report.end_date}'
|
||||
),
|
||||
agreed_flags AS (
|
||||
SELECT pa.agreed_by_id AS user_id,
|
||||
COUNT(*) AS flag_count
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = pa.agreed_by_id
|
||||
WHERE u.moderator = 'true'
|
||||
AND u.id > 0
|
||||
GROUP BY agreed_by_id
|
||||
),
|
||||
disagreed_flags AS (
|
||||
SELECT pa.disagreed_by_id AS user_id,
|
||||
COUNT(*) AS flag_count
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = pa.disagreed_by_id
|
||||
WHERE u.moderator = 'true'
|
||||
AND u.id > 0
|
||||
GROUP BY disagreed_by_id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(af.user_id, df.user_id) AS user_id,
|
||||
COALESCE(af.flag_count, 0) + COALESCE(df.flag_count, 0) AS flag_count
|
||||
FROM agreed_flags af
|
||||
FULL OUTER JOIN disagreed_flags df
|
||||
ON df.user_id = af.user_id
|
||||
SQL
|
||||
|
||||
topic_count_query = <<~SQL
|
||||
SELECT t.user_id,
|
||||
COUNT(*) AS topic_count
|
||||
FROM topics t
|
||||
JOIN users u
|
||||
ON u.id = t.user_id
|
||||
AND u.moderator = 'true'
|
||||
AND u.id > 0
|
||||
AND t.created_at >= '#{report.start_date}'
|
||||
AND t.created_at <= '#{report.end_date}'
|
||||
GROUP BY t.user_id
|
||||
SQL
|
||||
|
||||
post_count_query = <<~SQL
|
||||
SELECT p.user_id,
|
||||
COUNT(*) AS post_count
|
||||
FROM posts p
|
||||
JOIN users u
|
||||
ON u.id = p.user_id
|
||||
WHERE u.moderator = 'true'
|
||||
AND u.id > 0
|
||||
AND p.created_at >= '#{report.start_date}'
|
||||
AND p.created_at <= '#{report.end_date}'
|
||||
GROUP BY user_id
|
||||
SQL
|
||||
|
||||
DB.query(time_read_query).each do |row|
|
||||
mod_data[row.user_id][:time_read] = row.time_read
|
||||
end
|
||||
|
||||
DB.query(flag_count_query).each do |row|
|
||||
mod_data[row.user_id][:flag_count] = row.flag_count
|
||||
end
|
||||
|
||||
DB.query(topic_count_query).each do |row|
|
||||
mod_data[row.user_id][:topic_count] = row.topic_count
|
||||
end
|
||||
|
||||
DB.query(post_count_query).each do |row|
|
||||
mod_data[row.user_id][:post_count] = row.post_count
|
||||
end
|
||||
|
||||
report.data = mod_data.values
|
||||
end
|
||||
|
||||
def self.report_flags_status(report)
|
||||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:action_type], title: I18n.t("reports.flags_status.labels.flag") },
|
||||
{ type: :link, properties: [:staff_username, :staff_url], title: I18n.t("reports.flags_status.labels.assigned") },
|
||||
{ type: :link, properties: [:poster_username, :poster_url], title: I18n.t("reports.flags_status.labels.poster") },
|
||||
{ type: :link, properties: [:flagger_username, :flagger_url], title: I18n.t("reports.flags_status.labels.flagger") },
|
||||
{ type: :seconds, properties: [:response_time], title: I18n.t("reports.flags_status.labels.time_to_resolution") }
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
||||
flag_types = PostActionType.flag_types_without_custom
|
||||
|
||||
sql = <<~SQL
|
||||
WITH period_actions AS (
|
||||
SELECT id,
|
||||
post_action_type_id,
|
||||
created_at,
|
||||
agreed_at,
|
||||
disagreed_at,
|
||||
deferred_at,
|
||||
agreed_by_id,
|
||||
disagreed_by_id,
|
||||
deferred_by_id,
|
||||
post_id,
|
||||
user_id,
|
||||
COALESCE(disagreed_at, agreed_at, deferred_at) AS responded_at
|
||||
FROM post_actions
|
||||
WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(',')})
|
||||
AND created_at >= '#{report.start_date}'
|
||||
AND created_at <= '#{report.end_date}'
|
||||
),
|
||||
poster_data AS (
|
||||
SELECT pa.id,
|
||||
p.user_id AS poster_id,
|
||||
u.username AS poster_username
|
||||
FROM period_actions pa
|
||||
JOIN posts p
|
||||
ON p.id = pa.post_id
|
||||
JOIN users u
|
||||
ON u.id = p.user_id
|
||||
),
|
||||
flagger_data AS (
|
||||
SELECT pa.id,
|
||||
u.id AS flagger_id,
|
||||
u.username AS flagger_username
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = pa.user_id
|
||||
),
|
||||
staff_data AS (
|
||||
SELECT pa.id,
|
||||
u.id AS staff_id,
|
||||
u.username AS staff_username
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = COALESCE(pa.agreed_by_id, pa.disagreed_by_id, pa.deferred_by_id)
|
||||
)
|
||||
SELECT
|
||||
sd.staff_username,
|
||||
sd.staff_id,
|
||||
pd.poster_username,
|
||||
pd.poster_id,
|
||||
fd.flagger_username,
|
||||
fd.flagger_id,
|
||||
pa.post_action_type_id,
|
||||
pa.created_at,
|
||||
pa.agreed_at,
|
||||
pa.disagreed_at,
|
||||
pa.deferred_at,
|
||||
pa.agreed_by_id,
|
||||
pa.disagreed_by_id,
|
||||
pa.deferred_by_id,
|
||||
COALESCE(pa.disagreed_at, pa.agreed_at, pa.deferred_at) AS responded_at
|
||||
FROM period_actions pa
|
||||
FULL OUTER JOIN staff_data sd
|
||||
ON sd.id = pa.id
|
||||
FULL OUTER JOIN flagger_data fd
|
||||
ON fd.id = pa.id
|
||||
FULL OUTER JOIN poster_data pd
|
||||
ON pd.id = pa.id
|
||||
SQL
|
||||
|
||||
DB.query(sql).each do |row|
|
||||
data = {}
|
||||
data[:action_type] = flag_types.key(row.post_action_type_id).to_s
|
||||
data[:staff_username] = row.staff_username
|
||||
data[:staff_id] = row.staff_id
|
||||
if row.staff_username && row.staff_id
|
||||
data[:staff_url] = "/admin/users/#{row.staff_id}/#{row.staff_username}"
|
||||
end
|
||||
data[:poster_username] = row.poster_username
|
||||
data[:poster_id] = row.poster_id
|
||||
data[:poster_url] = "/admin/users/#{row.poster_id}/#{row.poster_username}"
|
||||
data[:flagger_id] = row.flagger_id
|
||||
data[:flagger_username] = row.flagger_username
|
||||
data[:flagger_url] = "/admin/users/#{row.flagger_id}/#{row.flagger_username}"
|
||||
if row.agreed_by_id
|
||||
data[:resolution] = I18n.t("reports.flags_status.values.agreed")
|
||||
elsif row.disagreed_by_id
|
||||
data[:resolution] = I18n.t("reports.flags_status.values.disagreed")
|
||||
elsif row.deferred_by_id
|
||||
data[:resolution] = I18n.t("reports.flags_status.values.deferred")
|
||||
else
|
||||
data[:resolution] = I18n.t("reports.flags_status.values.no_action")
|
||||
end
|
||||
data[:response_time] = row.responded_at ? row.responded_at - row.created_at : nil
|
||||
report.data << data
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_post_edits(report)
|
||||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:post_id, :post_url], title: I18n.t("reports.post_edits.labels.post") },
|
||||
{ type: :link, properties: [:editor_username, :editor_url], title: I18n.t("reports.post_edits.labels.editor") },
|
||||
{ type: :link, properties: [:author_username, :author_url], title: I18n.t("reports.post_edits.labels.author") },
|
||||
{ type: :text, properties: [:edit_reason], title: I18n.t("reports.post_edits.labels.edit_reason") }
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
||||
sql = <<~SQL
|
||||
WITH period_revisions AS (
|
||||
SELECT pr.user_id AS editor_id,
|
||||
pr.number AS revision_version,
|
||||
pr.created_at,
|
||||
pr.post_id,
|
||||
u.username AS editor_username
|
||||
FROM post_revisions pr
|
||||
JOIN users u
|
||||
ON u.id = pr.user_id
|
||||
WHERE pr.created_at >= '#{report.start_date}'
|
||||
AND pr.created_at <= '#{report.end_date}'
|
||||
ORDER BY pr.created_at DESC
|
||||
LIMIT 20
|
||||
)
|
||||
SELECT pr.editor_id,
|
||||
pr.editor_username,
|
||||
p.user_id AS author_id,
|
||||
u.username AS author_username,
|
||||
pr.revision_version,
|
||||
p.version AS post_version,
|
||||
pr.post_id,
|
||||
p.topic_id,
|
||||
p.post_number,
|
||||
p.edit_reason,
|
||||
pr.created_at
|
||||
FROM period_revisions pr
|
||||
JOIN posts p
|
||||
ON p.id = pr.post_id
|
||||
JOIN users u
|
||||
ON u.id = p.user_id
|
||||
SQL
|
||||
|
||||
DB.query(sql).each do |r|
|
||||
revision = {}
|
||||
revision[:editor_id] = r.editor_id
|
||||
revision[:editor_username] = r.editor_username
|
||||
revision[:editor_url] = "/admin/users/#{r.editor_id}/#{r.editor_username}"
|
||||
revision[:author_id] = r.author_id
|
||||
revision[:author_username] = r.author_username
|
||||
revision[:author_url] = "/admin/users/#{r.author_id}/#{r.author_username}"
|
||||
revision[:edit_reason] = r.revision_version == r.post_version ? r.edit_reason : nil
|
||||
revision[:created_at] = r.created_at
|
||||
revision[:post_id] = r.post_id
|
||||
revision[:post_url] = "/t/-/#{r.topic_id}/#{r.post_number}"
|
||||
|
||||
report.data << revision
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2777,9 +2777,14 @@ en:
|
|||
page_views_short: "Pageviews"
|
||||
show_traffic_report: "Show Detailed Traffic Report"
|
||||
community_health: Community health
|
||||
moderators_activity: Moderators activity
|
||||
whats_new_in_discourse: What’s new in Discourse?
|
||||
activity_metrics: Activity Metrics
|
||||
all_reports: "All reports"
|
||||
general_tab: "General"
|
||||
moderation_tab: "Moderation"
|
||||
disabled: Disabled
|
||||
timeout_error: Sorry, query is taking too long, please pick a shorter interval
|
||||
|
||||
reports:
|
||||
trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period."
|
||||
|
@ -2798,8 +2803,8 @@ en:
|
|||
end_date: "End Date"
|
||||
groups: "All groups"
|
||||
disabled: "This report is disabled"
|
||||
total_for_period: "Total for period"
|
||||
total: "Total"
|
||||
totals_for_sample: "Totals for sample"
|
||||
total: "All time total"
|
||||
trending_search:
|
||||
more: '<a href="/admin/logs/search_logs">Search logs</a>'
|
||||
disabled: 'Trending search report is disabled. Enable <a href="/admin/site_settings/category/all_results?filter=log%20search%20queries">log search queries</a> to collect data.'
|
||||
|
|
|
@ -840,6 +840,38 @@ en:
|
|||
write: "Write all"
|
||||
|
||||
reports:
|
||||
default:
|
||||
labels:
|
||||
count: Count
|
||||
day: Day
|
||||
post_edits:
|
||||
title: "Post edits"
|
||||
labels:
|
||||
post: Post
|
||||
editor: Editor
|
||||
author: Author
|
||||
edit_reason: Reason
|
||||
moderators_activity:
|
||||
title: "Moderators activity"
|
||||
labels:
|
||||
moderator: Moderator
|
||||
flag_count: Flags reviewed
|
||||
time_read: Time reading
|
||||
topic_count: Topics
|
||||
post_count: Posts
|
||||
flags_status:
|
||||
title: "Flags status"
|
||||
values:
|
||||
agreed: Agreed
|
||||
disagreed: Disagreed
|
||||
deferred: Deferred
|
||||
no_action: No action
|
||||
labels:
|
||||
flag: Type
|
||||
assigned: Assigned
|
||||
poster: Poster
|
||||
flagger: Flagger
|
||||
time_to_resolution: Resolution time
|
||||
visits:
|
||||
title: "User Visits"
|
||||
xaxis: "Day"
|
||||
|
@ -898,10 +930,14 @@ en:
|
|||
title: "Users per Trust Level"
|
||||
xaxis: "Trust Level"
|
||||
yaxis: "Number of Users"
|
||||
labels:
|
||||
level: Level
|
||||
users_by_type:
|
||||
title: "Users per Type"
|
||||
xaxis: "Type"
|
||||
yaxis: "Number of Users"
|
||||
labels:
|
||||
type: Type
|
||||
xaxis_labels:
|
||||
admin: Admin
|
||||
moderator: Moderator
|
||||
|
@ -952,10 +988,15 @@ en:
|
|||
num_clicks: "Clicks"
|
||||
num_topics: "Topics"
|
||||
num_users: "Users"
|
||||
labels:
|
||||
domain: Domain
|
||||
num_clicks: Clicks
|
||||
num_topics: Topics
|
||||
top_referred_topics:
|
||||
title: "Top Referred Topics"
|
||||
xaxis: "Topic"
|
||||
num_clicks: "Clicks"
|
||||
labels:
|
||||
num_clicks: "Clicks"
|
||||
topic: "Topic"
|
||||
page_view_anon_reqs:
|
||||
title: "Anonymous"
|
||||
xaxis: "Day"
|
||||
|
@ -1018,8 +1059,9 @@ en:
|
|||
yaxis: "Number of visits"
|
||||
web_crawlers:
|
||||
title: "Web Crawler Requests"
|
||||
xaxis: "User Agent"
|
||||
yaxis: "Pageviews"
|
||||
labels:
|
||||
user_agent: "User Agent"
|
||||
page_views: "Pageviews"
|
||||
|
||||
dashboard:
|
||||
rails_env_warning: "Your server is running in %{env} mode."
|
||||
|
|
|
@ -238,6 +238,8 @@ Discourse::Application.routes.draw do
|
|||
|
||||
get "dashboard-next" => "dashboard_next#index"
|
||||
get "dashboard-old" => "dashboard#index"
|
||||
get "dashboard/moderation" => "dashboard_next#moderation"
|
||||
get "dashboard/general" => "dashboard_next#general"
|
||||
|
||||
resources :dashboard, only: [:index] do
|
||||
collection do
|
||||
|
|
|
@ -346,7 +346,7 @@ describe Report do
|
|||
it "returns a report with data" do
|
||||
expect(report.data[0][:term]).to eq("ruby")
|
||||
expect(report.data[0][:unique_searches]).to eq(2)
|
||||
expect(report.data[0][:ctr]).to eq('33.4%')
|
||||
expect(report.data[0][:ctr]).to eq(33.4)
|
||||
|
||||
expect(report.data[1][:term]).to eq("php")
|
||||
expect(report.data[1][:unique_searches]).to eq(1)
|
||||
|
@ -435,4 +435,173 @@ describe Report do
|
|||
expect(r.data[0][:y]).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'flags_status' do
|
||||
let(:report) { Report.find('flags_status') }
|
||||
|
||||
context "no flags" do
|
||||
it "returns an empty report" do
|
||||
expect(report.data).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "with flags" do
|
||||
let(:flagger) { Fabricate(:user) }
|
||||
let(:post) { Fabricate(:post) }
|
||||
|
||||
before do
|
||||
PostAction.act(flagger, post, PostActionType.types[:spam], message: 'bad')
|
||||
end
|
||||
|
||||
it "returns a report with data" do
|
||||
expect(report.data).to be_present
|
||||
|
||||
row = report.data[0]
|
||||
expect(row[:action_type]).to eq("spam")
|
||||
expect(row[:staff_username]).to eq(nil)
|
||||
expect(row[:staff_id]).to eq(nil)
|
||||
expect(row[:staff_url]).to eq(nil)
|
||||
expect(row[:poster_username]).to eq(post.user.username)
|
||||
expect(row[:poster_id]).to eq(post.user.id)
|
||||
expect(row[:poster_url]).to eq("/admin/users/#{post.user.id}/#{post.user.username}")
|
||||
expect(row[:flagger_id]).to eq(flagger.id)
|
||||
expect(row[:flagger_username]).to eq(flagger.username)
|
||||
expect(row[:flagger_url]).to eq("/admin/users/#{flagger.id}/#{flagger.username}")
|
||||
expect(row[:resolution]).to eq("No action")
|
||||
expect(row[:response_time]).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'post_edits' do
|
||||
let(:report) { Report.find('post_edits') }
|
||||
|
||||
context "no edits" do
|
||||
it "returns an empty report" do
|
||||
expect(report.data).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "with edits" do
|
||||
let(:editor) { Fabricate(:user) }
|
||||
let(:post) { Fabricate(:post) }
|
||||
|
||||
before do
|
||||
post.revise(editor, raw: 'updated body', edit_reason: 'not cool')
|
||||
end
|
||||
|
||||
it "returns a report with data" do
|
||||
expect(report.data).to be_present
|
||||
expect(report.data.count).to be(1)
|
||||
|
||||
row = report.data[0]
|
||||
expect(row[:editor_id]).to eq(editor.id)
|
||||
expect(row[:editor_username]).to eq(editor.username)
|
||||
expect(row[:editor_url]).to eq("/admin/users/#{editor.id}/#{editor.username}")
|
||||
expect(row[:author_id]).to eq(post.user.id)
|
||||
expect(row[:author_username]).to eq(post.user.username)
|
||||
expect(row[:author_url]).to eq("/admin/users/#{post.user.id}/#{post.user.username}")
|
||||
expect(row[:edit_reason]).to eq("not cool")
|
||||
expect(row[:post_id]).to eq(post.id)
|
||||
expect(row[:post_url]).to eq("/t/-/#{post.topic.id}/#{post.post_number}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'moderator activity' do
|
||||
let(:current_report) { Report.find('moderators_activity', start_date: 1.months.ago.beginning_of_day, end_date: Date.today) }
|
||||
let(:previous_report) { Report.find('moderators_activity', start_date: 2.months.ago.beginning_of_day, end_date: 1.month.ago.end_of_day) }
|
||||
|
||||
context "no moderators" do
|
||||
it "returns an empty report" do
|
||||
expect(current_report.data).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "with moderators" do
|
||||
before do
|
||||
freeze_time(Date.today)
|
||||
|
||||
bob = Fabricate(:user, moderator: true, username: 'bob')
|
||||
bob.user_visits.create(visited_at: 2.days.ago, time_read: 200)
|
||||
bob.user_visits.create(visited_at: 1.day.ago, time_read: 100)
|
||||
Fabricate(:topic, user: bob, created_at: 1.day.ago)
|
||||
sally = Fabricate(:user, moderator: true, username: 'sally')
|
||||
sally.user_visits.create(visited_at: 2.days.ago, time_read: 1000)
|
||||
sally.user_visits.create(visited_at: 1.day.ago, time_read: 2000)
|
||||
topic = Fabricate(:topic)
|
||||
2.times {
|
||||
Fabricate(:post, user: sally, topic: topic, created_at: 1.day.ago)
|
||||
}
|
||||
flag_user = Fabricate(:user)
|
||||
flag_post = Fabricate(:post, user: flag_user)
|
||||
action = PostAction.new(user_id: flag_user.id,
|
||||
post_action_type_id: PostActionType.types[:off_topic],
|
||||
post_id: flag_post.id,
|
||||
agreed_by_id: sally.id,
|
||||
created_at: 1.day.ago,
|
||||
agreed_at: Time.now)
|
||||
action.save
|
||||
bob.user_visits.create(visited_at: 45.days.ago, time_read: 200)
|
||||
old_topic = Fabricate(:topic, user: bob, created_at: 45.days.ago)
|
||||
3.times {
|
||||
Fabricate(:post, user: bob, topic: old_topic, created_at: 45.days.ago)
|
||||
}
|
||||
old_flag_user = Fabricate(:user)
|
||||
old_flag_post = Fabricate(:post, user: old_flag_user, created_at: 45.days.ago)
|
||||
old_action = PostAction.new(user_id: old_flag_user.id,
|
||||
post_action_type_id: PostActionType.types[:spam],
|
||||
post_id: old_flag_post.id,
|
||||
agreed_by_id: bob.id,
|
||||
created_at: 44.days.ago,
|
||||
agreed_at: 44.days.ago)
|
||||
old_action.save
|
||||
end
|
||||
|
||||
it "returns a report with data" do
|
||||
expect(current_report.data).to be_present
|
||||
end
|
||||
|
||||
it "returns data for two moderators" do
|
||||
expect(current_report.data.count).to eq(2)
|
||||
end
|
||||
|
||||
it "returns the correct usernames" do
|
||||
expect(current_report.data[0][:username]).to eq('bob')
|
||||
expect(current_report.data[1][:username]).to eq('sally')
|
||||
end
|
||||
|
||||
it "returns the correct read times" do
|
||||
expect(current_report.data[0][:time_read]).to eq(300)
|
||||
expect(current_report.data[1][:time_read]).to eq(3000)
|
||||
end
|
||||
|
||||
it "returns the correct agreed flag count" do
|
||||
expect(current_report.data[0][:flag_count]).to be_blank
|
||||
expect(current_report.data[1][:flag_count]).to eq(1)
|
||||
end
|
||||
|
||||
it "returns the correct topic count" do
|
||||
expect(current_report.data[0][:topic_count]).to eq(1)
|
||||
expect(current_report.data[1][:topic_count]).to be_blank
|
||||
end
|
||||
|
||||
it "returns the correct post count" do
|
||||
expect(current_report.data[0][:post_count]).to be_blank
|
||||
expect(current_report.data[1][:post_count]).to eq(2)
|
||||
end
|
||||
|
||||
it "returns the correct data for the time period" do
|
||||
expect(previous_report.data[0][:flag_count]).to eq(1)
|
||||
expect(previous_report.data[0][:topic_count]).to eq(1)
|
||||
expect(previous_report.data[0][:post_count]).to eq(3)
|
||||
expect(previous_report.data[0][:time_read]).to eq(200)
|
||||
|
||||
expect(previous_report.data[1][:flag_count]).to be_blank
|
||||
expect(previous_report.data[1][:topic_count]).to be_blank
|
||||
expect(previous_report.data[1][:post_count]).to be_blank
|
||||
expect(previous_report.data[1][:time_read]).to be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,25 +7,18 @@ acceptance("Dashboard Next", {
|
|||
QUnit.test("Visit dashboard next page", async assert => {
|
||||
await visit("/admin");
|
||||
|
||||
assert.ok($(".dashboard-next").length, "has dashboard-next class");
|
||||
|
||||
assert.ok($(".dashboard-mini-chart.signups").length, "has a signups chart");
|
||||
|
||||
assert.ok($(".dashboard-mini-chart.posts").length, "has a posts chart");
|
||||
assert.ok(exists(".dashboard-next"), "has dashboard-next class");
|
||||
|
||||
assert.ok(exists(".admin-report.signups"), "signups report");
|
||||
assert.ok(exists(".admin-report.posts"), "posts report");
|
||||
assert.ok(exists(".admin-report.dau-by-mau"), "dau-by-mau report");
|
||||
assert.ok(
|
||||
$(".dashboard-mini-chart.dau_by_mau").length,
|
||||
"has a dau_by_mau chart"
|
||||
exists(".admin-report.daily-engaged-users"),
|
||||
"daily-engaged-users report"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
$(".dashboard-mini-chart.daily_engaged_users").length,
|
||||
"has a daily_engaged_users chart"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
$(".dashboard-mini-chart.new_contributors").length,
|
||||
"has a new_contributors chart"
|
||||
exists(".admin-report.new-contributors"),
|
||||
"new-contributors report"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import componentTest from "helpers/component-test";
|
||||
|
||||
moduleForComponent("admin-report", {
|
||||
integration: true
|
||||
});
|
||||
|
||||
componentTest("default", {
|
||||
template: "{{admin-report dataSourceName='signups'}}",
|
||||
|
||||
test(assert) {
|
||||
andThen(() => {
|
||||
assert.ok(exists(".admin-report.signups"));
|
||||
|
||||
assert.ok(
|
||||
exists(".admin-report.table.signups", "it defaults to table mode")
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-header .title")
|
||||
.text()
|
||||
.trim(),
|
||||
"Signups",
|
||||
"it has a title"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-header .info").attr("data-tooltip"),
|
||||
"New account registrations for this period",
|
||||
"it has a description"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-body .report-table thead tr th:first-child")
|
||||
.text()
|
||||
.trim(),
|
||||
"Day",
|
||||
"it has col headers"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-body .report-table thead tr th:nth-child(2)")
|
||||
.text()
|
||||
.trim(),
|
||||
"Count",
|
||||
"it has col headers"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-body .report-table tbody tr:nth-child(1) td:nth-child(1)")
|
||||
.text()
|
||||
.trim(),
|
||||
"June 16, 2018",
|
||||
"it has rows"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".report-body .report-table tbody tr:nth-child(1) td:nth-child(2)")
|
||||
.text()
|
||||
.trim(),
|
||||
"12",
|
||||
"it has rows"
|
||||
);
|
||||
|
||||
assert.ok(exists(".totals-sample-table"), "it has totals");
|
||||
});
|
||||
|
||||
click(".admin-report-table-header.y .sort-button");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".report-body .report-table tbody tr:nth-child(1) td:nth-child(2)")
|
||||
.text()
|
||||
.trim(),
|
||||
"7",
|
||||
"it can sort rows"
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
componentTest("options", {
|
||||
template: "{{admin-report dataSourceName='signups' reportOptions=options}}",
|
||||
|
||||
beforeEach() {
|
||||
this.set("options", {
|
||||
table: {
|
||||
perPage: 4,
|
||||
total: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
test(assert) {
|
||||
andThen(() => {
|
||||
assert.ok(exists(".pagination"), "it paginates the results");
|
||||
assert.equal(
|
||||
find(".pagination button").length,
|
||||
3,
|
||||
"it creates the correct number of pages"
|
||||
);
|
||||
|
||||
assert.notOk(exists(".totals-sample-table"), "it hides totals");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
componentTest("switch modes", {
|
||||
template: "{{admin-report dataSourceName='signups'}}",
|
||||
|
||||
test(assert) {
|
||||
click(".mode-button.chart");
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(
|
||||
exists(".admin-report.table.signups"),
|
||||
"it removes the table"
|
||||
);
|
||||
assert.ok(exists(".admin-report.chart.signups"), "it shows the chart");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
componentTest("timeout", {
|
||||
template: "{{admin-report dataSourceName='signups_timeout'}}",
|
||||
|
||||
test(assert) {
|
||||
andThen(() => {
|
||||
assert.ok(exists(".alert-error"), "it displays a timeout error");
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
export default {
|
||||
"/admin/general.json": {
|
||||
reports: [],
|
||||
last_backup_taken_at: "2018-04-13T12:51:19.926Z",
|
||||
updated_at: "2018-04-25T08:06:11.292Z",
|
||||
disk_space: {
|
||||
uploads_used: "74.5 KB",
|
||||
uploads_free: "117 GB",
|
||||
backups_used: "4.24 GB",
|
||||
backups_free: "117 GB"
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,20 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/daily_engaged_users": {
|
||||
report: {
|
||||
type: "daily_engaged_users",
|
||||
title: "Daily Engaged Users",
|
||||
xaxis: "Day",
|
||||
yaxis: "Engaged Users",
|
||||
description: "Number of users that have liked or posted in the last day",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-04-03",
|
||||
end_date: "2018-05-03",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "daily_engaged_users"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
export default {
|
||||
"/admin/dashboard-next.json": {
|
||||
reports: [],
|
||||
last_backup_taken_at: "2018-04-13T12:51:19.926Z",
|
||||
updated_at: "2018-04-25T08:06:11.292Z",
|
||||
disk_space: {
|
||||
uploads_used: "74.5 KB",
|
||||
uploads_free: "117 GB",
|
||||
backups_used: "4.24 GB",
|
||||
backups_free: "117 GB"
|
||||
}
|
||||
updated_at: "2018-04-25T08:06:11.292Z"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/dau_by_mau": {
|
||||
report: {
|
||||
type: "dau_by_mau",
|
||||
title: "DAU/MAU",
|
||||
xaxis: "Day",
|
||||
yaxis: "DAU/MAY",
|
||||
description: "Percentage of daily active users on monthly active users",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-01-26T00:00:00.000Z",
|
||||
end_date: "2018-04-27T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: 46,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "dau_by_mau"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,60 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/new_contributors": {
|
||||
report: {
|
||||
type: "new_contributors",
|
||||
title: "New Contributors",
|
||||
xaxis: "",
|
||||
yaxis: "",
|
||||
data: [
|
||||
{
|
||||
x: "2018-04-11",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-12",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-13",
|
||||
y: 60
|
||||
},
|
||||
{
|
||||
x: "2018-04-14",
|
||||
y: 60
|
||||
},
|
||||
{
|
||||
x: "2018-04-15",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-16",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-17",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-19",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-18",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-20",
|
||||
y: 1
|
||||
}
|
||||
],
|
||||
total: 121,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "new_contributors"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/posts": {
|
||||
report: {
|
||||
type: "topics",
|
||||
title: "Topics",
|
||||
xaxis: "Day",
|
||||
yaxis: "Number of new posts",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: 0,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "posts"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,53 +4,76 @@ export default {
|
|||
type: "signups",
|
||||
title: "Signups",
|
||||
xaxis: "Day",
|
||||
yaxis: "Number of new users",
|
||||
yaxis: "Number of signups",
|
||||
description: "New account registrations for this period",
|
||||
data: [
|
||||
{
|
||||
x: "2018-04-11",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-12",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-13",
|
||||
y: 22
|
||||
},
|
||||
{
|
||||
x: "2018-04-14",
|
||||
y: 58
|
||||
},
|
||||
{
|
||||
x: "2018-04-15",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-16",
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
x: "2018-04-17",
|
||||
y: 19
|
||||
},
|
||||
{
|
||||
x: "2018-04-18",
|
||||
y: 12
|
||||
},
|
||||
{
|
||||
x: "2018-04-19",
|
||||
y: 19
|
||||
}
|
||||
{ x: "2018-06-16", y: 12 },
|
||||
{ x: "2018-06-17", y: 16 },
|
||||
{ x: "2018-06-18", y: 42 },
|
||||
{ x: "2018-06-19", y: 38 },
|
||||
{ x: "2018-06-20", y: 41 },
|
||||
{ x: "2018-06-21", y: 32 },
|
||||
{ x: "2018-06-22", y: 23 },
|
||||
{ x: "2018-06-23", y: 23 },
|
||||
{ x: "2018-06-24", y: 17 },
|
||||
{ x: "2018-06-25", y: 27 },
|
||||
{ x: "2018-06-26", y: 32 },
|
||||
{ x: "2018-06-27", y: 7 }
|
||||
],
|
||||
total: 136,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
start_date: "2018-06-16T00:00:00Z",
|
||||
end_date: "2018-07-16T23:59:59Z",
|
||||
prev_data: [
|
||||
{ x: "2018-05-17", y: 32 },
|
||||
{ x: "2018-05-18", y: 30 },
|
||||
{ x: "2018-05-19", y: 12 },
|
||||
{ x: "2018-05-20", y: 23 },
|
||||
{ x: "2018-05-21", y: 50 },
|
||||
{ x: "2018-05-22", y: 39 },
|
||||
{ x: "2018-05-23", y: 51 },
|
||||
{ x: "2018-05-24", y: 48 },
|
||||
{ x: "2018-05-25", y: 37 },
|
||||
{ x: "2018-05-26", y: 17 },
|
||||
{ x: "2018-05-27", y: 6 },
|
||||
{ x: "2018-05-28", y: 20 },
|
||||
{ x: "2018-05-29", y: 37 },
|
||||
{ x: "2018-05-30", y: 37 },
|
||||
{ x: "2018-05-31", y: 37 },
|
||||
{ x: "2018-06-01", y: 38 },
|
||||
{ x: "2018-06-02", y: 23 },
|
||||
{ x: "2018-06-03", y: 18 },
|
||||
{ x: "2018-06-04", y: 39 },
|
||||
{ x: "2018-06-05", y: 26 },
|
||||
{ x: "2018-06-06", y: 39 },
|
||||
{ x: "2018-06-07", y: 52 },
|
||||
{ x: "2018-06-08", y: 35 },
|
||||
{ x: "2018-06-09", y: 19 },
|
||||
{ x: "2018-06-10", y: 15 },
|
||||
{ x: "2018-06-11", y: 31 },
|
||||
{ x: "2018-06-12", y: 38 },
|
||||
{ x: "2018-06-13", y: 30 },
|
||||
{ x: "2018-06-14", y: 45 },
|
||||
{ x: "2018-06-15", y: 37 },
|
||||
{ x: "2018-06-16", y: 12 }
|
||||
],
|
||||
prev_start_date: "2018-05-17T00:00:00Z",
|
||||
prev_end_date: "2018-06-17T00:00:00Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: 0,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
prev30Days: null,
|
||||
dates_filtering: true,
|
||||
report_key: "reports:signups::20180616:20180716::[:prev_period]:",
|
||||
labels: [
|
||||
{ type: "date", properties: ["x"], title: "Day" },
|
||||
{ type: "number", properties: ["y"], title: "Count" }
|
||||
],
|
||||
processing: false,
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
category_filtering: false,
|
||||
group_filtering: true,
|
||||
modes: ["table", "chart"],
|
||||
prev_period: 961
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
export default {
|
||||
"/admin/reports/signups_timeout": {
|
||||
report: {
|
||||
type: "signups",
|
||||
title: "Signups",
|
||||
xaxis: "Day",
|
||||
yaxis: "Number of signups",
|
||||
description: "New account registrations for this period",
|
||||
data: null,
|
||||
start_date: "2018-06-16T00:00:00Z",
|
||||
end_date: "2018-07-16T23:59:59Z",
|
||||
prev_data: null,
|
||||
prev_start_date: "2018-05-17T00:00:00Z",
|
||||
prev_end_date: "2018-06-17T00:00:00Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
dates_filtering: true,
|
||||
report_key: "reports:signups_timeout::20180616:20180716::[:prev_period]:",
|
||||
labels: [
|
||||
{ type: "date", properties: ["x"], title: "Day" },
|
||||
{ type: "number", properties: ["y"], title: "Count" }
|
||||
],
|
||||
processing: false,
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
category_filtering: false,
|
||||
group_filtering: true,
|
||||
modes: ["table", "chart"],
|
||||
prev_period: 961,
|
||||
timeout: true
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,18 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/top_referred_topics": {
|
||||
report: {
|
||||
type: "top_referred_topics",
|
||||
title: "Trending search",
|
||||
xaxis: "",
|
||||
yaxis: "",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: ["Topic", "Visits"]
|
||||
report_key: "top_referred_topics"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/topics": {
|
||||
report: {
|
||||
type: "topics",
|
||||
title: "Topics",
|
||||
xaxis: "Day",
|
||||
yaxis: "Number of new topics",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: 0,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "topics"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/trending_search": {
|
||||
report: {
|
||||
type: "trending_search",
|
||||
title: "Trending search",
|
||||
xaxis: "",
|
||||
yaxis: "",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-26T00:00:00.000Z",
|
||||
end_date: "2018-04-25T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: ["Term", "Searches", "Unique"],
|
||||
report_key: ""
|
||||
report_key: "trending_search"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,21 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/users_by_trust_level": {
|
||||
report: {
|
||||
type: "users_by_trust_level",
|
||||
title: "Users per Trust Level",
|
||||
xaxis: "Trust Level",
|
||||
yaxis: "Number of Users",
|
||||
description:
|
||||
"translation missing: en.reports.users_by_trust_level.description",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-30T00:00:00.000Z",
|
||||
end_date: "2018-04-29T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "users_by_trust_level"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
export default {
|
||||
"/admin/reports/users_by_type": {
|
||||
report: {
|
||||
type: "users_by_type",
|
||||
title: "Users per Type",
|
||||
xaxis: "Type",
|
||||
yaxis: "Number of Users",
|
||||
description: "translation missing: en.reports.users_by_type.description",
|
||||
data: null,
|
||||
total: null,
|
||||
start_date: "2018-03-30T00:00:00.000Z",
|
||||
end_date: "2018-04-29T23:59:59.999Z",
|
||||
category_id: null,
|
||||
group_id: null,
|
||||
prev30Days: null,
|
||||
labels: null,
|
||||
report_key: ""
|
||||
report_key: "users_by_type"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -391,3 +391,74 @@ QUnit.test("average", assert => {
|
|||
report.set("average", false);
|
||||
assert.ok(report.get("lastSevenDaysCount") === 35);
|
||||
});
|
||||
|
||||
QUnit.test("computed labels", assert => {
|
||||
const data = [
|
||||
{
|
||||
username: "joffrey",
|
||||
user_url: "/admin/users/1/joffrey",
|
||||
flag_count: 1876,
|
||||
time_read: 287362,
|
||||
note: "This is a long note"
|
||||
}
|
||||
];
|
||||
|
||||
const labels = [
|
||||
{
|
||||
type: "link",
|
||||
properties: ["username", "user_url"],
|
||||
title: "Username"
|
||||
},
|
||||
{ properties: ["flag_count"], title: "Flag count" },
|
||||
{ type: "seconds", properties: ["time_read"], title: "Time read" },
|
||||
{ type: "text", properties: ["note"], title: "Note" }
|
||||
];
|
||||
|
||||
const report = Report.create({
|
||||
type: "topics",
|
||||
labels,
|
||||
data
|
||||
});
|
||||
|
||||
const row = report.get("data.0");
|
||||
const computedLabels = report.get("computedLabels");
|
||||
|
||||
const usernameLabel = computedLabels[0];
|
||||
assert.equal(usernameLabel.property, "username");
|
||||
assert.equal(usernameLabel.sort_property, "username");
|
||||
assert.equal(usernameLabel.title, "Username");
|
||||
const computedUsernameLabel = usernameLabel.compute(row);
|
||||
assert.equal(
|
||||
computedUsernameLabel.formatedValue,
|
||||
'<a href="/admin/users/1/joffrey">joffrey</a>'
|
||||
);
|
||||
assert.equal(computedUsernameLabel.type, "link");
|
||||
assert.equal(computedUsernameLabel.value, "joffrey");
|
||||
|
||||
const flagCountLabel = computedLabels[1];
|
||||
assert.equal(flagCountLabel.property, "flag_count");
|
||||
assert.equal(flagCountLabel.sort_property, "flag_count");
|
||||
assert.equal(flagCountLabel.title, "Flag count");
|
||||
const computedFlagCountLabel = flagCountLabel.compute(row);
|
||||
assert.equal(computedFlagCountLabel.formatedValue, "1.9k");
|
||||
assert.equal(computedFlagCountLabel.type, "number");
|
||||
assert.equal(computedFlagCountLabel.value, 1876);
|
||||
|
||||
const timeReadLabel = computedLabels[2];
|
||||
assert.equal(timeReadLabel.property, "time_read");
|
||||
assert.equal(timeReadLabel.sort_property, "time_read");
|
||||
assert.equal(timeReadLabel.title, "Time read");
|
||||
const computedTimeReadLabel = timeReadLabel.compute(row);
|
||||
assert.equal(computedTimeReadLabel.formatedValue, "3d");
|
||||
assert.equal(computedTimeReadLabel.type, "seconds");
|
||||
assert.equal(computedTimeReadLabel.value, 287362);
|
||||
|
||||
const noteLabel = computedLabels[3];
|
||||
assert.equal(noteLabel.property, "note");
|
||||
assert.equal(noteLabel.sort_property, "note");
|
||||
assert.equal(noteLabel.title, "Note");
|
||||
const computedNoteLabel = noteLabel.compute(row);
|
||||
assert.equal(computedNoteLabel.formatedValue, "This is a long note");
|
||||
assert.equal(computedNoteLabel.type, "text");
|
||||
assert.equal(computedNoteLabel.value, "This is a long note");
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue