dashboard next: perf and UI tweaks

* cache CORE reports
* adds backups/uploads section
* few css tweaks
This commit is contained in:
Joffrey JAFFEUX 2018-04-18 21:30:41 +02:00 committed by GitHub
parent 5b93d69939
commit 01c061d20d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 269 additions and 55 deletions

View File

@ -16,23 +16,24 @@ export default Ember.Component.extend({
backgroundColor: "rgba(200,220,240,0.3)",
borderColor: "#08C",
didInsertElement() {
this._super();
loadScript("/javascripts/Chart.min.js").then(() => {
this.fetchReport();
});
},
didUpdateAttrs() {
this._super();
this.fetchReport();
loadScript("/javascripts/Chart.min.js").then(() => {
if (this.get("model") && !this.get("chartData")) {
this._setPropertiesFromModel(this.get("model"));
this._drawChart();
} else if (this.get("dataSource")) {
this._fetchReport();
}
});
},
@computed("dataSourceName")
dataSource(dataSourceName) {
return `/admin/reports/${dataSourceName}`;
if (dataSourceName) {
return `/admin/reports/${dataSourceName}`;
}
},
@computed("trend")
@ -44,7 +45,7 @@ export default Ember.Component.extend({
}
},
fetchReport() {
_fetchReport() {
if (this.get("isLoading")) return;
this.set("isLoading", true);
@ -61,29 +62,20 @@ export default Ember.Component.extend({
ajax(this.get("dataSource"), payload)
.then((response) => {
const report = response.report;
this.setProperties({
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
this.get("startDate").isSame(this.get("endDate"), 'day'),
total: report.total,
title: report.title,
trend: this._computeTrend(report.total, report.prev30Days),
chartData: report.data
});
this._setPropertiesFromModel(response.report);
})
.finally(() => {
this.set("isLoading", false);
Ember.run.schedule("afterRender", () => {
if (!this.get("oneDataPoint")) {
this.drawChart();
this._drawChart();
}
});
});
},
drawChart() {
_drawChart() {
const context = this.$(".chart-canvas")[0].getContext("2d");
const data = {
@ -98,6 +90,17 @@ export default Ember.Component.extend({
this._chart = new window.Chart(context, this._buildChartConfig(data));
},
_setPropertiesFromModel(model) {
this.setProperties({
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
this.get("startDate").isSame(this.get("endDate"), 'day'),
total: model.total,
title: model.title,
trend: this._computeTrend(model.total, model.prev30Days),
chartData: model.data
});
},
_buildChartConfig(data) {
const values = this.get("chartData").map(d => d.y);
const max = Math.max(...values);

View File

@ -13,11 +13,24 @@ export default Ember.Component.extend({
isLoading: false,
help: null,
helpPage: null,
model: null,
didInsertElement() {
this._super();
this.fetchReport();
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")
@ -25,25 +38,28 @@ export default Ember.Component.extend({
return `/admin/reports/${dataSourceName}`;
},
fetchReport() {
_fetchReport() {
if (this.get("isLoading")) return;
this.set("isLoading", true);
ajax(this.get("dataSource"))
.then((response) => {
const report = response.report;
const data = report.data.sort((a, b) => a.x >= b.x);
this.setProperties({
labels: data.map(r => r.x),
dataset: data.map(r => r.y),
total: report.total,
title: report.title,
chartData: data
});
this._setPropertiesFromModel(response.report);
}).finally(() => {
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
});
}
});

View File

@ -1,10 +1,41 @@
import DiscourseURL from 'discourse/lib/url';
import computed from 'ember-addons/ember-computed-decorators';
import DiscourseURL from "discourse/lib/url";
import computed from "ember-addons/ember-computed-decorators";
import AdminDashboardNext from 'admin/models/admin-dashboard-next';
import Report from 'admin/models/report';
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"];
const REPORTS = [ "global_reports", "user_reports" ];
export default Ember.Controller.extend({
queryParams: ["period"],
period: "all",
isLoading: false,
dashboardFetchedAt: null,
exceptionController: Ember.inject.controller('exception'),
fetchDashboard() {
if (this.get("isLoading")) return;
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
this.set("isLoading", true);
AdminDashboardNext.find().then(d => {
this.set("dashboardFetchedAt", new Date());
const reports = {};
REPORTS.forEach(name => d[name].forEach(r => reports[`${name}_${r.type}`] = Report.create(r)));
this.setProperties(reports);
ATTRIBUTES.forEach(a => this.set(a, d[a]));
}).catch(e => {
this.get("exceptionController").set("thrown", e.jqXHR);
this.replaceRoute("exception");
}).finally(() => {
this.set("isLoading", false);
});
};
},
@computed("period")
startDate(period) {
@ -34,6 +65,16 @@ export default Ember.Controller.extend({
return period === "all" ? null : moment().endOf("day");
},
@computed("updated_at")
updatedTimestamp(updatedAt) {
return moment(updatedAt).format("LLL");
},
@computed("last_backup_taken_at")
backupTimestamp(lastBackupTakenAt) {
return moment(lastBackupTakenAt).format("LLL");
},
actions: {
changePeriod(period) {
DiscourseURL.routeTo(this._reportsForPeriodURL(period));

View File

@ -0,0 +1,23 @@
import { ajax } from 'discourse/lib/ajax';
const AdminDashboardNext = Discourse.Model.extend({});
AdminDashboardNext.reopenClass({
/**
Fetch all dashboard data. This can be an expensive request when the cached data
has expired and the server must collect the data again.
@method find
@return {jqXHR} a jQuery Promise object
**/
find: function() {
return ajax("/admin/dashboard-next.json").then(function(json) {
var model = AdminDashboardNext.create(json);
model.set('loaded', true);
return model;
});
},
});
export default AdminDashboardNext;

View File

@ -1 +1,5 @@
export default Discourse.Route.extend({});
export default Discourse.Route.extend({
setupController(controller) {
controller.fetchDashboard();
}
});

View File

@ -1,6 +1,6 @@
{{plugin-outlet name="admin-dashboard-top"}}
{{#conditional-loading-spinner condition=loading}}
<div class="community-health section">
<div class="section-title">
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
@ -10,12 +10,14 @@
<div class="section-body">
<div class="charts">
{{dashboard-mini-chart
model=global_reports_signups
dataSourceName="signups"
startDate=startDate
endDate=endDate
help="admin.dashboard.charts.signups.help"}}
{{dashboard-mini-chart
model=global_reports_topics
dataSourceName="topics"
startDate=startDate
endDate=endDate
@ -26,11 +28,45 @@
<div class="section-columns">
<div class="section-column">
{{dashboard-mini-table dataSourceName="users_by_types"}}
{{dashboard-mini-table dataSourceName="users_by_trust_level"}}
{{dashboard-mini-table model=user_reports_users_by_type isLoading=isLoading}}
{{dashboard-mini-table model=user_reports_users_by_trust_level isLoading=isLoading}}
{{#conditional-loading-spinner isLoading=isLoading}}
<div class="misc">
<div class="durability">
{{#if currentUser.admin}}
<div class="backups">
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
<p>
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}})
<br />
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
</p>
</div>
{{/if}}
<div class="uploads">
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
<p>
{{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}})
</p>
</div>
</div>
<hr />
<p class="last-dashboard-update">
{{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}}
</p>
<a rel="noopener" target="_blank" href="https://meta.discourse.org/t/discourse-2-0-0-beta6-release-notes/85241" class="btn">
{{i18n "admin.dashboard.whats_new_in_discourse"}}
</a>
</div>
{{/conditional-loading-spinner}}
</div>
<div class="section-column">
</div>
</div>
{{/conditional-loading-spinner}}

View File

@ -82,8 +82,13 @@
.dashboard-mini-chart {
flex-grow: 1;
width: calc(100% * (1/3) - 1px);
width: calc(100% * (1/3));
margin-bottom: 1em;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
&.is-loading {
height: 150px;
@ -117,18 +122,20 @@
&.one-data-point {
.chart-container {
height: 100px;
min-height: 150px;
justify-content: center;
align-items: center;
display: flex;
}
.data-point {
font-size: $font-up-5;
width: 100%;
font-size: 6em;
font-weight: bold;
padding: 1em;
border-radius: 3px;
background: rgba(200,220,240,0.3);
text-align: center;
padding: .5em 0;
}
}
}
@ -153,4 +160,15 @@
height: 100%;
}
}
.misc {
.durability {
display: flex;
justify-content: space-between;
.durability-title {
text-transform: capitalize;
}
}
}
}

View File

@ -1,2 +1,9 @@
require 'disk_space'
class Admin::DashboardNextController < Admin::AdminController
def index
dashboard_data = AdminDashboardNextData.fetch_cached_stats || Jobs::DashboardNextStats.new.execute({})
dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks?
dashboard_data[:disk_space] = DiskSpace.cached_stats
render json: dashboard_data
end
end

View File

@ -0,0 +1,20 @@
require_dependency 'admin_dashboard_data'
require_dependency 'group'
require_dependency 'group_message'
module Jobs
class DashboardNextStats < Jobs::Scheduled
every 30.minutes
def execute(args)
problems_started_at = AdminDashboardNextData.problems_started_at
if problems_started_at && problems_started_at < 2.days.ago
# If there have been problems reported on the dashboard for a while,
# send a message to admins no more often than once per week.
GroupMessage.create(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i)
end
AdminDashboardNextData.refresh_stats
end
end
end

View File

@ -0,0 +1,44 @@
class AdminDashboardNextData
include StatsCacheable
GLOBAL_REPORTS ||= [
'signups',
'topics',
]
USER_REPORTS ||= [
'users_by_trust_level',
'users_by_type'
]
def initialize(opts = {})
@opts = opts
end
def self.fetch_stats
AdminDashboardNextData.new.as_json
end
def self.stats_cache_key
'dash-next-stats'
end
def as_json(_options = nil)
@json ||= {
global_reports: AdminDashboardNextData.reports(GLOBAL_REPORTS),
user_reports: AdminDashboardNextData.reports(USER_REPORTS),
last_backup_taken_at: last_backup_taken_at,
updated_at: Time.zone.now.as_json
}
end
def last_backup_taken_at
if last_backup = Backup.all.last
File.ctime(last_backup.path).utc
end
end
def self.reports(source)
source.map { |type| Report.find(type).as_json }
end
end

View File

@ -244,15 +244,15 @@ class Report
.map { |ua, count| { x: ua, y: count } }
end
def self.report_users_by_types(report)
def self.report_users_by_type(report)
report.data = []
label = Proc.new { |key| I18n.t("reports.users_by_types.xaxis_labels.#{key}") }
label = Proc.new { |key| I18n.t("reports.users_by_type.xaxis_labels.#{key}") }
admins = User.real.where(admin: true).count
admins = User.real.admins.count
report.data << { x: label.call("admin"), y: admins } if admins > 0
moderators = User.real.where(moderator: true).count
moderators = User.real.moderators.count
report.data << { x: label.call("moderator"), y: moderators } if moderators > 0
suspended = User.real.suspended.count

View File

@ -2732,12 +2732,14 @@ en:
space_free: "{{size}} free"
uploads: "uploads"
backups: "backups"
lastest_backup: "Latest: %{date}"
traffic_short: "Traffic"
traffic: "Application web requests"
page_views: "Pageviews"
page_views_short: "Pageviews"
show_traffic_report: "Show Detailed Traffic Report"
community_health: Community health
whats_new_in_discourse: Whats new in Discourse?
charts:
signups:

View File

@ -869,8 +869,8 @@ en:
title: "Users per Trust Level"
xaxis: "Trust Level"
yaxis: "Number of Users"
users_by_types:
title: "Users per types"
users_by_type:
title: "Users per Type"
xaxis: "Type"
yaxis: "Number of Users"
xaxis_labels:

View File

@ -251,7 +251,7 @@ describe Report do
end
describe 'users by types level report' do
let(:report) { Report.find('users_by_types') }
let(:report) { Report.find('users_by_type') }
context "no users" do
it "returns an empty report" do
@ -270,7 +270,7 @@ describe Report do
it "returns a report with data" do
expect(report.data).to be_present
label = Proc.new { |key| I18n.t("reports.users_by_types.xaxis_labels.#{key}") }
label = Proc.new { |key| I18n.t("reports.users_by_type.xaxis_labels.#{key}") }
expect(report.data.find { |d| d[:x] == label.call("admin") }[:y]).to eq 3
expect(report.data.find { |d| d[:x] == label.call("moderator") }[:y]).to eq 2
expect(report.data.find { |d| d[:x] == label.call("silenced") }[:y]).to eq 1