mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
366
app/assets/javascripts/admin/components/admin-report.js.es6
Normal file
366
app/assets/javascripts/admin/components/admin-report.js.es6
Normal file
@@ -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 { setting } from "discourse/lib/computed";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||||
import Report from "admin/models/report";
|
|
||||||
import VersionCheck from "admin/models/version-check";
|
import VersionCheck from "admin/models/version-check";
|
||||||
|
|
||||||
const PROBLEMS_CHECK_MINUTES = 1;
|
const PROBLEMS_CHECK_MINUTES = 1;
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
queryParams: ["period"],
|
|
||||||
period: "monthly",
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
dashboardFetchedAt: null,
|
dashboardFetchedAt: null,
|
||||||
exceptionController: Ember.inject.controller("exception"),
|
exceptionController: Ember.inject.controller("exception"),
|
||||||
showVersionChecks: setting("version_checks"),
|
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")
|
@computed("problems.length")
|
||||||
foundProblems(problemsLength) {
|
foundProblems(problemsLength) {
|
||||||
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
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() {
|
fetchDashboard() {
|
||||||
if (this.get("isLoading")) return;
|
const versionChecks = this.siteSettings.version_checks;
|
||||||
|
|
||||||
|
if (this.get("isLoading") || !versionChecks) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.get("dashboardFetchedAt") ||
|
!this.get("dashboardFetchedAt") ||
|
||||||
@@ -38,22 +42,17 @@ export default Ember.Controller.extend({
|
|||||||
) {
|
) {
|
||||||
this.set("isLoading", true);
|
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) {
|
if (versionChecks) {
|
||||||
this.set(
|
properties.versionCheck = VersionCheck.create(model.version_check);
|
||||||
"versionCheck",
|
|
||||||
VersionCheck.create(adminDashboardNextModel.version_check)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties(properties);
|
||||||
dashboardFetchedAt: new Date(),
|
|
||||||
model: adminDashboardNextModel,
|
|
||||||
reports: adminDashboardNextModel.reports.map(x => Report.create(x))
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||||
@@ -63,27 +62,17 @@ export default Ember.Controller.extend({
|
|||||||
this.set("isLoading", false);
|
this.set("isLoading", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!this.get("problemsFetchedAt") ||
|
|
||||||
moment()
|
|
||||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
|
||||||
.toDate() > this.get("problemsFetchedAt")
|
|
||||||
) {
|
|
||||||
this.loadProblems();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProblems() {
|
_loadProblems() {
|
||||||
this.set("loadingProblems", true);
|
this.setProperties({
|
||||||
this.set("problemsFetchedAt", new Date());
|
loadingProblems: true,
|
||||||
|
problemsFetchedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
AdminDashboardNext.fetchProblems()
|
AdminDashboardNext.fetchProblems()
|
||||||
.then(d => {
|
.then(model => this.set("problems", model.problems))
|
||||||
this.set("problems", d.problems);
|
.finally(() => this.set("loadingProblems", false));
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set("loadingProblems", false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("problemsFetchedAt")
|
@computed("problemsFetchedAt")
|
||||||
@@ -93,69 +82,9 @@ export default Ember.Controller.extend({
|
|||||||
.format("LLL");
|
.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: {
|
actions: {
|
||||||
changePeriod(period) {
|
|
||||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
|
||||||
},
|
|
||||||
refreshProblems() {
|
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";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
queryParams: ["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,
|
|
||||||
categoryId: null,
|
categoryId: null,
|
||||||
groupId: 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")
|
@computed("model.type")
|
||||||
showCategoryOptions(modelType) {
|
reportOptions(type) {
|
||||||
return [
|
let options = { table: { perPage: 50, limit: 50 } };
|
||||||
"topics",
|
|
||||||
"posts",
|
|
||||||
"time_to_first_response_total",
|
|
||||||
"topics_with_no_response",
|
|
||||||
"flags",
|
|
||||||
"likes",
|
|
||||||
"bookmarks"
|
|
||||||
].includes(modelType);
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("model.type")
|
if (type === "top_referred_topics") {
|
||||||
showGroupOptions(modelType) {
|
options.table.limit = 10;
|
||||||
return (
|
}
|
||||||
modelType === "visits" ||
|
|
||||||
modelType === "signups" ||
|
return options;
|
||||||
modelType === "profile_views"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
refreshReport() {
|
onSelectStartDate(startDate) {
|
||||||
var q;
|
this.set("start_date", startDate);
|
||||||
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)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
viewAsTable() {
|
onSelectCategory(categoryId) {
|
||||||
this.set("viewMode", "table");
|
this.set("category_id", categoryId);
|
||||||
},
|
},
|
||||||
|
|
||||||
viewAsGraph() {
|
onSelectGroup(groupId) {
|
||||||
this.set("viewMode", "graph");
|
this.set("group_id", groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
exportCsv() {
|
onSelectEndDate(endDate) {
|
||||||
exportEntity("report", {
|
this.set("end_date", endDate);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
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({});
|
const AdminDashboardNext = Discourse.Model.extend({});
|
||||||
|
|
||||||
AdminDashboardNext.reopenClass({
|
AdminDashboardNext.reopenClass({
|
||||||
/**
|
fetch() {
|
||||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
return ajax("/admin/dashboard-next.json").then(json => {
|
||||||
has expired and the server must collect the data again.
|
const model = AdminDashboardNext.create();
|
||||||
|
|
||||||
@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);
|
|
||||||
model.set("version_check", json.version_check);
|
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 = {};
|
const attributes = {};
|
||||||
ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
GENERAL_ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||||
model.set("attributes", attributes);
|
|
||||||
|
|
||||||
model.set("loaded", true);
|
model.setProperties({
|
||||||
|
reports: json.reports,
|
||||||
|
attributes,
|
||||||
|
loaded: true
|
||||||
|
});
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,105 @@
|
|||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import round from "discourse/lib/round";
|
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 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({
|
const Report = Discourse.Model.extend({
|
||||||
average: false,
|
average: false,
|
||||||
percent: false,
|
percent: false,
|
||||||
higher_is_better: true,
|
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")
|
@computed("type", "start_date", "end_date")
|
||||||
reportUrl(type, start_date, end_date) {
|
reportUrl(type, start_date, end_date) {
|
||||||
start_date = moment(start_date)
|
start_date = moment
|
||||||
|
.utc(start_date)
|
||||||
.locale("en")
|
.locale("en")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
end_date = moment(end_date)
|
|
||||||
|
end_date = moment
|
||||||
|
.utc(end_date)
|
||||||
.locale("en")
|
.locale("en")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
|
|
||||||
return Discourse.getURL(
|
return Discourse.getURL(
|
||||||
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
`/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);
|
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")
|
@computed("type")
|
||||||
method(type) {
|
method(type) {
|
||||||
if (type === "time_to_first_response") {
|
if (type === "time_to_first_response") {
|
||||||
@@ -290,18 +350,23 @@ const Report = Discourse.Model.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
Report.reopenClass({
|
Report.reopenClass({
|
||||||
fillMissingDates(report) {
|
fillMissingDates(report, options = {}) {
|
||||||
if (_.isArray(report.data)) {
|
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
|
const startDateFormatted = moment
|
||||||
.utc(report.start_date)
|
.utc(report[startDate])
|
||||||
.locale("en")
|
.locale("en")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
const endDateFormatted = moment
|
const endDateFormatted = moment
|
||||||
.utc(report.end_date)
|
.utc(report[endDate])
|
||||||
.locale("en")
|
.locale("en")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
report.data = fillMissingDates(
|
report[filledField] = fillMissingDates(
|
||||||
report.data,
|
JSON.parse(JSON.stringify(report[dataField])),
|
||||||
startDateFormatted,
|
startDateFormatted,
|
||||||
endDateFormatted
|
endDateFormatted
|
||||||
);
|
);
|
||||||
@@ -317,8 +382,12 @@ Report.reopenClass({
|
|||||||
group_id: groupId
|
group_id: groupId
|
||||||
}
|
}
|
||||||
}).then(json => {
|
}).then(json => {
|
||||||
// Add zero values for missing dates
|
// don’t fill for large multi column tables
|
||||||
Report.fillMissingDates(json.report);
|
// 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 });
|
const model = Report.create({ type: type });
|
||||||
model.setProperties(json.report);
|
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({
|
export default Discourse.Route.extend({
|
||||||
activate() {
|
activate() {
|
||||||
|
this.controllerFor("admin-dashboard-next").fetchProblems();
|
||||||
this.controllerFor("admin-dashboard-next").fetchDashboard();
|
this.controllerFor("admin-dashboard-next").fetchDashboard();
|
||||||
scrollTop();
|
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({
|
export default Discourse.Route.extend({
|
||||||
queryParams: {
|
setupController(controller) {
|
||||||
mode: {},
|
this._super(...arguments);
|
||||||
start_date: {},
|
|
||||||
end_date: {},
|
|
||||||
category_id: {},
|
|
||||||
group_id: {}
|
|
||||||
},
|
|
||||||
|
|
||||||
model(params) {
|
if (!controller.get("start_date")) {
|
||||||
return Report.find(
|
controller.set(
|
||||||
params.type,
|
"start_date",
|
||||||
params["start_date"],
|
moment()
|
||||||
params["end_date"],
|
.subtract("30", "day")
|
||||||
params["category_id"],
|
.format("YYYY-MM-DD")
|
||||||
params["group_id"]
|
);
|
||||||
);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
if (!controller.get("end_date")) {
|
||||||
controller.setProperties({
|
controller.set("end_date", moment().format("YYYY-MM-DD"));
|
||||||
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")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
export default function() {
|
export default function() {
|
||||||
this.route("admin", { resetNamespace: true }, function() {
|
this.route("admin", { resetNamespace: true }, function() {
|
||||||
this.route("dashboard", { path: "/dashboard-old" });
|
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(
|
this.route(
|
||||||
"adminSiteSettings",
|
"adminSiteSettings",
|
||||||
{ path: "/site_settings", resetNamespace: true },
|
{ 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}}
|
{{#if showVersionChecks}}
|
||||||
<div class="section-top">
|
<div class="section-top">
|
||||||
<div class="version-checks">
|
<div class="version-checks">
|
||||||
{{partial 'admin/templates/version-checks'}}
|
{{partial "admin/templates/version-checks"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{partial 'admin/templates/dashboard-problems'}}
|
{{partial "admin/templates/dashboard-problems"}}
|
||||||
|
|
||||||
<div class="community-health section">
|
<ul class="navigation">
|
||||||
<div class="section-title">
|
<li class="navigation-item general">
|
||||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
{{#link-to "admin.dashboardNext.general" class="navigation-link"}}
|
||||||
{{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}}
|
{{i18n "admin.dashboard.general_tab"}}
|
||||||
</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"}}
|
|
||||||
{{/link-to}}
|
{{/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">
|
{{outlet}}
|
||||||
{{dashboard-inline-table dataSourceNames="users_by_type" lastRefreshedAt=lastRefreshedAt}}
|
|
||||||
|
|
||||||
{{dashboard-inline-table dataSourceNames="users_by_trust_level" lastRefreshedAt=lastRefreshedAt}}
|
{{plugin-outlet name="admin-dashboard-bottom"}}
|
||||||
</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>
|
|
||||||
|
|||||||
@@ -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="report-container">
|
||||||
<div class="visualization">
|
<div class="visualization">
|
||||||
{{#conditional-loading-spinner condition=refreshing}}
|
{{admin-report
|
||||||
<div class='view-options'>
|
showAllReportsLink=true
|
||||||
{{#if viewingTable}}
|
dataSourceName=model.type
|
||||||
{{i18n 'admin.dashboard.reports.view_table'}}
|
categoryId=category_id
|
||||||
{{else}}
|
groupId=group_id
|
||||||
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
|
reportOptions=reportOptions
|
||||||
{{/if}}
|
startDate=start_date
|
||||||
|
|
endDate=end_date
|
||||||
{{#if viewingGraph}}
|
showFilteringUI=true
|
||||||
{{i18n 'admin.dashboard.reports.view_graph'}}
|
onSelectCategory=(action "onSelectCategory")
|
||||||
{{else}}
|
onSelectStartDate=(action "onSelectStartDate")
|
||||||
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
|
onSelectEndDate=(action "onSelectEndDate")}}
|
||||||
{{/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"}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export default DatePicker.extend({
|
|||||||
|
|
||||||
_opts() {
|
_opts() {
|
||||||
return {
|
return {
|
||||||
defaultDate: new Date(this.get("defaultDate")) || new Date(),
|
defaultDate:
|
||||||
|
moment(this.get("defaultDate"), "YYYY-MM-DD").toDate() || new Date(),
|
||||||
setDefaultDate: !!this.get("defaultDate"),
|
setDefaultDate: !!this.get("defaultDate"),
|
||||||
maxDate: new Date()
|
maxDate: new Date()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ export default Ember.Component.extend({
|
|||||||
weekdays: moment.weekdays(),
|
weekdays: moment.weekdays(),
|
||||||
weekdaysShort: moment.weekdaysShort()
|
weekdaysShort: moment.weekdaysShort()
|
||||||
},
|
},
|
||||||
onSelect: date =>
|
onSelect: date => {
|
||||||
this.set(
|
const formattedDate = moment(date).format("YYYY-MM-DD");
|
||||||
"value",
|
|
||||||
moment(date)
|
if (this.attrs.onSelect) {
|
||||||
.locale("en")
|
this.attrs.onSelect(formattedDate);
|
||||||
.format("YYYY-MM-DD")
|
}
|
||||||
)
|
|
||||||
|
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||||
|
|
||||||
|
this.set("value", formattedDate);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||||
|
|||||||
@@ -591,6 +591,10 @@ export function clipboardData(e, canUpload) {
|
|||||||
return { clipboard, types, canUpload, canPasteHtml };
|
return { clipboard, types, canUpload, canPasteHtml };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isNumeric(input) {
|
||||||
|
return !isNaN(parseFloat(input)) && isFinite(input);
|
||||||
|
}
|
||||||
|
|
||||||
export function fillMissingDates(data, startDate, endDate) {
|
export function fillMissingDates(data, startDate, endDate) {
|
||||||
const startMoment = moment(startDate, "YYYY-MM-DD");
|
const startMoment = moment(startDate, "YYYY-MM-DD");
|
||||||
const endMoment = moment(endDate, "YYYY-MM-DD");
|
const endMoment = moment(endDate, "YYYY-MM-DD");
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ export default Ember.Component.extend(
|
|||||||
if (get(this.actions, actionName)) {
|
if (get(this.actions, actionName)) {
|
||||||
run.next(() => this.send(actionName, ...params));
|
run.next(() => this.send(actionName, ...params));
|
||||||
} else if (this.get(actionName)) {
|
} 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/backups";
|
||||||
@import "common/admin/plugins";
|
@import "common/admin/plugins";
|
||||||
@import "common/admin/admin_reports";
|
@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";
|
@import "common/admin/dashboard_previous";
|
||||||
|
|||||||
127
app/assets/stylesheets/common/admin/admin_report.scss
Normal file
127
app/assets/stylesheets/common/admin/admin_report.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
app/assets/stylesheets/common/admin/admin_report_table.scss
Normal file
101
app/assets/stylesheets/common/admin/admin_report_table.scss
Normal file
@@ -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 {
|
.admin-reports {
|
||||||
h3 {
|
.admin-report {
|
||||||
border-bottom: 1px solid $primary-low;
|
width: 100%;
|
||||||
margin-bottom: 0.5em;
|
|
||||||
padding-bottom: 0.5em;
|
.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 {
|
.reports-list {
|
||||||
|
|||||||
@@ -10,6 +10,40 @@
|
|||||||
margin-bottom: 1em;
|
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 {
|
.section-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -177,7 +211,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.community-health {
|
.section {
|
||||||
.period-chooser .period-chooser-header {
|
.period-chooser .period-chooser-header {
|
||||||
.selected-name,
|
.selected-name,
|
||||||
.d-icon {
|
.d-icon {
|
||||||
@@ -203,105 +237,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-mini-chart {
|
.admin-report-table {
|
||||||
.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 {
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
&.is-disabled {
|
&.is-disabled {
|
||||||
@@ -320,92 +256,14 @@
|
|||||||
&.is-loading {
|
&.is-loading {
|
||||||
height: 150px;
|
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 {
|
.user-metrics {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-left: -5%;
|
flex-direction: column;
|
||||||
margin: 2em 0 0.75em -5%; // Negative margin allows for a margin when in 2-columns,
|
|
||||||
.dashboard-inline-table {
|
.dashboard-inline-table {
|
||||||
// and "hides" margin when the item spans 100% width
|
// and "hides" margin when the item spans 100% width
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
@@ -468,23 +326,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-inline-table {
|
.admin-report.activity-metrics {
|
||||||
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 {
|
|
||||||
table {
|
table {
|
||||||
@media screen and (min-width: 400px) {
|
@media screen and (min-width: 400px) {
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
@@ -505,6 +347,60 @@
|
|||||||
padding: 8px 0 8px 4px;
|
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
|
class Admin::DashboardNextController < Admin::AdminController
|
||||||
def index
|
def index
|
||||||
dashboard_data = AdminDashboardNextData.fetch_cached_stats
|
data = AdminDashboardNextIndexData.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
|
if SiteSetting.version_checks?
|
||||||
render json: dashboard_data
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
require_dependency 'admin_dashboard_data'
|
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'
|
||||||
require_dependency 'group_message'
|
require_dependency 'group_message'
|
||||||
|
|
||||||
@@ -15,7 +18,9 @@ module Jobs
|
|||||||
end
|
end
|
||||||
|
|
||||||
# TODO: decide if we want to keep caching this every 30 minutes
|
# 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
|
AdminDashboardData.refresh_stats
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,51 +1,31 @@
|
|||||||
class AdminDashboardNextData
|
class AdminDashboardNextData
|
||||||
include StatsCacheable
|
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 = {})
|
def initialize(opts = {})
|
||||||
@opts = opts
|
@opts = opts
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.fetch_stats
|
def self.fetch_stats
|
||||||
AdminDashboardNextData.new.as_json
|
self.class.new.as_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stats_cache_key
|
def self.fetch_stats
|
||||||
'dash-next-stats'
|
new.as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_json
|
||||||
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_json(_options = nil)
|
def as_json(_options = nil)
|
||||||
@json ||= get_json
|
@json ||= get_json
|
||||||
end
|
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)
|
def self.reports(source)
|
||||||
source.map { |type| Report.find(type).as_json }
|
source.map { |type| Report.find(type).as_json }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.stats_cache_key
|
||||||
|
'dashboard-next-data'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
27
app/models/admin_dashboard_next_general_data.rb
Normal file
27
app/models/admin_dashboard_next_general_data.rb
Normal file
@@ -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
|
||||||
14
app/models/admin_dashboard_next_index_data.rb
Normal file
14
app/models/admin_dashboard_next_index_data.rb
Normal file
@@ -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
|
||||||
19
app/models/admin_dashboard_next_moderation_data.rb
Normal file
19
app/models/admin_dashboard_next_moderation_data.rb
Normal file
@@ -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
|
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)
|
def initialize(type)
|
||||||
@type = type
|
@type = type
|
||||||
@@ -15,7 +15,8 @@ class IncomingLinksReport
|
|||||||
xaxis: I18n.t("reports.#{self.type}.xaxis"),
|
xaxis: I18n.t("reports.#{self.type}.xaxis"),
|
||||||
ytitles: self.y_titles,
|
ytitles: self.y_titles,
|
||||||
data: self.data,
|
data: self.data,
|
||||||
start_date: start_date
|
start_date: start_date,
|
||||||
|
end_date: end_date
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ class IncomingLinksReport
|
|||||||
report = IncomingLinksReport.new(type)
|
report = IncomingLinksReport.new(type)
|
||||||
|
|
||||||
report.start_date = _opts[:start_date] || 30.days.ago
|
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]
|
report.limit = _opts[:limit].to_i if _opts[:limit]
|
||||||
|
|
||||||
send(report_method, report)
|
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_clicks] = I18n.t("reports.#{report.type}.num_clicks")
|
||||||
report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics")
|
report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics")
|
||||||
|
|
||||||
num_clicks = link_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)
|
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; }
|
user_id_lookup = User.where(username: num_clicks.keys).select(:id, :username).inject({}) { |sum, v| sum[v.username] = v.id; sum; }
|
||||||
report.data = []
|
report.data = []
|
||||||
num_clicks.each_key do |username|
|
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]
|
report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.per_user(start_date:)
|
def self.per_user(start_date:, end_date:)
|
||||||
@per_user_query ||= public_incoming_links
|
@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)
|
.joins(:user)
|
||||||
.group('users.username')
|
.group('users.username')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.link_count_per_user(start_date:)
|
def self.link_count_per_user(start_date:, end_date:)
|
||||||
per_user(start_date: start_date).count
|
per_user(start_date: start_date, end_date: end_date).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.topic_count_per_user(start_date:)
|
def self.topic_count_per_user(start_date:, end_date:)
|
||||||
per_user(start_date: start_date).joins(:post).count("DISTINCT posts.topic_id")
|
per_user(start_date: start_date, end_date: end_date).joins(:post).count("DISTINCT posts.topic_id")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return top 10 domains that brought traffic to the site within the last 30 days
|
# 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_topics] = I18n.t("reports.#{report.type}.num_topics")
|
||||||
report.y_titles[:num_users] = I18n.t("reports.#{report.type}.num_users")
|
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)
|
num_topics = topic_count_per_domain(num_clicks.keys)
|
||||||
report.data = []
|
report.data = []
|
||||||
num_clicks.each_key do |domain|
|
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]
|
report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10]
|
||||||
end
|
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
|
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)
|
.joins(incoming_referer: :incoming_domain)
|
||||||
.group('incoming_domains.name')
|
.group('incoming_domains.name')
|
||||||
.order('count_all DESC')
|
.order('count_all DESC')
|
||||||
@@ -102,7 +104,7 @@ class IncomingLinksReport
|
|||||||
|
|
||||||
def self.report_top_referred_topics(report)
|
def self.report_top_referred_topics(report)
|
||||||
report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks")
|
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
|
num_clicks = num_clicks.to_a.sort_by { |x| x[1] }.last(report.limit || 10).reverse
|
||||||
report.data = []
|
report.data = []
|
||||||
topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] })
|
topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] })
|
||||||
@@ -115,9 +117,9 @@ class IncomingLinksReport
|
|||||||
report.data
|
report.data
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.link_count_per_topic(start_date:)
|
def self.link_count_per_topic(start_date:, end_date:)
|
||||||
public_incoming_links
|
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')
|
.group('topic_id')
|
||||||
.count
|
.count
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
require_dependency 'topic_subtype'
|
require_dependency 'topic_subtype'
|
||||||
|
|
||||||
class Report
|
class Report
|
||||||
|
|
||||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||||
:end_date, :category_id, :group_id, :labels, :async,
|
:end_date, :category_id, :group_id, :labels, :async,
|
||||||
:prev_period, :facets, :limit, :processing, :average, :percent,
|
: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
|
def self.default_days
|
||||||
30
|
30
|
||||||
@@ -15,9 +16,15 @@ class Report
|
|||||||
@type = type
|
@type = type
|
||||||
@start_date ||= Report.default_days.days.ago.beginning_of_day
|
@start_date ||= Report.default_days.days.ago.beginning_of_day
|
||||||
@end_date ||= Time.zone.now.end_of_day
|
@end_date ||= Time.zone.now.end_of_day
|
||||||
|
@prev_end_date = @start_date
|
||||||
@average = false
|
@average = false
|
||||||
@percent = false
|
@percent = false
|
||||||
@higher_is_better = true
|
@higher_is_better = true
|
||||||
|
@category_filtering = false
|
||||||
|
@group_filtering = false
|
||||||
|
@modes = [:table, :chart]
|
||||||
|
@prev_data = nil
|
||||||
|
@dates_filtering = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cache_key(report)
|
def self.cache_key(report)
|
||||||
@@ -39,6 +46,28 @@ class Report
|
|||||||
end
|
end
|
||||||
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)
|
def as_json(options = nil)
|
||||||
description = I18n.t("reports.#{type}.description", default: "")
|
description = I18n.t("reports.#{type}.description", default: "")
|
||||||
|
|
||||||
@@ -51,18 +80,29 @@ class Report
|
|||||||
data: data,
|
data: data,
|
||||||
start_date: start_date&.iso8601,
|
start_date: start_date&.iso8601,
|
||||||
end_date: end_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,
|
category_id: category_id,
|
||||||
group_id: group_id,
|
group_id: group_id,
|
||||||
prev30Days: self.prev30Days,
|
prev30Days: self.prev30Days,
|
||||||
|
dates_filtering: self.dates_filtering,
|
||||||
report_key: Report.cache_key(self),
|
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,
|
processing: self.processing,
|
||||||
average: self.average,
|
average: self.average,
|
||||||
percent: self.percent,
|
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|
|
}.tap do |json|
|
||||||
json[:total] = total if total
|
json[:timeout] = self.timeout if self.timeout
|
||||||
json[:prev_period] = prev_period if prev_period
|
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[:prev30Days] = self.prev30Days if self.prev30Days
|
||||||
json[:limit] = self.limit if self.limit
|
json[:limit] = self.limit if self.limit
|
||||||
|
|
||||||
@@ -105,6 +145,8 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.find(type, opts = nil)
|
def self.find(type, opts = nil)
|
||||||
|
clear_cache
|
||||||
|
|
||||||
report = _get(type, opts)
|
report = _get(type, opts)
|
||||||
report_method = :"report_#{type}"
|
report_method = :"report_#{type}"
|
||||||
|
|
||||||
@@ -129,6 +171,10 @@ class Report
|
|||||||
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if filter == :page_view_total
|
||||||
|
report.icon = 'file'
|
||||||
|
end
|
||||||
|
|
||||||
report.data = []
|
report.data = []
|
||||||
data.where('date >= ? AND date <= ?', report.start_date, report.end_date)
|
data.where('date >= ? AND date <= ?', report.start_date, report.end_date)
|
||||||
.order(date: :asc)
|
.order(date: :asc)
|
||||||
@@ -147,6 +193,9 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_visits(report)
|
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
|
basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id
|
||||||
|
|
||||||
add_counts report, UserVisit, 'visited_at'
|
add_counts report, UserVisit, 'visited_at'
|
||||||
@@ -159,13 +208,16 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_signups(report)
|
def self.report_signups(report)
|
||||||
|
report.group_filtering = true
|
||||||
|
|
||||||
if report.group_id
|
if report.group_id
|
||||||
basic_report_about report, User.real, :count_by_signup_date, report.start_date, report.end_date, 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'
|
add_counts report, User.real, 'users.created_at'
|
||||||
else
|
else
|
||||||
|
|
||||||
report_about report, User.real, :count_by_signup_date
|
report_about report, User.real, :count_by_signup_date
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_new_contributors(report)
|
def self.report_new_contributors(report)
|
||||||
@@ -183,8 +235,9 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
if report.facets.include?(:prev_period)
|
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_period = prev_period_data.sum { |k, v| v }
|
||||||
|
report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } }
|
||||||
end
|
end
|
||||||
|
|
||||||
data.each do |key, value|
|
data.each do |key, value|
|
||||||
@@ -209,7 +262,7 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
if report.facets.include?(:prev_period)
|
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 }
|
prev = prev_data.sum { |k, v| v }
|
||||||
if prev > 0
|
if prev > 0
|
||||||
@@ -252,7 +305,7 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
if report.facets.include?(:prev_period)
|
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
|
end
|
||||||
|
|
||||||
if report.facets.include?(:prev30Days)
|
if report.facets.include?(:prev30Days)
|
||||||
@@ -261,6 +314,7 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_profile_views(report)
|
def self.report_profile_views(report)
|
||||||
|
report.group_filtering = true
|
||||||
start_date = report.start_date
|
start_date = report.start_date
|
||||||
end_date = report.end_date
|
end_date = report.end_date
|
||||||
basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id
|
basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id
|
||||||
@@ -270,6 +324,7 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_topics(report)
|
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
|
basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id
|
||||||
countable = Topic.listable_topics
|
countable = Topic.listable_topics
|
||||||
countable = countable.where(category_id: report.category_id) if report.category_id
|
countable = countable.where(category_id: report.category_id) if report.category_id
|
||||||
@@ -277,6 +332,8 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_posts(report)
|
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
|
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 = Post.public_posts.where(post_type: Post.types[:regular])
|
||||||
countable = countable.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
|
countable = countable.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
|
||||||
@@ -284,6 +341,8 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_time_to_first_response(report)
|
def self.report_time_to_first_response(report)
|
||||||
|
report.category_filtering = true
|
||||||
|
report.icon = 'reply'
|
||||||
report.higher_is_better = false
|
report.higher_is_better = false
|
||||||
report.data = []
|
report.data = []
|
||||||
Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r|
|
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
|
end
|
||||||
|
|
||||||
def self.report_topics_with_no_response(report)
|
def self.report_topics_with_no_response(report)
|
||||||
|
report.category_filtering = true
|
||||||
report.data = []
|
report.data = []
|
||||||
Topic.with_no_response_per_day(report.start_date, report.end_date, report.category_id).each do |r|
|
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 }
|
report.data << { x: r["date"], y: r["count"].to_i }
|
||||||
@@ -319,12 +379,21 @@ class Report
|
|||||||
end
|
end
|
||||||
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')
|
def self.add_counts(report, subject_class, query_column = 'created_at')
|
||||||
if report.facets.include?(:prev_period)
|
if report.facets.include?(:prev_period)
|
||||||
report.prev_period = subject_class
|
prev_data = subject_class
|
||||||
.where("#{query_column} >= ? and #{query_column} < ?",
|
.where("#{query_column} >= ? and #{query_column} < ?",
|
||||||
(report.start_date - (report.end_date - report.start_date)),
|
report.prev_start_date,
|
||||||
report.start_date).count
|
report.prev_end_date)
|
||||||
|
|
||||||
|
report.prev_period = prev_data.count
|
||||||
end
|
end
|
||||||
|
|
||||||
if report.facets.include?(:total)
|
if report.facets.include?(:total)
|
||||||
@@ -342,6 +411,15 @@ class Report
|
|||||||
def self.report_users_by_trust_level(report)
|
def self.report_users_by_trust_level(report)
|
||||||
report.data = []
|
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|
|
User.real.group('trust_level').count.sort.each do |level, count|
|
||||||
key = TrustLevel.levels[level.to_i]
|
key = TrustLevel.levels[level.to_i]
|
||||||
url = Proc.new { |k| "/admin/users/list/#{k}" }
|
url = Proc.new { |k| "/admin/users/list/#{k}" }
|
||||||
@@ -351,6 +429,8 @@ class Report
|
|||||||
|
|
||||||
# Post action counts:
|
# Post action counts:
|
||||||
def self.report_flags(report)
|
def self.report_flags(report)
|
||||||
|
report.category_filtering = true
|
||||||
|
report.icon = 'flag'
|
||||||
report.higher_is_better = false
|
report.higher_is_better = false
|
||||||
|
|
||||||
basic_report_about report, PostAction, :flag_count_by_date, report.start_date, report.end_date, report.category_id
|
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
|
end
|
||||||
|
|
||||||
def self.report_likes(report)
|
def self.report_likes(report)
|
||||||
|
report.category_filtering = true
|
||||||
|
report.icon = 'heart'
|
||||||
post_action_report report, PostActionType.types[:like]
|
post_action_report report, PostActionType.types[:like]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_bookmarks(report)
|
def self.report_bookmarks(report)
|
||||||
|
report.category_filtering = true
|
||||||
|
report.icon = 'bookmark'
|
||||||
post_action_report report, PostActionType.types[:bookmark]
|
post_action_report report, PostActionType.types[:bookmark]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -380,47 +464,68 @@ class Report
|
|||||||
# Private messages counts:
|
# Private messages counts:
|
||||||
|
|
||||||
def self.private_messages_report(report, topic_subtype)
|
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
|
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'
|
add_counts report, Topic.private_messages.with_subtype(topic_subtype), 'topics.created_at'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_user_to_user_private_messages(report)
|
def self.report_user_to_user_private_messages(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
private_messages_report report, TopicSubtype.user_to_user
|
private_messages_report report, TopicSubtype.user_to_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_user_to_user_private_messages_with_replies(report)
|
def self.report_user_to_user_private_messages_with_replies(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
topic_subtype = TopicSubtype.user_to_user
|
topic_subtype = TopicSubtype.user_to_user
|
||||||
basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype
|
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'
|
add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_system_private_messages(report)
|
def self.report_system_private_messages(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
private_messages_report report, TopicSubtype.system_message
|
private_messages_report report, TopicSubtype.system_message
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_moderator_warning_private_messages(report)
|
def self.report_moderator_warning_private_messages(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
private_messages_report report, TopicSubtype.moderator_warning
|
private_messages_report report, TopicSubtype.moderator_warning
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_notify_moderators_private_messages(report)
|
def self.report_notify_moderators_private_messages(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
private_messages_report report, TopicSubtype.notify_moderators
|
private_messages_report report, TopicSubtype.notify_moderators
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_notify_user_private_messages(report)
|
def self.report_notify_user_private_messages(report)
|
||||||
|
report.icon = 'envelope'
|
||||||
private_messages_report report, TopicSubtype.notify_user
|
private_messages_report report, TopicSubtype.notify_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_web_crawlers(report)
|
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)
|
report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date)
|
||||||
.limit(200)
|
.limit(200)
|
||||||
.order('sum_count DESC')
|
.order('sum_count DESC')
|
||||||
.group(:user_agent).sum(:count)
|
.group(:user_agent).sum(:count)
|
||||||
.map { |ua, count| { x: ua, y: count } }
|
.map { |ua, count| { user_agent: ua, count: count } }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_users_by_type(report)
|
def self.report_users_by_type(report)
|
||||||
report.data = []
|
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}") }
|
label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") }
|
||||||
url = Proc.new { |key| "/admin/users/list/#{key}" }
|
url = Proc.new { |key| "/admin/users/list/#{key}" }
|
||||||
|
|
||||||
@@ -438,15 +543,49 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.report_top_referred_topics(report)
|
def self.report_top_referred_topics(report)
|
||||||
report.labels = [I18n.t("reports.top_referred_topics.xaxis"),
|
report.modes = [:table]
|
||||||
I18n.t("reports.top_referred_topics.num_clicks")]
|
|
||||||
result = IncomingLinksReport.find(:top_referred_topics, start_date: 7.days.ago, limit: report.limit)
|
report.labels = [
|
||||||
report.data = result.data
|
{ 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
|
end
|
||||||
|
|
||||||
def self.report_trending_search(report)
|
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.data = []
|
||||||
|
|
||||||
|
report.modes = [:table]
|
||||||
|
|
||||||
select_sql = <<~SQL
|
select_sql = <<~SQL
|
||||||
lower(term) term,
|
lower(term) term,
|
||||||
COUNT(*) AS searches,
|
COUNT(*) AS searches,
|
||||||
@@ -463,10 +602,6 @@ class Report
|
|||||||
.order('unique_searches DESC, click_through ASC, term ASC')
|
.order('unique_searches DESC, click_through ASC, term ASC')
|
||||||
.limit(report.limit || 20).to_a
|
.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|
|
trends.each do |trend|
|
||||||
ctr =
|
ctr =
|
||||||
if trend.click_through == 0 || trend.searches == 0
|
if trend.click_through == 0 || trend.searches == 0
|
||||||
@@ -478,8 +613,300 @@ class Report
|
|||||||
report.data << {
|
report.data << {
|
||||||
term: trend.term,
|
term: trend.term,
|
||||||
unique_searches: trend.unique_searches,
|
unique_searches: trend.unique_searches,
|
||||||
ctr: (ctr * 100).ceil(1).to_s + "%"
|
ctr: (ctr * 100).ceil(1)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -2777,9 +2777,14 @@ en:
|
|||||||
page_views_short: "Pageviews"
|
page_views_short: "Pageviews"
|
||||||
show_traffic_report: "Show Detailed Traffic Report"
|
show_traffic_report: "Show Detailed Traffic Report"
|
||||||
community_health: Community health
|
community_health: Community health
|
||||||
|
moderators_activity: Moderators activity
|
||||||
whats_new_in_discourse: What’s new in Discourse?
|
whats_new_in_discourse: What’s new in Discourse?
|
||||||
activity_metrics: Activity Metrics
|
activity_metrics: Activity Metrics
|
||||||
all_reports: "All reports"
|
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:
|
reports:
|
||||||
trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period."
|
trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period."
|
||||||
@@ -2798,8 +2803,8 @@ en:
|
|||||||
end_date: "End Date"
|
end_date: "End Date"
|
||||||
groups: "All groups"
|
groups: "All groups"
|
||||||
disabled: "This report is disabled"
|
disabled: "This report is disabled"
|
||||||
total_for_period: "Total for period"
|
totals_for_sample: "Totals for sample"
|
||||||
total: "Total"
|
total: "All time total"
|
||||||
trending_search:
|
trending_search:
|
||||||
more: '<a href="/admin/logs/search_logs">Search logs</a>'
|
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.'
|
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"
|
write: "Write all"
|
||||||
|
|
||||||
reports:
|
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:
|
visits:
|
||||||
title: "User Visits"
|
title: "User Visits"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
@@ -898,10 +930,14 @@ en:
|
|||||||
title: "Users per Trust Level"
|
title: "Users per Trust Level"
|
||||||
xaxis: "Trust Level"
|
xaxis: "Trust Level"
|
||||||
yaxis: "Number of Users"
|
yaxis: "Number of Users"
|
||||||
|
labels:
|
||||||
|
level: Level
|
||||||
users_by_type:
|
users_by_type:
|
||||||
title: "Users per Type"
|
title: "Users per Type"
|
||||||
xaxis: "Type"
|
xaxis: "Type"
|
||||||
yaxis: "Number of Users"
|
yaxis: "Number of Users"
|
||||||
|
labels:
|
||||||
|
type: Type
|
||||||
xaxis_labels:
|
xaxis_labels:
|
||||||
admin: Admin
|
admin: Admin
|
||||||
moderator: Moderator
|
moderator: Moderator
|
||||||
@@ -952,10 +988,15 @@ en:
|
|||||||
num_clicks: "Clicks"
|
num_clicks: "Clicks"
|
||||||
num_topics: "Topics"
|
num_topics: "Topics"
|
||||||
num_users: "Users"
|
num_users: "Users"
|
||||||
|
labels:
|
||||||
|
domain: Domain
|
||||||
|
num_clicks: Clicks
|
||||||
|
num_topics: Topics
|
||||||
top_referred_topics:
|
top_referred_topics:
|
||||||
title: "Top Referred Topics"
|
title: "Top Referred Topics"
|
||||||
xaxis: "Topic"
|
labels:
|
||||||
num_clicks: "Clicks"
|
num_clicks: "Clicks"
|
||||||
|
topic: "Topic"
|
||||||
page_view_anon_reqs:
|
page_view_anon_reqs:
|
||||||
title: "Anonymous"
|
title: "Anonymous"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
@@ -1018,8 +1059,9 @@ en:
|
|||||||
yaxis: "Number of visits"
|
yaxis: "Number of visits"
|
||||||
web_crawlers:
|
web_crawlers:
|
||||||
title: "Web Crawler Requests"
|
title: "Web Crawler Requests"
|
||||||
xaxis: "User Agent"
|
labels:
|
||||||
yaxis: "Pageviews"
|
user_agent: "User Agent"
|
||||||
|
page_views: "Pageviews"
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
rails_env_warning: "Your server is running in %{env} mode."
|
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-next" => "dashboard_next#index"
|
||||||
get "dashboard-old" => "dashboard#index"
|
get "dashboard-old" => "dashboard#index"
|
||||||
|
get "dashboard/moderation" => "dashboard_next#moderation"
|
||||||
|
get "dashboard/general" => "dashboard_next#general"
|
||||||
|
|
||||||
resources :dashboard, only: [:index] do
|
resources :dashboard, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ describe Report do
|
|||||||
it "returns a report with data" do
|
it "returns a report with data" do
|
||||||
expect(report.data[0][:term]).to eq("ruby")
|
expect(report.data[0][:term]).to eq("ruby")
|
||||||
expect(report.data[0][:unique_searches]).to eq(2)
|
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][:term]).to eq("php")
|
||||||
expect(report.data[1][:unique_searches]).to eq(1)
|
expect(report.data[1][:unique_searches]).to eq(1)
|
||||||
@@ -435,4 +435,173 @@ describe Report do
|
|||||||
expect(r.data[0][:y]).to eq(1)
|
expect(r.data[0][:y]).to eq(1)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -7,25 +7,18 @@ acceptance("Dashboard Next", {
|
|||||||
QUnit.test("Visit dashboard next page", async assert => {
|
QUnit.test("Visit dashboard next page", async assert => {
|
||||||
await visit("/admin");
|
await visit("/admin");
|
||||||
|
|
||||||
assert.ok($(".dashboard-next").length, "has dashboard-next class");
|
assert.ok(exists(".dashboard-next"), "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(".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(
|
assert.ok(
|
||||||
$(".dashboard-mini-chart.dau_by_mau").length,
|
exists(".admin-report.daily-engaged-users"),
|
||||||
"has a dau_by_mau chart"
|
"daily-engaged-users report"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
$(".dashboard-mini-chart.daily_engaged_users").length,
|
exists(".admin-report.new-contributors"),
|
||||||
"has a daily_engaged_users chart"
|
"new-contributors report"
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
$(".dashboard-mini-chart.new_contributors").length,
|
|
||||||
"has a new_contributors chart"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
130
test/javascripts/components/admin-report-test.js.es6
Normal file
130
test/javascripts/components/admin-report-test.js.es6
Normal file
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
13
test/javascripts/fixtures/admin-general.js.es6
Normal file
13
test/javascripts/fixtures/admin-general.js.es6
Normal file
@@ -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 {
|
export default {
|
||||||
"/admin/reports/daily_engaged_users": {
|
"/admin/reports/daily_engaged_users": {
|
||||||
report: {
|
report: {
|
||||||
type: "daily_engaged_users",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/dashboard-next.json": {
|
"/admin/dashboard-next.json": {
|
||||||
reports: [],
|
updated_at: "2018-04-25T08:06:11.292Z"
|
||||||
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 {
|
export default {
|
||||||
"/admin/reports/dau_by_mau": {
|
"/admin/reports/dau_by_mau": {
|
||||||
report: {
|
report: {
|
||||||
type: "dau_by_mau",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/new_contributors": {
|
"/admin/reports/new_contributors": {
|
||||||
report: {
|
report: {
|
||||||
type: "new_contributors",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/posts": {
|
"/admin/reports/posts": {
|
||||||
report: {
|
report: {
|
||||||
type: "topics",
|
report_key: "posts"
|
||||||
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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,53 +4,76 @@ export default {
|
|||||||
type: "signups",
|
type: "signups",
|
||||||
title: "Signups",
|
title: "Signups",
|
||||||
xaxis: "Day",
|
xaxis: "Day",
|
||||||
yaxis: "Number of new users",
|
yaxis: "Number of signups",
|
||||||
|
description: "New account registrations for this period",
|
||||||
data: [
|
data: [
|
||||||
{
|
{ x: "2018-06-16", y: 12 },
|
||||||
x: "2018-04-11",
|
{ x: "2018-06-17", y: 16 },
|
||||||
y: 10
|
{ x: "2018-06-18", y: 42 },
|
||||||
},
|
{ x: "2018-06-19", y: 38 },
|
||||||
{
|
{ x: "2018-06-20", y: 41 },
|
||||||
x: "2018-04-12",
|
{ x: "2018-06-21", y: 32 },
|
||||||
y: 10
|
{ x: "2018-06-22", y: 23 },
|
||||||
},
|
{ x: "2018-06-23", y: 23 },
|
||||||
{
|
{ x: "2018-06-24", y: 17 },
|
||||||
x: "2018-04-13",
|
{ x: "2018-06-25", y: 27 },
|
||||||
y: 22
|
{ x: "2018-06-26", y: 32 },
|
||||||
},
|
{ x: "2018-06-27", y: 7 }
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
total: 136,
|
start_date: "2018-06-16T00:00:00Z",
|
||||||
start_date: "2018-03-26T00:00:00.000Z",
|
end_date: "2018-07-16T23:59:59Z",
|
||||||
end_date: "2018-04-25T23:59:59.999Z",
|
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,
|
category_id: null,
|
||||||
group_id: null,
|
group_id: null,
|
||||||
prev30Days: 0,
|
prev30Days: null,
|
||||||
labels: null,
|
dates_filtering: true,
|
||||||
report_key: ""
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
35
test/javascripts/fixtures/signups_timeout.js.es6
Normal file
35
test/javascripts/fixtures/signups_timeout.js.es6
Normal file
@@ -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 {
|
export default {
|
||||||
"/admin/reports/top_referred_topics": {
|
"/admin/reports/top_referred_topics": {
|
||||||
report: {
|
report: {
|
||||||
type: "top_referred_topics",
|
report_key: "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"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/topics": {
|
"/admin/reports/topics": {
|
||||||
report: {
|
report: {
|
||||||
type: "topics",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/trending_search": {
|
"/admin/reports/trending_search": {
|
||||||
report: {
|
report: {
|
||||||
type: "trending_search",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/users_by_trust_level": {
|
"/admin/reports/users_by_trust_level": {
|
||||||
report: {
|
report: {
|
||||||
type: "users_by_trust_level",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"/admin/reports/users_by_type": {
|
"/admin/reports/users_by_type": {
|
||||||
report: {
|
report: {
|
||||||
type: "users_by_type",
|
report_key: "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: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -391,3 +391,74 @@ QUnit.test("average", assert => {
|
|||||||
report.set("average", false);
|
report.set("average", false);
|
||||||
assert.ok(report.get("lastSevenDaysCount") === 35);
|
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");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user