mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:27:42 +08:00

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
785 lines
24 KiB
Plaintext
785 lines
24 KiB
Plaintext
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>
|
|
}
|