mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 14:12:10 +08:00
DEV: Convert AdminReport
component to gjs (#31011)
This commit converts the `AdminReport` component, which is quite high complexity, to gjs. After this initial round, ideally this component would be broken up into smaller components because it is getting quite big now. Also in this commit: * Add an option to display the report description in a tooltip, which was the main way the description was shown until recently. We want to use this on the dashboard view mostly. * Move admin report "mode" definitions to the server-side Report model, inside a `Report::MODES` constant, collecting the modes defined in various places in the UI into one place * Refactor report code to refer to mode definitions * Add a `REPORT_MODES` constant in JS via javascript.rake and refactor JS to refer to the modes * Delete old admin report components that are no longer used (trust-level-counts, counts, per-day-counts) which were replaced by admin-report-counters a while ago * Add a new `registerReportModeComponent` plugin API, some plugins introduce their own modes (like AI's `emotion`) and components and we need a way to render them
This commit is contained in:
@ -1,36 +0,0 @@
|
|||||||
<td class="title">
|
|
||||||
{{#if this.report.icon}}
|
|
||||||
{{d-icon this.report.icon}}
|
|
||||||
{{/if}}
|
|
||||||
<a href={{this.report.reportUrl}}>{{this.report.title}}</a>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="value">{{number this.report.todayCount}}</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="value {{this.report.yesterdayTrend}}"
|
|
||||||
title={{this.report.yesterdayCountTitle}}
|
|
||||||
>
|
|
||||||
{{number this.report.yesterdayCount}}
|
|
||||||
{{d-icon this.report.yesterdayTrendIcon}}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="value {{this.report.sevenDaysTrend}}"
|
|
||||||
title={{this.report.sevenDaysCountTitle}}
|
|
||||||
>
|
|
||||||
{{number this.report.lastSevenDaysCount}}
|
|
||||||
{{d-icon this.report.sevenDaysTrendIcon}}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="value {{this.report.thirtyDaysTrend}}"
|
|
||||||
title={{this.report.thirtyDaysCountTitle}}
|
|
||||||
>
|
|
||||||
{{number this.report.lastThirtyDaysCount}}
|
|
||||||
{{d-icon this.report.thirtyDaysTrendIcon}}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{{#if this.allTime}}
|
|
||||||
<td class="value">{{number this.report.total}}</td>
|
|
||||||
{{/if}}
|
|
@ -1,12 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import { match } from "@ember/object/computed";
|
|
||||||
import { classNameBindings, tagName } from "@ember-decorators/component";
|
|
||||||
|
|
||||||
@tagName("tr")
|
|
||||||
@classNameBindings("reverseColors")
|
|
||||||
export default class AdminReportCounts extends Component {
|
|
||||||
allTime = true;
|
|
||||||
|
|
||||||
@match("report.type", /^(time_to_first_response|topics_with_no_response)$/)
|
|
||||||
reverseColors;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
<td class="title"><a
|
|
||||||
href={{this.report.reportUrl}}
|
|
||||||
>{{this.report.title}}</a></td>
|
|
||||||
<td class="value">{{this.report.todayCount}}</td>
|
|
||||||
<td class="value">{{this.report.yesterdayCount}}</td>
|
|
||||||
<td class="value">{{this.report.sevenDaysAgoCount}}</td>
|
|
||||||
<td class="value">{{this.report.thirtyDaysAgoCount}}</td>
|
|
||||||
<td class="value"></td>
|
|
@ -1,5 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import { tagName } from "@ember-decorators/component";
|
|
||||||
|
|
||||||
@tagName("tr")
|
|
||||||
export default class AdminReportPerDayCounts extends Component {}
|
|
@ -1,26 +0,0 @@
|
|||||||
<td class="title">{{this.report.title}}</td>
|
|
||||||
<td class="value">
|
|
||||||
<LinkTo @route="adminUsersList.show" @model="newuser">
|
|
||||||
{{number (value-at-tl this.report.data level="0")}}
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
||||||
<td class="value">
|
|
||||||
<LinkTo @route="adminUsersList.show" @model="basic">
|
|
||||||
{{number (value-at-tl this.report.data level="1")}}
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
||||||
<td class="value">
|
|
||||||
<LinkTo @route="adminUsersList.show" @model="member">
|
|
||||||
{{number (value-at-tl this.report.data level="2")}}
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
||||||
<td class="value">
|
|
||||||
<LinkTo @route="adminUsersList.show" @model="regular">
|
|
||||||
{{number (value-at-tl this.report.data level="3")}}
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
||||||
<td class="value">
|
|
||||||
<LinkTo @route="adminUsersList.show" @model="leader">
|
|
||||||
{{number (value-at-tl this.report.data level="4")}}
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
@ -1,5 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import { tagName } from "@ember-decorators/component";
|
|
||||||
|
|
||||||
@tagName("tr")
|
|
||||||
export default class AdminReportTrustLevelCounts extends Component {}
|
|
784
app/assets/javascripts/admin/addon/components/admin-report.gjs
Normal file
784
app/assets/javascripts/admin/addon/components/admin-report.gjs
Normal file
@ -0,0 +1,784 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { concat, fn } from "@ember/helper";
|
||||||
|
import EmberObject, { action } from "@ember/object";
|
||||||
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||||
|
import { next } from "@ember/runloop";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import { isPresent } from "@ember/utils";
|
||||||
|
import ConditionalLoadingSection from "discourse/components/conditional-loading-section";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||||
|
import DateTimeInputRange from "discourse/components/date-time-input-range";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import dIcon from "discourse/helpers/d-icon";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { reportModeComponent } from "discourse/lib/admin-report-additional-modes";
|
||||||
|
import { REPORT_MODES } from "discourse/lib/constants";
|
||||||
|
import { bind } from "discourse/lib/decorators";
|
||||||
|
import { isTesting } from "discourse/lib/environment";
|
||||||
|
import { exportEntity } from "discourse/lib/export-csv";
|
||||||
|
import { outputExportResult } from "discourse/lib/export-result";
|
||||||
|
import { makeArray } from "discourse/lib/helpers";
|
||||||
|
import ReportLoader from "discourse/lib/reports-loader";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AdminReportChart from "admin/components/admin-report-chart";
|
||||||
|
import AdminReportCounters from "admin/components/admin-report-counters";
|
||||||
|
import AdminReportInlineTable from "admin/components/admin-report-inline-table";
|
||||||
|
import AdminReportRadar from "admin/components/admin-report-radar";
|
||||||
|
import AdminReportStackedChart from "admin/components/admin-report-stacked-chart";
|
||||||
|
import AdminReportStackedLineChart from "admin/components/admin-report-stacked-line-chart";
|
||||||
|
import AdminReportStorageStats from "admin/components/admin-report-storage-stats";
|
||||||
|
import AdminReportTable from "admin/components/admin-report-table";
|
||||||
|
import ReportFilterBoolComponent from "admin/components/report-filters/bool";
|
||||||
|
import ReportFilterCategoryComponent from "admin/components/report-filters/category";
|
||||||
|
import ReportFilterGroupComponent from "admin/components/report-filters/group";
|
||||||
|
import ReportFilterListComponent from "admin/components/report-filters/list";
|
||||||
|
import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report";
|
||||||
|
import DTooltip from "float-kit/components/d-tooltip";
|
||||||
|
|
||||||
|
const TABLE_OPTIONS = {
|
||||||
|
perPage: 8,
|
||||||
|
total: true,
|
||||||
|
limit: 20,
|
||||||
|
formatNumbers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHART_OPTIONS = {};
|
||||||
|
|
||||||
|
export default class AdminReport extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
@tracked isEnabled = true;
|
||||||
|
@tracked isLoading = false;
|
||||||
|
@tracked rateLimitationString = null;
|
||||||
|
@tracked report = null;
|
||||||
|
@tracked model = null;
|
||||||
|
@tracked showTitle = true;
|
||||||
|
@tracked currentMode = this.args.filters?.mode;
|
||||||
|
@tracked options = null;
|
||||||
|
@tracked dateRangeFrom = null;
|
||||||
|
@tracked dateRangeTo = null;
|
||||||
|
|
||||||
|
showHeader = this.args.showHeader ?? true;
|
||||||
|
showFilteringUI = this.args.showFilteringUI ?? false;
|
||||||
|
showDescriptionInTooltip = this.args.showDescriptionInTooltip ?? true;
|
||||||
|
_reports = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.fetchOrRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
get startDate() {
|
||||||
|
if (this.dateRangeFrom) {
|
||||||
|
return moment(this.dateRangeFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
let startDate = moment();
|
||||||
|
if (this.args.filters && isPresent(this.args.filters.startDate)) {
|
||||||
|
startDate = moment(this.args.filters.startDate, "YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
get endDate() {
|
||||||
|
if (this.dateRangeTo) {
|
||||||
|
return moment(this.dateRangeTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let endDate = moment();
|
||||||
|
if (this.args.filters && isPresent(this.args.filters.endDate)) {
|
||||||
|
endDate = moment(this.args.filters.endDate, "YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reportClasses() {
|
||||||
|
const builtReportClasses = [];
|
||||||
|
|
||||||
|
if (this.isHidden) {
|
||||||
|
builtReportClasses.push("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isHidden) {
|
||||||
|
builtReportClasses.push("is-visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isEnabled) {
|
||||||
|
builtReportClasses.push("is-enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isLoading) {
|
||||||
|
builtReportClasses.push("is-loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showDescriptionInTooltip) {
|
||||||
|
builtReportClasses.push("description-in-tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
|
builtReportClasses.push(this.dasherizedDataSourceName);
|
||||||
|
|
||||||
|
return builtReportClasses.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
get showDatesOptions() {
|
||||||
|
return this.model?.dates_filtering;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showRefresh() {
|
||||||
|
return this.showDatesOptions || this.model?.available_filters.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldDisplayTrend() {
|
||||||
|
return this.args.showTrend && this.model?.prev_period;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showError() {
|
||||||
|
return (
|
||||||
|
this.showTimeoutError || this.showExceptionError || this.showNotFoundError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showNotFoundError() {
|
||||||
|
return this.model?.error === "not_found";
|
||||||
|
}
|
||||||
|
|
||||||
|
get showTimeoutError() {
|
||||||
|
return this.model?.error === "timeout";
|
||||||
|
}
|
||||||
|
|
||||||
|
get showExceptionError() {
|
||||||
|
return this.model?.error === "exception";
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasData() {
|
||||||
|
return this.model?.data?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get disabledLabel() {
|
||||||
|
return this.args.disabledLabel || i18n("admin.dashboard.disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
get isHidden() {
|
||||||
|
return (this.siteSettings.dashboard_hidden_reports || "")
|
||||||
|
.split("|")
|
||||||
|
.filter(Boolean)
|
||||||
|
.includes(this.args.dataSourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dasherizedDataSourceName() {
|
||||||
|
return (this.args.dataSourceName || this.model.type || "undefined").replace(
|
||||||
|
/_/g,
|
||||||
|
"-"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dataSource() {
|
||||||
|
let dataSourceName = this.args.dataSourceName || this.model.type;
|
||||||
|
return `/admin/reports/${dataSourceName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showModes() {
|
||||||
|
return this.displayedModes.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isChartMode() {
|
||||||
|
return this.currentMode === REPORT_MODES.chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
changeGrouping(grouping) {
|
||||||
|
this.refreshReport({ chartGrouping: grouping });
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayedModes() {
|
||||||
|
const modes = this.args.forcedModes
|
||||||
|
? this.args.forcedModes.split(",")
|
||||||
|
: this.model?.modes;
|
||||||
|
|
||||||
|
return makeArray(modes).map((mode) => {
|
||||||
|
const base = `btn-default mode-btn ${mode}`;
|
||||||
|
const cssClass = this.currentMode === mode ? `${base} btn-primary` : base;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
cssClass,
|
||||||
|
icon: mode === REPORT_MODES.table ? "table" : "signal",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reportFilterComponent(filter) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case "bool":
|
||||||
|
return ReportFilterBoolComponent;
|
||||||
|
case "category":
|
||||||
|
return ReportFilterCategoryComponent;
|
||||||
|
case "group":
|
||||||
|
return ReportFilterGroupComponent;
|
||||||
|
case "list":
|
||||||
|
return ReportFilterListComponent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get modeComponent() {
|
||||||
|
const reportMode = this.currentMode.replace(/-/g, "_");
|
||||||
|
switch (reportMode) {
|
||||||
|
case REPORT_MODES.table:
|
||||||
|
return AdminReportTable;
|
||||||
|
case REPORT_MODES.inline_table:
|
||||||
|
return AdminReportInlineTable;
|
||||||
|
case REPORT_MODES.chart:
|
||||||
|
return AdminReportChart;
|
||||||
|
case REPORT_MODES.stacked_chart:
|
||||||
|
return AdminReportStackedChart;
|
||||||
|
case REPORT_MODES.stacked_line_chart:
|
||||||
|
return AdminReportStackedLineChart;
|
||||||
|
case REPORT_MODES.counters:
|
||||||
|
return AdminReportCounters;
|
||||||
|
case REPORT_MODES.radar:
|
||||||
|
return AdminReportRadar;
|
||||||
|
case REPORT_MODES.storage_stats:
|
||||||
|
return AdminReportStorageStats;
|
||||||
|
default:
|
||||||
|
if (reportModeComponent(reportMode)) {
|
||||||
|
return reportModeComponent(reportMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get reportKey() {
|
||||||
|
if (!this.args.dataSourceName || !this.startDate || !this.endDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedStartDate = this.startDate.toISOString(true).split("T")[0];
|
||||||
|
const formattedEndDate = this.endDate.toISOString(true).split("T")[0];
|
||||||
|
|
||||||
|
let reportKey = "reports:";
|
||||||
|
reportKey += [
|
||||||
|
this.args.dataSourceName,
|
||||||
|
isTesting() ? "start" : formattedStartDate.replace(/-/g, ""),
|
||||||
|
isTesting() ? "end" : formattedEndDate.replace(/-/g, ""),
|
||||||
|
"[:prev_period]",
|
||||||
|
this.args.reportOptions?.table?.limit,
|
||||||
|
// Convert all filter values to strings to ensure unique serialization
|
||||||
|
this.args.filters?.customFilters
|
||||||
|
? JSON.stringify(this.args.filters?.customFilters, (k, v) =>
|
||||||
|
k ? `${v}` : v
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
SCHEMA_VERSION,
|
||||||
|
]
|
||||||
|
.filter((x) => x)
|
||||||
|
.map((x) => x.toString())
|
||||||
|
.join(":");
|
||||||
|
|
||||||
|
return reportKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartGroupings() {
|
||||||
|
const chartGrouping = this.options?.chartGrouping;
|
||||||
|
const options = ["daily", "weekly", "monthly"];
|
||||||
|
|
||||||
|
return options.map((id) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
disabled:
|
||||||
|
id === "daily" && this.model.chartData.length >= DAILY_LIMIT_DAYS,
|
||||||
|
label: `admin.dashboard.reports.${id}`,
|
||||||
|
class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onChangeDateRange(range) {
|
||||||
|
this.dateRangeFrom = range.from;
|
||||||
|
this.dateRangeTo = range.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applyFilter(id, value) {
|
||||||
|
let customFilters = this.args.filters?.customFilters || {};
|
||||||
|
|
||||||
|
if (typeof value === "undefined") {
|
||||||
|
delete customFilters[id];
|
||||||
|
} else {
|
||||||
|
customFilters[id] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshReport({ filters: customFilters });
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
refreshReport(options = {}) {
|
||||||
|
if (!this.args.onRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.onRefresh({
|
||||||
|
type: this.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.args.filters?.customFilters
|
||||||
|
: options.filters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
exportCsv() {
|
||||||
|
const args = {
|
||||||
|
name: this.model.type,
|
||||||
|
start_date: this.startDate.toISOString(true).split("T")[0],
|
||||||
|
end_date: this.endDate.toISOString(true).split("T")[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const customFilters = this.args.filters?.customFilters;
|
||||||
|
if (customFilters) {
|
||||||
|
Object.assign(args, customFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportEntity("report", args).then(outputExportResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onChangeMode(mode) {
|
||||||
|
this.currentMode = mode;
|
||||||
|
this.refreshReport({ chartGrouping: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
fetchOrRender() {
|
||||||
|
if (this.report) {
|
||||||
|
this._renderReport(this.report);
|
||||||
|
} else if (this.args.dataSourceName) {
|
||||||
|
this._fetchReport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_computeReport() {
|
||||||
|
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 foundReport;
|
||||||
|
|
||||||
|
const sort = (report) => {
|
||||||
|
if (report.length > 1) {
|
||||||
|
return report.findBy("type", this.args.dataSourceName);
|
||||||
|
} else {
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.startDate || !this.endDate) {
|
||||||
|
foundReport = sort(filteredReports)[0];
|
||||||
|
} else {
|
||||||
|
const reportKey = this.reportKey;
|
||||||
|
foundReport = sort(
|
||||||
|
filteredReports.filter((report) =>
|
||||||
|
report.report_key.includes(reportKey)
|
||||||
|
)
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (!foundReport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundReport.error === "not_found") {
|
||||||
|
this.showFilteringUI = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._renderReport(foundReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_renderReport(report) {
|
||||||
|
const modes = this.args.forcedModes?.split(",") || report.modes;
|
||||||
|
const currentMode = this.currentMode || modes?.[0];
|
||||||
|
|
||||||
|
this.model = report;
|
||||||
|
this.currentMode = currentMode;
|
||||||
|
this.options = this._buildOptions(currentMode, report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_fetchReport() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.rateLimitationString = null;
|
||||||
|
|
||||||
|
next(() => {
|
||||||
|
let payload = this._buildPayload(["prev_period"]);
|
||||||
|
|
||||||
|
const callback = (response) => {
|
||||||
|
if (this.isDestroying || this.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
if (response === 429) {
|
||||||
|
this.rateLimitationString = i18n("admin.dashboard.too_many_requests");
|
||||||
|
} else if (response === 500) {
|
||||||
|
this.model?.set("error", "exception");
|
||||||
|
} else if (response) {
|
||||||
|
this._reports.push(this._loadReport(response));
|
||||||
|
this._computeReport();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ReportLoader.enqueue(this.args.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.args.reportOptions?.table?.limit) {
|
||||||
|
payload.data.limit = this.args.reportOptions?.table?.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.args.filters?.customFilters) {
|
||||||
|
payload.data.filters = this.args.filters?.customFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildOptions(mode, report) {
|
||||||
|
if (mode === REPORT_MODES.table) {
|
||||||
|
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
||||||
|
return EmberObject.create(
|
||||||
|
Object.assign(tableOptions, this.args.reportOptions?.table || {})
|
||||||
|
);
|
||||||
|
} else if (mode === REPORT_MODES.chart) {
|
||||||
|
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
||||||
|
return EmberObject.create(
|
||||||
|
Object.assign(chartOptions, this.args.reportOptions?.chart || {}, {
|
||||||
|
chartGrouping:
|
||||||
|
this.args.reportOptions?.chartGrouping ||
|
||||||
|
Report.groupingForDatapoints(report.chartData.length),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (mode === REPORT_MODES.stacked_chart) {
|
||||||
|
return this.args.reportOptions?.stackedChart || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadReport(jsonReport) {
|
||||||
|
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
jsonReport.chartData &&
|
||||||
|
jsonReport.modes[0] === REPORT_MODES.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class={{concatClass "admin-report" this.reportClasses}}
|
||||||
|
{{didUpdate this.fetchOrRender @filters.startDate @filters.endDate}}
|
||||||
|
>
|
||||||
|
{{#unless this.isHidden}}
|
||||||
|
{{#if this.isEnabled}}
|
||||||
|
<ConditionalLoadingSection @isLoading={{this.isLoading}}>
|
||||||
|
{{#if this.showHeader}}
|
||||||
|
<div class="header">
|
||||||
|
{{#if this.showTitle}}
|
||||||
|
{{#unless this.showNotFoundError}}
|
||||||
|
<DPageSubheader
|
||||||
|
@titleLabel={{this.model.title}}
|
||||||
|
@titleUrl={{this.model.reportUrl}}
|
||||||
|
@descriptionLabel={{unless
|
||||||
|
this.showDescriptionInTooltip
|
||||||
|
this.model.description
|
||||||
|
}}
|
||||||
|
@learnMoreUrl={{this.model.description_link}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.showDescriptionInTooltip}}
|
||||||
|
{{#if this.model.description}}
|
||||||
|
<DTooltip
|
||||||
|
@interactive={{this.model.description_link.length}}
|
||||||
|
>
|
||||||
|
<:trigger>
|
||||||
|
{{dIcon "circle-question"}}
|
||||||
|
</:trigger>
|
||||||
|
<:content>
|
||||||
|
{{#if this.model.description_link}}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={{this.model.description_link}}
|
||||||
|
class="info"
|
||||||
|
>
|
||||||
|
{{this.model.description}}
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<span>{{this.model.description}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</:content>
|
||||||
|
</DTooltip>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.shouldDisplayTrend}}
|
||||||
|
<div class="trend {{this.model.trend}}">
|
||||||
|
<span class="value" title={{this.model.trendTitle}}>
|
||||||
|
{{#if this.model.average}}
|
||||||
|
{{number this.model.currentAverage}}{{#if
|
||||||
|
this.model.percent
|
||||||
|
}}%{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{number this.model.currentTotal noTitle="true"}}{{#if
|
||||||
|
this.model.percent
|
||||||
|
}}%{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.model.trendIcon}}
|
||||||
|
{{dIcon this.model.trendIcon class="icon"}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="main">
|
||||||
|
{{#if this.showError}}
|
||||||
|
{{#if this.showTimeoutError}}
|
||||||
|
<div class="alert alert-error report-alert timeout">
|
||||||
|
{{dIcon "triangle-exclamation"}}
|
||||||
|
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showExceptionError}}
|
||||||
|
<div class="alert alert-error report-alert exception">
|
||||||
|
{{dIcon "triangle-exclamation"}}
|
||||||
|
<span>{{i18n "admin.dashboard.exception_error"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showNotFoundError}}
|
||||||
|
<div class="alert alert-error report-alert not-found">
|
||||||
|
{{dIcon "triangle-exclamation"}}
|
||||||
|
<span>{{i18n "admin.dashboard.not_found_error"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if this.hasData}}
|
||||||
|
{{#if this.currentMode}}
|
||||||
|
{{component
|
||||||
|
this.modeComponent
|
||||||
|
model=this.model
|
||||||
|
options=this.options
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{#if this.model.relatedReport}}
|
||||||
|
<AdminReport
|
||||||
|
@showFilteringUI={{false}}
|
||||||
|
@dataSourceName={{this.model.relatedReport.type}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if this.rateLimitationString}}
|
||||||
|
<div class="alert alert-error report-alert rate-limited">
|
||||||
|
{{dIcon "temperature-three-quarters"}}
|
||||||
|
<span>{{this.rateLimitationString}}</span>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="alert alert-info report-alert no-data">
|
||||||
|
{{dIcon "chart-pie"}}
|
||||||
|
{{#if this.model.reportUrl}}
|
||||||
|
<a href={{this.model.reportUrl}} class="report-url">
|
||||||
|
<span>
|
||||||
|
{{#if this.model.title}}
|
||||||
|
{{this.model.title}}
|
||||||
|
—
|
||||||
|
{{/if}}
|
||||||
|
{{i18n "admin.dashboard.reports.no_data"}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<span>{{i18n
|
||||||
|
"admin.dashboard.reports.no_data"
|
||||||
|
}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.showFilteringUI}}
|
||||||
|
<div class="filters">
|
||||||
|
{{#if this.showModes}}
|
||||||
|
<div class="modes">
|
||||||
|
{{#each this.displayedModes as |displayedMode|}}
|
||||||
|
<DButton
|
||||||
|
@action={{fn this.onChangeMode displayedMode.mode}}
|
||||||
|
@icon={{displayedMode.icon}}
|
||||||
|
class={{displayedMode.cssClass}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.isChartMode}}
|
||||||
|
{{#if this.model.average}}
|
||||||
|
<span class="average-chart">
|
||||||
|
{{i18n "admin.dashboard.reports.average_chart_label"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
<div class="chart-groupings">
|
||||||
|
{{#each this.chartGroupings as |chartGrouping|}}
|
||||||
|
<DButton
|
||||||
|
@label={{chartGrouping.label}}
|
||||||
|
@action={{fn this.changeGrouping chartGrouping.id}}
|
||||||
|
@disabled={{chartGrouping.disabled}}
|
||||||
|
class={{chartGrouping.class}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showDatesOptions}}
|
||||||
|
<div class="control">
|
||||||
|
<span class="label">
|
||||||
|
{{i18n "admin.dashboard.reports.dates"}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
<DateTimeInputRange
|
||||||
|
@from={{this.startDate}}
|
||||||
|
@to={{this.endDate}}
|
||||||
|
@onChange={{this.onChangeDateRange}}
|
||||||
|
@showFromTime={{false}}
|
||||||
|
@showToTime={{false}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#each this.model.available_filters as |filter|}}
|
||||||
|
<div class="control">
|
||||||
|
<span class="label">
|
||||||
|
{{i18n
|
||||||
|
(concat
|
||||||
|
"admin.dashboard.reports.filters."
|
||||||
|
filter.id
|
||||||
|
".label"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
{{component
|
||||||
|
(this.reportFilterComponent filter)
|
||||||
|
model=this.model
|
||||||
|
filter=filter
|
||||||
|
applyFilter=this.applyFilter
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<div class="input">
|
||||||
|
<DButton
|
||||||
|
@action={{this.exportCsv}}
|
||||||
|
@label="admin.export_csv.button_text"
|
||||||
|
@icon="download"
|
||||||
|
class="btn-default export-csv-btn"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.showRefresh}}
|
||||||
|
<div class="control">
|
||||||
|
<div class="input">
|
||||||
|
<DButton
|
||||||
|
@action={{this.refreshReport}}
|
||||||
|
@label="admin.dashboard.reports.refresh_report"
|
||||||
|
@icon="arrows-rotate"
|
||||||
|
class="refresh-report-btn btn-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</ConditionalLoadingSection>
|
||||||
|
{{else}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{htmlSafe this.disabledLabel}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
@ -1,209 +0,0 @@
|
|||||||
{{#unless this.isHidden}}
|
|
||||||
{{#if this.isEnabled}}
|
|
||||||
<ConditionalLoadingSection @isLoading={{this.isLoading}}>
|
|
||||||
{{#if this.showHeader}}
|
|
||||||
<div class="header">
|
|
||||||
{{#if this.showTitle}}
|
|
||||||
{{#unless this.showNotFoundError}}
|
|
||||||
<DPageSubheader
|
|
||||||
@titleLabel={{this.model.title}}
|
|
||||||
@titleUrl={{this.model.reportUrl}}
|
|
||||||
@descriptionLabel={{this.model.description}}
|
|
||||||
@learnMoreUrl={{this.model.description_link}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.shouldDisplayTrend}}
|
|
||||||
<div class="trend {{this.model.trend}}">
|
|
||||||
<span class="value" title={{this.model.trendTitle}}>
|
|
||||||
{{#if this.model.average}}
|
|
||||||
{{number this.model.currentAverage}}{{#if
|
|
||||||
this.model.percent
|
|
||||||
}}%{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{number this.model.currentTotal noTitle="true"}}{{#if
|
|
||||||
this.model.percent
|
|
||||||
}}%{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.model.trendIcon}}
|
|
||||||
{{d-icon this.model.trendIcon class="icon"}}
|
|
||||||
{{/if}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="body">
|
|
||||||
<div class="main">
|
|
||||||
{{#if this.showError}}
|
|
||||||
{{#if this.showTimeoutError}}
|
|
||||||
<div class="alert alert-error report-alert timeout">
|
|
||||||
{{d-icon "triangle-exclamation"}}
|
|
||||||
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showExceptionError}}
|
|
||||||
<div class="alert alert-error report-alert exception">
|
|
||||||
{{d-icon "triangle-exclamation"}}
|
|
||||||
<span>{{i18n "admin.dashboard.exception_error"}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showNotFoundError}}
|
|
||||||
<div class="alert alert-error report-alert not-found">
|
|
||||||
{{d-icon "triangle-exclamation"}}
|
|
||||||
<span>{{i18n "admin.dashboard.not_found_error"}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{#if this.hasData}}
|
|
||||||
{{#if this.currentMode}}
|
|
||||||
{{component
|
|
||||||
this.modeComponent
|
|
||||||
model=this.model
|
|
||||||
options=this.options
|
|
||||||
}}
|
|
||||||
|
|
||||||
{{#if this.model.relatedReport}}
|
|
||||||
<AdminReport
|
|
||||||
@showFilteringUI={{false}}
|
|
||||||
@dataSourceName={{this.model.relatedReport.type}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{#if this.rateLimitationString}}
|
|
||||||
<div class="alert alert-error report-alert rate-limited">
|
|
||||||
{{d-icon "temperature-three-quarters"}}
|
|
||||||
<span>{{this.rateLimitationString}}</span>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="alert alert-info report-alert no-data">
|
|
||||||
{{d-icon "chart-pie"}}
|
|
||||||
{{#if this.model.reportUrl}}
|
|
||||||
<a href={{this.model.reportUrl}} class="report-url">
|
|
||||||
<span>
|
|
||||||
{{#if this.model.title}}
|
|
||||||
{{this.model.title}}
|
|
||||||
—
|
|
||||||
{{/if}}
|
|
||||||
{{i18n "admin.dashboard.reports.no_data"}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.showFilteringUI}}
|
|
||||||
<div class="filters">
|
|
||||||
{{#if this.showModes}}
|
|
||||||
<div class="modes">
|
|
||||||
{{#each this.displayedModes as |displayedMode|}}
|
|
||||||
<DButton
|
|
||||||
@action={{fn this.onChangeMode displayedMode.mode}}
|
|
||||||
@icon={{displayedMode.icon}}
|
|
||||||
class={{displayedMode.cssClass}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.isChartMode}}
|
|
||||||
{{#if this.model.average}}
|
|
||||||
<span class="average-chart">
|
|
||||||
{{i18n "admin.dashboard.reports.average_chart_label"}}
|
|
||||||
</span>
|
|
||||||
{{/if}}
|
|
||||||
<div class="chart-groupings">
|
|
||||||
{{#each this.chartGroupings as |chartGrouping|}}
|
|
||||||
<DButton
|
|
||||||
@label={{chartGrouping.label}}
|
|
||||||
@action={{fn this.changeGrouping chartGrouping.id}}
|
|
||||||
@disabled={{chartGrouping.disabled}}
|
|
||||||
class={{chartGrouping.class}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showDatesOptions}}
|
|
||||||
<div class="control">
|
|
||||||
<span class="label">
|
|
||||||
{{i18n "admin.dashboard.reports.dates"}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="input">
|
|
||||||
<DateTimeInputRange
|
|
||||||
@from={{this.startDate}}
|
|
||||||
@to={{this.endDate}}
|
|
||||||
@onChange={{this.onChangeDateRange}}
|
|
||||||
@showFromTime={{false}}
|
|
||||||
@showToTime={{false}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#each this.model.available_filters as |filter|}}
|
|
||||||
<div class="control">
|
|
||||||
<span class="label">
|
|
||||||
{{i18n
|
|
||||||
(concat
|
|
||||||
"admin.dashboard.reports.filters." filter.id ".label"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="input">
|
|
||||||
{{component
|
|
||||||
(concat "report-filters/" filter.type)
|
|
||||||
model=this.model
|
|
||||||
filter=filter
|
|
||||||
applyFilter=this.applyFilter
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
<div class="control">
|
|
||||||
<div class="input">
|
|
||||||
<DButton
|
|
||||||
@action={{this.exportCsv}}
|
|
||||||
@label="admin.export_csv.button_text"
|
|
||||||
@icon="download"
|
|
||||||
class="btn-default export-csv-btn"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.showRefresh}}
|
|
||||||
<div class="control">
|
|
||||||
<div class="input">
|
|
||||||
<DButton
|
|
||||||
@action={{this.refreshReport}}
|
|
||||||
@label="admin.dashboard.reports.refresh_report"
|
|
||||||
@icon="arrows-rotate"
|
|
||||||
class="refresh-report-btn btn-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</ConditionalLoadingSection>
|
|
||||||
{{else}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
{{html-safe this.disabledLabel}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/unless}}
|
|
@ -1,427 +0,0 @@
|
|||||||
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 discourseComputed from "discourse/lib/decorators";
|
|
||||||
import { isTesting } from "discourse/lib/environment";
|
|
||||||
import { exportEntity } from "discourse/lib/export-csv";
|
|
||||||
import { outputExportResult } from "discourse/lib/export-result";
|
|
||||||
import { makeArray } from "discourse/lib/helpers";
|
|
||||||
import ReportLoader from "discourse/lib/reports-loader";
|
|
||||||
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("admin.dashboard.disabled");
|
|
||||||
isLoading = false;
|
|
||||||
rateLimitationString = null;
|
|
||||||
dataSourceName = null;
|
|
||||||
report = null;
|
|
||||||
model = null;
|
|
||||||
reportOptions = null;
|
|
||||||
forcedModes = null;
|
|
||||||
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("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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ import { inject as controller } from "@ember/controller";
|
|||||||
import { computed } from "@ember/object";
|
import { computed } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { setting } from "discourse/lib/computed";
|
import { setting } from "discourse/lib/computed";
|
||||||
|
import { REPORT_MODES } from "discourse/lib/constants";
|
||||||
import discourseComputed from "discourse/lib/decorators";
|
import discourseComputed from "discourse/lib/decorators";
|
||||||
import getURL from "discourse/lib/get-url";
|
import getURL from "discourse/lib/get-url";
|
||||||
import { makeArray } from "discourse/lib/helpers";
|
import { makeArray } from "discourse/lib/helpers";
|
||||||
@ -30,6 +31,10 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo
|
|||||||
@staticReport("users_by_trust_level") usersByTrustLevelReport;
|
@staticReport("users_by_trust_level") usersByTrustLevelReport;
|
||||||
@staticReport("storage_report") storageReport;
|
@staticReport("storage_report") storageReport;
|
||||||
|
|
||||||
|
get reportModes() {
|
||||||
|
return REPORT_MODES;
|
||||||
|
}
|
||||||
|
|
||||||
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")
|
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")
|
||||||
activityMetrics(metrics) {
|
activityMetrics(metrics) {
|
||||||
return (metrics || "").split("|").filter(Boolean);
|
return (metrics || "").split("|").filter(Boolean);
|
||||||
@ -53,7 +58,7 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo
|
|||||||
@computed("hiddenReports")
|
@computed("hiddenReports")
|
||||||
get isSearchReportsVisible() {
|
get isSearchReportsVisible() {
|
||||||
return ["top_referred_topics", "trending_search"].some(
|
return ["top_referred_topics", "trending_search"].some(
|
||||||
(x) => !this.hiddenReports.includes(x)
|
(report) => !this.hiddenReports.includes(report)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +73,7 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo
|
|||||||
"dau_by_mau",
|
"dau_by_mau",
|
||||||
"daily_engaged_users",
|
"daily_engaged_users",
|
||||||
"new_contributors",
|
"new_contributors",
|
||||||
].some((x) => !this.hiddenReports.includes(x));
|
].some((report) => !this.hiddenReports.includes(report));
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
@ -76,11 +81,6 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo
|
|||||||
return moment().locale("en").utc().endOf("day");
|
return moment().locale("en").utc().endOf("day");
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("startDate", "endDate")
|
|
||||||
get filters() {
|
|
||||||
return { startDate: this.startDate, endDate: this.endDate };
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
activityMetricsFilters() {
|
activityMetricsFilters() {
|
||||||
const lastMonth = moment()
|
const lastMonth = moment()
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import { action, computed } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||||
import CustomDateRangeModal from "../components/modal/custom-date-range";
|
import CustomDateRangeModal from "../components/modal/custom-date-range";
|
||||||
|
|
||||||
export default class AdminDashboardTabController extends Controller {
|
export default class AdminDashboardTabController extends Controller {
|
||||||
@service modal;
|
@service modal;
|
||||||
|
|
||||||
|
@tracked endDate = moment().locale("en").utc().endOf("day");
|
||||||
|
@tracked startDate = this.calculateStartDate();
|
||||||
|
@tracked
|
||||||
|
filters = new TrackedObject({
|
||||||
|
startDate: this.startDate,
|
||||||
|
endDate: this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
queryParams = ["period"];
|
queryParams = ["period"];
|
||||||
period = "monthly";
|
period = "monthly";
|
||||||
|
|
||||||
endDate = moment().locale("en").utc().endOf("day");
|
calculateStartDate() {
|
||||||
_startDate;
|
|
||||||
|
|
||||||
@computed("_startDate", "period")
|
|
||||||
get startDate() {
|
|
||||||
if (this._startDate) {
|
|
||||||
return this._startDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullDay = moment().locale("en").utc().endOf("day");
|
const fullDay = moment().locale("en").utc().endOf("day");
|
||||||
|
|
||||||
switch (this.period) {
|
switch (this.period) {
|
||||||
@ -35,13 +37,19 @@ export default class AdminDashboardTabController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setCustomDateRange(_startDate, endDate) {
|
setCustomDateRange(startDate, endDate) {
|
||||||
this.setProperties({ _startDate, endDate });
|
this.startDate = startDate;
|
||||||
|
this.endDate = endDate;
|
||||||
|
this.filters.startDate = this.startDate;
|
||||||
|
this.filters.endDate = this.endDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setPeriod(period) {
|
setPeriod(period) {
|
||||||
this.setProperties({ period, _startDate: null });
|
this.set("period", period);
|
||||||
|
this.startDate = this.calculateStartDate();
|
||||||
|
this.filters.startDate = this.startDate;
|
||||||
|
this.filters.endDate = this.endDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
@startDate={{this.startDate}}
|
@startDate={{this.startDate}}
|
||||||
@endDate={{this.endDate}}
|
@endDate={{this.endDate}}
|
||||||
@setCustomDateRange={{this.setCustomDateRange}}
|
@setCustomDateRange={{this.setCustomDateRange}}
|
||||||
|
@onDateChange={{this.onDateChange}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -25,13 +26,13 @@
|
|||||||
{{#if this.siteSettings.use_legacy_pageviews}}
|
{{#if this.siteSettings.use_legacy_pageviews}}
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="consolidated_page_views"
|
@dataSourceName="consolidated_page_views"
|
||||||
@forcedModes="stacked-chart"
|
@forcedModes={{this.reportModes.stacked_chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="site_traffic"
|
@dataSourceName="site_traffic"
|
||||||
@forcedModes="stacked-chart"
|
@forcedModes={{this.reportModes.stacked_chart}}
|
||||||
@reportOptions={{this.siteTrafficOptions}}
|
@reportOptions={{this.siteTrafficOptions}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
@ -40,42 +41,42 @@
|
|||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="signups"
|
@dataSourceName="signups"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="topics"
|
@dataSourceName="topics"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="posts"
|
@dataSourceName="posts"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="dau_by_mau"
|
@dataSourceName="dau_by_mau"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="daily_engaged_users"
|
@dataSourceName="daily_engaged_users"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@dataSourceName="new_contributors"
|
@dataSourceName="new_contributors"
|
||||||
@showTrend={{true}}
|
@showTrend={{true}}
|
||||||
@forcedModes="chart"
|
@forcedModes={{this.reportModes.chart}}
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -120,7 +121,7 @@
|
|||||||
<AdminReport
|
<AdminReport
|
||||||
@showHeader={{false}}
|
@showHeader={{false}}
|
||||||
@filters={{this.activityMetricsFilters}}
|
@filters={{this.activityMetricsFilters}}
|
||||||
@forcedModes="counters"
|
@forcedModes={{this.reportModes.counters}}
|
||||||
@dataSourceName={{metric}}
|
@dataSourceName={{metric}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@ -133,12 +134,12 @@
|
|||||||
<div class="user-metrics">
|
<div class="user-metrics">
|
||||||
<ConditionalLoadingSection @isLoading={{this.isLoading}}>
|
<ConditionalLoadingSection @isLoading={{this.isLoading}}>
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@forcedModes="inline-table"
|
@forcedModes={{this.reportModes.inline_table}}
|
||||||
@dataSourceName="users_by_type"
|
@dataSourceName="users_by_type"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@forcedModes="inline-table"
|
@forcedModes={{this.reportModes.inline_table}}
|
||||||
@dataSourceName="users_by_trust_level"
|
@dataSourceName="users_by_trust_level"
|
||||||
/>
|
/>
|
||||||
</ConditionalLoadingSection>
|
</ConditionalLoadingSection>
|
||||||
@ -146,7 +147,7 @@
|
|||||||
|
|
||||||
<div class="misc">
|
<div class="misc">
|
||||||
<AdminReport
|
<AdminReport
|
||||||
@forcedModes="storage-stats"
|
@forcedModes={{this.reportModes.storage_stats}}
|
||||||
@dataSourceName="storage_stats"
|
@dataSourceName="storage_stats"
|
||||||
@showHeader={{false}}
|
@showHeader={{false}}
|
||||||
/>
|
/>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
@filters={{this.model}}
|
@filters={{this.model}}
|
||||||
@reportOptions={{this.reportOptions}}
|
@reportOptions={{this.reportOptions}}
|
||||||
@showFilteringUI={{true}}
|
@showFilteringUI={{true}}
|
||||||
|
@showDescriptionInTooltip={{false}}
|
||||||
@onRefresh={{route-action "onParamsChange"}}
|
@onRefresh={{route-action "onParamsChange"}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
let additionalReportModes = new Map();
|
||||||
|
export function registerReportModeComponent(mode, componentClass) {
|
||||||
|
additionalReportModes.set(mode, componentClass);
|
||||||
|
}
|
||||||
|
export function resetAdditionalReportModes() {
|
||||||
|
additionalReportModes.clear();
|
||||||
|
}
|
||||||
|
export function reportModeComponent(mode) {
|
||||||
|
return additionalReportModes.get(mode);
|
||||||
|
}
|
@ -106,3 +106,14 @@ export const USER_FIELD_FLAGS = [
|
|||||||
"show_on_user_card",
|
"show_on_user_card",
|
||||||
"searchable",
|
"searchable",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const REPORT_MODES = {
|
||||||
|
table: "table",
|
||||||
|
chart: "chart",
|
||||||
|
stacked_chart: "stacked_chart",
|
||||||
|
stacked_line_chart: "stacked_line_chart",
|
||||||
|
radar: "radar",
|
||||||
|
counters: "counters",
|
||||||
|
inline_table: "inline_table",
|
||||||
|
storage_stats: "storage_stats",
|
||||||
|
};
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
||||||
|
|
||||||
export const PLUGIN_API_VERSION = "2.0.0";
|
export const PLUGIN_API_VERSION = "2.0.1";
|
||||||
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import { h } from "virtual-dom";
|
import { h } from "virtual-dom";
|
||||||
@ -61,6 +61,7 @@ import {
|
|||||||
registerAdminPluginConfigNav,
|
registerAdminPluginConfigNav,
|
||||||
} from "discourse/lib/admin-plugin-config-nav";
|
} from "discourse/lib/admin-plugin-config-nav";
|
||||||
import { registerPluginHeaderActionComponent } from "discourse/lib/admin-plugin-header-actions";
|
import { registerPluginHeaderActionComponent } from "discourse/lib/admin-plugin-header-actions";
|
||||||
|
import { registerReportModeComponent } from "discourse/lib/admin-report-additional-modes";
|
||||||
import classPrepend, {
|
import classPrepend, {
|
||||||
withPrependsRolledBack,
|
withPrependsRolledBack,
|
||||||
} from "discourse/lib/class-prepend";
|
} from "discourse/lib/class-prepend";
|
||||||
@ -3375,6 +3376,19 @@ class PluginApi {
|
|||||||
registeredTabs.push(tab);
|
registeredTabs.push(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a report mode and an associated component, which will be rendered
|
||||||
|
* by the AdminReport component. A mode is a different way of displaying the
|
||||||
|
* report data, core modes are things like "table" and "chart". For all core modes
|
||||||
|
* see Admin::Report::MODES.
|
||||||
|
*
|
||||||
|
* @param {String} mode - The identifier of the mode to register
|
||||||
|
* @param {Class} componentClass - The class of the component to render
|
||||||
|
*/
|
||||||
|
registerReportModeComponent(mode, componentClass) {
|
||||||
|
registerReportModeComponent(mode, componentClass);
|
||||||
|
}
|
||||||
|
|
||||||
#deprecatedWidgetOverride(widgetName, override) {
|
#deprecatedWidgetOverride(widgetName, override) {
|
||||||
// insert here the code to handle widget deprecations, e.g. for the header widgets we used:
|
// insert here the code to handle widget deprecations, e.g. for the header widgets we used:
|
||||||
// if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) {
|
// if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) {
|
||||||
|
@ -36,6 +36,7 @@ import { resetUsernameDecorators } from "discourse/helpers/decorate-username-sel
|
|||||||
import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete";
|
import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete";
|
||||||
import { resetAdminPluginConfigNav } from "discourse/lib/admin-plugin-config-nav";
|
import { resetAdminPluginConfigNav } from "discourse/lib/admin-plugin-config-nav";
|
||||||
import { clearPluginHeaderActionComponents } from "discourse/lib/admin-plugin-header-actions";
|
import { clearPluginHeaderActionComponents } from "discourse/lib/admin-plugin-header-actions";
|
||||||
|
import { resetAdditionalReportModes } from "discourse/lib/admin-report-additional-modes";
|
||||||
import { rollbackAllPrepends } from "discourse/lib/class-prepend";
|
import { rollbackAllPrepends } from "discourse/lib/class-prepend";
|
||||||
import { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options";
|
import { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options";
|
||||||
import deprecated from "discourse/lib/deprecated";
|
import deprecated from "discourse/lib/deprecated";
|
||||||
@ -201,6 +202,7 @@ export function testCleanup(container, app) {
|
|||||||
|
|
||||||
User.resetCurrent();
|
User.resetCurrent();
|
||||||
resetMobile();
|
resetMobile();
|
||||||
|
resetAdditionalReportModes();
|
||||||
resetExtraClasses();
|
resetExtraClasses();
|
||||||
clearOutletCache();
|
clearOutletCache();
|
||||||
clearHTMLCache();
|
clearHTMLCache();
|
||||||
|
@ -8,7 +8,9 @@ module("Integration | Component | admin-report", function (hooks) {
|
|||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test("default", async function (assert) {
|
test("default", async function (assert) {
|
||||||
await render(hbs`<AdminReport @dataSourceName="signups" />`);
|
await render(
|
||||||
|
hbs`<AdminReport @dataSourceName="signups" @showDescriptionInTooltip={{false}} />`
|
||||||
|
);
|
||||||
|
|
||||||
assert.dom(".admin-report.signups").exists();
|
assert.dom(".admin-report.signups").exists();
|
||||||
assert.dom(".admin-report-table").exists("defaults to table mode");
|
assert.dom(".admin-report-table").exists("defaults to table mode");
|
||||||
|
@ -80,7 +80,6 @@
|
|||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-top: var(--space-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
@ -137,6 +137,19 @@
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-report.description-in-tooltip .header {
|
||||||
|
.d-page-subheader {
|
||||||
|
.d-page-subheader__title-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fk-d-tooltip__trigger {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
max-width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.charts {
|
.charts {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
@ -153,7 +166,7 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "title trend" "description description";
|
grid-template-areas: "title tooltip trend" "description description description";
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
|
|
||||||
.d-page-subheader {
|
.d-page-subheader {
|
||||||
|
@ -227,7 +227,7 @@ module Jobs
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if report.modes == [:stacked_chart]
|
if report.modes == [Report::MODES[:stacked_chart]]
|
||||||
header = [:x]
|
header = [:x]
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ module Reports::ConsolidatedApiRequests
|
|||||||
def report_consolidated_api_requests(report)
|
def report_consolidated_api_requests(report)
|
||||||
filters = %w[api user_api]
|
filters = %w[api user_api]
|
||||||
|
|
||||||
report.modes = [:stacked_chart]
|
report.modes = [Report::MODES[:stacked_chart]]
|
||||||
|
|
||||||
requests =
|
requests =
|
||||||
filters.map do |filter|
|
filters.map do |filter|
|
||||||
|
@ -11,7 +11,7 @@ module Reports::ConsolidatedPageViews
|
|||||||
def report_consolidated_page_views(report)
|
def report_consolidated_page_views(report)
|
||||||
filters = %w[page_view_logged_in page_view_anon page_view_crawler]
|
filters = %w[page_view_logged_in page_view_anon page_view_crawler]
|
||||||
|
|
||||||
report.modes = [:stacked_chart]
|
report.modes = [Report::MODES[:stacked_chart]]
|
||||||
|
|
||||||
requests =
|
requests =
|
||||||
filters.map do |filter|
|
filters.map do |filter|
|
||||||
|
@ -5,7 +5,7 @@ module Reports::FlagsStatus
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_flags_status(report)
|
def report_flags_status(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -47,7 +47,7 @@ module Reports::ModeratorsActivity
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
query = <<~SQL
|
query = <<~SQL
|
||||||
|
@ -8,7 +8,7 @@ module Reports::PostEdits
|
|||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
editor_username = report.filters["editor"]
|
editor_username = report.filters["editor"]
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -5,8 +5,6 @@ module Reports::Posts
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_posts(report)
|
def report_posts(report)
|
||||||
report.modes = %i[table chart]
|
|
||||||
|
|
||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
|
|
||||||
basic_report_about report,
|
basic_report_about report,
|
||||||
|
@ -5,7 +5,7 @@ module Reports::SiteTraffic
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_site_traffic(report)
|
def report_site_traffic(report)
|
||||||
report.modes = [:stacked_chart]
|
report.modes = [Report::MODES[:stacked_chart]]
|
||||||
|
|
||||||
first_browser_pageview_date =
|
first_browser_pageview_date =
|
||||||
DB.query_single(
|
DB.query_single(
|
||||||
|
@ -5,7 +5,7 @@ module Reports::StaffLogins
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_staff_logins(report)
|
def report_staff_logins(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ module Reports::SuspiciousLogins
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_suspicious_logins(report)
|
def report_suspicious_logins(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ module Reports::TopIgnoredUsers
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_top_ignored_users(report)
|
def report_top_ignored_users(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@ module Reports::TopReferredTopics
|
|||||||
def report_top_referred_topics(report)
|
def report_top_referred_topics(report)
|
||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ module Reports::TopReferrers
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_top_referrers(report)
|
def report_top_referrers(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@ module Reports::TopTrafficSources
|
|||||||
def report_top_traffic_sources(report)
|
def report_top_traffic_sources(report)
|
||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.labels = [
|
report.labels = [
|
||||||
{ property: :domain, title: I18n.t("reports.top_traffic_sources.labels.domain") },
|
{ property: :domain, title: I18n.t("reports.top_traffic_sources.labels.domain") },
|
||||||
|
@ -5,7 +5,7 @@ module Reports::TopUploads
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_top_uploads(report)
|
def report_top_uploads(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
extension_filter = report.filters.dig(:file_extension)
|
extension_filter = report.filters.dig(:file_extension)
|
||||||
report.add_filter(
|
report.add_filter(
|
||||||
|
@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceived
|
|||||||
report.icon = "heart"
|
report.icon = "heart"
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = true
|
report.dates_filtering = true
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople
|
|||||||
report.icon = "heart"
|
report.icon = "heart"
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = true
|
report.dates_filtering = true
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel
|
|||||||
report.icon = "heart"
|
report.icon = "heart"
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = true
|
report.dates_filtering = true
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ module Reports::TopicViewStats
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_topic_view_stats(report)
|
def report_topic_view_stats(report)
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ module Reports::TrendingSearch
|
|||||||
|
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
trends =
|
trends =
|
||||||
SearchLog.trending_from(report.start_date, end_date: report.end_date, limit: report.limit)
|
SearchLog.trending_from(report.start_date, end_date: report.end_date, limit: report.limit)
|
||||||
|
@ -5,7 +5,7 @@ module Reports::TrustLevelGrowth
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def report_trust_level_growth(report)
|
def report_trust_level_growth(report)
|
||||||
report.modes = [:stacked_chart]
|
report.modes = [Report::MODES[:stacked_chart]]
|
||||||
|
|
||||||
filters = %w[tl1_reached tl2_reached tl3_reached tl4_reached]
|
filters = %w[tl1_reached tl2_reached tl3_reached tl4_reached]
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ module Reports::UserFlaggingRatio
|
|||||||
def report_user_flagging_ratio(report)
|
def report_user_flagging_ratio(report)
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = true
|
report.dates_filtering = true
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ module Reports::UsersByTrustLevel
|
|||||||
def report_users_by_trust_level(report)
|
def report_users_by_trust_level(report)
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = false
|
report.dates_filtering = false
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ module Reports::UsersByType
|
|||||||
def report_users_by_type(report)
|
def report_users_by_type(report)
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.dates_filtering = false
|
report.dates_filtering = false
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ module Reports::WebCrawlers
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
report.modes = [:table]
|
report.modes = [Report::MODES[:table]]
|
||||||
|
|
||||||
report.data =
|
report.data =
|
||||||
WebCrawlerRequest
|
WebCrawlerRequest
|
||||||
|
@ -16,6 +16,17 @@ class Report
|
|||||||
include_subcategories
|
include_subcategories
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MODES = {
|
||||||
|
table: :table,
|
||||||
|
chart: :chart,
|
||||||
|
stacked_chart: :stacked_chart,
|
||||||
|
stacked_line_chart: :stacked_line_chart,
|
||||||
|
radar: :radar,
|
||||||
|
counters: :counters,
|
||||||
|
inline_table: :inline_table,
|
||||||
|
storage_stats: :storage_stats,
|
||||||
|
}
|
||||||
|
|
||||||
include Reports::Bookmarks
|
include Reports::Bookmarks
|
||||||
include Reports::ConsolidatedApiRequests
|
include Reports::ConsolidatedApiRequests
|
||||||
include Reports::ConsolidatedPageViews
|
include Reports::ConsolidatedPageViews
|
||||||
@ -106,7 +117,7 @@ class Report
|
|||||||
@average = false
|
@average = false
|
||||||
@percent = false
|
@percent = false
|
||||||
@higher_is_better = true
|
@higher_is_better = true
|
||||||
@modes = %i[table chart]
|
@modes = [MODES[:chart], MODES[:table]]
|
||||||
@prev_data = nil
|
@prev_data = nil
|
||||||
@dates_filtering = true
|
@dates_filtering = true
|
||||||
@available_filters = {}
|
@available_filters = {}
|
||||||
@ -374,7 +385,7 @@ class Report
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.add_prev_data(report, subject_class, report_method, *args)
|
def self.add_prev_data(report, subject_class, report_method, *args)
|
||||||
if report.modes.include?(:chart) && report.facets.include?(:prev_period)
|
if report.modes.include?(Report::MODES[:chart]) && report.facets.include?(:prev_period)
|
||||||
prev_data = subject_class.public_send(report_method, *args)
|
prev_data = subject_class.public_send(report_method, *args)
|
||||||
report.prev_data = prev_data.map { |k, v| { x: k, y: v } }
|
report.prev_data = prev_data.map { |k, v| { x: k, y: v } }
|
||||||
end
|
end
|
||||||
|
@ -7,6 +7,10 @@ in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.0.1] - 2025-01-29
|
||||||
|
|
||||||
|
- Added `registerReportModeComponent`. This allows plugins to register different report display modes in addition to the built-in core ones like `chart`, `table`, and so on defined in `Report::MODES`.
|
||||||
|
|
||||||
## [2.0.0] - 2025-01-07
|
## [2.0.0] - 2025-01-07
|
||||||
|
|
||||||
- Removed `decorateTopicTitle`. This has been deprecated for more than a year, and we are not aware of any remaining uses in the ecosystem.
|
- Removed `decorateTopicTitle`. This has been deprecated for more than a year, and we are not aware of any remaining uses in the ecosystem.
|
||||||
|
@ -167,6 +167,8 @@ task "javascript:update_constants" => :environment do
|
|||||||
export const MAX_UNOPTIMIZED_CATEGORIES = #{CategoryList::MAX_UNOPTIMIZED_CATEGORIES};
|
export const MAX_UNOPTIMIZED_CATEGORIES = #{CategoryList::MAX_UNOPTIMIZED_CATEGORIES};
|
||||||
|
|
||||||
export const USER_FIELD_FLAGS = #{UserField::FLAG_ATTRIBUTES};
|
export const USER_FIELD_FLAGS = #{UserField::FLAG_ATTRIBUTES};
|
||||||
|
|
||||||
|
export const REPORT_MODES = #{Report::MODES.to_json};
|
||||||
JS
|
JS
|
||||||
|
|
||||||
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")
|
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")
|
||||||
|
Reference in New Issue
Block a user