From 37252c1a5eb1316d8d3d28a9f4ece82d86d909c6 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 31 Jul 2018 17:35:13 -0400 Subject: [PATCH] UI: improves dashboard table reports - support for avatars - support for topic/post/user type in reports - improved totals row UI - minor css tweaks --- .../admin-report-table-header.js.es6 | 4 +- .../components/admin-report-table.js.es6 | 4 +- .../admin/components/admin-report.js.es6 | 34 +- .../javascripts/admin/models/report.js.es6 | 256 ++++++++++---- .../components/admin-report-table.hbs | 44 +-- .../common/admin/admin_report.scss | 4 +- .../common/admin/admin_report_table.scss | 20 +- app/models/admin_dashboard_next_data.rb | 2 +- .../admin_dashboard_next_general_data.rb | 2 +- app/models/admin_dashboard_next_index_data.rb | 2 +- app/models/report.rb | 317 +++++++++++++----- .../components/admin-report-test.js.es6 | 2 +- test/javascripts/models/report-test.js.es6 | 88 +++-- 13 files changed, 555 insertions(+), 224 deletions(-) diff --git a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 index b7c569cc25f..1f614db4a81 100644 --- a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 @@ -3,10 +3,10 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ tagName: "th", classNames: ["admin-report-table-header"], - classNameBindings: ["label.property", "isCurrentSort"], + classNameBindings: ["label.mainProperty", "isCurrentSort"], attributeBindings: ["label.title:title"], - @computed("currentSortLabel.sort_property", "label.sort_property") + @computed("currentSortLabel.sortProperty", "label.sortProperty") isCurrentSort(currentSortField, labelSortField) { return currentSortField === labelSortField; }, diff --git a/app/assets/javascripts/admin/components/admin-report-table.js.es6 b/app/assets/javascripts/admin/components/admin-report-table.js.es6 index b39afc8a898..6d90eb73f89 100644 --- a/app/assets/javascripts/admin/components/admin-report-table.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table.js.es6 @@ -67,14 +67,14 @@ export default Ember.Component.extend({ const computedLabel = label.compute(row); const value = computedLabel.value; - if (computedLabel.type === "link" || (value && !isNumeric(value))) { + if (!computedLabel.countable || !value || !isNumeric(value)) { return undefined; } else { return sum + value; } }; - totalsRow[label.property] = rows.reduce(reducer, 0); + totalsRow[label.mainProperty] = rows.reduce(reducer, 0); }); return totalsRow; diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 74c96f814a0..95d831af821 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -2,7 +2,7 @@ import Category from "discourse/models/category"; import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import { ajax } from "discourse/lib/ajax"; -import Report from "admin/models/report"; +import { SCHEMA_VERSION, default as Report } from "admin/models/report"; import computed from "ember-addons/ember-computed-decorators"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; @@ -189,24 +189,20 @@ export default Ember.Component.extend({ reportKey(dataSourceName, categoryId, groupId, startDate, endDate) { if (!dataSourceName || !startDate || !endDate) return null; - let reportKey = `reports:${dataSourceName}`; - - if (categoryId && categoryId !== "all") { - reportKey += `:${categoryId}`; - } else { - reportKey += `:`; - } - - reportKey += `:${startDate.replace(/-/g, "")}`; - reportKey += `:${endDate.replace(/-/g, "")}`; - - if (groupId && groupId !== "all") { - reportKey += `:${groupId}`; - } else { - reportKey += `:`; - } - - reportKey += `:`; + let reportKey = "reports:"; + reportKey += [ + dataSourceName, + categoryId, + startDate.replace(/-/g, ""), + endDate.replace(/-/g, ""), + groupId, + "[:prev_period]", + this.get("reportOptions.table.limit"), + SCHEMA_VERSION + ] + .filter(x => x) + .map(x => x.toString()) + .join(":"); return reportKey; }, diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 566aad01708..5f70b66e6d3 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,87 +1,24 @@ import { escapeExpression } from "discourse/lib/utilities"; import { ajax } from "discourse/lib/ajax"; import round from "discourse/lib/round"; -import { fillMissingDates, isNumeric } from "discourse/lib/utilities"; +import { + fillMissingDates, + isNumeric, + formatUsername +} from "discourse/lib/utilities"; import computed from "ember-addons/ember-computed-decorators"; import { number, durationTiny } from "discourse/lib/formatter"; +import { renderAvatar } from "discourse/helpers/user-avatar"; + +// Change this line each time report format change +// and you want to ensure cache is reset +export const SCHEMA_VERSION = 1; const Report = Discourse.Model.extend({ average: false, percent: false, higher_is_better: true, - @computed("labels") - computedLabels(labels) { - return labels.map(label => { - const type = label.type; - const properties = label.properties; - const property = properties[0]; - - return { - title: label.title, - sort_property: label.sort_property || property, - property, - compute: row => { - let value = row[property]; - let escapedValue = escapeExpression(value); - let tooltip; - let base = { property, value, type }; - - if (value === null || typeof value === "undefined") { - return _.assign(base, { - value: null, - formatedValue: "-", - type: "undefined" - }); - } - - if (type === "seconds") { - return _.assign(base, { - formatedValue: escapeExpression(durationTiny(value)) - }); - } - - if (type === "link") { - return _.assign(base, { - formatedValue: `${escapedValue}` - }); - } - - if (type === "percent") { - return _.assign(base, { - formatedValue: `${escapedValue}%` - }); - } - - if (type === "number" || isNumeric(value)) - return _.assign(base, { - type: "number", - formatedValue: number(value) - }); - - if (type === "date") { - const date = moment(value, "YYYY-MM-DD"); - if (date.isValid()) { - return _.assign(base, { - formatedValue: date.format("LL") - }); - } - } - - if (type === "text") tooltip = escapedValue; - - return _.assign(base, { - tooltip, - type: type || "string", - formatedValue: escapedValue - }); - } - }; - }); - }, - @computed("modes") onlyTable(modes) { return modes.length === 1 && modes[0] === "table"; @@ -312,6 +249,179 @@ const Report = Discourse.Model.extend({ return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); }, + @computed("labels") + computedLabels(labels) { + return labels.map(label => { + const type = label.type; + + let mainProperty; + if (label.property) mainProperty = label.property; + else if (type === "user") mainProperty = label.properties["username"]; + else if (type === "topic") mainProperty = label.properties["title"]; + else if (type === "post") + mainProperty = label.properties["truncated_raw"]; + else mainProperty = label.properties[0]; + + return { + title: label.title, + sortProperty: label.sort_property || mainProperty, + mainProperty, + compute: row => { + const value = row[mainProperty]; + + if (type === "user") return this._userLabel(label.properties, row); + if (type === "post") return this._postLabel(label.properties, row); + if (type === "topic") return this._topicLabel(label.properties, row); + if (type === "seconds") + return this._secondsLabel(mainProperty, value); + if (type === "link") return this._linkLabel(label.properties, row); + if (type === "percent") + return this._percentLabel(mainProperty, value); + if (type === "number" || isNumeric(value)) { + return this._numberLabel(mainProperty, value); + } + if (type === "date") { + const date = moment(value, "YYYY-MM-DD"); + if (date.isValid()) + return this._dateLabel(mainProperty, value, date); + } + if (type === "text") return this._textLabel(mainProperty, value); + if (!value) return this._undefinedLabel(); + + return { + property: mainProperty, + value, + type: type || "string", + formatedValue: escapeExpression(value) + }; + } + }; + }); + }, + + _undefinedLabel() { + return { + value: null, + formatedValue: "-", + type: "undefined" + }; + }, + + _userLabel(properties, row) { + const username = row[properties.username]; + + if (!username) return this._undefinedLabel(); + + const user = Ember.Object.create({ + username, + name: formatUsername(username), + avatar_template: row[properties.avatar] + }); + + const avatarImg = renderAvatar(user, { + imageSize: "small", + ignoreTitle: true + }); + + const href = `/admin/users/${row[properties.id]}/${username}`; + + return { + type: "user", + property: properties.username, + value: username, + formatedValue: `${avatarImg}${username}` + }; + }, + + _topicLabel(properties, row) { + const topicTitle = row[properties.title]; + const topicId = row[properties.id]; + const href = `/t/-/${topicId}`; + + return { + type: "topic", + property: properties.title, + value: topicTitle, + formatedValue: `${topicTitle}` + }; + }, + + _postLabel(properties, row) { + const postTitle = row[properties.truncated_raw]; + const postNumber = row[properties.number]; + const topicId = row[properties.topic_id]; + const href = `/t/-/${topicId}/${postNumber}`; + + return { + type: "post", + property: properties.title, + value: postTitle, + formatedValue: `${postTitle}` + }; + }, + + _secondsLabel(property, value) { + return { + value, + property, + countable: true, + type: "seconds", + formatedValue: durationTiny(value) + }; + }, + + _percentLabel(property, value) { + return { + type: "percent", + property, + value, + formatedValue: `${value}%` + }; + }, + + _numberLabel(property, value) { + return { + type: "number", + countable: true, + property, + value, + formatedValue: number(value) + }; + }, + + _dateLabel(property, value, date) { + return { + type: "date", + property, + value, + formatedValue: date.format("LL") + }; + }, + + _textLabel(property, value) { + const escaped = escapeExpression(value); + + return { + type: "text", + property, + value, + formatedValue: escaped + }; + }, + + _linkLabel(properties, row) { + const property = properties[0]; + const value = row[property]; + return { + type: "link", + property, + value, + formatedValue: `${escapeExpression(value)}` + }; + }, + _computeChange(valAtT1, valAtT2) { return ((valAtT2 - valAtT1) / valAtT1) * 100; }, diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs index 330dc6f8679..9a1af7d0ace 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs @@ -21,33 +21,35 @@ {{#each paginatedData as |data|}} {{admin-report-table-row data=data labels=model.computedLabels}} {{/each}} - - -{{#if showTotalForSample}} - {{i18n 'admin.dashboard.reports.totals_for_sample'}} - - - + {{#if showTotalForSample}} + + + + {{#each totalsForSample as |total|}} - + {{/each}} - -
+ {{i18n 'admin.dashboard.reports.totals_for_sample'}} +
{{total.formatedValue}} + {{total.formatedValue}} +
-{{/if}} + {{/if}} -{{#if showTotal}} - {{i18n 'admin.dashboard.reports.total'}} - - - - - + {{#if showTotal}} + + - -
-{{number model.total}}
+ {{i18n 'admin.dashboard.reports.total'}} +
-{{/if}} + + - + {{number model.total}} + + {{/if}} + +