From b95165b83825e4c8ac511309ff988285f7dc70b8 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 21 Jan 2019 15:17:04 +0100 Subject: [PATCH] FEATURE: adds a new chart report to track pageviews (#6913) --- .../admin-report-stacked-chart.js.es6 | 127 ++++++++++++++++++ .../admin/components/admin-report.js.es6 | 15 ++- .../javascripts/admin/models/report.js.es6 | 25 +++- .../components/admin-report-stacked-chart.hbs | 3 + .../templates/dashboard_next_general.hbs | 5 + .../stylesheets/common/admin/admin_base.scss | 1 + .../admin/admin_report_stacked_chart.scss | 9 ++ .../common/admin/dashboard_next.scss | 9 ++ app/models/report.rb | 61 +++++++-- config/locales/server.en.yml | 5 + spec/models/report_spec.rb | 39 ++++++ 11 files changed, 283 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/admin-report-stacked-chart.hbs create mode 100644 app/assets/stylesheets/common/admin/admin_report_stacked_chart.scss diff --git a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 new file mode 100644 index 00000000000..dbc310b32a5 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 @@ -0,0 +1,127 @@ +import { number } from "discourse/lib/formatter"; +import loadScript from "discourse/lib/load-script"; + +export default Ember.Component.extend({ + classNames: ["admin-report-chart", "admin-report-stacked-chart"], + + init() { + this._super(...arguments); + + this.resizeHandler = () => + Ember.run.debounce(this, this._scheduleChartRendering, 500); + }, + + didInsertElement() { + this._super(...arguments); + + $(window).on("resize.chart", this.resizeHandler); + }, + + willDestroyElement() { + this._super(...arguments); + + $(window).off("resize.chart", this.resizeHandler); + + this._resetChart(); + }, + + didReceiveAttrs() { + this._super(...arguments); + + Ember.run.debounce(this, this._scheduleChartRendering, 100); + }, + + _scheduleChartRendering() { + Ember.run.schedule("afterRender", () => { + this._renderChart(this.get("model"), this.$(".chart-canvas")); + }); + }, + + _renderChart(model, $chartCanvas) { + if (!$chartCanvas || !$chartCanvas.length) return; + + const context = $chartCanvas[0].getContext("2d"); + + const chartData = Ember.makeArray( + model.get("chartData") || model.get("data") + ); + + const data = { + labels: chartData[0].data.map(cd => cd.x), + datasets: chartData.map(cd => { + return { + label: cd.req, + stack: "pageviews-stack", + data: cd.data.map(d => Math.round(parseFloat(d.y))), + backgroundColor: cd.color + }; + }) + }; + + loadScript("/javascripts/Chart.min.js").then(() => { + this._resetChart(); + this._chart = new window.Chart(context, this._buildChartConfig(data)); + }); + }, + + _buildChartConfig(data) { + return { + type: "bar", + data, + responsive: true, + maintainAspectRatio: false, + options: { + hover: { mode: "index" }, + tooltips: { + mode: "index", + intersect: false, + callbacks: { + title: tooltipItem => + moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL") + } + }, + legend: { display: false }, + layout: { + padding: { + left: 0, + top: 0, + right: 0, + bottom: 0 + } + }, + scales: { + yAxes: [ + { + stacked: true, + display: true, + ticks: { + userCallback: label => { + if (Math.floor(label) === label) return label; + }, + callback: label => number(label) + } + } + ], + xAxes: [ + { + display: true, + gridLines: { display: false }, + type: "time", + time: { + parser: "YYYY-MM-DD", + minUnit: "day" + } + } + ] + } + } + }; + }, + + _resetChart() { + if (this._chart) { + this._chart.destroy(); + this._chart = null; + } + } +}); diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 2fe345c9d0d..2f647f6631b 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -34,6 +34,7 @@ function collapseWeekly(data, average) { bucket = bucket || { x: data[i].x, y: 0 }; bucket.y += data[i].y; } + return aggregate; } @@ -389,7 +390,19 @@ export default Ember.Component.extend({ _loadReport(jsonReport) { Report.fillMissingDates(jsonReport, { filledField: "chartData" }); - if (jsonReport.chartData && jsonReport.chartData.length > 40) { + if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") { + jsonReport.chartData = jsonReport.chartData.map(chartData => { + if (chartData.length > 40) { + return { + data: collapseWeekly(chartData.data), + req: chartData.req, + color: chartData.color + }; + } else { + return chartData; + } + }); + } else if (jsonReport.chartData && jsonReport.chartData.length > 40) { jsonReport.chartData = collapseWeekly( jsonReport.chartData, jsonReport.average diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index e5a7634fb36..ae0205b9d84 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -473,11 +473,26 @@ Report.reopenClass({ .utc(report[endDate]) .locale("en") .format("YYYY-MM-DD"); - report[filledField] = fillMissingDates( - JSON.parse(JSON.stringify(report[dataField])), - startDateFormatted, - endDateFormatted - ); + + if (report.modes[0] === "stacked_chart") { + report[filledField] = report[dataField].map(rep => { + return { + req: rep.req, + color: rep.color, + data: fillMissingDates( + JSON.parse(JSON.stringify(rep.data)), + startDateFormatted, + endDateFormatted + ) + }; + }); + } else { + report[filledField] = fillMissingDates( + JSON.parse(JSON.stringify(report[dataField])), + startDateFormatted, + endDateFormatted + ); + } } }, diff --git a/app/assets/javascripts/admin/templates/components/admin-report-stacked-chart.hbs b/app/assets/javascripts/admin/templates/components/admin-report-stacked-chart.hbs new file mode 100644 index 00000000000..9c36cfacea9 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-stacked-chart.hbs @@ -0,0 +1,3 @@ +
+ +
diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index 000fcf152c5..9921a92ee32 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -14,6 +14,11 @@
+ {{admin-report + dataSourceName="consolidated_page_views" + forcedModes="stacked-chart" + filters=filters}} + {{admin-report dataSourceName="signups" showTrend=true diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index f462ec295e6..17ccf65e94f 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -998,6 +998,7 @@ table#user-badges { @import "common/admin/admin_report"; @import "common/admin/admin_report_counters"; @import "common/admin/admin_report_chart"; +@import "common/admin/admin_report_stacked_chart"; @import "common/admin/admin_report_table"; @import "common/admin/admin_report_inline_table"; @import "common/admin/dashboard_previous"; diff --git a/app/assets/stylesheets/common/admin/admin_report_stacked_chart.scss b/app/assets/stylesheets/common/admin/admin_report_stacked_chart.scss new file mode 100644 index 00000000000..bbbd92c6e07 --- /dev/null +++ b/app/assets/stylesheets/common/admin/admin_report_stacked_chart.scss @@ -0,0 +1,9 @@ +.admin-report-stacked-chart { + display: flex; + justify-content: space-between; + + .chart-canvas-container { + flex: 5; + margin: 0; + } +} diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index 5a0b6da568a..eebb8879869 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -144,6 +144,11 @@ -ms-grid-rows: 1fr 1fr; .admin-report { -ms-grid-column-span: 4; + + &.consolidated-page-views { + -ms-grid-column-span: 12; + } + &:nth-of-type(1) { -ms-grid-row: 1; -ms-grid-column: 1; @@ -172,6 +177,10 @@ .admin-report { grid-column: span 4; + + &.consolidated-page-views { + grid-column: span 12; + } } @include breakpoint(medium) { diff --git a/app/models/report.rb b/app/models/report.rb index fa110517900..57a40b06743 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -196,6 +196,47 @@ class Report report end + def self.report_consolidated_page_views(report) + filters = %w[ + page_view_crawler + page_view_logged_in + page_view_anon + ] + + report.modes = [:stacked_chart] + + tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' + danger = ColorScheme.hex_for_name('danger') || 'e45735' + + requests = filters.map do |filter| + color = report.rgba_color(tertiary) + + if filter == "page_view_anon" + color = report.rgba_color(tertiary, 0.5) + end + + if filter == "page_view_crawler" + color = report.rgba_color(danger, 0.75) + end + + { + req: filter, + color: color, + data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) + } + end + + requests.each do |request| + request[:data] = request[:data].where('date >= ? AND date <= ?', report.start_date, report.end_date) + .order(date: :asc) + .group(:date) + .sum(:count) + .map { |date, count| { x: date, y: count } } + end + + report.data = requests + end + def self.req_report(report, filter = nil) data = if filter == :page_view_total @@ -1505,16 +1546,6 @@ class Report end end - private - - def hex_to_rgbs(hex_color) - hex_color = hex_color.gsub('#', '') - rgbs = hex_color.scan(/../) - rgbs - .map! { |color| color.hex } - .map! { |rgb| rgb.to_i } - end - def rgba_color(hex, opacity = 1) if hex.size == 3 chars = hex.scan(/\w/) @@ -1529,4 +1560,14 @@ class Report "rgba(#{rgbs.join(',')},#{opacity})" end + + private + + def hex_to_rgbs(hex_color) + hex_color = hex_color.gsub('#', '') + rgbs = hex_color.scan(/../) + rgbs + .map! { |color| color.hex } + .map! { |rgb| rgb.to_i } + end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e0b34fe7b8f..5dc09aa8436 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -967,6 +967,11 @@ en: xaxis: "Day" yaxis: "Number of new contributors" description: "Number of users who made their first post during this period." + consolidated_page_views: + title: "Consolidated Pageviews" + xaxis: "Pagesviews" + yaxis: "Day" + description: "Pageviews for logged in users, anonymous users and crawlers." dau_by_mau: title: "DAU/MAU" xaxis: "Day" diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 5bc013e0af9..04f09de81a9 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -1097,4 +1097,43 @@ describe Report do include_examples "no data" end + + describe "report_top_uploads" do + after do + ApplicationRequest.clear_cache! + end + + let(:reports) { Report.find('consolidated_page_views') } + + context "with no data" do + it "works" do + reports.data.each do |report| + expect(report[:data]).to be_empty + end + end + end + + context "with data" do + it "works" do + freeze_time(Time.now.at_midnight) + 3.times { ApplicationRequest.increment!(:page_view_crawler) } + 2.times { ApplicationRequest.increment!(:page_view_logged_in) } + ApplicationRequest.increment!(:page_view_anon) + ApplicationRequest.write_cache! + + page_view_crawler_report = reports.data.find { |r| r[:req] == "page_view_crawler" } + page_view_logged_in_report = reports.data.find { |r| r[:req] == "page_view_logged_in" } + page_view_anon_report = reports.data.find { |r| r[:req] == "page_view_anon" } + + expect(page_view_crawler_report[:color]).to eql("rgba(228,87,53,0.75)") + expect(page_view_crawler_report[:data][0][:y]).to eql(3) + + expect(page_view_logged_in_report[:color]).to eql("rgba(0,136,204,1)") + expect(page_view_logged_in_report[:data][0][:y]).to eql(2) + + expect(page_view_anon_report[:color]).to eql("rgba(0,136,204,0.5)") + expect(page_view_anon_report[:data][0][:y]).to eql(1) + end + end + end end