mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 05:43:41 +08:00
56877e9acf
Followup 14b436923c
On the standalone Site Traffic report page, we also need
to hide the 'other' and 'crawler' pageviews by default
like we do on the admin dashboard.
429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
import Component from "@ember/component";
|
|
import EmberObject, { action, computed } from "@ember/object";
|
|
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
|
|
import { next } from "@ember/runloop";
|
|
import { isPresent } from "@ember/utils";
|
|
import { classNameBindings, classNames } from "@ember-decorators/component";
|
|
import { exportEntity } from "discourse/lib/export-csv";
|
|
import { outputExportResult } from "discourse/lib/export-result";
|
|
import ReportLoader from "discourse/lib/reports-loader";
|
|
import { isTesting } from "discourse-common/config/environment";
|
|
import { makeArray } from "discourse-common/lib/helpers";
|
|
import discourseComputed from "discourse-common/utils/decorators";
|
|
import I18n from "discourse-i18n";
|
|
import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report";
|
|
|
|
const TABLE_OPTIONS = {
|
|
perPage: 8,
|
|
total: true,
|
|
limit: 20,
|
|
formatNumbers: true,
|
|
};
|
|
|
|
const CHART_OPTIONS = {};
|
|
|
|
@classNameBindings(
|
|
"isHidden:hidden",
|
|
"isHidden::is-visible",
|
|
"isEnabled",
|
|
"isLoading",
|
|
"dasherizedDataSourceName"
|
|
)
|
|
@classNames("admin-report")
|
|
export default class AdminReport extends Component {
|
|
isEnabled = true;
|
|
disabledLabel = I18n.t("admin.dashboard.disabled");
|
|
isLoading = false;
|
|
rateLimitationString = null;
|
|
dataSourceName = null;
|
|
report = null;
|
|
model = null;
|
|
reportOptions = null;
|
|
forcedModes = null;
|
|
showAllReportsLink = false;
|
|
filters = null;
|
|
showTrend = false;
|
|
showHeader = true;
|
|
showTitle = true;
|
|
showFilteringUI = false;
|
|
|
|
@alias("model.dates_filtering") showDatesOptions;
|
|
|
|
@or("showDatesOptions", "model.available_filters.length") showRefresh;
|
|
|
|
@and("showTrend", "model.prev_period") shouldDisplayTrend;
|
|
|
|
endDate = null;
|
|
startDate = null;
|
|
|
|
@or("showTimeoutError", "showExceptionError", "showNotFoundError") showError;
|
|
@equal("model.error", "not_found") showNotFoundError;
|
|
@equal("model.error", "timeout") showTimeoutError;
|
|
@equal("model.error", "exception") showExceptionError;
|
|
@notEmpty("model.data") hasData;
|
|
|
|
_reports = [];
|
|
|
|
@computed("siteSettings.dashboard_hidden_reports")
|
|
get isHidden() {
|
|
return (this.siteSettings.dashboard_hidden_reports || "")
|
|
.split("|")
|
|
.filter(Boolean)
|
|
.includes(this.dataSourceName);
|
|
}
|
|
|
|
didReceiveAttrs() {
|
|
super.didReceiveAttrs(...arguments);
|
|
|
|
let startDate = moment();
|
|
if (this.filters && isPresent(this.filters.startDate)) {
|
|
startDate = moment(this.filters.startDate, "YYYY-MM-DD");
|
|
}
|
|
this.set("startDate", startDate);
|
|
|
|
let endDate = moment();
|
|
if (this.filters && isPresent(this.filters.endDate)) {
|
|
endDate = moment(this.filters.endDate, "YYYY-MM-DD");
|
|
}
|
|
this.set("endDate", endDate);
|
|
|
|
if (this.filters) {
|
|
this.set("currentMode", this.filters.mode);
|
|
}
|
|
|
|
if (this.report) {
|
|
this._renderReport(this.report, this.forcedModes, this.currentMode);
|
|
} else if (this.dataSourceName) {
|
|
this._fetchReport();
|
|
}
|
|
}
|
|
|
|
@discourseComputed("dataSourceName", "model.type")
|
|
dasherizedDataSourceName(dataSourceName, type) {
|
|
return (dataSourceName || type || "undefined").replace(/_/g, "-");
|
|
}
|
|
|
|
@discourseComputed("dataSourceName", "model.type")
|
|
dataSource(dataSourceName, type) {
|
|
dataSourceName = dataSourceName || type;
|
|
return `/admin/reports/${dataSourceName}`;
|
|
}
|
|
|
|
@discourseComputed("displayedModes.length")
|
|
showModes(displayedModesLength) {
|
|
return displayedModesLength > 1;
|
|
}
|
|
|
|
@discourseComputed("currentMode")
|
|
isChartMode(currentMode) {
|
|
return currentMode === "chart";
|
|
}
|
|
|
|
@action
|
|
changeGrouping(grouping) {
|
|
this.send("refreshReport", {
|
|
chartGrouping: grouping,
|
|
});
|
|
}
|
|
|
|
@discourseComputed("currentMode", "model.modes", "forcedModes")
|
|
displayedModes(currentMode, reportModes, forcedModes) {
|
|
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
|
|
|
return makeArray(modes).map((mode) => {
|
|
const base = `btn-default mode-btn ${mode}`;
|
|
const cssClass = currentMode === mode ? `${base} btn-primary` : base;
|
|
|
|
return {
|
|
mode,
|
|
cssClass,
|
|
icon: mode === "table" ? "table" : "signal",
|
|
};
|
|
});
|
|
}
|
|
|
|
@discourseComputed("currentMode")
|
|
modeComponent(currentMode) {
|
|
return `admin-report-${currentMode.replace(/_/g, "-")}`;
|
|
}
|
|
|
|
@discourseComputed(
|
|
"dataSourceName",
|
|
"startDate",
|
|
"endDate",
|
|
"filters.customFilters"
|
|
)
|
|
reportKey(dataSourceName, startDate, endDate, customFilters) {
|
|
if (!dataSourceName || !startDate || !endDate) {
|
|
return null;
|
|
}
|
|
|
|
startDate = startDate.toISOString(true).split("T")[0];
|
|
endDate = endDate.toISOString(true).split("T")[0];
|
|
|
|
let reportKey = "reports:";
|
|
reportKey += [
|
|
dataSourceName,
|
|
isTesting() ? "start" : startDate.replace(/-/g, ""),
|
|
isTesting() ? "end" : endDate.replace(/-/g, ""),
|
|
"[:prev_period]",
|
|
this.get("reportOptions.table.limit"),
|
|
// Convert all filter values to strings to ensure unique serialization
|
|
customFilters
|
|
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
|
|
: null,
|
|
SCHEMA_VERSION,
|
|
]
|
|
.filter((x) => x)
|
|
.map((x) => x.toString())
|
|
.join(":");
|
|
|
|
return reportKey;
|
|
}
|
|
|
|
@discourseComputed("options.chartGrouping", "model.chartData.length")
|
|
chartGroupings(grouping, count) {
|
|
const options = ["daily", "weekly", "monthly"];
|
|
|
|
return options.map((id) => {
|
|
return {
|
|
id,
|
|
disabled: id === "daily" && count >= DAILY_LIMIT_DAYS,
|
|
label: `admin.dashboard.reports.${id}`,
|
|
class: `chart-grouping ${grouping === id ? "active" : "inactive"}`,
|
|
};
|
|
});
|
|
}
|
|
|
|
@action
|
|
onChangeDateRange(range) {
|
|
this.setProperties({
|
|
startDate: range.from,
|
|
endDate: range.to,
|
|
});
|
|
}
|
|
|
|
@action
|
|
applyFilter(id, value) {
|
|
let customFilters = this.get("filters.customFilters") || {};
|
|
|
|
if (typeof value === "undefined") {
|
|
delete customFilters[id];
|
|
} else {
|
|
customFilters[id] = value;
|
|
}
|
|
|
|
this.send("refreshReport", {
|
|
filters: customFilters,
|
|
});
|
|
}
|
|
|
|
@action
|
|
refreshReport(options = {}) {
|
|
if (!this.onRefresh) {
|
|
return;
|
|
}
|
|
|
|
this.onRefresh({
|
|
type: this.get("model.type"),
|
|
mode: this.currentMode,
|
|
chartGrouping: options.chartGrouping,
|
|
startDate:
|
|
typeof options.startDate === "undefined"
|
|
? this.startDate
|
|
: options.startDate,
|
|
endDate:
|
|
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
|
|
filters:
|
|
typeof options.filters === "undefined"
|
|
? this.get("filters.customFilters")
|
|
: options.filters,
|
|
});
|
|
}
|
|
|
|
@action
|
|
exportCsv() {
|
|
const args = {
|
|
name: this.get("model.type"),
|
|
start_date: this.startDate.toISOString(true).split("T")[0],
|
|
end_date: this.endDate.toISOString(true).split("T")[0],
|
|
};
|
|
|
|
const customFilters = this.get("filters.customFilters");
|
|
if (customFilters) {
|
|
Object.assign(args, customFilters);
|
|
}
|
|
|
|
exportEntity("report", args).then(outputExportResult);
|
|
}
|
|
|
|
@action
|
|
onChangeMode(mode) {
|
|
this.set("currentMode", mode);
|
|
|
|
this.send("refreshReport", {
|
|
chartGrouping: null,
|
|
});
|
|
}
|
|
|
|
_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.dataSourceName);
|
|
} else {
|
|
return r;
|
|
}
|
|
};
|
|
|
|
if (!this.startDate || !this.endDate) {
|
|
report = sort(filteredReports)[0];
|
|
} else {
|
|
report = sort(
|
|
filteredReports.filter((r) => r.report_key.includes(this.reportKey))
|
|
)[0];
|
|
|
|
if (!report) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (report.error === "not_found") {
|
|
this.set("showFilteringUI", false);
|
|
}
|
|
|
|
this._renderReport(report, this.forcedModes, this.currentMode);
|
|
}
|
|
|
|
_renderReport(report, forcedModes, currentMode) {
|
|
const modes = forcedModes ? forcedModes.split(",") : report.modes;
|
|
currentMode = currentMode || (modes ? modes[0] : null);
|
|
|
|
this.setProperties({
|
|
model: report,
|
|
currentMode,
|
|
options: this._buildOptions(currentMode, report),
|
|
});
|
|
}
|
|
|
|
_fetchReport() {
|
|
this.setProperties({ isLoading: true, rateLimitationString: null });
|
|
|
|
next(() => {
|
|
let payload = this._buildPayload(["prev_period"]);
|
|
|
|
const callback = (response) => {
|
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
return;
|
|
}
|
|
|
|
this.set("isLoading", false);
|
|
|
|
if (response === 429) {
|
|
this.set(
|
|
"rateLimitationString",
|
|
I18n.t("admin.dashboard.too_many_requests")
|
|
);
|
|
} else if (response === 500) {
|
|
this.set("model.error", "exception");
|
|
} else if (response) {
|
|
this._reports.push(this._loadReport(response));
|
|
this._computeReport();
|
|
}
|
|
};
|
|
|
|
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
|
|
});
|
|
}
|
|
|
|
_buildPayload(facets) {
|
|
let payload = { data: { facets } };
|
|
|
|
if (this.startDate) {
|
|
payload.data.start_date = moment(this.startDate)
|
|
.toISOString(true)
|
|
.split("T")[0];
|
|
}
|
|
|
|
if (this.endDate) {
|
|
payload.data.end_date = moment(this.endDate)
|
|
.toISOString(true)
|
|
.split("T")[0];
|
|
}
|
|
|
|
if (this.get("reportOptions.table.limit")) {
|
|
payload.data.limit = this.get("reportOptions.table.limit");
|
|
}
|
|
|
|
if (this.get("filters.customFilters")) {
|
|
payload.data.filters = this.get("filters.customFilters");
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
_buildOptions(mode, report) {
|
|
if (mode === "table") {
|
|
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
|
return EmberObject.create(
|
|
Object.assign(tableOptions, this.get("reportOptions.table") || {})
|
|
);
|
|
} else if (mode === "chart") {
|
|
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
|
return EmberObject.create(
|
|
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
|
|
chartGrouping:
|
|
this.get("reportOptions.chartGrouping") ||
|
|
Report.groupingForDatapoints(report.chartData.length),
|
|
})
|
|
);
|
|
} else if (mode === "stacked-chart" || mode === "stacked_chart") {
|
|
return this.get("reportOptions.stackedChart") || {};
|
|
}
|
|
}
|
|
|
|
_loadReport(jsonReport) {
|
|
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
|
|
|
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
|
|
jsonReport.chartData = jsonReport.chartData.map((chartData) => {
|
|
if (chartData.length > 40) {
|
|
return {
|
|
data: chartData.data,
|
|
req: chartData.req,
|
|
label: chartData.label,
|
|
color: chartData.color,
|
|
};
|
|
} else {
|
|
return chartData;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (jsonReport.prev_data) {
|
|
Report.fillMissingDates(jsonReport, {
|
|
filledField: "prevChartData",
|
|
dataField: "prev_data",
|
|
starDate: jsonReport.prev_startDate,
|
|
endDate: jsonReport.prev_endDate,
|
|
});
|
|
}
|
|
|
|
return Report.create(jsonReport);
|
|
}
|
|
}
|