dashboard next: perf and UI tweaks
* cache CORE reports * adds backups/uploads section * few css tweaks
This commit is contained in:
parent
5b93d69939
commit
01c061d20d
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
|
@ -1 +1,5 @@
|
|||
export default Discourse.Route.extend({});
|
||||
export default Discourse.Route.extend({
|
||||
setupController(controller) {
|
||||
controller.fetchDashboard();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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: What’s new in Discourse?
|
||||
|
||||
charts:
|
||||
signups:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue