From 980972182f188a92269617d212c5bbd9aa96f5b7 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 3 May 2018 15:41:41 +0200 Subject: [PATCH] dashboard next: caching, mobile support and new charts --- .../components/dashboard-inline-table.js.es6 | 60 ++------ .../components/dashboard-mini-chart.js.es6 | 137 +++++++----------- .../dashboard-table-trending-search.js.es6 | 15 +- .../admin/components/dashboard-table.js.es6 | 85 ++++------- .../controllers/admin-dashboard-next.js.es6 | 13 +- .../admin/mixins/async-report.js.es6 | 68 +++++++++ .../admin/models/admin-dashboard-next.js.es6 | 12 +- .../javascripts/admin/models/report.js.es6 | 90 +++++++----- .../admin/routes/admin-dashboard-next.js.es6 | 6 +- .../components/dashboard-inline-table.hbs | 10 +- .../components/dashboard-mini-chart.hbs | 31 ++-- .../templates/components/dashboard-table.hbs | 6 +- .../admin/templates/dashboard_next.hbs | 82 ++++++----- .../conditional-loading-section.js.es6 | 14 ++ .../conditional-loading-section.hbs | 6 + .../common/admin/dashboard_next.scss | 69 +++++++-- .../conditional-loading-section.scss | 17 +++ app/controllers/admin/reports_controller.rb | 11 +- app/jobs/regular/retrieve_report.rb | 24 +++ app/models/admin_dashboard_next_data.rb | 15 +- app/models/concerns/date_groupable.rb | 22 +-- app/models/report.rb | 97 +++++++++++-- app/models/user.rb | 57 +++++++- app/models/user_action.rb | 13 +- app/models/user_visit.rb | 25 ++++ config/locales/client.en.yml | 3 + config/locales/server.en.yml | 21 ++- lib/cache.rb | 6 +- spec/components/cache_spec.rb | 7 + spec/models/report_spec.rb | 70 +++++++++ .../acceptance/dashboard-next-test.js.es6 | 2 +- .../fixtures/daily-engaged-users.js.es6 | 19 +++ .../fixtures/dashboard-next.js.es6 | 135 +---------------- test/javascripts/fixtures/dau-by-mau.js.es6 | 19 +++ .../fixtures/inactive-users.js.es6 | 19 +++ test/javascripts/fixtures/topics.js.es6 | 34 +---- .../fixtures/trending-search.js.es6 | 6 +- .../fixtures/users-by-trust-level.js.es6 | 19 +++ .../javascripts/fixtures/users-by-type.js.es6 | 19 +++ 39 files changed, 819 insertions(+), 545 deletions(-) create mode 100644 app/assets/javascripts/admin/mixins/async-report.js.es6 create mode 100644 app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs create mode 100644 app/assets/stylesheets/common/components/conditional-loading-section.scss create mode 100644 app/jobs/regular/retrieve_report.rb create mode 100644 test/javascripts/fixtures/daily-engaged-users.js.es6 create mode 100644 test/javascripts/fixtures/dau-by-mau.js.es6 create mode 100644 test/javascripts/fixtures/inactive-users.js.es6 create mode 100644 test/javascripts/fixtures/users-by-trust-level.js.es6 create mode 100644 test/javascripts/fixtures/users-by-type.js.es6 diff --git a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 index dd8582461bf..22443906acb 100644 --- a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 @@ -1,65 +1,23 @@ import { ajax } from 'discourse/lib/ajax'; -import computed from 'ember-addons/ember-computed-decorators'; +import Report from "admin/models/report"; +import AsyncReport from "admin/mixins/async-report"; -export default Ember.Component.extend({ +export default Ember.Component.extend(AsyncReport, { classNames: ["dashboard-table", "dashboard-inline-table", "fixed"], - - classNameBindings: ["isLoading"], - - total: null, - labels: null, - title: null, - chartData: null, - isLoading: false, + isLoading: true, help: null, helpPage: null, - model: null, - - didInsertElement() { - this._super(); - - if (this.get("dataSourceName")){ - this._fetchReport(); - } else if (this.get("model")) { - this._setPropertiesFromModel(this.get("model")); - } - }, - - didUpdateAttrs() { - this._super(); - - if (this.get("model")) { - this._setPropertiesFromModel(this.get("model")); - } - }, - - @computed("dataSourceName") - dataSource(dataSourceName) { - return `/admin/reports/${dataSourceName}`; - }, - - _fetchReport() { - if (this.get("isLoading")) return; + fetchReport() { this.set("isLoading", true); ajax(this.get("dataSource")) .then((response) => { - this._setPropertiesFromModel(response.report); + this._setPropertiesFromReport(Report.create(response.report)); }).finally(() => { - this.set("isLoading", false); + if (!Ember.isEmpty(this.get("report.data"))) { + this.set("isLoading", false); + }; }); - }, - - _setPropertiesFromModel(model) { - const data = model.data.sort((a, b) => a.x >= b.x); - - this.setProperties({ - labels: data.map(r => r.x), - dataset: data.map(r => r.y), - total: model.total, - title: model.title, - chartData: data - }); } }); diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 index 3b56917f997..f5ff432a84d 100644 --- a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 @@ -1,43 +1,23 @@ import { ajax } from "discourse/lib/ajax"; import computed from "ember-addons/ember-computed-decorators"; -import loadScript from "discourse/lib/load-script"; +import AsyncReport from "admin/mixins/async-report"; import Report from "admin/models/report"; +import { number } from 'discourse/lib/formatter'; -export default Ember.Component.extend({ +export default Ember.Component.extend(AsyncReport, { classNames: ["dashboard-mini-chart"], - - classNameBindings: ["trend", "oneDataPoint", "isLoading"], - - isLoading: false, - total: null, - trend: null, - title: null, + classNameBindings: ["thirtyDayTrend", "oneDataPoint"], + isLoading: true, + thirtyDayTrend: Ember.computed.alias("report.thirtyDayTrend"), oneDataPoint: false, backgroundColor: "rgba(200,220,240,0.3)", borderColor: "#08C", + average: false, - didInsertElement() { + willDestroyEelement() { this._super(); - if (this.get("model")) { - loadScript("/javascripts/Chart.min.js").then(() => { - this._setPropertiesFromModel(this.get("model")); - this._drawChart(); - }); - } - }, - - didUpdateAttrs() { - this._super(); - - loadScript("/javascripts/Chart.min.js").then(() => { - if (this.get("model") && !this.get("values")) { - this._setPropertiesFromModel(this.get("model")); - this._drawChart(); - } else if (this.get("dataSource")) { - this._fetchReport(); - } - }); + this.messageBus.unsubscribe(this.get("dataSource")); }, @computed("dataSourceName") @@ -47,9 +27,9 @@ export default Ember.Component.extend({ } }, - @computed("trend") - trendIcon(trend) { - switch (trend) { + @computed("thirtyDayTrend") + trendIcon(thirtyDayTrend) { + switch (thirtyDayTrend) { case "trending-up": return "angle-up"; case "trending-down": @@ -63,79 +43,73 @@ export default Ember.Component.extend({ } }, - _fetchReport() { - if (this.get("isLoading")) return; - + fetchReport() { this.set("isLoading", true); let payload = { - data: {} + data: { async: true } }; if (this.get("startDate")) { - payload.data.start_date = this.get("startDate").toISOString(); + payload.data.start_date = this.get("startDate").format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ'); } if (this.get("endDate")) { - payload.data.end_date = this.get("endDate").toISOString(); + payload.data.end_date = this.get("endDate").format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ'); } ajax(this.get("dataSource"), payload) .then((response) => { - this._setPropertiesFromModel(Report.create(response.report)); + // if (!Ember.isEmpty(response.report.data)) { + this._setPropertiesFromReport(Report.create(response.report)); + // } }) .finally(() => { - this.set("isLoading", false); + if (this.get("oneDataPoint")) { + this.set("isLoading", false); + return; + } - Ember.run.schedule("afterRender", () => { - if (!this.get("oneDataPoint")) { - this._drawChart(); - } - }); + if (!Ember.isEmpty(this.get("report.data"))) { + this.set("isLoading", false); + this.renderReport(); + } }); }, - _drawChart() { - const $chartCanvas = this.$(".chart-canvas"); - if (!$chartCanvas.length) return; + renderReport() { + if (!this.element || this.isDestroying || this.isDestroyed) { return; } + if (this.get("oneDataPoint")) return; - const context = $chartCanvas[0].getContext("2d"); + Ember.run.schedule("afterRender", () => { + const $chartCanvas = this.$(".chart-canvas"); - const data = { - labels: this.get("labels"), - datasets: [{ - data: Ember.makeArray(this.get("values")), - backgroundColor: this.get("backgroundColor"), - borderColor: this.get("borderColor") - }] - }; + if (!$chartCanvas.length) return; + const context = $chartCanvas[0].getContext("2d"); - this._chart = new window.Chart(context, this._buildChartConfig(data)); - }, + const data = { + labels: this.get("labels"), + datasets: [{ + data: Ember.makeArray(this.get("values")), + backgroundColor: this.get("backgroundColor"), + borderColor: this.get("borderColor") + }] + }; - _setPropertiesFromModel(report) { - const oneDataPoint = (this.get("startDate") && this.get("endDate")) && - this.get("startDate").isSame(this.get("endDate"), "day"); - - this.setProperties({ - oneDataPoint, - labels: report.get("data").map(r => r.x), - values: report.get("data").map(r => r.y), - total: report.get("total"), - description: report.get("description"), - title: report.get("title"), - trend: report.get("sevenDayTrend"), - prev30Days: report.get("prev30Days"), + this._chart = new window.Chart(context, this._buildChartConfig(data)); }); }, + _setPropertiesFromReport(report) { + const oneDataPoint = (this.get("startDate") && this.get("endDate")) && + this.get("startDate").isSame(this.get("endDate"), "day"); + + report.set("average", this.get("average")); + + this.setProperties({ oneDataPoint, report }); + }, + _buildChartConfig(data) { - const values = data.datasets[0].data; - const max = Math.max(...values); - const min = Math.min(...values); - - const stepSize = Math.max(...[Math.ceil((max - min) / 5) * 5, 20]); - return { type: "line", data, @@ -144,7 +118,6 @@ export default Ember.Component.extend({ display: false }, responsive: true, - maintainAspectRatio: false, layout: { padding: { left: 0, @@ -156,11 +129,7 @@ export default Ember.Component.extend({ scales: { yAxes: [{ display: true, - ticks: { - suggestedMin: 0, - stepSize, - suggestedMax: max + stepSize - } + ticks: { callback: (label) => number(label) } }], xAxes: [{ display: true, diff --git a/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 b/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 index aac53bb9c0c..f6523f6841b 100644 --- a/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 @@ -1,17 +1,8 @@ import DashboardTable from "admin/components/dashboard-table"; -import { number } from 'discourse/lib/formatter'; +import AsyncReport from "admin/mixins/async-report"; -export default DashboardTable.extend({ +export default DashboardTable.extend(AsyncReport, { layoutName: "admin/templates/components/dashboard-table", - classNames: ["dashboard-table", "dashboard-table-trending-search"], - - transformModel(model) { - return { - labels: model.labels, - values: model.data.map(data => { - return [data[0], number(data[1]), number(data[2])]; - }) - }; - }, + classNames: ["dashboard-table", "dashboard-table-trending-search"] }); diff --git a/app/assets/javascripts/admin/components/dashboard-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-table.js.es6 index 2bf5929443d..b6748367079 100644 --- a/app/assets/javascripts/admin/components/dashboard-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-table.js.es6 @@ -1,83 +1,50 @@ -import { ajax } from 'discourse/lib/ajax'; -import computed from 'ember-addons/ember-computed-decorators'; +import { ajax } from "discourse/lib/ajax"; +import Report from "admin/models/report"; +import AsyncReport from "admin/mixins/async-report"; +import computed from "ember-addons/ember-computed-decorators"; +import { number } from 'discourse/lib/formatter'; -export default Ember.Component.extend({ +export default Ember.Component.extend(AsyncReport, { classNames: ["dashboard-table"], - - classNameBindings: ["isLoading"], - - total: null, - labels: null, - title: null, - chartData: null, - isLoading: false, help: null, helpPage: null, - model: null, - transformModel(model) { - const data = model.data.sort((a, b) => a.x >= b.x); - - return { - labels: model.labels, - values: data - }; + @computed("report") + values(report) { + if (!report) return; + return Ember.makeArray(report.data) + .sort((a, b) => a.x >= b.x) + .map(x => { + return [ x[0], number(x[1]), number(x[2]) ]; + }); }, - didInsertElement() { - this._super(); - this._initializeTable(); + @computed("report") + labels(report) { + if (!report) return; + return Ember.makeArray(report.labels); }, - didUpdateAttrs() { - this._super(); - this._initializeTable(); - }, - - @computed("dataSourceName") - dataSource(dataSourceName) { - return `/admin/reports/${dataSourceName}`; - }, - - _initializeTable() { - if (this.get("model") && !this.get("values")) { - this._setPropertiesFromModel(this.get("model")); - } else if (this.get("dataSource")) { - this._fetchReport(); - } - }, - - _fetchReport() { - if (this.get("isLoading")) return; - + fetchReport() { this.set("isLoading", true); - let payload = {data: {}}; + let payload = { data: { async: true } }; if (this.get("startDate")) { - payload.data.start_date = this.get("startDate").toISOString(); + payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ"); } if (this.get("endDate")) { - payload.data.end_date = this.get("endDate").toISOString(); + payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ"); } ajax(this.get("dataSource"), payload) .then((response) => { - this._setPropertiesFromModel(response.report); + this._setPropertiesFromReport(Report.create(response.report)); }).finally(() => { - this.set("isLoading", false); + if (!Ember.isEmpty(this.get("report.data"))) { + this.set("isLoading", false); + }; }); - }, - - _setPropertiesFromModel(model) { - const { labels, values } = this.transformModel(model); - - this.setProperties({ - labels, - values, - total: model.total, - title: model.title - }); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 index 0c56cfe971a..2d7fc621839 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 @@ -1,14 +1,14 @@ import DiscourseURL from "discourse/lib/url"; import computed from "ember-addons/ember-computed-decorators"; -import AdminDashboardNext from 'admin/models/admin-dashboard-next'; +import AdminDashboardNext from "admin/models/admin-dashboard-next"; +import Report from "admin/models/report"; export default Ember.Controller.extend({ queryParams: ["period"], period: "all", isLoading: false, dashboardFetchedAt: null, - exceptionController: Ember.inject.controller('exception'), - + exceptionController: Ember.inject.controller("exception"), diskSpace: Ember.computed.alias("model.attributes.disk_space"), fetchDashboard() { @@ -18,8 +18,11 @@ export default Ember.Controller.extend({ this.set("isLoading", true); AdminDashboardNext.find().then(adminDashboardNextModel => { - this.set("dashboardFetchedAt", new Date()); - this.set("model", adminDashboardNextModel); + this.setProperties({ + dashboardFetchedAt: new Date(), + model: adminDashboardNextModel, + reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + }); }).catch(e => { this.get("exceptionController").set("thrown", e.jqXHR); this.replaceRoute("exception"); diff --git a/app/assets/javascripts/admin/mixins/async-report.js.es6 b/app/assets/javascripts/admin/mixins/async-report.js.es6 new file mode 100644 index 00000000000..5206f651862 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/async-report.js.es6 @@ -0,0 +1,68 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import Report from "admin/models/report"; + +export default Ember.Mixin.create({ + classNameBindings: ["isLoading"], + + report: null, + + init() { + this._super(); + + this.messageBus.subscribe(this.get("dataSource"), report => { + const formatDate = (date) => moment(date).format("YYYYMMDD"); + + // this check is done to avoid loading a chart after period has changed + if ( + (this.get("startDate") && formatDate(report.start_date) === formatDate(this.get("startDate"))) && + (this.get("endDate") && formatDate(report.end_date) === formatDate(this.get("endDate"))) + ) { + this._setPropertiesFromReport(Report.create(report)); + this.set("isLoading", false); + this.renderReport(); + } else { + this._setPropertiesFromReport(Report.create(report)); + this.set("isLoading", false); + this.renderReport(); + } + }); + }, + + didInsertElement() { + this._super(); + + Ember.run.later(this, function() { + this.fetchReport(); + }, 500); + }, + + didUpdateAttrs() { + this._super(); + + this.fetchReport(); + }, + + renderReport() {}, + + @computed("dataSourceName") + dataSource(dataSourceName) { + return `/admin/reports/${dataSourceName}`; + }, + + @computed("report") + labels(report) { + if (!report) return; + return Ember.makeArray(report.data).map(r => r.x); + }, + + @computed("report") + values(report) { + if (!report) return; + return Ember.makeArray(report.data).map(r => r.y); + }, + + _setPropertiesFromReport(report) { + if (!this.element || this.isDestroying || this.isDestroyed) { return; } + this.setProperties({ report }); + } +}); 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 880c2d5488f..4747529fe7e 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 @@ -1,9 +1,6 @@ import { ajax } from "discourse/lib/ajax"; -import Report from "admin/models/report"; -const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"]; - -const REPORTS = [ "global_reports", "user_reports" ]; +const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at" ]; const AdminDashboardNext = Discourse.Model.extend({}); @@ -19,12 +16,7 @@ AdminDashboardNext.reopenClass({ return ajax("/admin/dashboard-next.json").then(function(json) { var model = AdminDashboardNext.create(); - const reports = {}; - REPORTS.forEach(name => json[name].forEach(r => { - if (!reports[name]) reports[name] = {}; - reports[name][r.type] = Report.create(r); - })); - model.set("reports", reports); + model.set("reports", json.reports); const attributes = {}; ATTRIBUTES.forEach(a => attributes[a] = json[a]); diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index fac048a62a4..1113f2e5d1a 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -5,6 +5,8 @@ import { fillMissingDates } from 'discourse/lib/utilities'; import computed from 'ember-addons/ember-computed-decorators'; const Report = Discourse.Model.extend({ + average: false, + reportUrl: fmt("type", "/admin/reports/%@"), valueAt(numDaysAgo) { @@ -35,30 +37,43 @@ const Report = Discourse.Model.extend({ } }, - todayCount: function() { return this.valueAt(0); }.property("data"), - yesterdayCount: function() { return this.valueAt(1); }.property("data"), - sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data"), - thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data"), + todayCount: function() { return this.valueAt(0); }.property("data", "average"), + yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"), + sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"), + thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"), + lastSevenDaysCount: function() { + return this.averageCount(7, this.valueFor(1, 7)); + }.property("data", "average"), + lastThirtyDaysCount: function() { + return this.averageCount(30, this.valueFor(1, 30)); + }.property("data", "average"), - lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"), - lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"), + averageCount(count, value) { + return this.get("average") ? value / count : value; + }, - @computed('data') - yesterdayTrend() { - const yesterdayVal = this.valueAt(1); + @computed('yesterdayCount') + yesterdayTrend(yesterdayCount) { + const yesterdayVal = yesterdayCount; const twoDaysAgoVal = this.valueAt(2); - if (yesterdayVal > twoDaysAgoVal) { + const change = ((yesterdayVal - twoDaysAgoVal) / yesterdayVal) * 100; + + if (change > 50) { + return "high-trending-up"; + } else if (change > 0) { return "trending-up"; - } else if (yesterdayVal < twoDaysAgoVal) { - return "trending-down"; - } else { + } else if (change === 0) { return "no-change"; + } else if (change < -50) { + return "high-trending-down"; + } else if (change < 0) { + return "trending-down"; } }, - @computed('data') - sevenDayTrend() { - const currentPeriod = this.valueFor(1, 7); + @computed('lastSevenDaysCount') + sevenDayTrend(lastSevenDaysCount) { + const currentPeriod = lastSevenDaysCount; const prevPeriod = this.valueFor(8, 14); const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100; @@ -75,17 +90,22 @@ const Report = Discourse.Model.extend({ } }, - @computed('prev30Days', 'data') - thirtyDayTrend(prev30Days) { - if (prev30Days) { - const currentPeriod = this.valueFor(1, 30); - if (currentPeriod > this.get("prev30Days")) { - return "trending-up"; - } else if (currentPeriod < prev30Days) { - return "trending-down"; - } + @computed('prev30Days', 'lastThirtyDaysCount') + thirtyDayTrend(prev30Days, lastThirtyDaysCount) { + const currentPeriod = lastThirtyDaysCount; + const change = ((currentPeriod - prev30Days) / currentPeriod) * 100; + + if (change > 50) { + return "high-trending-up"; + } else if (change > 0) { + return "trending-up"; + } else if (change === 0) { + return "no-change"; + } else if (change < -50) { + return "high-trending-down"; + } else if (change < 0) { + return "trending-down"; } - return "no-change"; }, @computed('type') @@ -126,19 +146,19 @@ const Report = Discourse.Model.extend({ return title; }, - @computed('data') - yesterdayCountTitle() { - return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago"); + @computed('yesterdayCount') + yesterdayCountTitle(yesterdayCount) { + return this.changeTitle(yesterdayCount, this.valueAt(2), "two days ago"); }, - @computed('data') - sevenDayCountTitle() { - return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago"); + @computed('lastSevenDaysCount') + sevenDayCountTitle(lastSevenDaysCount) { + return this.changeTitle(lastSevenDaysCount, this.valueFor(8, 14), "two weeks ago"); }, - @computed('prev30Days', 'data') - thirtyDayCountTitle(prev30Days) { - return this.changeTitle(this.valueFor(1, 30), prev30Days, "in the previous 30 day period"); + @computed('prev30Days', 'lastThirtyDaysCount') + thirtyDayCountTitle(prev30Days, lastThirtyDaysCount) { + return this.changeTitle(lastThirtyDaysCount, prev30Days, "in the previous 30 day period"); }, @computed('data') 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 30ca9b033c0..c877f07bf50 100644 --- a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -1,5 +1,9 @@ +import loadScript from "discourse/lib/load-script"; + export default Discourse.Route.extend({ activate() { - this.controllerFor('admin-dashboard-next').fetchDashboard(); + loadScript("/javascripts/Chart.min.js").then(() => { + this.controllerFor('admin-dashboard-next').fetchDashboard(); + }); } }); diff --git a/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs index b7b0bdc61eb..a5c22f86c68 100644 --- a/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs +++ b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs @@ -1,6 +1,6 @@ -{{#conditional-loading-spinner condition=isLoading}} +{{#conditional-loading-section isLoading=isLoading title=report.title}}
-

{{title}}

+

{{report.title}}

{{#if help}} {{i18n help}} @@ -18,11 +18,11 @@ - {{#each dataset as |data|}} - {{number data}} + {{#each values as |value|}} + {{number value}} {{/each}}
-{{/conditional-loading-spinner}} +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs index 20fb90063d6..a0b22b0c8c6 100644 --- a/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs +++ b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs @@ -1,28 +1,31 @@ -{{#conditional-loading-spinner condition=isLoading}} +{{#conditional-loading-section isLoading=isLoading title=report.title}}
-

{{title}}

+

+ {{report.title}} - {{#if description}} - + {{#if report.description}} {{d-icon "question-circle"}} + {{/if}} +

+ +
+ + {{number report.lastThirtyDaysCount}} - {{/if}} + + {{#if trendIcon}} + {{d-icon trendIcon}} + {{/if}} +
{{#if oneDataPoint}} - {{number chartData.lastObject.y}} + {{number values.lastObject.y}} {{else}} -
- {{number prev30Days}} - - {{#if trendIcon}} - {{d-icon trendIcon}} - {{/if}} -
{{/if}}
-{{/conditional-loading-spinner}} +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs index cb4070e40ea..d658f4bb019 100644 --- a/app/assets/javascripts/admin/templates/components/dashboard-table.hbs +++ b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs @@ -1,6 +1,6 @@ -{{#conditional-loading-spinner condition=isLoading}} +{{#conditional-loading-section isLoading=isLoading title=report.title}}
-

{{title}}

+

{{report.title}}

{{#if help}} {{i18n help}} @@ -28,4 +28,4 @@
-{{/conditional-loading-spinner}} +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/dashboard_next.hbs b/app/assets/javascripts/admin/templates/dashboard_next.hbs index 3d0cca00c3d..ec9fd3b93b7 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next.hbs @@ -1,4 +1,4 @@ -{{plugin-outlet name="admin-dashboard-top"}} + {{plugin-outlet name="admin-dashboard-top"}}
@@ -9,22 +9,35 @@
{{dashboard-mini-chart - model=model.reports.global_reports.signups dataSourceName="signups" startDate=startDate endDate=endDate}} {{dashboard-mini-chart - model=model.reports.global_reports.topics dataSourceName="topics" startDate=startDate endDate=endDate}} {{dashboard-mini-chart - model=model.reports.global_reports.new_contributors dataSourceName="new_contributors" startDate=startDate endDate=endDate}} + + {{dashboard-mini-chart + dataSourceName="dau_by_mau" + average=true + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceName="daily_engaged_users" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceName="inactive_users" + startDate=startDate + endDate=endDate}}
@@ -32,42 +45,42 @@
-
-

{{i18n "admin.dashboard.activity_metrics"}}

-
+ {{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}} +
+

{{i18n "admin.dashboard.activity_metrics"}}

+
-
- - - - - - - - - - - - - {{admin-report-counts report=model.reports.global_reports.topics}} - {{admin-report-counts report=model.reports.global_reports.signups}} - {{admin-report-counts report=model.reports.global_reports.new_contributors}} - -
{{i18n 'admin.dashboard.reports.today'}}{{i18n 'admin.dashboard.reports.yesterday'}}{{i18n 'admin.dashboard.reports.last_7_days'}}{{i18n 'admin.dashboard.reports.last_30_days'}}{{i18n 'admin.dashboard.reports.all'}}
-
+
+ + + + + + + + + + + + + {{#each reports as |report|}} + {{admin-report-counts report=report}} + {{/each}} + +
{{i18n 'admin.dashboard.reports.today'}}{{i18n 'admin.dashboard.reports.yesterday'}}{{i18n 'admin.dashboard.reports.last_7_days'}}{{i18n 'admin.dashboard.reports.last_30_days'}}{{i18n 'admin.dashboard.reports.all'}}
+
+ {{/conditional-loading-section}}
{{dashboard-inline-table - model=model.reports.user_reports.users_by_type - lastRefreshedAt=lastRefreshedAt - isLoading=isLoading}} + dataSourceName="users_by_type" + lastRefreshedAt=lastRefreshedAt}} {{dashboard-inline-table - model=model.reports.user_reports.users_by_trust_level - lastRefreshedAt=lastRefreshedAt - isLoading=isLoading}} + dataSourceName="users_by_trust_level" + lastRefreshedAt=lastRefreshedAt}} - {{#conditional-loading-spinner isLoading=isLoading}} + {{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
{{#if currentUser.admin}} @@ -99,12 +112,11 @@ {{i18n "admin.dashboard.whats_new_in_discourse"}}
- {{/conditional-loading-spinner}} + {{/conditional-loading-section}}
{{dashboard-table-trending-search - model=global_reports_trending_search dataSourceName="trending_search" startDate=startDate endDate=endDate}} diff --git a/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 new file mode 100644 index 00000000000..7e9323eb5ed --- /dev/null +++ b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 @@ -0,0 +1,14 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ["conditional-loading-section"], + + classNameBindings: ["isLoading"], + + isLoading: false, + + @computed("title") + computedTitle(title) { + return title || I18n.t("conditional_loading_section.loading"); + } +}); diff --git a/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs b/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs new file mode 100644 index 00000000000..2cc7599b98e --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs @@ -0,0 +1,6 @@ +{{#if isLoading}} + {{computedTitle}} +
+{{else}} + {{yield}} +{{/if}} diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index 88654d1de8f..6a5457eb76e 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -8,8 +8,24 @@ display: flex; justify-content: space-between; + @include small-width { + flex-direction: column; + } + .section-column { min-width: calc(50% - .5em); + + @include small-width { + min-width: 100%; + + &:last-child { + order: 1; + } + + &:first-child { + order: 2; + } + } } .section-column:last-child { @@ -19,6 +35,12 @@ .section-column:first-child { margin-right: .5em; } + + @include small-width { + .section-column:last-child, .section-column:first-child { + margin: 0; + } + } } .section { @@ -43,8 +65,12 @@ .dashboard-table { margin-bottom: 1em; - &.fixed table { - table-layout: fixed; + @include small-width { + table { + tbody tr td { + font-size: $font-down-2; + } + } } &.is-loading { @@ -63,6 +89,7 @@ table { border: 1px solid $primary-low-mid; + table-layout: fixed; thead { tr { @@ -70,7 +97,6 @@ th { border: 1px solid $primary-low-mid; text-align: center; - text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -80,6 +106,12 @@ tbody { tr { + td:first-child { + text-overflow: ellipsis; + overflow: hidden; + white-space: normal; + } + td { border: 1px solid $primary-low-mid; text-align: center; @@ -120,22 +152,34 @@ flex-wrap: wrap; .dashboard-mini-chart { - width: calc(100% * (1/3)); + max-width: calc(100% * (1/3)); + width: 100%; margin-bottom: 1em; flex-grow: 1; + @include small-width { + max-width: 100%; + } + &.is-loading { - height: 150px; + height: 200px; + } + + .loading-container.visible { + display: flex; + align-items: center; + height: 100%; + width: 100%; } .d-icon-question-circle { cursor: pointer; - margin-left: .25em; } .chart-title { align-items: center; display: flex; + justify-content: space-between; h3 { margin: 1em 0; @@ -174,21 +218,24 @@ } } + @include small-width { + .dashboard-mini-chart { + width: 100%; + } + } + .chart-container { position: relative; padding: 0 1em; - min-height: 200px; } .chart-trend { - font-size: $font-up-5; - position: absolute; - right: 40px; - top: 5px; + font-size: $font-up-3; display: flex; justify-content: space-between; align-items: center; font-weight: bold; + margin-right: 1em; } .chart-canvas { diff --git a/app/assets/stylesheets/common/components/conditional-loading-section.scss b/app/assets/stylesheets/common/components/conditional-loading-section.scss new file mode 100644 index 00000000000..e081dcde463 --- /dev/null +++ b/app/assets/stylesheets/common/components/conditional-loading-section.scss @@ -0,0 +1,17 @@ +.conditional-loading-section { + + &.is-loading { + padding: 2em; + margin: 1em; + display: flex; + align-items: center; + justify-content: center; + background: $primary-very-low; + flex-direction: column; + + .title { + font-size: $font-up-1; + font-weight: 700; + } + } +} diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index dec448528d4..17e52f6159b 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -7,8 +7,8 @@ class Admin::ReportsController < Admin::AdminController raise Discourse::NotFound unless report_type =~ /^[a-z0-9\_]+$/ - start_date = (params[:start_date].present? ? Time.zone.parse(params[:start_date]) : 30.days.ago).beginning_of_day - end_date = (params[:end_date].present? ? Time.zone.parse(params[:end_date]) : start_date + 30.days).end_of_day + start_date = (params[:start_date].present? ? params[:start_date].to_date : 30.days.ago).beginning_of_day + end_date = (params[:end_date].present? ? params[:end_date].to_date : start_date + 30.days).end_of_day if params.has_key?(:category_id) && params[:category_id].to_i > 0 category_id = params[:category_id].to_i @@ -22,7 +22,12 @@ class Admin::ReportsController < Admin::AdminController group_id = nil end - report = Report.find(report_type, start_date: start_date, end_date: end_date, category_id: category_id, group_id: group_id) + report = Report.find(report_type, + start_date: start_date, + end_date: end_date, + category_id: category_id, + group_id: group_id, + async: params[:async]) raise Discourse::NotFound if report.blank? diff --git a/app/jobs/regular/retrieve_report.rb b/app/jobs/regular/retrieve_report.rb new file mode 100644 index 00000000000..15a37377cb0 --- /dev/null +++ b/app/jobs/regular/retrieve_report.rb @@ -0,0 +1,24 @@ +require_dependency 'report' + +module Jobs + class RetrieveReport < Jobs::Base + sidekiq_options retry: false + + def execute(args) + raise Discourse::InvalidParameters.new(:report_type) if !args["report_type"] + + type = args.delete("report_type") + report = Report.new(type) + report.start_date = args["start_date"].to_date if args["start_date"] + report.end_date = args["end_date"].to_date if args["end_date"] + report.category_id = args["category_id"] if args["category_id"] + report.group_id = args["group_id"] if args["group_id"] + + Report.send("report_#{type}", report) + + Discourse.cache.write(Report.cache_key(report), report.as_json, force: true, expires_in: 30.minutes) + + MessageBus.publish("/admin/reports/#{type}", report.as_json, user_ids: User.staff.pluck(:id)) + end + end +end diff --git a/app/models/admin_dashboard_next_data.rb b/app/models/admin_dashboard_next_data.rb index 2bd8c567bfa..4fff1a40dc4 100644 --- a/app/models/admin_dashboard_next_data.rb +++ b/app/models/admin_dashboard_next_data.rb @@ -1,17 +1,7 @@ class AdminDashboardNextData include StatsCacheable - GLOBAL_REPORTS ||= [ - 'signups', - 'topics', - 'trending_search', - 'new_contributors' - ] - - USER_REPORTS ||= [ - 'users_by_trust_level', - 'users_by_type' - ] + REPORTS = [ "visits", "posts", "time_to_first_response", "likes", "flags" ] def initialize(opts = {}) @opts = opts @@ -27,8 +17,7 @@ class AdminDashboardNextData def as_json(_options = nil) @json ||= { - global_reports: AdminDashboardNextData.reports(GLOBAL_REPORTS), - user_reports: AdminDashboardNextData.reports(USER_REPORTS), + reports: AdminDashboardNextData.reports(REPORTS), last_backup_taken_at: last_backup_taken_at, updated_at: Time.zone.now.as_json } diff --git a/app/models/concerns/date_groupable.rb b/app/models/concerns/date_groupable.rb index 165a5f35527..03cdae08cf7 100644 --- a/app/models/concerns/date_groupable.rb +++ b/app/models/concerns/date_groupable.rb @@ -25,21 +25,25 @@ module DateGroupable extend ActiveSupport::Concern .order("date_trunc('#{aggregation_unit}', #{column})::DATE") end - def smart_group_by_date(column, start_date, end_date) + def aggregation_unit_for_period(start_date, end_date) days = (start_date.to_date..end_date.to_date).count case when days <= 40 - aggregation_unit = :day - when days <= 210 # 30 weeks - aggregation_unit = :week - when days <= 550 # ~18 months - aggregation_unit = :month - when days <= 1461 # ~4 years - aggregation_unit = :quarter + return :day + when days <= 210 # 30 weeks + return :week + when days <= 550 # ~18 months + return :month + when days <= 1461 # ~4 years + return :quarter else - aggregation_unit = :year + return :year end + end + + def smart_group_by_date(column, start_date, end_date) + aggregation_unit = aggregation_unit_for_period(start_date, end_date) where("#{column} BETWEEN ? AND ?", start_date, end_date) .group_by_unit(aggregation_unit, column) diff --git a/app/models/report.rb b/app/models/report.rb index e6f789ac7e6..52496a08867 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -2,7 +2,8 @@ require_dependency 'topic_subtype' class Report - attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id, :group_id, :labels + attr_accessor :type, :data, :total, :prev30Days, :start_date, + :end_date, :category_id, :group_id, :labels, :async def self.default_days 30 @@ -14,6 +15,16 @@ class Report @end_date ||= Time.zone.now.end_of_day end + def self.cache_key(report) + "reports:#{report.type}:#{report.start_date.strftime("%Y%m%d")}:#{report.end_date.strftime("%Y%m%d")}" + end + + def self.clear_cache + Discourse.cache.keys("reports:*").each do |key| + Discourse.cache.redis.del(key) + end + end + def as_json(options = nil) { type: type, @@ -45,14 +56,22 @@ class Report # Load the report report = Report.new(type) - report.start_date = opts[:start_date] if opts[:start_date] - report.end_date = opts[:end_date] if opts[:end_date] + report.start_date = opts[:start_date].to_date if opts[:start_date] + report.end_date = opts[:end_date].to_date if opts[:end_date] report.category_id = opts[:category_id] if opts[:category_id] report.group_id = opts[:group_id] if opts[:group_id] + report.async = opts[:async] || false report_method = :"report_#{type}" if respond_to?(report_method) - send(report_method, report) + cached_report = Discourse.cache.read(cache_key(report)) + if cached_report + return cached_report + elsif report.async + Jobs.enqueue(:retrieve_report, opts.merge(report_type: type)) + else + send(report_method, report) + end elsif type =~ /_reqs$/ req_report(report, type.split(/_reqs$/)[0].to_sym) else @@ -73,7 +92,7 @@ class Report end report.data = [] - data.where('date >= ? AND date <= ?', report.start_date.to_date, report.end_date.to_date) + data.where('date >= ? AND date <= ?', report.start_date, report.end_date) .order(date: :asc) .group(:date) .sum(:count) @@ -85,7 +104,7 @@ class Report report.prev30Days = data.where( 'date >= ? AND date < ?', - (report.start_date - 31.days).to_date, report.start_date.to_date + (report.start_date - 31.days), report.start_date ).sum(:count) end @@ -110,13 +129,73 @@ class Report end end + def self.report_inactive_users(report) + report.data = [] + + data = User.real.count_by_inactivity(report.start_date, report.end_date) + + data.each do |data_point| + report.data << { x: data_point["date_trunc"], y: data_point["count"] } + end + end + def self.report_new_contributors(report) - report_about report, User.real, :count_by_first_post + report.data = [] + + data = User.real.count_by_first_post(report.start_date, report.end_date) + + prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date) + report.prev30Days = prev30DaysData.sum { |k, v| v } + + report.total = User.real.count_by_first_post + + data.each do |key, value| + report.data << { x: key, y: value } + end + end + + def self.report_daily_engaged_users(report) + report.data = [] + + data = UserAction.count_daily_engaged_users(report.start_date, report.end_date) + prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date) + + report.total = UserAction.count_daily_engaged_users + + report.prev30Days = prev30DaysData.sum { |k, v| v } + + data.each do |key, value| + report.data << { x: key, y: value } + end + end + + def self.report_dau_by_mau(report) + data_points = UserVisit.count_by_active_users(report.start_date, report.end_date) + + report.data = [] + + compute_dau_by_mau = Proc.new { |data_point| + if data_point["mau"] == 0 + 0 + else + ((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil + end + } + + data_points.each do |data_point| + report.data << { x: data_point["date"], y: compute_dau_by_mau.call(data_point) } + end + + prev_data_points = UserVisit.count_by_active_users(report.start_date - 30.days, report.start_date) + if !prev_data_points.empty? + sum = prev_data_points.sum { |data_point| compute_dau_by_mau.call(data_point) } + report.prev30Days = sum / prev_data_points.count + end end def self.report_profile_views(report) - start_date = report.start_date.to_date - end_date = report.end_date.to_date + start_date = report.start_date + end_date = report.end_date basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id report.total = UserProfile.sum(:views) diff --git a/app/models/user.rb b/app/models/user.rb index cea62754d9b..3b28ec9b480 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -829,8 +829,49 @@ class User < ActiveRecord::Base (tl_badge + other_badges).take(limit) end - def self.count_by_signup_date(start_date, end_date, group_id = nil) - result = smart_group_by_date("users.created_at", start_date, end_date) + def self.count_by_inactivity(start_date, end_date) + sql = < 0 AND + NOT EXISTS( + SELECT 1 + FROM user_custom_fields ucf + WHERE + ucf.user_id = u.id AND + ucf.name = 'master_id' AND + ucf.value :: int > 0 + ) AND + NOT EXISTS( + SELECT 1 + FROM user_visits v + WHERE v.visited_at BETWEEN (d.generated_date - INTERVAL '89 days') :: DATE AND d.generated_date + AND v.user_id = u.id + ) AND + NOT EXISTS( + SELECT 1 + FROM incoming_emails e + WHERE e.user_id = u.id AND + e.post_id IS NOT NULL AND + e.created_at BETWEEN (d.generated_date - INTERVAL '89 days') :: DATE AND d.generated_date + ) + GROUP BY date_trunc('day', d.generated_date) :: DATE + ORDER BY date_trunc('day', d.generated_date) :: DATE +SQL + + exec_sql(sql).to_a + end + + def self.count_by_signup_date(start_date = nil, end_date = nil, group_id = nil) + result = self + + if start_date && end_date + result = result.smart_group_by_date("users.created_at", start_date, end_date) + end if group_id result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id") @@ -840,10 +881,14 @@ class User < ActiveRecord::Base result.count end - def self.count_by_first_post(start_date, end_date) - joins('INNER JOIN user_stats AS us ON us.user_id = users.id') - .smart_group_by_date("us.first_post_created_at", start_date, end_date) - .count + def self.count_by_first_post(start_date = nil, end_date = nil) + result = joins('INNER JOIN user_stats AS us ON us.user_id = users.id') + + if start_date && end_date + result = result.smart_group_by_date("us.first_post_created_at", start_date, end_date) + end + + result.count end def secure_category_ids diff --git a/app/models/user_action.rb b/app/models/user_action.rb index f20cc8c6376..22b27b8cb1c 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -121,11 +121,16 @@ SQL end - def self.count_daily_engaged_users(start_date, end_date) - select(:user_id).distinct + def self.count_daily_engaged_users(start_date = nil, end_date = nil) + result = select(:user_id) + .distinct .where(action_type: [LIKE, NEW_TOPIC, REPLY, NEW_PRIVATE_MESSAGE]) - .smart_group_by_date(:created_at, start_date, end_date) - .count + + if start_date && end_date + result = result.smart_group_by_date(:created_at, start_date, end_date) + end + + result.count end def self.stream_item(action_id, guardian) diff --git a/app/models/user_visit.rb b/app/models/user_visit.rb index c0f5a464529..be4bd917c1b 100644 --- a/app/models/user_visit.rb +++ b/app/models/user_visit.rb @@ -1,4 +1,5 @@ class UserVisit < ActiveRecord::Base + include DateGroupable def self.counts_by_day_query(start_date, end_date, group_id = nil) result = where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date) @@ -11,6 +12,30 @@ class UserVisit < ActiveRecord::Base result.group(:visited_at).order(:visited_at) end + def self.count_by_active_users(start_date, end_date) + aggregation_unit = aggregation_unit_for_period(start_date, end_date) + + sql = < { +QUnit.test("Visit dashboard next page", assert => { visit("/admin/dashboard-next"); andThen(() => { diff --git a/test/javascripts/fixtures/daily-engaged-users.js.es6 b/test/javascripts/fixtures/daily-engaged-users.js.es6 new file mode 100644 index 00000000000..902beb981cb --- /dev/null +++ b/test/javascripts/fixtures/daily-engaged-users.js.es6 @@ -0,0 +1,19 @@ +export default { + "/admin/reports/daily_engaged_users": { + "report": { + "type": "daily_engaged_users", + "title": "Daily Engaged Users", + "xaxis": "Day", + "yaxis": "Engaged Users", + "description": "Number of users that have liked or posted in the last day", + "data": null, + "total": null, + "start_date": "2018-04-03", + "end_date": "2018-05-03", + "category_id": null, + "group_id": null, + "prev30Days": null, + "labels": null + } + } +}; diff --git a/test/javascripts/fixtures/dashboard-next.js.es6 b/test/javascripts/fixtures/dashboard-next.js.es6 index 4d5a8967064..91ff5ea2bf8 100644 --- a/test/javascripts/fixtures/dashboard-next.js.es6 +++ b/test/javascripts/fixtures/dashboard-next.js.es6 @@ -1,139 +1,6 @@ export default { "/admin/dashboard-next.json": { - "global_reports": [{ - "type": "signups", - "title": "New Users", - "xaxis": "Day", - "yaxis": "Number of new users", - "data": [{ - "x": "2018-04-11", - "y": 10 - }, { - "x": "2018-04-12", - "y": 10 - }, { - "x": "2018-04-13", - "y": 58 - }, { - "x": "2018-04-15", - "y": 10 - }, { - "x": "2018-04-16", - "y": 10 - }, { - "x": "2018-04-17", - "y": 19 - }, { - "x": "2018-04-19", - "y": 19 - }], - "total": 136, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": 0, - "labels": null - }, { - "type": "topics", - "title": "Topics", - "xaxis": "Day", - "yaxis": "Number of new topics", - "data": [{ - "x": "2018-04-11", - "y": 10 - }, { - "x": "2018-04-12", - "y": 10 - }, { - "x": "2018-04-13", - "y": 60 - }, { - "x": "2018-04-15", - "y": 10 - }, { - "x": "2018-04-16", - "y": 10 - }, { - "x": "2018-04-17", - "y": 10 - }, { - "x": "2018-04-19", - "y": 10 - }, { - "x": "2018-04-20", - "y": 1 - }], - "total": 121, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": 0, - "labels": null - }, { - "type": "trending_search", - "title": "Trending search", - "xaxis": "translation missing: en.reports.trending_search.xaxis", - "yaxis": "translation missing: en.reports.trending_search.yaxis", - "data": [ - ["lon", 3, 1], - ["pub", 1, 1], - ["something", 1, 1] - ], - "total": null, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": null, - "labels": ["Term", "Searches", "Unique"] - }], - "user_reports": [{ - "type": "users_by_trust_level", - "title": "Users per Trust Level", - "xaxis": "Trust Level", - "yaxis": "Number of Users", - "data": [{ - "x": 0, - "y": 132 - }, { - "x": 1, - "y": 1 - }, { - "x": 3, - "y": 1 - }, { - "x": 2, - "y": 1 - }, { - "x": 4, - "y": 1 - }], - "total": null, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": null, - "labels": null - }, { - "type": "users_by_type", - "title": "Users per Type", - "xaxis": "Type", - "yaxis": "Number of Users", - "data": [{ - "x": "Admin", - "y": 1 - }], - "total": null, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": null, - "labels": null - }], + "reports": [], "last_backup_taken_at": "2018-04-13T12:51:19.926Z", "updated_at": "2018-04-25T08:06:11.292Z", "disk_space": { diff --git a/test/javascripts/fixtures/dau-by-mau.js.es6 b/test/javascripts/fixtures/dau-by-mau.js.es6 new file mode 100644 index 00000000000..d7ab39d3a0c --- /dev/null +++ b/test/javascripts/fixtures/dau-by-mau.js.es6 @@ -0,0 +1,19 @@ +export default { + "/admin/reports/dau_by_mau": { + "report": { + "type": "dau_by_mau", + "title": "DAU/MAU", + "xaxis": "Day", + "yaxis": "DAU/MAY", + "description": "Percentage of daily active users on monthly active users", + "data": null, + "total": null, + "start_date": "2018-01-26T00:00:00.000Z", + "end_date": "2018-04-27T23:59:59.999Z", + "category_id": null, + "group_id": null, + "prev30Days": 46, + "labels": null + } + } +}; diff --git a/test/javascripts/fixtures/inactive-users.js.es6 b/test/javascripts/fixtures/inactive-users.js.es6 new file mode 100644 index 00000000000..b94a48db579 --- /dev/null +++ b/test/javascripts/fixtures/inactive-users.js.es6 @@ -0,0 +1,19 @@ +export default { + "/admin/reports/inactive_users": { + "report": { + "type": "inactive_users", + "title": "Inactive Users", + "xaxis": "Day", + "yaxis": "Number of new inactive users", + "description": "Number of users that haven’t logged on for the last 3 months", + "data": null, + "total": null, + "start_date": "2018-04-26", + "end_date": "2018-05-03", + "category_id": null, + "group_id": null, + "prev30Days": null, + "labels": null + } + } +}; diff --git a/test/javascripts/fixtures/topics.js.es6 b/test/javascripts/fixtures/topics.js.es6 index 7291642b5c2..86e888af4fc 100644 --- a/test/javascripts/fixtures/topics.js.es6 +++ b/test/javascripts/fixtures/topics.js.es6 @@ -5,38 +5,8 @@ export default { "title": "Topics", "xaxis": "Day", "yaxis": "Number of new topics", - "data": [{ - "x": "2018-04-11", - "y": 10 - }, { - "x": "2018-04-12", - "y": 10 - }, { - "x": "2018-04-13", - "y": 60 - }, { - "x": "2018-04-14", - "y": 60 - }, { - "x": "2018-04-15", - "y": 10 - }, { - "x": "2018-04-16", - "y": 10 - }, { - "x": "2018-04-17", - "y": 10 - }, { - "x": "2018-04-19", - "y": 10 - }, { - "x": "2018-04-18", - "y": 10 - }, { - "x": "2018-04-20", - "y": 1 - }], - "total": 121, + "data": null, + "total": null, "start_date": "2018-03-26T00:00:00.000Z", "end_date": "2018-04-25T23:59:59.999Z", "category_id": null, diff --git a/test/javascripts/fixtures/trending-search.js.es6 b/test/javascripts/fixtures/trending-search.js.es6 index 11a6070aa80..a8255e1adec 100644 --- a/test/javascripts/fixtures/trending-search.js.es6 +++ b/test/javascripts/fixtures/trending-search.js.es6 @@ -5,11 +5,7 @@ export default { "title": "Trending search", "xaxis": "", "yaxis": "", - "data": [ - ["lon", 3, 1], - ["pub", 1, 1], - ["something", 1, 1] - ], + "data": null, "total": null, "start_date": "2018-03-26T00:00:00.000Z", "end_date": "2018-04-25T23:59:59.999Z", diff --git a/test/javascripts/fixtures/users-by-trust-level.js.es6 b/test/javascripts/fixtures/users-by-trust-level.js.es6 new file mode 100644 index 00000000000..11d227e8b19 --- /dev/null +++ b/test/javascripts/fixtures/users-by-trust-level.js.es6 @@ -0,0 +1,19 @@ +export default { + "/admin/reports/users_by_trust_level": { + "report": { + "type": "users_by_trust_level", + "title": "Users per Trust Level", + "xaxis": "Trust Level", + "yaxis": "Number of Users", + "description": "translation missing: en.reports.users_by_trust_level.description", + "data": null, + "total": null, + "start_date": "2018-03-30T00:00:00.000Z", + "end_date": "2018-04-29T23:59:59.999Z", + "category_id": null, + "group_id": null, + "prev30Days": null, + "labels": null + } + } +}; diff --git a/test/javascripts/fixtures/users-by-type.js.es6 b/test/javascripts/fixtures/users-by-type.js.es6 new file mode 100644 index 00000000000..e72d5cbf498 --- /dev/null +++ b/test/javascripts/fixtures/users-by-type.js.es6 @@ -0,0 +1,19 @@ +export default { + "/admin/reports/users_by_type": { + "report": { + "type": "users_by_type", + "title": "Users per Type", + "xaxis": "Type", + "yaxis": "Number of Users", + "description": "translation missing: en.reports.users_by_type.description", + "data": null, + "total": null, + "start_date": "2018-03-30T00:00:00.000Z", + "end_date": "2018-04-29T23:59:59.999Z", + "category_id": null, + "group_id": null, + "prev30Days": null, + "labels": null + } + } +};