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 DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||
import Report from "admin/models/report";
|
||||
import VersionCheck from "admin/models/version-check";
|
||||
|
||||
const PROBLEMS_CHECK_MINUTES = 1;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["period"],
|
||||
period: "monthly",
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller("exception"),
|
||||
showVersionChecks: setting("version_checks"),
|
||||
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||
lastBackupTakenAt: Ember.computed.alias(
|
||||
"model.attributes.last_backup_taken_at"
|
||||
),
|
||||
logSearchQueriesEnabled: setting("log_search_queries"),
|
||||
availablePeriods: ["yearly", "quarterly", "monthly", "weekly"],
|
||||
shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"),
|
||||
|
||||
@computed("problems.length")
|
||||
foundProblems(problemsLength) {
|
||||
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
||||
},
|
||||
|
||||
fetchProblems() {
|
||||
if (this.get("isLoadingProblems")) return;
|
||||
|
||||
if (
|
||||
!this.get("problemsFetchedAt") ||
|
||||
moment()
|
||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
||||
.toDate() > this.get("problemsFetchedAt")
|
||||
) {
|
||||
this._loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
|
||||
if (this.get("isLoading") || !versionChecks) return;
|
||||
|
||||
if (
|
||||
!this.get("dashboardFetchedAt") ||
|
||||
@@ -38,22 +42,17 @@ export default Ember.Controller.extend({
|
||||
) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
AdminDashboardNext.fetch()
|
||||
.then(model => {
|
||||
let properties = {
|
||||
dashboardFetchedAt: new Date()
|
||||
};
|
||||
|
||||
AdminDashboardNext.find()
|
||||
.then(adminDashboardNextModel => {
|
||||
if (versionChecks) {
|
||||
this.set(
|
||||
"versionCheck",
|
||||
VersionCheck.create(adminDashboardNextModel.version_check)
|
||||
);
|
||||
properties.versionCheck = VersionCheck.create(model.version_check);
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
dashboardFetchedAt: new Date(),
|
||||
model: adminDashboardNextModel,
|
||||
reports: adminDashboardNextModel.reports.map(x => Report.create(x))
|
||||
});
|
||||
this.setProperties(properties);
|
||||
})
|
||||
.catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
@@ -63,27 +62,17 @@ export default Ember.Controller.extend({
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!this.get("problemsFetchedAt") ||
|
||||
moment()
|
||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
||||
.toDate() > this.get("problemsFetchedAt")
|
||||
) {
|
||||
this.loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
loadProblems() {
|
||||
this.set("loadingProblems", true);
|
||||
this.set("problemsFetchedAt", new Date());
|
||||
_loadProblems() {
|
||||
this.setProperties({
|
||||
loadingProblems: true,
|
||||
problemsFetchedAt: new Date()
|
||||
});
|
||||
|
||||
AdminDashboardNext.fetchProblems()
|
||||
.then(d => {
|
||||
this.set("problems", d.problems);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loadingProblems", false);
|
||||
});
|
||||
.then(model => this.set("problems", model.problems))
|
||||
.finally(() => this.set("loadingProblems", false));
|
||||
},
|
||||
|
||||
@computed("problemsFetchedAt")
|
||||
@@ -93,69 +82,9 @@ export default Ember.Controller.extend({
|
||||
.format("LLL");
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.endOf("day")
|
||||
.subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day")
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
@computed("model.attributes.updated_at")
|
||||
updatedTimestamp(updatedAt) {
|
||||
return moment(updatedAt).format("LLL");
|
||||
},
|
||||
|
||||
@computed("lastBackupTakenAt")
|
||||
backupTimestamp(lastBackupTakenAt) {
|
||||
return moment(lastBackupTakenAt).format("LLL");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
},
|
||||
refreshProblems() {
|
||||
this.loadProblems();
|
||||
this._loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return Discourse.getURL(`/admin?period=${period}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,108 +1,36 @@
|
||||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import Report from "admin/models/report";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
||||
viewMode: "graph",
|
||||
viewingTable: Em.computed.equal("viewMode", "table"),
|
||||
viewingGraph: Em.computed.equal("viewMode", "graph"),
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
queryParams: ["start_date", "end_date", "category_id", "group_id"],
|
||||
categoryId: null,
|
||||
groupId: null,
|
||||
refreshing: false,
|
||||
|
||||
@computed()
|
||||
categoryOptions() {
|
||||
const arr = [{ name: I18n.t("category.all"), value: "all" }];
|
||||
return arr.concat(
|
||||
Discourse.Site.currentProp("sortedCategories").map(i => {
|
||||
return { name: i.get("name"), value: i.get("id") };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed()
|
||||
groupOptions() {
|
||||
const arr = [
|
||||
{ name: I18n.t("admin.dashboard.reports.groups"), value: "all" }
|
||||
];
|
||||
return arr.concat(
|
||||
this.site.groups.map(i => {
|
||||
return { name: i["name"], value: i["id"] };
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@computed("model.type")
|
||||
showCategoryOptions(modelType) {
|
||||
return [
|
||||
"topics",
|
||||
"posts",
|
||||
"time_to_first_response_total",
|
||||
"topics_with_no_response",
|
||||
"flags",
|
||||
"likes",
|
||||
"bookmarks"
|
||||
].includes(modelType);
|
||||
},
|
||||
reportOptions(type) {
|
||||
let options = { table: { perPage: 50, limit: 50 } };
|
||||
|
||||
@computed("model.type")
|
||||
showGroupOptions(modelType) {
|
||||
return (
|
||||
modelType === "visits" ||
|
||||
modelType === "signups" ||
|
||||
modelType === "profile_views"
|
||||
);
|
||||
if (type === "top_referred_topics") {
|
||||
options.table.limit = 10;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
actions: {
|
||||
refreshReport() {
|
||||
var q;
|
||||
this.set("refreshing", true);
|
||||
|
||||
this.setProperties({
|
||||
start_date: this.get("startDate"),
|
||||
end_date: this.get("endDate"),
|
||||
category_id: this.get("categoryId")
|
||||
});
|
||||
|
||||
if (this.get("groupId")) {
|
||||
this.set("group_id", this.get("groupId"));
|
||||
}
|
||||
|
||||
q = Report.find(
|
||||
this.get("model.type"),
|
||||
this.get("startDate"),
|
||||
this.get("endDate"),
|
||||
this.get("categoryId"),
|
||||
this.get("groupId")
|
||||
);
|
||||
q.then(m => this.set("model", m)).finally(() =>
|
||||
this.set("refreshing", false)
|
||||
);
|
||||
onSelectStartDate(startDate) {
|
||||
this.set("start_date", startDate);
|
||||
},
|
||||
|
||||
viewAsTable() {
|
||||
this.set("viewMode", "table");
|
||||
onSelectCategory(categoryId) {
|
||||
this.set("category_id", categoryId);
|
||||
},
|
||||
|
||||
viewAsGraph() {
|
||||
this.set("viewMode", "graph");
|
||||
onSelectGroup(groupId) {
|
||||
this.set("group_id", groupId);
|
||||
},
|
||||
|
||||
exportCsv() {
|
||||
exportEntity("report", {
|
||||
name: this.get("model.type"),
|
||||
start_date: this.get("startDate"),
|
||||
end_date: this.get("endDate"),
|
||||
category_id:
|
||||
this.get("categoryId") === "all" ? undefined : this.get("categoryId"),
|
||||
group_id:
|
||||
this.get("groupId") === "all" ? undefined : this.get("groupId")
|
||||
}).then(outputExportResult);
|
||||
onSelectEndDate(endDate) {
|
||||
this.set("end_date", endDate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: ["isLoading", "dataSourceNames"],
|
||||
reports: null,
|
||||
isLoading: false,
|
||||
dataSourceNames: "",
|
||||
title: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.set("reports", []);
|
||||
},
|
||||
|
||||
@computed("dataSourceNames")
|
||||
dataSources(dataSourceNames) {
|
||||
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
|
||||
},
|
||||
|
||||
buildPayload(facets) {
|
||||
let payload = { data: { cache: true, facets } };
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").format(
|
||||
"YYYY-MM-DD[T]HH:mm:ss.SSSZZ"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = this.get("endDate").format(
|
||||
"YYYY-MM-DD[T]HH:mm:ss.SSSZZ"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get("limit")) {
|
||||
payload.data.limit = this.get("limit");
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
@computed("reports.[]", "startDate", "endDate", "dataSourceNames")
|
||||
reportsForPeriod(reports, startDate, endDate, dataSourceNames) {
|
||||
// on a slow network fetchReport could be called multiple times between
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
reports = reports.uniqBy("report_key");
|
||||
|
||||
const sort = r => {
|
||||
if (r.length > 1) {
|
||||
return dataSourceNames.split(",").map(name => r.findBy("type", name));
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return sort(reports);
|
||||
}
|
||||
|
||||
return sort(
|
||||
reports.filter(report => {
|
||||
return (
|
||||
report.report_key.includes(startDate.format("YYYYMMDD")) &&
|
||||
report.report_key.includes(endDate.format("YYYYMMDD"))
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport().finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport().finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
this.set(
|
||||
"title",
|
||||
this.get("reportsForPeriod")
|
||||
.map(r => r.title)
|
||||
.join(", ")
|
||||
);
|
||||
this.set("isLoading", false);
|
||||
},
|
||||
|
||||
loadReport(jsonReport) {
|
||||
return Report.create(jsonReport);
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this.set("reports", []);
|
||||
this.set("isLoading", true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
queryParams: ["period"],
|
||||
|
||||
period: "monthly",
|
||||
|
||||
availablePeriods: ["yearly", "quarterly", "monthly", "weekly"],
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.endOf("day")
|
||||
.subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.subtract(1, "day")
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,29 +1,41 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
const ATTRIBUTES = ["disk_space", "updated_at", "last_backup_taken_at"];
|
||||
const GENERAL_ATTRIBUTES = ["disk_space", "updated_at", "last_backup_taken_at"];
|
||||
|
||||
const AdminDashboardNext = Discourse.Model.extend({});
|
||||
|
||||
AdminDashboardNext.reopenClass({
|
||||
/**
|
||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||
has expired and the server must collect the data again.
|
||||
|
||||
@method find
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
find() {
|
||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||
var model = AdminDashboardNext.create();
|
||||
|
||||
model.set("reports", json.reports);
|
||||
fetch() {
|
||||
return ajax("/admin/dashboard-next.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
model.set("version_check", json.version_check);
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchModeration() {
|
||||
return ajax("/admin/dashboard/moderation.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
model.setProperties({
|
||||
reports: json.reports,
|
||||
loaded: true
|
||||
});
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchGeneral() {
|
||||
return ajax("/admin/dashboard/general.json").then(json => {
|
||||
const model = AdminDashboardNext.create();
|
||||
|
||||
const attributes = {};
|
||||
ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||
model.set("attributes", attributes);
|
||||
GENERAL_ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||
|
||||
model.set("loaded", true);
|
||||
model.setProperties({
|
||||
reports: json.reports,
|
||||
attributes,
|
||||
loaded: true
|
||||
});
|
||||
|
||||
return model;
|
||||
});
|
||||
|
||||
@@ -1,22 +1,105 @@
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fillMissingDates } from "discourse/lib/utilities";
|
||||
import { fillMissingDates, isNumeric } from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import { number, durationTiny } from "discourse/lib/formatter";
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("labels")
|
||||
computedLabels(labels) {
|
||||
return labels.map(label => {
|
||||
const type = label.type;
|
||||
const properties = label.properties;
|
||||
const property = properties[0];
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
sort_property: label.sort_property || property,
|
||||
property,
|
||||
compute: row => {
|
||||
let value = row[property];
|
||||
let escapedValue = escapeExpression(value);
|
||||
let tooltip;
|
||||
let base = { property, value, type };
|
||||
|
||||
if (value === null || typeof value === "undefined") {
|
||||
return _.assign(base, {
|
||||
value: null,
|
||||
formatedValue: "-",
|
||||
type: "undefined"
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "seconds") {
|
||||
return _.assign(base, {
|
||||
formatedValue: escapeExpression(durationTiny(value))
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "link") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `<a href="${escapeExpression(
|
||||
row[properties[1]]
|
||||
)}">${escapedValue}</a>`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "percent") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `${escapedValue}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "number" || isNumeric(value))
|
||||
return _.assign(base, {
|
||||
type: "number",
|
||||
formatedValue: number(value)
|
||||
});
|
||||
|
||||
if (type !== "string" && type !== "text") {
|
||||
const date = moment(value, "YYYY-MM-DD");
|
||||
if (type === "date" || date.isValid()) {
|
||||
return _.assign(base, {
|
||||
type: "date",
|
||||
formatedValue: date.format("LL")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "text") tooltip = escapedValue;
|
||||
|
||||
return _.assign(base, {
|
||||
tooltip,
|
||||
type: type || "string",
|
||||
formatedValue: escapedValue
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@computed("modes")
|
||||
onlyTable(modes) {
|
||||
return modes.length === 1 && modes[0] === "table";
|
||||
},
|
||||
|
||||
@computed("type", "start_date", "end_date")
|
||||
reportUrl(type, start_date, end_date) {
|
||||
start_date = moment(start_date)
|
||||
start_date = moment
|
||||
.utc(start_date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
end_date = moment(end_date)
|
||||
|
||||
end_date = moment
|
||||
.utc(end_date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
|
||||
return Discourse.getURL(
|
||||
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
||||
);
|
||||
@@ -142,29 +225,6 @@ const Report = Discourse.Model.extend({
|
||||
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("type")
|
||||
icon(type) {
|
||||
if (type.indexOf("message") > -1) {
|
||||
return "envelope";
|
||||
}
|
||||
switch (type) {
|
||||
case "page_view_total_reqs":
|
||||
return "file";
|
||||
case "visits":
|
||||
return "user";
|
||||
case "time_to_first_response":
|
||||
return "reply";
|
||||
case "flags":
|
||||
return "flag";
|
||||
case "likes":
|
||||
return "heart";
|
||||
case "bookmarks":
|
||||
return "bookmark";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("type")
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
@@ -290,18 +350,23 @@ const Report = Discourse.Model.extend({
|
||||
});
|
||||
|
||||
Report.reopenClass({
|
||||
fillMissingDates(report) {
|
||||
if (_.isArray(report.data)) {
|
||||
fillMissingDates(report, options = {}) {
|
||||
const dataField = options.dataField || "data";
|
||||
const filledField = options.filledField || "data";
|
||||
const startDate = options.startDate || "start_date";
|
||||
const endDate = options.endDate || "end_date";
|
||||
|
||||
if (_.isArray(report[dataField])) {
|
||||
const startDateFormatted = moment
|
||||
.utc(report.start_date)
|
||||
.utc(report[startDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
const endDateFormatted = moment
|
||||
.utc(report.end_date)
|
||||
.utc(report[endDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
report.data = fillMissingDates(
|
||||
report.data,
|
||||
report[filledField] = fillMissingDates(
|
||||
JSON.parse(JSON.stringify(report[dataField])),
|
||||
startDateFormatted,
|
||||
endDateFormatted
|
||||
);
|
||||
@@ -317,8 +382,12 @@ Report.reopenClass({
|
||||
group_id: groupId
|
||||
}
|
||||
}).then(json => {
|
||||
// Add zero values for missing dates
|
||||
Report.fillMissingDates(json.report);
|
||||
// don’t fill for large multi column tables
|
||||
// which are not date based
|
||||
const modes = json.report.modes;
|
||||
if (modes.length !== 1 && modes[0] !== "table") {
|
||||
Report.fillMissingDates(json.report);
|
||||
}
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next-general").fetchDashboard();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next-moderation").fetchDashboard();
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,22 @@ import { scrollTop } from "discourse/mixins/scroll-top";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor("admin-dashboard-next").fetchProblems();
|
||||
this.controllerFor("admin-dashboard-next").fetchDashboard();
|
||||
scrollTop();
|
||||
},
|
||||
|
||||
afterModel(model, transition) {
|
||||
if (transition.targetName === "admin.dashboardNext.index") {
|
||||
this.transitionTo("admin.dashboardNext.general");
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
if (transition.targetName === "admin.dashboardNext.index") {
|
||||
this.transitionTo("admin.dashboardNext.general");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: {
|
||||
mode: {},
|
||||
start_date: {},
|
||||
end_date: {},
|
||||
category_id: {},
|
||||
group_id: {}
|
||||
},
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
|
||||
model(params) {
|
||||
return Report.find(
|
||||
params.type,
|
||||
params["start_date"],
|
||||
params["end_date"],
|
||||
params["category_id"],
|
||||
params["group_id"]
|
||||
);
|
||||
},
|
||||
if (!controller.get("start_date")) {
|
||||
controller.set(
|
||||
"start_date",
|
||||
moment()
|
||||
.subtract("30", "day")
|
||||
.format("YYYY-MM-DD")
|
||||
);
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
categoryId: model.get("category_id") || "all",
|
||||
groupId: model.get("group_id"),
|
||||
startDate: moment(model.get("start_date"))
|
||||
.utc()
|
||||
.format("YYYY-MM-DD"),
|
||||
endDate: moment(model.get("end_date"))
|
||||
.utc()
|
||||
.format("YYYY-MM-DD")
|
||||
});
|
||||
if (!controller.get("end_date")) {
|
||||
controller.set("end_date", moment().format("YYYY-MM-DD"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export default function() {
|
||||
this.route("admin", { resetNamespace: true }, function() {
|
||||
this.route("dashboard", { path: "/dashboard-old" });
|
||||
this.route("dashboardNext", { path: "/" });
|
||||
|
||||
this.route("dashboardNext", { path: "/" }, function() {
|
||||
this.route("general", { path: "/dashboard/general" });
|
||||
this.route("moderation", { path: "/dashboard/moderation" });
|
||||
});
|
||||
|
||||
this.route(
|
||||
"adminSiteSettings",
|
||||
{ path: "/site_settings", resetNamespace: true },
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="chart-canvas-container">
|
||||
<canvas class="chart-canvas"></canvas>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="table-container">
|
||||
{{#each model.data as |data|}}
|
||||
<a class="table-cell user-{{data.key}}" href="{{data.url}}">
|
||||
<span class="label">
|
||||
{{#if data.icon}}
|
||||
{{d-icon data.icon}}
|
||||
{{/if}}
|
||||
{{data.x}}
|
||||
</span>
|
||||
<span class="value">
|
||||
{{number data.y}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
{{#if showSortingUI}}
|
||||
{{d-button action=sortByLabel icon=sortIcon class="sort-button"}}
|
||||
{{/if}}
|
||||
|
||||
<span>{{label.title}}</span>
|
||||
@@ -0,0 +1,5 @@
|
||||
{{#each cells as |cell|}}
|
||||
<td class="{{cell.type}} {{cell.property}}" title="{{cell.tooltip}}">
|
||||
{{{cell.formatedValue}}}
|
||||
</td>
|
||||
{{/each}}
|
||||
@@ -0,0 +1,60 @@
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{#if model.computedLabels}}
|
||||
{{#each model.computedLabels as |label index|}}
|
||||
{{admin-report-table-header
|
||||
showSortingUI=showSortingUI
|
||||
currentSortDirection=sortDirection
|
||||
currentSortLabel=sortLabel
|
||||
label=label
|
||||
sortByLabel=(action "sortByLabel" label)}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each model.data as |data|}}
|
||||
<th>{{data.x}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each paginatedData as |data|}}
|
||||
{{admin-report-table-row data=data labels=model.computedLabels}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{#if showTotalForSample}}
|
||||
<small>{{i18n 'admin.dashboard.reports.totals_for_sample'}}</small>
|
||||
<table class="totals-sample-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#each totalsForSample as |total|}}
|
||||
<td>{{total.formatedValue}}</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
{{#if showTotal}}
|
||||
<small>{{i18n 'admin.dashboard.reports.total'}}</small>
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>{{number model.total}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
<div class="pagination">
|
||||
{{#each pages as |pageState|}}
|
||||
{{d-button
|
||||
translatedLabel=pageState.page
|
||||
action="changePage"
|
||||
actionParam=pageState.index
|
||||
class=pageState.class}}
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -0,0 +1,154 @@
|
||||
{{#if isEnabled}}
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{#if showTimeoutError}}
|
||||
<div class="alert alert-error">
|
||||
{{i18n "admin.dashboard.timeout_error"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showHeader}}
|
||||
<div class="report-header">
|
||||
{{#if showTitle}}
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#if showAllReportsLink}}
|
||||
{{#link-to "adminReports" class="all-report-link"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
<span class="separator">|</span>
|
||||
{{/if}}
|
||||
|
||||
<a href="{{model.reportUrl}}" class="report-link">
|
||||
{{model.title}}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{{#if model.description}}
|
||||
<span class="info" data-tooltip="{{model.description}}">
|
||||
{{d-icon "question-circle"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showTrend}}
|
||||
{{#if model.prev_period}}
|
||||
<div class="trend {{model.trend}}">
|
||||
<span class="trend-value" title="{{model.trendTitle}}">
|
||||
{{#if model.average}}
|
||||
{{number model.currentAverage}}{{#if model.percent}}%{{/if}}
|
||||
{{else}}
|
||||
{{number model.currentTotal noTitle="true"}}{{#if model.percent}}%{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if model.trendIcon}}
|
||||
{{d-icon model.trendIcon class="trend-icon"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showModes}}
|
||||
<ul class="mode-switch">
|
||||
{{#each displayedModes as |displayedMode|}}
|
||||
<li class="mode">
|
||||
{{d-button
|
||||
action="changeMode"
|
||||
actionParam=displayedMode.mode
|
||||
class=displayedMode.cssClass
|
||||
icon=displayedMode.icon}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="report-body">
|
||||
{{#unless showTimeoutError}}
|
||||
{{#if currentMode}}
|
||||
{{component modeComponent model=model options=options}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showFilteringUI}}
|
||||
{{#if hasFilteringActions}}
|
||||
<div class="report-filters">
|
||||
{{#if showDatesOptions}}
|
||||
<div class="filtering-control">
|
||||
<span class="filtering-label">
|
||||
{{i18n 'admin.dashboard.reports.start_date'}}
|
||||
</span>
|
||||
|
||||
<div class="filtering-input">
|
||||
{{date-picker-past
|
||||
value=startDate
|
||||
defaultDate=startDate
|
||||
onSelect=onSelectStartDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filtering-control">
|
||||
<span class="filtering-label">
|
||||
{{i18n 'admin.dashboard.reports.end_date'}}
|
||||
</span>
|
||||
|
||||
<div class="filtering-input">
|
||||
{{date-picker-past
|
||||
value=endDate
|
||||
defaultDate=endDate
|
||||
onSelect=onSelectEndDate}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showCategoryOptions}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{combo-box
|
||||
onSelect=onSelectCategory
|
||||
filterable=true
|
||||
valueAttribute="value"
|
||||
content=categoryOptions
|
||||
castInteger=true
|
||||
value=categoryId}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showGroupOptions}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{combo-box
|
||||
onSelect=onSelectGroup
|
||||
castInteger=true
|
||||
filterable=true
|
||||
valueAttribute="value"
|
||||
content=groupOptions
|
||||
value=groupId}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showExport}}
|
||||
<div class="filtering-control">
|
||||
<div class="filtering-input">
|
||||
{{d-button class="export-btn" action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-report dataSourceName=model.relatedReport.type}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-section}}
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{{i18n disabledLabel}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -1,37 +0,0 @@
|
||||
{{#if model.sortedData}}
|
||||
<table class="table report {{model.type}}">
|
||||
<tr>
|
||||
<th>{{model.xaxis}}</th>
|
||||
<th>{{model.yaxis}}</th>
|
||||
</tr>
|
||||
|
||||
{{#each model.sortedData as |row|}}
|
||||
<tr>
|
||||
<td class="x-value">{{row.x}}</td>
|
||||
<td>
|
||||
{{row.y}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
<tr class="total-for-period">
|
||||
<td class="x-value">
|
||||
{{i18n 'admin.dashboard.reports.total_for_period'}}
|
||||
</td>
|
||||
<td>
|
||||
{{totalForPeriod}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{#if model.total}}
|
||||
<tr class="total">
|
||||
<td class="x-value">
|
||||
{{i18n 'admin.dashboard.reports.total'}}
|
||||
</td>
|
||||
<td>
|
||||
{{model.total}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
{{/if}}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="table-container">
|
||||
{{#unless hasBlock}}
|
||||
{{#each report.data as |data|}}
|
||||
<a class="table-cell user-{{data.key}}" href="{{data.url}}">
|
||||
<span class="label">
|
||||
{{#if data.icon}}
|
||||
{{d-icon data.icon}}
|
||||
{{/if}}
|
||||
{{data.x}}
|
||||
</span>
|
||||
<span class="value">
|
||||
{{number data.y}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{yield (hash report=report)}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/conditional-loading-section}}
|
||||
@@ -1,33 +0,0 @@
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="status">
|
||||
<h4 class="title">
|
||||
<a href="{{report.reportUrl}}">
|
||||
{{report.title}}
|
||||
</a>
|
||||
|
||||
<span class="info" data-tooltip="{{report.description}}">
|
||||
{{d-icon "question-circle"}}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="trend {{report.trend}}">
|
||||
<span class="trend-value" title="{{report.trendTitle}}">
|
||||
{{#if report.average}}
|
||||
{{number report.currentAverage}}{{#if report.percent}}%{{/if}}
|
||||
{{else}}
|
||||
{{number report.currentTotal noTitle="true"}}{{#if report.percent}}%{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if report.trendIcon}}
|
||||
{{d-icon report.trendIcon class="trend-icon"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div class="chart-canvas-container">
|
||||
<canvas class="chart-canvas"></canvas>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
@@ -1,36 +0,0 @@
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{#each reportsForPeriod as |report|}}
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{{#if report.labels}}
|
||||
{{#each report.labels as |label|}}
|
||||
<th>{{label}}</th>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each report.data as |data|}}
|
||||
<th>{{data.x}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#unless hasBlock}}
|
||||
{{#each report.data as |data|}}
|
||||
<tr>
|
||||
<td>{{number data.y}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{yield (hash report=report)}}
|
||||
{{/unless}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/conditional-loading-section}}
|
||||
@@ -3,179 +3,26 @@
|
||||
{{#if showVersionChecks}}
|
||||
<div class="section-top">
|
||||
<div class="version-checks">
|
||||
{{partial 'admin/templates/version-checks'}}
|
||||
{{partial "admin/templates/version-checks"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{partial 'admin/templates/dashboard-problems'}}
|
||||
{{partial "admin/templates/dashboard-problems"}}
|
||||
|
||||
<div class="community-health section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
{{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="signups"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="topics"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="posts"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="dau_by_mau"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="daily_engaged_users"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceNames="new_contributors"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
<div class="dashboard-table activity-metrics">
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
|
||||
<div class="table-title">
|
||||
<h3>{{i18n "admin.dashboard.activity_metrics"}}</h3>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each reports as |report|}}
|
||||
{{admin-report-counts report=report allTime=false}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
<ul class="navigation">
|
||||
<li class="navigation-item general">
|
||||
{{#link-to "admin.dashboardNext.general" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.general_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="navigation-item moderation">
|
||||
{{#link-to "admin.dashboardNext.moderation" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.moderation_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="user-metrics">
|
||||
{{dashboard-inline-table dataSourceNames="users_by_type" lastRefreshedAt=lastRefreshedAt}}
|
||||
{{outlet}}
|
||||
|
||||
{{dashboard-inline-table dataSourceNames="users_by_trust_level" lastRefreshedAt=lastRefreshedAt}}
|
||||
</div>
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
|
||||
<div class="misc">
|
||||
{{#if shouldDisplayDurability}}
|
||||
<div class="durability">
|
||||
{{#if currentUser.admin}}
|
||||
<div class="backups">
|
||||
<h3 class="durability-title">
|
||||
<a href="/admin/backups">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
|
||||
</h3>
|
||||
<p>
|
||||
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="uploads">
|
||||
<h3 class="durability-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<p>
|
||||
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="last-dashboard-update">
|
||||
<div>
|
||||
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
|
||||
<p>{{updatedTimestamp}}</p>
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
|
||||
</p>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
<div class="top-referred-topics">
|
||||
{{#dashboard-table
|
||||
dataSourceNames="top_referred_topics"
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
limit=8
|
||||
as |context|}}
|
||||
{{#each context.report.data as |data|}}
|
||||
<tr>
|
||||
<td class='left'>
|
||||
<a href="{{data.topic_url}}">
|
||||
{{data.topic_title}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{data.num_clicks}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/dashboard-table}}
|
||||
</div>
|
||||
|
||||
<div class="trending-search">
|
||||
{{#dashboard-table
|
||||
limit=8
|
||||
dataSourceNames="trending_search"
|
||||
isEnabled=logSearchQueriesEnabled
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"
|
||||
startDate=lastWeek
|
||||
endDate=endDate as |context|}}
|
||||
{{#each context.report.data as |data|}}
|
||||
<tr>
|
||||
<td class='left'>
|
||||
{{data.term}}
|
||||
</td>
|
||||
<td>
|
||||
{{number data.unique_searches}}
|
||||
</td>
|
||||
<td>
|
||||
{{data.ctr}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/dashboard-table}}
|
||||
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{plugin-outlet name="admin-dashboard-bottom"}}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
{{plugin-outlet name="admin-dashboard-general-top"}}
|
||||
|
||||
<div class="community-health section">
|
||||
<div class="period-section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
{{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{admin-report
|
||||
dataSourceName="signups"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="topics"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="posts"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="dau_by_mau"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="daily_engaged_users"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="new_contributors"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
<div class="admin-report activity-metrics">
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
|
||||
<div class="report-header">
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#link-to "adminReports" class="report-link"}}
|
||||
{{i18n "admin.dashboard.activity_metrics"}}
|
||||
{{/link-to}}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-body">
|
||||
<div class="admin-report-table">
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="admin-report-table-header"></th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.today'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.yesterday'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_7_days'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_30_days'}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each activityMetricsReports as |report|}}
|
||||
{{admin-report-counts report=report allTime=false class="admin-report-table-row"}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
|
||||
<div class="user-metrics">
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTypeReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTrustLevelReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
|
||||
<div class="misc">
|
||||
|
||||
{{#if shouldDisplayDurability}}
|
||||
<div class="durability">
|
||||
{{#if currentUser.admin}}
|
||||
<div class="backups">
|
||||
<h3 class="durability-title">
|
||||
<a href="/admin/backups">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
|
||||
</h3>
|
||||
<p>
|
||||
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="uploads">
|
||||
<h3 class="durability-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<p>
|
||||
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="last-dashboard-update">
|
||||
<div>
|
||||
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
|
||||
<p>{{updatedTimestamp}}</p>
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
|
||||
</p>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
{{admin-report
|
||||
report=topReferredTopicsReport
|
||||
reportOptions=topReferredTopicsTopions}}
|
||||
|
||||
{{admin-report
|
||||
reportOptions=trendingSearchOptions
|
||||
report=trendingSearchReport
|
||||
isEnabled=logSearchQueriesEnabled
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="admin-dashboard-general-bottom"}}
|
||||
{{/conditional-loading-spinner}}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
<div class="sections">
|
||||
{{plugin-outlet name="admin-dashboard-moderation-top"}}
|
||||
|
||||
<div class="moderators-activity section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.moderators_activity"}}</h2>
|
||||
{{period-chooser
|
||||
period=period
|
||||
action="changePeriod"
|
||||
content=availablePeriods
|
||||
fullDay=true}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
{{admin-report
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
showHeader=false
|
||||
dataSourceName="moderators_activity"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-section">
|
||||
{{admin-report
|
||||
report=flagsStatusReport
|
||||
startDate=lastWeek
|
||||
reportOptions=flagsStatusOptions
|
||||
endDate=endDate}}
|
||||
|
||||
{{admin-report
|
||||
report=postEditsReport
|
||||
startDate=lastWeek
|
||||
endDate=endDate}}
|
||||
|
||||
{{plugin-outlet name="admin-dashboard-moderation-bottom"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
@@ -1,60 +1,16 @@
|
||||
<h3>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
|
||||
|
|
||||
|
||||
{{model.title}}
|
||||
</h3>
|
||||
|
||||
{{#if model.description}}
|
||||
<p>{{model.description}}</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="report-container">
|
||||
<div class="visualization">
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
<div class='view-options'>
|
||||
{{#if viewingTable}}
|
||||
{{i18n 'admin.dashboard.reports.view_table'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if viewingGraph}}
|
||||
{{i18n 'admin.dashboard.reports.view_graph'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if viewingGraph}}
|
||||
{{admin-graph model=model}}
|
||||
{{else}}
|
||||
{{admin-table-report model=model}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-table-report model=model.relatedReport}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
|
||||
</span>
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
|
||||
</span>
|
||||
{{#if showCategoryOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{/if}}
|
||||
{{#if showGroupOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
|
||||
{{/if}}
|
||||
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
|
||||
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
{{admin-report
|
||||
showAllReportsLink=true
|
||||
dataSourceName=model.type
|
||||
categoryId=category_id
|
||||
groupId=group_id
|
||||
reportOptions=reportOptions
|
||||
startDate=start_date
|
||||
endDate=end_date
|
||||
showFilteringUI=true
|
||||
onSelectCategory=(action "onSelectCategory")
|
||||
onSelectStartDate=(action "onSelectStartDate")
|
||||
onSelectEndDate=(action "onSelectEndDate")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,8 @@ export default DatePicker.extend({
|
||||
|
||||
_opts() {
|
||||
return {
|
||||
defaultDate: new Date(this.get("defaultDate")) || new Date(),
|
||||
defaultDate:
|
||||
moment(this.get("defaultDate"), "YYYY-MM-DD").toDate() || new Date(),
|
||||
setDefaultDate: !!this.get("defaultDate"),
|
||||
maxDate: new Date()
|
||||
};
|
||||
|
||||
@@ -29,13 +29,17 @@ export default Ember.Component.extend({
|
||||
weekdays: moment.weekdays(),
|
||||
weekdaysShort: moment.weekdaysShort()
|
||||
},
|
||||
onSelect: date =>
|
||||
this.set(
|
||||
"value",
|
||||
moment(date)
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD")
|
||||
)
|
||||
onSelect: date => {
|
||||
const formattedDate = moment(date).format("YYYY-MM-DD");
|
||||
|
||||
if (this.attrs.onSelect) {
|
||||
this.attrs.onSelect(formattedDate);
|
||||
}
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
|
||||
this.set("value", formattedDate);
|
||||
}
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||
|
||||
@@ -591,6 +591,10 @@ export function clipboardData(e, canUpload) {
|
||||
return { clipboard, types, canUpload, canPasteHtml };
|
||||
}
|
||||
|
||||
export function isNumeric(input) {
|
||||
return !isNaN(parseFloat(input)) && isFinite(input);
|
||||
}
|
||||
|
||||
export function fillMissingDates(data, startDate, endDate) {
|
||||
const startMoment = moment(startDate, "YYYY-MM-DD");
|
||||
const endMoment = moment(endDate, "YYYY-MM-DD");
|
||||
|
||||
@@ -447,7 +447,7 @@ export default Ember.Component.extend(
|
||||
if (get(this.actions, actionName)) {
|
||||
run.next(() => this.send(actionName, ...params));
|
||||
} else if (this.get(actionName)) {
|
||||
run.next(() => this.get(actionName)());
|
||||
run.next(() => this.get(actionName)(...params));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user