mirror of
https://github.com/discourse/discourse.git
synced 2025-03-24 07:45:40 +08:00
FEATURE: adds a new chart report to track pageviews (#6913)
This commit is contained in:
parent
52f2e0d6b9
commit
b95165b838
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -34,6 +34,7 @@ function collapseWeekly(data, average) {
|
|||||||
bucket = bucket || { x: data[i].x, y: 0 };
|
bucket = bucket || { x: data[i].x, y: 0 };
|
||||||
bucket.y += data[i].y;
|
bucket.y += data[i].y;
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregate;
|
return aggregate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +390,19 @@ export default Ember.Component.extend({
|
|||||||
_loadReport(jsonReport) {
|
_loadReport(jsonReport) {
|
||||||
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
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 = collapseWeekly(
|
||||||
jsonReport.chartData,
|
jsonReport.chartData,
|
||||||
jsonReport.average
|
jsonReport.average
|
||||||
|
@ -473,12 +473,27 @@ Report.reopenClass({
|
|||||||
.utc(report[endDate])
|
.utc(report[endDate])
|
||||||
.locale("en")
|
.locale("en")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
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(
|
report[filledField] = fillMissingDates(
|
||||||
JSON.parse(JSON.stringify(report[dataField])),
|
JSON.parse(JSON.stringify(report[dataField])),
|
||||||
startDateFormatted,
|
startDateFormatted,
|
||||||
endDateFormatted
|
endDateFormatted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
find(type, startDate, endDate, categoryId, groupId) {
|
find(type, startDate, endDate, categoryId, groupId) {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<div class="chart-canvas-container">
|
||||||
|
<canvas class="chart-canvas" height="250"></canvas>
|
||||||
|
</div>
|
@ -14,6 +14,11 @@
|
|||||||
|
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
|
{{admin-report
|
||||||
|
dataSourceName="consolidated_page_views"
|
||||||
|
forcedModes="stacked-chart"
|
||||||
|
filters=filters}}
|
||||||
|
|
||||||
{{admin-report
|
{{admin-report
|
||||||
dataSourceName="signups"
|
dataSourceName="signups"
|
||||||
showTrend=true
|
showTrend=true
|
||||||
|
@ -998,6 +998,7 @@ table#user-badges {
|
|||||||
@import "common/admin/admin_report";
|
@import "common/admin/admin_report";
|
||||||
@import "common/admin/admin_report_counters";
|
@import "common/admin/admin_report_counters";
|
||||||
@import "common/admin/admin_report_chart";
|
@import "common/admin/admin_report_chart";
|
||||||
|
@import "common/admin/admin_report_stacked_chart";
|
||||||
@import "common/admin/admin_report_table";
|
@import "common/admin/admin_report_table";
|
||||||
@import "common/admin/admin_report_inline_table";
|
@import "common/admin/admin_report_inline_table";
|
||||||
@import "common/admin/dashboard_previous";
|
@import "common/admin/dashboard_previous";
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.admin-report-stacked-chart {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.chart-canvas-container {
|
||||||
|
flex: 5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -144,6 +144,11 @@
|
|||||||
-ms-grid-rows: 1fr 1fr;
|
-ms-grid-rows: 1fr 1fr;
|
||||||
.admin-report {
|
.admin-report {
|
||||||
-ms-grid-column-span: 4;
|
-ms-grid-column-span: 4;
|
||||||
|
|
||||||
|
&.consolidated-page-views {
|
||||||
|
-ms-grid-column-span: 12;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-of-type(1) {
|
&:nth-of-type(1) {
|
||||||
-ms-grid-row: 1;
|
-ms-grid-row: 1;
|
||||||
-ms-grid-column: 1;
|
-ms-grid-column: 1;
|
||||||
@ -172,6 +177,10 @@
|
|||||||
|
|
||||||
.admin-report {
|
.admin-report {
|
||||||
grid-column: span 4;
|
grid-column: span 4;
|
||||||
|
|
||||||
|
&.consolidated-page-views {
|
||||||
|
grid-column: span 12;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include breakpoint(medium) {
|
@include breakpoint(medium) {
|
||||||
|
@ -196,6 +196,47 @@ class Report
|
|||||||
report
|
report
|
||||||
end
|
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)
|
def self.req_report(report, filter = nil)
|
||||||
data =
|
data =
|
||||||
if filter == :page_view_total
|
if filter == :page_view_total
|
||||||
@ -1505,16 +1546,6 @@ class Report
|
|||||||
end
|
end
|
||||||
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)
|
def rgba_color(hex, opacity = 1)
|
||||||
if hex.size == 3
|
if hex.size == 3
|
||||||
chars = hex.scan(/\w/)
|
chars = hex.scan(/\w/)
|
||||||
@ -1529,4 +1560,14 @@ class Report
|
|||||||
|
|
||||||
"rgba(#{rgbs.join(',')},#{opacity})"
|
"rgba(#{rgbs.join(',')},#{opacity})"
|
||||||
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
|
||||||
end
|
end
|
||||||
|
@ -967,6 +967,11 @@ en:
|
|||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
yaxis: "Number of new contributors"
|
yaxis: "Number of new contributors"
|
||||||
description: "Number of users who made their first post during this period."
|
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:
|
dau_by_mau:
|
||||||
title: "DAU/MAU"
|
title: "DAU/MAU"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
|
@ -1097,4 +1097,43 @@ describe Report do
|
|||||||
|
|
||||||
include_examples "no data"
|
include_examples "no data"
|
||||||
end
|
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
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user