diff --git a/app/assets/javascripts/admin/addon/components/admin-report-counts.hbs b/app/assets/javascripts/admin/addon/components/admin-report-counts.hbs deleted file mode 100644 index e0a77431745..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-counts.hbs +++ /dev/null @@ -1,36 +0,0 @@ - - {{#if this.report.icon}} - {{d-icon this.report.icon}} - {{/if}} - {{this.report.title}} - - -{{number this.report.todayCount}} - - - {{number this.report.yesterdayCount}} - {{d-icon this.report.yesterdayTrendIcon}} - - - - {{number this.report.lastSevenDaysCount}} - {{d-icon this.report.sevenDaysTrendIcon}} - - - - {{number this.report.lastThirtyDaysCount}} - {{d-icon this.report.thirtyDaysTrendIcon}} - - -{{#if this.allTime}} - {{number this.report.total}} -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-report-counts.js b/app/assets/javascripts/admin/addon/components/admin-report-counts.js deleted file mode 100644 index e6ef645aa0e..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-counts.js +++ /dev/null @@ -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; -} diff --git a/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.hbs b/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.hbs deleted file mode 100644 index 9c9e3ebb764..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{this.report.title}} -{{this.report.todayCount}} -{{this.report.yesterdayCount}} -{{this.report.sevenDaysAgoCount}} -{{this.report.thirtyDaysAgoCount}} - \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.js b/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.js deleted file mode 100644 index b5d5984f988..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-per-day-counts.js +++ /dev/null @@ -1,5 +0,0 @@ -import Component from "@ember/component"; -import { tagName } from "@ember-decorators/component"; - -@tagName("tr") -export default class AdminReportPerDayCounts extends Component {} diff --git a/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.hbs b/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.hbs deleted file mode 100644 index 81fce07818f..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{this.report.title}} - - - {{number (value-at-tl this.report.data level="0")}} - - - - - {{number (value-at-tl this.report.data level="1")}} - - - - - {{number (value-at-tl this.report.data level="2")}} - - - - - {{number (value-at-tl this.report.data level="3")}} - - - - - {{number (value-at-tl this.report.data level="4")}} - - \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.js b/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.js deleted file mode 100644 index 0c41a0f6167..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report-trust-level-counts.js +++ /dev/null @@ -1,5 +0,0 @@ -import Component from "@ember/component"; -import { tagName } from "@ember-decorators/component"; - -@tagName("tr") -export default class AdminReportTrustLevelCounts extends Component {} diff --git a/app/assets/javascripts/admin/addon/components/admin-report.gjs b/app/assets/javascripts/admin/addon/components/admin-report.gjs new file mode 100644 index 00000000000..1b61ede6be6 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-report.gjs @@ -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); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-report.hbs b/app/assets/javascripts/admin/addon/components/admin-report.hbs deleted file mode 100644 index 0e45208054a..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report.hbs +++ /dev/null @@ -1,209 +0,0 @@ -{{#unless this.isHidden}} - {{#if this.isEnabled}} - - {{#if this.showHeader}} -
- {{#if this.showTitle}} - {{#unless this.showNotFoundError}} - - {{/unless}} - {{/if}} - - {{#if this.shouldDisplayTrend}} -
- - {{#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}} - -
- {{/if}} -
- {{/if}} - -
-
- {{#if this.showError}} - {{#if this.showTimeoutError}} -
- {{d-icon "triangle-exclamation"}} - {{i18n "admin.dashboard.timeout_error"}} -
- {{/if}} - - {{#if this.showExceptionError}} -
- {{d-icon "triangle-exclamation"}} - {{i18n "admin.dashboard.exception_error"}} -
- {{/if}} - - {{#if this.showNotFoundError}} -
- {{d-icon "triangle-exclamation"}} - {{i18n "admin.dashboard.not_found_error"}} -
- {{/if}} - {{else}} - {{#if this.hasData}} - {{#if this.currentMode}} - {{component - this.modeComponent - model=this.model - options=this.options - }} - - {{#if this.model.relatedReport}} - - {{/if}} - {{/if}} - {{else}} - {{#if this.rateLimitationString}} -
- {{d-icon "temperature-three-quarters"}} - {{this.rateLimitationString}} -
- {{else}} -
- {{d-icon "chart-pie"}} - {{#if this.model.reportUrl}} - - - {{#if this.model.title}} - {{this.model.title}} - — - {{/if}} - {{i18n "admin.dashboard.reports.no_data"}} - - - {{else}} - {{i18n "admin.dashboard.reports.no_data"}} - {{/if}} -
- {{/if}} - {{/if}} - {{/if}} -
- - {{#if this.showFilteringUI}} -
- {{#if this.showModes}} -
- {{#each this.displayedModes as |displayedMode|}} - - {{/each}} -
- {{/if}} - - {{#if this.isChartMode}} - {{#if this.model.average}} - - {{i18n "admin.dashboard.reports.average_chart_label"}} - - {{/if}} -
- {{#each this.chartGroupings as |chartGrouping|}} - - {{/each}} -
- {{/if}} - - {{#if this.showDatesOptions}} -
- - {{i18n "admin.dashboard.reports.dates"}} - - -
- -
-
- {{/if}} - - {{#each this.model.available_filters as |filter|}} -
- - {{i18n - (concat - "admin.dashboard.reports.filters." filter.id ".label" - ) - }} - - -
- {{component - (concat "report-filters/" filter.type) - model=this.model - filter=filter - applyFilter=this.applyFilter - }} -
-
- {{/each}} - -
-
- -
-
- - {{#if this.showRefresh}} -
-
- -
-
- {{/if}} -
- {{/if}} -
-
- {{else}} -
- {{html-safe this.disabledLabel}} -
- {{/if}} -{{/unless}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-report.js b/app/assets/javascripts/admin/addon/components/admin-report.js deleted file mode 100644 index e9012f3fd94..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-report.js +++ /dev/null @@ -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); - } -} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-dashboard-general.js b/app/assets/javascripts/admin/addon/controllers/admin-dashboard-general.js index b46b9991045..3877d929f40 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-dashboard-general.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-dashboard-general.js @@ -2,6 +2,7 @@ import { inject as controller } from "@ember/controller"; import { computed } from "@ember/object"; import { service } from "@ember/service"; import { setting } from "discourse/lib/computed"; +import { REPORT_MODES } from "discourse/lib/constants"; import discourseComputed from "discourse/lib/decorators"; import getURL from "discourse/lib/get-url"; import { makeArray } from "discourse/lib/helpers"; @@ -30,6 +31,10 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo @staticReport("users_by_trust_level") usersByTrustLevelReport; @staticReport("storage_report") storageReport; + get reportModes() { + return REPORT_MODES; + } + @discourseComputed("siteSettings.dashboard_general_tab_activity_metrics") activityMetrics(metrics) { return (metrics || "").split("|").filter(Boolean); @@ -53,7 +58,7 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo @computed("hiddenReports") get isSearchReportsVisible() { 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", "daily_engaged_users", "new_contributors", - ].some((x) => !this.hiddenReports.includes(x)); + ].some((report) => !this.hiddenReports.includes(report)); } @discourseComputed @@ -76,11 +81,6 @@ export default class AdminDashboardGeneralController extends AdminDashboardTabCo return moment().locale("en").utc().endOf("day"); } - @computed("startDate", "endDate") - get filters() { - return { startDate: this.startDate, endDate: this.endDate }; - } - @discourseComputed activityMetricsFilters() { const lastMonth = moment() diff --git a/app/assets/javascripts/admin/addon/controllers/admin-dashboard-tab.js b/app/assets/javascripts/admin/addon/controllers/admin-dashboard-tab.js index b5cc96a4a43..dd1ccf4d342 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-dashboard-tab.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-dashboard-tab.js @@ -1,23 +1,25 @@ +import { tracked } from "@glimmer/tracking"; import Controller from "@ember/controller"; -import { action, computed } from "@ember/object"; +import { action } from "@ember/object"; import { service } from "@ember/service"; +import { TrackedObject } from "@ember-compat/tracked-built-ins"; import CustomDateRangeModal from "../components/modal/custom-date-range"; export default class AdminDashboardTabController extends Controller { @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"]; period = "monthly"; - endDate = moment().locale("en").utc().endOf("day"); - _startDate; - - @computed("_startDate", "period") - get startDate() { - if (this._startDate) { - return this._startDate; - } - + calculateStartDate() { const fullDay = moment().locale("en").utc().endOf("day"); switch (this.period) { @@ -35,13 +37,19 @@ export default class AdminDashboardTabController extends Controller { } @action - setCustomDateRange(_startDate, endDate) { - this.setProperties({ _startDate, endDate }); + setCustomDateRange(startDate, endDate) { + this.startDate = startDate; + this.endDate = endDate; + this.filters.startDate = this.startDate; + this.filters.endDate = this.endDate; } @action 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 diff --git a/app/assets/javascripts/admin/addon/templates/dashboard_general.hbs b/app/assets/javascripts/admin/addon/templates/dashboard_general.hbs index c132400371d..d6b2fdbc779 100644 --- a/app/assets/javascripts/admin/addon/templates/dashboard_general.hbs +++ b/app/assets/javascripts/admin/addon/templates/dashboard_general.hbs @@ -17,6 +17,7 @@ @startDate={{this.startDate}} @endDate={{this.endDate}} @setCustomDateRange={{this.setCustomDateRange}} + @onDateChange={{this.onDateChange}} /> @@ -25,13 +26,13 @@ {{#if this.siteSettings.use_legacy_pageviews}} {{else}} @@ -40,42 +41,42 @@ @@ -120,7 +121,7 @@ {{/each}} @@ -133,12 +134,12 @@
@@ -146,7 +147,7 @@
diff --git a/app/assets/javascripts/admin/addon/templates/reports-show.hbs b/app/assets/javascripts/admin/addon/templates/reports-show.hbs index c7ac2ba9255..4ac65d8b479 100644 --- a/app/assets/javascripts/admin/addon/templates/reports-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/reports-show.hbs @@ -6,6 +6,7 @@ @filters={{this.model}} @reportOptions={{this.reportOptions}} @showFilteringUI={{true}} + @showDescriptionInTooltip={{false}} @onRefresh={{route-action "onParamsChange"}} />
diff --git a/app/assets/javascripts/discourse/app/lib/admin-report-additional-modes.js b/app/assets/javascripts/discourse/app/lib/admin-report-additional-modes.js new file mode 100644 index 00000000000..950285f1773 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/admin-report-additional-modes.js @@ -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); +} diff --git a/app/assets/javascripts/discourse/app/lib/constants.js b/app/assets/javascripts/discourse/app/lib/constants.js index e08fcd52947..0aff2a48b0c 100644 --- a/app/assets/javascripts/discourse/app/lib/constants.js +++ b/app/assets/javascripts/discourse/app/lib/constants.js @@ -106,3 +106,14 @@ export const USER_FIELD_FLAGS = [ "show_on_user_card", "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", +}; diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs index faa2f236c77..1fee4b3ff48 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs @@ -3,7 +3,7 @@ // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // 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 { h } from "virtual-dom"; @@ -61,6 +61,7 @@ import { registerAdminPluginConfigNav, } from "discourse/lib/admin-plugin-config-nav"; import { registerPluginHeaderActionComponent } from "discourse/lib/admin-plugin-header-actions"; +import { registerReportModeComponent } from "discourse/lib/admin-report-additional-modes"; import classPrepend, { withPrependsRolledBack, } from "discourse/lib/class-prepend"; @@ -3375,6 +3376,19 @@ class PluginApi { 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) { // insert here the code to handle widget deprecations, e.g. for the header widgets we used: // if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) { diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 4bedff6e52a..7a01a298e2c 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -36,6 +36,7 @@ import { resetUsernameDecorators } from "discourse/helpers/decorate-username-sel import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete"; import { resetAdminPluginConfigNav } from "discourse/lib/admin-plugin-config-nav"; 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 { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options"; import deprecated from "discourse/lib/deprecated"; @@ -201,6 +202,7 @@ export function testCleanup(container, app) { User.resetCurrent(); resetMobile(); + resetAdditionalReportModes(); resetExtraClasses(); clearOutletCache(); clearHTMLCache(); diff --git a/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js b/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js index ce0b783861c..6f1d35664ee 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js @@ -8,7 +8,9 @@ module("Integration | Component | admin-report", function (hooks) { setupRenderingTest(hooks); test("default", async function (assert) { - await render(hbs``); + await render( + hbs`` + ); assert.dom(".admin-report.signups").exists(); assert.dom(".admin-report-table").exists("defaults to table mode"); diff --git a/app/assets/stylesheets/common/admin/admin_report.scss b/app/assets/stylesheets/common/admin/admin_report.scss index c7e61b59e9b..9c331a7688e 100644 --- a/app/assets/stylesheets/common/admin/admin_report.scss +++ b/app/assets/stylesheets/common/admin/admin_report.scss @@ -80,7 +80,6 @@ .body { display: flex; flex-direction: row; - margin-top: var(--space-3); } .main { diff --git a/app/assets/stylesheets/common/admin/dashboard.scss b/app/assets/stylesheets/common/admin/dashboard.scss index c48f4e56ad4..859c9d227a6 100644 --- a/app/assets/stylesheets/common/admin/dashboard.scss +++ b/app/assets/stylesheets/common/admin/dashboard.scss @@ -137,6 +137,19 @@ 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 { display: grid; grid-template-columns: repeat(12, 1fr); @@ -153,7 +166,7 @@ .header { display: grid; - grid-template-areas: "title trend" "description description"; + grid-template-areas: "title tooltip trend" "description description description"; grid-template-columns: auto 1fr; .d-page-subheader { diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index fb28fabf239..06ef5fd1024 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -227,7 +227,7 @@ module Jobs end end - if report.modes == [:stacked_chart] + if report.modes == [Report::MODES[:stacked_chart]] header = [:x] data = {} diff --git a/app/models/concerns/reports/consolidated_api_requests.rb b/app/models/concerns/reports/consolidated_api_requests.rb index e50d240c56c..b7e094a7bea 100644 --- a/app/models/concerns/reports/consolidated_api_requests.rb +++ b/app/models/concerns/reports/consolidated_api_requests.rb @@ -7,7 +7,7 @@ module Reports::ConsolidatedApiRequests def report_consolidated_api_requests(report) filters = %w[api user_api] - report.modes = [:stacked_chart] + report.modes = [Report::MODES[:stacked_chart]] requests = filters.map do |filter| diff --git a/app/models/concerns/reports/consolidated_page_views.rb b/app/models/concerns/reports/consolidated_page_views.rb index 4d48f18415b..1815ccd9b63 100644 --- a/app/models/concerns/reports/consolidated_page_views.rb +++ b/app/models/concerns/reports/consolidated_page_views.rb @@ -11,7 +11,7 @@ module Reports::ConsolidatedPageViews def report_consolidated_page_views(report) filters = %w[page_view_logged_in page_view_anon page_view_crawler] - report.modes = [:stacked_chart] + report.modes = [Report::MODES[:stacked_chart]] requests = filters.map do |filter| diff --git a/app/models/concerns/reports/flags_status.rb b/app/models/concerns/reports/flags_status.rb index 572185ebc53..0219ba90598 100644 --- a/app/models/concerns/reports/flags_status.rb +++ b/app/models/concerns/reports/flags_status.rb @@ -5,7 +5,7 @@ module Reports::FlagsStatus class_methods do def report_flags_status(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/moderators_activity.rb b/app/models/concerns/reports/moderators_activity.rb index 6b733e93661..5ab3156a85b 100644 --- a/app/models/concerns/reports/moderators_activity.rb +++ b/app/models/concerns/reports/moderators_activity.rb @@ -47,7 +47,7 @@ module Reports::ModeratorsActivity }, ] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.data = [] query = <<~SQL diff --git a/app/models/concerns/reports/post_edits.rb b/app/models/concerns/reports/post_edits.rb index af97ddc19a8..c096e1ffa95 100644 --- a/app/models/concerns/reports/post_edits.rb +++ b/app/models/concerns/reports/post_edits.rb @@ -8,7 +8,7 @@ module Reports::PostEdits category_id, include_subcategories = report.add_category_filter editor_username = report.filters["editor"] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/posts.rb b/app/models/concerns/reports/posts.rb index b2ac5199099..338d962c29c 100644 --- a/app/models/concerns/reports/posts.rb +++ b/app/models/concerns/reports/posts.rb @@ -5,8 +5,6 @@ module Reports::Posts class_methods do def report_posts(report) - report.modes = %i[table chart] - category_id, include_subcategories = report.add_category_filter basic_report_about report, diff --git a/app/models/concerns/reports/site_traffic.rb b/app/models/concerns/reports/site_traffic.rb index b77c977f0a0..b3b3c658d9c 100644 --- a/app/models/concerns/reports/site_traffic.rb +++ b/app/models/concerns/reports/site_traffic.rb @@ -5,7 +5,7 @@ module Reports::SiteTraffic class_methods do def report_site_traffic(report) - report.modes = [:stacked_chart] + report.modes = [Report::MODES[:stacked_chart]] first_browser_pageview_date = DB.query_single( diff --git a/app/models/concerns/reports/staff_logins.rb b/app/models/concerns/reports/staff_logins.rb index d65c2e2df05..bba58aeee24 100644 --- a/app/models/concerns/reports/staff_logins.rb +++ b/app/models/concerns/reports/staff_logins.rb @@ -5,7 +5,7 @@ module Reports::StaffLogins class_methods do def report_staff_logins(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.data = [] diff --git a/app/models/concerns/reports/suspicious_logins.rb b/app/models/concerns/reports/suspicious_logins.rb index cd07d427d11..64d7ac65361 100644 --- a/app/models/concerns/reports/suspicious_logins.rb +++ b/app/models/concerns/reports/suspicious_logins.rb @@ -5,7 +5,7 @@ module Reports::SuspiciousLogins class_methods do def report_suspicious_logins(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/top_ignored_users.rb b/app/models/concerns/reports/top_ignored_users.rb index 3403de992e7..ebb572d9fdf 100644 --- a/app/models/concerns/reports/top_ignored_users.rb +++ b/app/models/concerns/reports/top_ignored_users.rb @@ -5,7 +5,7 @@ module Reports::TopIgnoredUsers class_methods do def report_top_ignored_users(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/top_referred_topics.rb b/app/models/concerns/reports/top_referred_topics.rb index c8cacb79297..c57480dc698 100644 --- a/app/models/concerns/reports/top_referred_topics.rb +++ b/app/models/concerns/reports/top_referred_topics.rb @@ -7,7 +7,7 @@ module Reports::TopReferredTopics def report_top_referred_topics(report) category_id, include_subcategories = report.add_category_filter - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/top_referrers.rb b/app/models/concerns/reports/top_referrers.rb index 3454f17632b..e3f3eecc8af 100644 --- a/app/models/concerns/reports/top_referrers.rb +++ b/app/models/concerns/reports/top_referrers.rb @@ -5,7 +5,7 @@ module Reports::TopReferrers class_methods do def report_top_referrers(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { diff --git a/app/models/concerns/reports/top_traffic_sources.rb b/app/models/concerns/reports/top_traffic_sources.rb index a1b313f01f9..d3068c13bff 100644 --- a/app/models/concerns/reports/top_traffic_sources.rb +++ b/app/models/concerns/reports/top_traffic_sources.rb @@ -7,7 +7,7 @@ module Reports::TopTrafficSources def report_top_traffic_sources(report) category_id, include_subcategories = report.add_category_filter - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.labels = [ { property: :domain, title: I18n.t("reports.top_traffic_sources.labels.domain") }, diff --git a/app/models/concerns/reports/top_uploads.rb b/app/models/concerns/reports/top_uploads.rb index b9caaa63fab..1aff15b4873 100644 --- a/app/models/concerns/reports/top_uploads.rb +++ b/app/models/concerns/reports/top_uploads.rb @@ -5,7 +5,7 @@ module Reports::TopUploads class_methods do def report_top_uploads(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] extension_filter = report.filters.dig(:file_extension) report.add_filter( diff --git a/app/models/concerns/reports/top_users_by_likes_received.rb b/app/models/concerns/reports/top_users_by_likes_received.rb index 2861cbefc51..e0a78a7aefb 100644 --- a/app/models/concerns/reports/top_users_by_likes_received.rb +++ b/app/models/concerns/reports/top_users_by_likes_received.rb @@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceived report.icon = "heart" report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = true diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb index a6a42c72181..2445b244015 100644 --- a/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb +++ b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb @@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople report.icon = "heart" report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = true diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb index 0daad06783f..927a468bf15 100644 --- a/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb +++ b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb @@ -8,7 +8,7 @@ module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel report.icon = "heart" report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = true diff --git a/app/models/concerns/reports/topic_view_stats.rb b/app/models/concerns/reports/topic_view_stats.rb index ce33f30d3ab..d939c2715ab 100644 --- a/app/models/concerns/reports/topic_view_stats.rb +++ b/app/models/concerns/reports/topic_view_stats.rb @@ -5,7 +5,7 @@ module Reports::TopicViewStats class_methods do def report_topic_view_stats(report) - report.modes = [:table] + report.modes = [Report::MODES[:table]] category_id, include_subcategories = report.add_category_filter diff --git a/app/models/concerns/reports/trending_search.rb b/app/models/concerns/reports/trending_search.rb index 8d4f5ff38a3..9b447a30d44 100644 --- a/app/models/concerns/reports/trending_search.rb +++ b/app/models/concerns/reports/trending_search.rb @@ -21,7 +21,7 @@ module Reports::TrendingSearch report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] trends = SearchLog.trending_from(report.start_date, end_date: report.end_date, limit: report.limit) diff --git a/app/models/concerns/reports/trust_level_growth.rb b/app/models/concerns/reports/trust_level_growth.rb index 3b393426c08..0e0c45850ac 100644 --- a/app/models/concerns/reports/trust_level_growth.rb +++ b/app/models/concerns/reports/trust_level_growth.rb @@ -5,7 +5,7 @@ module Reports::TrustLevelGrowth class_methods do 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] diff --git a/app/models/concerns/reports/user_flagging_ratio.rb b/app/models/concerns/reports/user_flagging_ratio.rb index 0147792a8d8..c72474dd4d7 100644 --- a/app/models/concerns/reports/user_flagging_ratio.rb +++ b/app/models/concerns/reports/user_flagging_ratio.rb @@ -7,7 +7,7 @@ module Reports::UserFlaggingRatio def report_user_flagging_ratio(report) report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = true diff --git a/app/models/concerns/reports/users_by_trust_level.rb b/app/models/concerns/reports/users_by_trust_level.rb index f0226a90c8a..688574a50ce 100644 --- a/app/models/concerns/reports/users_by_trust_level.rb +++ b/app/models/concerns/reports/users_by_trust_level.rb @@ -7,7 +7,7 @@ module Reports::UsersByTrustLevel def report_users_by_trust_level(report) report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = false diff --git a/app/models/concerns/reports/users_by_type.rb b/app/models/concerns/reports/users_by_type.rb index a7c77cc7eb4..6dcf279b8cc 100644 --- a/app/models/concerns/reports/users_by_type.rb +++ b/app/models/concerns/reports/users_by_type.rb @@ -7,7 +7,7 @@ module Reports::UsersByType def report_users_by_type(report) report.data = [] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.dates_filtering = false diff --git a/app/models/concerns/reports/web_crawlers.rb b/app/models/concerns/reports/web_crawlers.rb index cdb731eeab5..298ecd1a179 100644 --- a/app/models/concerns/reports/web_crawlers.rb +++ b/app/models/concerns/reports/web_crawlers.rb @@ -18,7 +18,7 @@ module Reports::WebCrawlers }, ] - report.modes = [:table] + report.modes = [Report::MODES[:table]] report.data = WebCrawlerRequest diff --git a/app/models/report.rb b/app/models/report.rb index 032094cc9af..b52162f7cdc 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -16,6 +16,17 @@ class Report 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::ConsolidatedApiRequests include Reports::ConsolidatedPageViews @@ -106,7 +117,7 @@ class Report @average = false @percent = false @higher_is_better = true - @modes = %i[table chart] + @modes = [MODES[:chart], MODES[:table]] @prev_data = nil @dates_filtering = true @available_filters = {} @@ -374,7 +385,7 @@ class Report end 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) report.prev_data = prev_data.map { |k, v| { x: k, y: v } } end diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md index ce597732f99..b007e2fdbc4 100644 --- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -7,6 +7,10 @@ in this file. 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). +## [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 - Removed `decorateTopicTitle`. This has been deprecated for more than a year, and we are not aware of any remaining uses in the ecosystem. diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index 08ae71850b4..180cea69d61 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -167,6 +167,8 @@ task "javascript:update_constants" => :environment do export const MAX_UNOPTIMIZED_CATEGORIES = #{CategoryList::MAX_UNOPTIMIZED_CATEGORIES}; export const USER_FIELD_FLAGS = #{UserField::FLAG_ATTRIBUTES}; + + export const REPORT_MODES = #{Report::MODES.to_json}; JS pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")