diff --git a/.eslintrc b/.eslintrc index 4d652d710ac2e..844933e68b1f7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ "selectKitSelectRowByValue":true, "selectKitSelectRowByName":true, "selectKitSelectRowByIndex":true, + "keyboardHelper":true, "selectKitSelectNoneRow":true, "selectKitFillInFilter":true, "asyncTestDiscourse":true, diff --git a/.travis.yml b/.travis.yml index d905c1e37a872..4a9baa55004d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,9 @@ addons: matrix: fast_finish: true + exclude: + - rvm: 2.4.4 + env: "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1" rvm: - 2.5.1 diff --git a/Dangerfile b/Dangerfile index f418ed5b2cb0f..3941cffbed3fa 100644 --- a/Dangerfile +++ b/Dangerfile @@ -5,7 +5,7 @@ end prettier_offenses = `prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split('\n') if !prettier_offenses.empty? fail(%{ -This PR has multiple prettier offenses. Using prettier\n +This PR doesn't match our required code formatting standards, as enforced by prettier.io. Here's how to set up prettier in your code editor.\n #{prettier_offenses.map { |o| github.html_link(o) }.join("\n")} }) end diff --git a/Gemfile b/Gemfile index 3ec355c0a8ff1..b71cbfab200b5 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.55' +gem 'onebox', '1.8.57' gem 'http_accept_language', '~>2.0.5', require: false @@ -88,6 +88,7 @@ gem 'thor', require: false gem 'rinku' gem 'sanitize' gem 'sidekiq' +gem 'mini_scheduler' # for sidekiq web gem 'tilt', require: false @@ -180,6 +181,8 @@ gem 'rqrcode' gem 'sshkey', require: false +gem 'rchardet', require: false + if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index 2118599739de4..521090e7948b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,6 +200,7 @@ GEM mini_portile2 (2.3.0) mini_racer (0.2.0) libv8 (>= 6.3) + mini_scheduler (0.8.1) mini_sql (0.1.10) mini_suffix (0.3.0) ffi (~> 1.9) @@ -256,7 +257,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.55) + onebox (1.8.57) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -320,6 +321,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) + rchardet (1.8.0) redis (4.0.1) redis-namespace (1.6.0) redis (>= 3.0.4) @@ -489,6 +491,7 @@ DEPENDENCIES message_bus mini_mime mini_racer + mini_scheduler mini_sql mini_suffix minitest @@ -506,7 +509,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.55) + onebox (= 1.8.57) openid-redis-store pg pry-nav @@ -521,6 +524,7 @@ DEPENDENCIES rb-fsevent rb-inotify (~> 0.9) rbtrace + rchardet redis redis-namespace rinku @@ -550,4 +554,4 @@ DEPENDENCIES webpush BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/app/assets/javascripts/admin/components/admin-report-counters.js.es6 b/app/assets/javascripts/admin/components/admin-report-counters.js.es6 new file mode 100644 index 0000000000000..3806e29ecbf5d --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-counters.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ["admin-report-counters"] +}); diff --git a/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 new file mode 100644 index 0000000000000..7140b69668edf --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 @@ -0,0 +1,18 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + tagName: "td", + classNames: ["admin-report-table-cell"], + classNameBindings: ["type", "property"], + options: null, + + @computed("label", "data", "options") + computedLabel(label, data, options) { + return label.compute(data, options || {}); + }, + + type: Ember.computed.alias("label.type"), + property: Ember.computed.alias("label.mainProperty"), + formatedValue: Ember.computed.alias("computedLabel.formatedValue"), + value: Ember.computed.alias("computedLabel.value") +}); 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 b7c569cc25f87..ab986f29460be 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", "label.type", "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-row.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 index 3c4de4970e399..3be140c308e19 100644 --- a/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 @@ -1,13 +1,5 @@ -import computed from "ember-addons/ember-computed-decorators"; - export default Ember.Component.extend({ tagName: "tr", classNames: ["admin-report-table-row"], - - @computed("data", "labels") - cells(row, labels) { - return labels.map(label => { - return label.compute(row); - }); - } + options: null }); 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 b39afc8a8980d..dc007a79cc494 100644 --- a/app/assets/javascripts/admin/components/admin-report-table.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table.js.es6 @@ -1,6 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; -import { isNumeric } from "discourse/lib/utilities"; const PAGES_LIMIT = 8; @@ -67,14 +66,16 @@ export default Ember.Component.extend({ const computedLabel = label.compute(row); const value = computedLabel.value; - if (computedLabel.type === "link" || (value && !isNumeric(value))) { - return undefined; + if (!["seconds", "number", "percent"].includes(label.type)) { + return; } else { - return sum + value; + return sum + Math.round(value || 0); } }; - totalsRow[label.property] = rows.reduce(reducer, 0); + const total = rows.reduce(reducer, 0); + totalsRow[label.mainProperty] = + label.type === "percent" ? Math.round(total / rows.length) : total; }); 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 67d5b5737827f..fa57c3d8213e8 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -2,14 +2,15 @@ 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"; const TABLE_OPTIONS = { perPage: 8, total: true, - limit: 20 + limit: 20, + formatNumbers: true }; const CHART_OPTIONS = {}; @@ -50,9 +51,10 @@ export default Ember.Component.extend({ reportOptions: null, forcedModes: null, showAllReportsLink: false, + filters: null, startDate: null, endDate: null, - categoryId: null, + category: null, groupId: null, showTrend: false, showHeader: true, @@ -77,7 +79,7 @@ export default Ember.Component.extend({ didReceiveAttrs() { this._super(...arguments); - const state = this.get("filteringState") || {}; + const state = this.get("filters") || {}; this.setProperties({ category: Category.findById(state.categoryId), groupId: state.groupId, @@ -109,7 +111,9 @@ export default Ember.Component.extend({ unregisterTooltip($(".info[data-tooltip]")); }, - showTimeoutError: Ember.computed.alias("model.timeout"), + showError: Ember.computed.or("showTimeoutError", "showExceptionError"), + showTimeoutError: Ember.computed.equal("model.error", "timeout"), + showExceptionError: Ember.computed.equal("model.error", "exception"), hasData: Ember.computed.notEmpty("model.data"), @@ -129,6 +133,8 @@ export default Ember.Component.extend({ return displayedModesLength > 1; }, + categoryId: Ember.computed.alias("category.id"), + @computed("currentMode", "model.modes", "forcedModes") displayedModes(currentMode, reportModes, forcedModes) { const modes = forcedModes ? forcedModes.split(",") : reportModes; @@ -186,24 +192,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; }, @@ -211,7 +213,7 @@ export default Ember.Component.extend({ actions: { refreshReport() { this.attrs.onRefresh({ - categoryId: this.get("category.id"), + categoryId: this.get("categoryId"), groupId: this.get("groupId"), startDate: this.get("startDate"), endDate: this.get("endDate") @@ -346,12 +348,12 @@ export default Ember.Component.extend({ if (mode === "table") { const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS)); return Ember.Object.create( - _.assign(tableOptions, this.get("reportOptions.table") || {}) + Object.assign(tableOptions, this.get("reportOptions.table") || {}) ); } else { const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS)); return Ember.Object.create( - _.assign(chartOptions, this.get("reportOptions.chart") || {}) + Object.assign(chartOptions, this.get("reportOptions.chart") || {}) ); } }, diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js.es6 index 60e4a3cda7379..9e656bed70722 100644 --- a/app/assets/javascripts/admin/components/value-list.js.es6 +++ b/app/assets/javascripts/admin/components/value-list.js.es6 @@ -1,113 +1,95 @@ +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ classNameBindings: [":value-list"], - _enableSorting: function() { - const self = this; - const placeholder = document.createElement("div"); - placeholder.className = "placeholder"; - - let dragging = null; - let over = null; - let nodePlacement; - - this.$().on("dragstart.discourse", ".values .value", function(e) { - dragging = e.currentTarget; - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/html", e.currentTarget); - }); - - this.$().on("dragend.discourse", ".values .value", function() { - Ember.run(function() { - dragging.parentNode.removeChild(placeholder); - dragging.style.display = "block"; - - // Update data - const from = Number(dragging.dataset.index); - let to = Number(over.dataset.index); - if (from < to) to--; - if (nodePlacement === "after") to++; - - const collection = self.get("collection"); - const fromObj = collection.objectAt(from); - collection.replace(from, 1); - collection.replace(to, 0, [fromObj]); - self._saveValues(); - }); - return false; - }); - - this.$().on("dragover.discourse", ".values", function(e) { - e.preventDefault(); - dragging.style.display = "none"; - if (e.target.className === "placeholder") { - return; - } - over = e.target; - - const relY = e.originalEvent.clientY - over.offsetTop; - const height = over.offsetHeight / 2; - const parent = e.target.parentNode; - - if (relY > height) { - nodePlacement = "after"; - parent.insertBefore(placeholder, e.target.nextElementSibling); - } else if (relY < height) { - nodePlacement = "before"; - parent.insertBefore(placeholder, e.target); - } - }); - }.on("didInsertElement"), - - _removeSorting: function() { - this.$() - .off("dragover.discourse") - .off("dragend.discourse") - .off("dragstart.discourse"); - }.on("willDestroyElement"), - - _setupCollection: function() { + inputInvalid: Ember.computed.empty("newValue"), + + inputDelimiter: null, + inputType: null, + newValue: "", + collection: null, + values: null, + noneKey: Ember.computed.alias("addKey"), + + @on("didReceiveAttrs") + _setupCollection() { const values = this.get("values"); if (this.get("inputType") === "array") { this.set("collection", values || []); - } else { - this.set("collection", values && values.length ? values.split("\n") : []); + return; } - } - .on("init") - .observes("values"), - _saveValues: function() { - if (this.get("inputType") === "array") { - this.set("values", this.get("collection")); - } else { - this.set("values", this.get("collection").join("\n")); - } + this.set( + "collection", + this._splitValues(values, this.get("inputDelimiter") || "\n") + ); }, - inputInvalid: Ember.computed.empty("newValue"), + @computed("choices.[]", "collection.[]") + filteredChoices(choices, collection) { + return Ember.makeArray(choices).filter(i => collection.indexOf(i) < 0); + }, - keyDown(e) { - if (e.keyCode === 13) { - this.send("addValue"); - } + keyDown(event) { + if (event.keyCode === 13) this.send("addValue", this.get("newValue")); }, actions: { - addValue() { - if (this.get("inputInvalid")) { - return; - } + changeValue(index, newValue) { + this._replaceValue(index, newValue); + }, - this.get("collection").addObject(this.get("newValue")); - this.set("newValue", ""); + addValue(newValue) { + if (this.get("inputInvalid")) return; - this._saveValues(); + this.set("newValue", ""); + this._addValue(newValue); }, removeValue(value) { - const collection = this.get("collection"); - collection.removeObject(value); - this._saveValues(); + this._removeValue(value); + }, + + selectChoice(choice) { + this._addValue(choice); + } + }, + + _addValue(value) { + this.get("collection").addObject(value); + this._saveValues(); + }, + + _removeValue(value) { + const collection = this.get("collection"); + collection.removeObject(value); + this._saveValues(); + }, + + _replaceValue(index, newValue) { + this.get("collection").replace(index, 1, [newValue]); + this._saveValues(); + }, + + _saveValues() { + if (this.get("inputType") === "array") { + this.set("values", this.get("collection")); + return; + } + + this.set( + "values", + this.get("collection").join(this.get("inputDelimiter") || "\n") + ); + }, + + _splitValues(values, delimiter) { + if (values && values.length) { + return values.split(delimiter).filter(x => x); + } else { + return []; } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 42bdcc78241c9..7f81972a16bf5 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -4,18 +4,11 @@ import AdminDashboardNext from "admin/models/admin-dashboard-next"; import Report from "admin/models/report"; import PeriodComputationMixin from "admin/mixins/period-computation"; -const ACTIVITY_METRICS_REPORTS = [ - "page_view_total_reqs", - "visits", - "time_to_first_response", - "likes", - "flags", - "user_to_user_private_messages_with_replies" -]; - function staticReport(reportType) { return function() { - return this.get("reports").find(x => x.type === reportType); + return Ember.makeArray(this.get("reports")).find( + report => report.type === reportType + ); }.property("reports.[]"); } @@ -28,13 +21,25 @@ export default Ember.Controller.extend(PeriodComputationMixin, { lastBackupTakenAt: Ember.computed.alias( "model.attributes.last_backup_taken_at" ), - shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"), + shouldDisplayDurability: Ember.computed.and("diskSpace"), @computed topReferredTopicsTopions() { return { table: { total: false, limit: 8 } }; }, + @computed + activityMetrics() { + return [ + "page_view_total_reqs", + "visits", + "time_to_first_response", + "likes", + "flags", + "user_to_user_private_messages_with_replies" + ]; + }, + @computed trendingSearchOptions() { return { table: { total: false, limit: 8 } }; @@ -43,13 +48,6 @@ export default Ember.Controller.extend(PeriodComputationMixin, { usersByTypeReport: staticReport("users_by_type"), usersByTrustLevelReport: staticReport("users_by_trust_level"), - @computed("reports.[]") - activityMetricsReports(reports) { - return reports.filter(report => - ACTIVITY_METRICS_REPORTS.includes(report.type) - ); - }, - fetchDashboard() { if (this.get("isLoading")) return; @@ -66,7 +64,9 @@ export default Ember.Controller.extend(PeriodComputationMixin, { this.setProperties({ dashboardFetchedAt: new Date(), model: adminDashboardNextModel, - reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + reports: Ember.makeArray(adminDashboardNextModel.reports).map(x => + Report.create(x) + ) }); }) .catch(e => { @@ -77,17 +77,26 @@ export default Ember.Controller.extend(PeriodComputationMixin, { } }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + @computed("model.attributes.updated_at") updatedTimestamp(updatedAt) { - return moment(updatedAt).format("LLL"); + return moment(updatedAt) + .tz(moment.tz.guess()) + .format("LLL"); }, @computed("lastBackupTakenAt") backupTimestamp(lastBackupTakenAt) { - return moment(lastBackupTakenAt).format("LLL"); + return moment(lastBackupTakenAt) + .tz(moment.tz.guess()) + .format("LLL"); }, _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin/dashboard/general?period=${period}`); + return Discourse.getURL(`/admin?period=${period}`); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 index 0958bc9d6225d..059bcd6176699 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 @@ -2,8 +2,6 @@ import computed from "ember-addons/ember-computed-decorators"; import PeriodComputationMixin from "admin/mixins/period-computation"; export default Ember.Controller.extend(PeriodComputationMixin, { - exceptionController: Ember.inject.controller("exception"), - @computed flagsStatusOptions() { return { @@ -14,6 +12,16 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }; }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + + @computed("lastWeek", "endDate") + lastWeekfilters(startDate, endDate) { + return { startDate, endDate }; + }, + _reportsForPeriodURL(period) { return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`); } diff --git a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 index fdabfd896e355..e04b16f6a7548 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 @@ -5,7 +5,7 @@ export default Ember.Controller.extend({ @computed("model.type") reportOptions(type) { - let options = { table: { perPage: 50, limit: 50 } }; + let options = { table: { perPage: 50, limit: 50, formatNumbers: false } }; if (type === "top_referred_topics") { options.table.limit = 10; @@ -15,7 +15,7 @@ export default Ember.Controller.extend({ }, @computed("category_id", "group_id", "start_date", "end_date") - filteringState(categoryId, groupId, startDate, endDate) { + filters(categoryId, groupId, startDate, endDate) { return { categoryId, groupId, diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index e00f14f4b3ac6..9636c47732d15 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -10,7 +10,8 @@ const CUSTOM_TYPES = [ "category_list", "value_list", "category", - "uploaded_image_list" + "uploaded_image_list", + "compact_list" ]; export default Ember.Mixin.create({ @@ -59,11 +60,20 @@ export default Ember.Mixin.create({ return setting.replace(/\_/g, " "); }, - @computed("setting.type") + @computed("type") componentType(type) { return CUSTOM_TYPES.indexOf(type) !== -1 ? type : "string"; }, + @computed("setting") + type(setting) { + if (setting.type === "list" && setting.list_type) { + return `${setting.list_type}_list`; + } + + return setting.type; + }, + @computed("typeClass") componentName(typeClass) { return "site-settings/" + typeClass; diff --git a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 index 5ad85c0399e42..6898f8191a088 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 @@ -6,7 +6,7 @@ const AdminDashboardNext = Discourse.Model.extend({}); AdminDashboardNext.reopenClass({ fetch() { - return ajax("/admin/dashboard-next.json").then(json => { + return ajax("/admin/dashboard.json").then(json => { const model = AdminDashboardNext.create(); model.set("version_check", json.version_check); return model; diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 index 75757656d6a6b..9ce5a79e151d9 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 @@ -11,7 +11,7 @@ AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ find: function() { - return ajax("/admin/dashboard.json").then(function(json) { + return ajax("/admin/dashboard-old.json").then(function(json) { var model = AdminDashboard.create(json); model.set("loaded", true); return model; diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 39a2b7ee1dd8d..ec230ffff482d 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -530,11 +530,14 @@ const AdminUser = Discourse.User.extend({ } }, - @computed("suspended_by") suspendedBy: wrapAdmin, + @computed("suspended_by") + suspendedBy: wrapAdmin, - @computed("silenced_by") silencedBy: wrapAdmin, + @computed("silenced_by") + silencedBy: wrapAdmin, - @computed("approved_by") approvedBy: wrapAdmin + @computed("approved_by") + approvedBy: wrapAdmin }); AdminUser.reopenClass({ diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 566aad0170865..fc4fb8e86afc9 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,87 +1,20 @@ 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, 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 = 2; 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 +245,169 @@ 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 || "string"; + + 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, + type, + compute: (row, opts = {}) => { + 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(value); + if (type === "link") return this._linkLabel(label.properties, row); + if (type === "percent") return this._percentLabel(value); + if (type === "number") { + return this._numberLabel(value, opts); + } + if (type === "date") { + const date = moment(value, "YYYY-MM-DD"); + if (date.isValid()) return this._dateLabel(value, date); + } + if (type === "text") return this._textLabel(value); + + return { + value, + type, + property: mainProperty, + formatedValue: value ? escapeExpression(value) : "-" + }; + } + }; + }); + }, + + _userLabel(properties, row) { + const username = row[properties.username]; + + const formatedValue = () => { + const userId = row[properties.id]; + + const user = Ember.Object.create({ + username, + name: formatUsername(username), + avatar_template: row[properties.avatar] + }); + + const href = `/admin/users/${userId}/${username}`; + + const avatarImg = renderAvatar(user, { + imageSize: "tiny", + ignoreTitle: true + }); + + return `${avatarImg}${ + user.name + }`; + }; + + return { + value: username, + formatedValue: username ? formatedValue(username) : "-" + }; + }, + + _topicLabel(properties, row) { + const topicTitle = row[properties.title]; + + const formatedValue = () => { + const topicId = row[properties.id]; + const href = `/t/-/${topicId}`; + return `${topicTitle}`; + }; + + return { + value: topicTitle, + formatedValue: topicTitle ? formatedValue() : "-" + }; + }, + + _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 { + property: properties.title, + value: postTitle, + formatedValue: `${postTitle}` + }; + }, + + _secondsLabel(value) { + return { + value, + formatedValue: durationTiny(value) + }; + }, + + _percentLabel(value) { + return { + value, + formatedValue: value ? `${value}%` : "-" + }; + }, + + _numberLabel(value, options = {}) { + const formatNumbers = Ember.isEmpty(options.formatNumbers) + ? true + : options.formatNumbers; + + const formatedValue = () => (formatNumbers ? number(value) : value); + + return { + value, + formatedValue: value ? formatedValue() : "-" + }; + }, + + _dateLabel(value, date) { + return { + value, + formatedValue: value ? date.format("LL") : "-" + }; + }, + + _textLabel(value) { + const escaped = escapeExpression(value); + + return { + value, + formatedValue: value ? escaped : "-" + }; + }, + + _linkLabel(properties, row) { + const property = properties[0]; + const value = row[property]; + const formatedValue = (href, anchor) => { + return `${escapeExpression( + anchor + )}`; + }; + + return { + value, + formatedValue: value ? formatedValue(value, row[properties[1]]) : "-" + }; + }, + _computeChange(valAtT1, valAtT2) { return ((valAtT2 - valAtT1) / valAtT1) * 100; }, diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 index 5aa907b55ce78..b2ace398fed44 100644 --- a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -5,19 +5,5 @@ export default Discourse.Route.extend({ this.controllerFor("admin-dashboard-next").fetchProblems(); this.controllerFor("admin-dashboard-next").fetchDashboard(); scrollTop(); - }, - - afterModel(model, transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - }, - - actions: { - willTransition(transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - } } }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index bdcdcd4a96f4a..110ec6231b2fe 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -3,8 +3,11 @@ export default function() { this.route("dashboard", { path: "/dashboard-old" }); this.route("dashboardNext", { path: "/" }, function() { - this.route("general", { path: "/dashboard/general" }); - this.route("moderation", { path: "/dashboard/moderation" }); + this.route("general", { path: "/" }); + this.route("admin.dashboardNextModeration", { + path: "/dashboard/moderation", + resetNamespace: true + }); }); this.route( diff --git a/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl b/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl deleted file mode 100644 index 23cc50b1f3522..0000000000000 Binary files a/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl and /dev/null differ diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs new file mode 100644 index 0000000000000..605119360c61c --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs @@ -0,0 +1,20 @@ +
+ {{#if model.icon}} + {{d-icon model.icon}} + {{/if}} + {{model.title}} +
+ +
{{number model.todayCount}}
+ +
+ {{number model.yesterdayCount}} {{d-icon model.yesterdayTrendIcon}} +
+ +
+ {{number model.lastSevenDaysCount}} {{d-icon model.sevenDaysTrendIcon}} +
+ +
+ {{number model.lastThirtyDaysCount}} {{d-icon model.thirtyDaysTrendIcon}} +
diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs new file mode 100644 index 0000000000000..973e8412be1c4 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs @@ -0,0 +1 @@ +{{{formatedValue}}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs index b9d0c950227c9..8feefaf11067c 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs @@ -1,5 +1,3 @@ -{{#each cells as |cell|}} - - {{{cell.formatedValue}}} - +{{#each labels as |label|}} + {{admin-report-table-cell label=label data=data options=options}} {{/each}} 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 330dc6f86799e..1eb6923b5118b 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs @@ -19,35 +19,37 @@ {{#each paginatedData as |data|}} - {{admin-report-table-row data=data labels=model.computedLabels}} + {{admin-report-table-row data=data labels=model.computedLabels options=options}} {{/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}} + +