mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 03:36:18 +08:00
dashboard next: perf and UI tweaks
* cache CORE reports * adds backups/uploads section * few css tweaks
This commit is contained in:
@ -16,23 +16,24 @@ export default Ember.Component.extend({
|
|||||||
backgroundColor: "rgba(200,220,240,0.3)",
|
backgroundColor: "rgba(200,220,240,0.3)",
|
||||||
borderColor: "#08C",
|
borderColor: "#08C",
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super();
|
|
||||||
|
|
||||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
||||||
this.fetchReport();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
didUpdateAttrs() {
|
didUpdateAttrs() {
|
||||||
this._super();
|
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")
|
@computed("dataSourceName")
|
||||||
dataSource(dataSourceName) {
|
dataSource(dataSourceName) {
|
||||||
|
if (dataSourceName) {
|
||||||
return `/admin/reports/${dataSourceName}`;
|
return `/admin/reports/${dataSourceName}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("trend")
|
@computed("trend")
|
||||||
@ -44,7 +45,7 @@ export default Ember.Component.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchReport() {
|
_fetchReport() {
|
||||||
if (this.get("isLoading")) return;
|
if (this.get("isLoading")) return;
|
||||||
|
|
||||||
this.set("isLoading", true);
|
this.set("isLoading", true);
|
||||||
@ -61,29 +62,20 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
ajax(this.get("dataSource"), payload)
|
ajax(this.get("dataSource"), payload)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const report = response.report;
|
this._setPropertiesFromModel(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
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.set("isLoading", false);
|
this.set("isLoading", false);
|
||||||
|
|
||||||
Ember.run.schedule("afterRender", () => {
|
Ember.run.schedule("afterRender", () => {
|
||||||
if (!this.get("oneDataPoint")) {
|
if (!this.get("oneDataPoint")) {
|
||||||
this.drawChart();
|
this._drawChart();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
drawChart() {
|
_drawChart() {
|
||||||
const context = this.$(".chart-canvas")[0].getContext("2d");
|
const context = this.$(".chart-canvas")[0].getContext("2d");
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -98,6 +90,17 @@ export default Ember.Component.extend({
|
|||||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
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) {
|
_buildChartConfig(data) {
|
||||||
const values = this.get("chartData").map(d => d.y);
|
const values = this.get("chartData").map(d => d.y);
|
||||||
const max = Math.max(...values);
|
const max = Math.max(...values);
|
||||||
|
@ -13,11 +13,24 @@ export default Ember.Component.extend({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
help: null,
|
help: null,
|
||||||
helpPage: null,
|
helpPage: null,
|
||||||
|
model: null,
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super();
|
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")
|
@computed("dataSourceName")
|
||||||
@ -25,25 +38,28 @@ export default Ember.Component.extend({
|
|||||||
return `/admin/reports/${dataSourceName}`;
|
return `/admin/reports/${dataSourceName}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchReport() {
|
_fetchReport() {
|
||||||
if (this.get("isLoading")) return;
|
if (this.get("isLoading")) return;
|
||||||
|
|
||||||
this.set("isLoading", true);
|
this.set("isLoading", true);
|
||||||
|
|
||||||
ajax(this.get("dataSource"))
|
ajax(this.get("dataSource"))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const report = response.report;
|
this._setPropertiesFromModel(response.report);
|
||||||
const data = report.data.sort((a, b) => a.x >= b.x);
|
}).finally(() => {
|
||||||
|
this.set("isLoading", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPropertiesFromModel(model) {
|
||||||
|
const data = model.data.sort((a, b) => a.x >= b.x);
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
labels: data.map(r => r.x),
|
labels: data.map(r => r.x),
|
||||||
dataset: data.map(r => r.y),
|
dataset: data.map(r => r.y),
|
||||||
total: report.total,
|
total: model.total,
|
||||||
title: report.title,
|
title: model.title,
|
||||||
chartData: data
|
chartData: data
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
|
||||||
this.set("isLoading", false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,41 @@
|
|||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
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({
|
export default Ember.Controller.extend({
|
||||||
queryParams: ["period"],
|
queryParams: ["period"],
|
||||||
|
|
||||||
period: "all",
|
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")
|
@computed("period")
|
||||||
startDate(period) {
|
startDate(period) {
|
||||||
@ -34,6 +65,16 @@ export default Ember.Controller.extend({
|
|||||||
return period === "all" ? null : moment().endOf("day");
|
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: {
|
actions: {
|
||||||
changePeriod(period) {
|
changePeriod(period) {
|
||||||
DiscourseURL.routeTo(this._reportsForPeriodURL(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"}}
|
{{plugin-outlet name="admin-dashboard-top"}}
|
||||||
|
|
||||||
{{#conditional-loading-spinner condition=loading}}
|
|
||||||
<div class="community-health section">
|
<div class="community-health section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||||
@ -10,12 +10,14 @@
|
|||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
{{dashboard-mini-chart
|
{{dashboard-mini-chart
|
||||||
|
model=global_reports_signups
|
||||||
dataSourceName="signups"
|
dataSourceName="signups"
|
||||||
startDate=startDate
|
startDate=startDate
|
||||||
endDate=endDate
|
endDate=endDate
|
||||||
help="admin.dashboard.charts.signups.help"}}
|
help="admin.dashboard.charts.signups.help"}}
|
||||||
|
|
||||||
{{dashboard-mini-chart
|
{{dashboard-mini-chart
|
||||||
|
model=global_reports_topics
|
||||||
dataSourceName="topics"
|
dataSourceName="topics"
|
||||||
startDate=startDate
|
startDate=startDate
|
||||||
endDate=endDate
|
endDate=endDate
|
||||||
@ -26,11 +28,45 @@
|
|||||||
|
|
||||||
<div class="section-columns">
|
<div class="section-columns">
|
||||||
<div class="section-column">
|
<div class="section-column">
|
||||||
{{dashboard-mini-table dataSourceName="users_by_types"}}
|
{{dashboard-mini-table model=user_reports_users_by_type isLoading=isLoading}}
|
||||||
{{dashboard-mini-table dataSourceName="users_by_trust_level"}}
|
{{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>
|
||||||
|
|
||||||
<div class="section-column">
|
<div class="section-column">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/conditional-loading-spinner}}
|
|
||||||
|
@ -82,8 +82,13 @@
|
|||||||
|
|
||||||
.dashboard-mini-chart {
|
.dashboard-mini-chart {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: calc(100% * (1/3) - 1px);
|
width: calc(100% * (1/3));
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-loading {
|
&.is-loading {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
@ -117,18 +122,20 @@
|
|||||||
|
|
||||||
&.one-data-point {
|
&.one-data-point {
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 100px;
|
min-height: 150px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-point {
|
.data-point {
|
||||||
font-size: $font-up-5;
|
width: 100%;
|
||||||
|
font-size: 6em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 1em;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: rgba(200,220,240,0.3);
|
background: rgba(200,220,240,0.3);
|
||||||
|
text-align: center;
|
||||||
|
padding: .5em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,4 +160,15 @@
|
|||||||
height: 100%;
|
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
|
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
|
end
|
||||||
|
20
app/jobs/scheduled/dashboard_next_stats.rb
Normal file
20
app/jobs/scheduled/dashboard_next_stats.rb
Normal 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
|
44
app/models/admin_dashboard_next_data.rb
Normal file
44
app/models/admin_dashboard_next_data.rb
Normal 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
|
@ -244,15 +244,15 @@ class Report
|
|||||||
.map { |ua, count| { x: ua, y: count } }
|
.map { |ua, count| { x: ua, y: count } }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.report_users_by_types(report)
|
def self.report_users_by_type(report)
|
||||||
report.data = []
|
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
|
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
|
report.data << { x: label.call("moderator"), y: moderators } if moderators > 0
|
||||||
|
|
||||||
suspended = User.real.suspended.count
|
suspended = User.real.suspended.count
|
||||||
|
@ -2732,12 +2732,14 @@ en:
|
|||||||
space_free: "{{size}} free"
|
space_free: "{{size}} free"
|
||||||
uploads: "uploads"
|
uploads: "uploads"
|
||||||
backups: "backups"
|
backups: "backups"
|
||||||
|
lastest_backup: "Latest: %{date}"
|
||||||
traffic_short: "Traffic"
|
traffic_short: "Traffic"
|
||||||
traffic: "Application web requests"
|
traffic: "Application web requests"
|
||||||
page_views: "Pageviews"
|
page_views: "Pageviews"
|
||||||
page_views_short: "Pageviews"
|
page_views_short: "Pageviews"
|
||||||
show_traffic_report: "Show Detailed Traffic Report"
|
show_traffic_report: "Show Detailed Traffic Report"
|
||||||
community_health: Community health
|
community_health: Community health
|
||||||
|
whats_new_in_discourse: What’s new in Discourse?
|
||||||
|
|
||||||
charts:
|
charts:
|
||||||
signups:
|
signups:
|
||||||
|
@ -869,8 +869,8 @@ en:
|
|||||||
title: "Users per Trust Level"
|
title: "Users per Trust Level"
|
||||||
xaxis: "Trust Level"
|
xaxis: "Trust Level"
|
||||||
yaxis: "Number of Users"
|
yaxis: "Number of Users"
|
||||||
users_by_types:
|
users_by_type:
|
||||||
title: "Users per types"
|
title: "Users per Type"
|
||||||
xaxis: "Type"
|
xaxis: "Type"
|
||||||
yaxis: "Number of Users"
|
yaxis: "Number of Users"
|
||||||
xaxis_labels:
|
xaxis_labels:
|
||||||
|
@ -251,7 +251,7 @@ describe Report do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'users by types level report' do
|
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
|
context "no users" do
|
||||||
it "returns an empty report" do
|
it "returns an empty report" do
|
||||||
@ -270,7 +270,7 @@ describe Report do
|
|||||||
it "returns a report with data" do
|
it "returns a report with data" do
|
||||||
expect(report.data).to be_present
|
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("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("moderator") }[:y]).to eq 2
|
||||||
expect(report.data.find { |d| d[:x] == label.call("silenced") }[:y]).to eq 1
|
expect(report.data.find { |d| d[:x] == label.call("silenced") }[:y]).to eq 1
|
||||||
|
Reference in New Issue
Block a user