mirror of
https://github.com/discourse/discourse.git
synced 2025-03-09 14:34:35 +00:00
dashboard next: activity metrics and new contributors
This commit also introduces a better grouping of data points.
This commit is contained in:
parent
b26e27bdab
commit
9fabf2543b
@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax';
|
|||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
classNames: ["dashboard-table", "dashboard-inline-table"],
|
classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
|
||||||
|
|
||||||
classNameBindings: ["isLoading"],
|
classNameBindings: ["isLoading"],
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ajax } from 'discourse/lib/ajax';
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
import loadScript from 'discourse/lib/load-script';
|
import loadScript from "discourse/lib/load-script";
|
||||||
|
import Report from "admin/models/report";
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
classNames: ["dashboard-mini-chart"],
|
classNames: ["dashboard-mini-chart"],
|
||||||
@ -17,12 +18,26 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super();
|
this._super();
|
||||||
this._initializeChart();
|
|
||||||
|
if (this.get("model")) {
|
||||||
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||||
|
this._setPropertiesFromModel(this.get("model"));
|
||||||
|
this._drawChart();
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
didUpdateAttrs() {
|
didUpdateAttrs() {
|
||||||
this._super();
|
this._super();
|
||||||
this._initializeChart();
|
|
||||||
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||||
|
if (this.get("model") && !this.get("values")) {
|
||||||
|
this._setPropertiesFromModel(this.get("model"));
|
||||||
|
this._drawChart();
|
||||||
|
} else if (this.get("dataSource")) {
|
||||||
|
this._fetchReport();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("dataSourceName")
|
@computed("dataSourceName")
|
||||||
@ -34,10 +49,17 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
@computed("trend")
|
@computed("trend")
|
||||||
trendIcon(trend) {
|
trendIcon(trend) {
|
||||||
if (trend === "stable") {
|
switch (trend) {
|
||||||
return null;
|
case "trending-up":
|
||||||
} else {
|
return "angle-up";
|
||||||
return `angle-${trend}`;
|
case "trending-down":
|
||||||
|
return "angle-down";
|
||||||
|
case "high-trending-up":
|
||||||
|
return "angle-double-up";
|
||||||
|
case "high-trending-down":
|
||||||
|
return "angle-double-down";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -46,7 +68,9 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
this.set("isLoading", true);
|
this.set("isLoading", true);
|
||||||
|
|
||||||
let payload = {data: {}};
|
let payload = {
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
|
||||||
if (this.get("startDate")) {
|
if (this.get("startDate")) {
|
||||||
payload.data.start_date = this.get("startDate").toISOString();
|
payload.data.start_date = this.get("startDate").toISOString();
|
||||||
@ -58,7 +82,7 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
ajax(this.get("dataSource"), payload)
|
ajax(this.get("dataSource"), payload)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this._setPropertiesFromModel(response.report);
|
this._setPropertiesFromModel(Report.create(response.report));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.set("isLoading", false);
|
this.set("isLoading", false);
|
||||||
@ -71,17 +95,6 @@ export default Ember.Component.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_initializeChart() {
|
|
||||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
||||||
if (this.get("model") && !this.get("values")) {
|
|
||||||
this._setPropertiesFromModel(this.get("model"));
|
|
||||||
this._drawChart();
|
|
||||||
} else if (this.get("dataSource")) {
|
|
||||||
this._fetchReport();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_drawChart() {
|
_drawChart() {
|
||||||
const $chartCanvas = this.$(".chart-canvas");
|
const $chartCanvas = this.$(".chart-canvas");
|
||||||
if (!$chartCanvas.length) return;
|
if (!$chartCanvas.length) return;
|
||||||
@ -91,7 +104,7 @@ export default Ember.Component.extend({
|
|||||||
const data = {
|
const data = {
|
||||||
labels: this.get("labels"),
|
labels: this.get("labels"),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: this.get("values"),
|
data: Ember.makeArray(this.get("values")),
|
||||||
backgroundColor: this.get("backgroundColor"),
|
backgroundColor: this.get("backgroundColor"),
|
||||||
borderColor: this.get("borderColor")
|
borderColor: this.get("borderColor")
|
||||||
}]
|
}]
|
||||||
@ -100,72 +113,64 @@ 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) {
|
_setPropertiesFromModel(report) {
|
||||||
|
const oneDataPoint = (this.get("startDate") && this.get("endDate")) &&
|
||||||
|
this.get("startDate").isSame(this.get("endDate"), "day");
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
labels: model.data.map(r => r.x),
|
oneDataPoint,
|
||||||
values: model.data.map(r => r.y),
|
labels: report.get("data").map(r => r.x),
|
||||||
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
|
values: report.get("data").map(r => r.y),
|
||||||
this.get("startDate").isSame(this.get("endDate"), 'day'),
|
total: report.get("total"),
|
||||||
total: model.total,
|
description: report.get("description"),
|
||||||
title: model.title,
|
title: report.get("title"),
|
||||||
trend: this._computeTrend(model.total, model.prev30Days)
|
trend: report.get("sevenDayTrend"),
|
||||||
|
prev30Days: report.get("prev30Days"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_buildChartConfig(data) {
|
_buildChartConfig(data) {
|
||||||
const values = this.get("values");
|
const values = data.datasets[0].data;
|
||||||
const max = Math.max(...values);
|
const max = Math.max(...values);
|
||||||
const min = Math.min(...values);
|
const min = Math.min(...values);
|
||||||
const stepSize = Math.max(...[Math.ceil((max - min)/5), 20]);
|
|
||||||
|
|
||||||
const startDate = this.get("startDate") || moment();
|
const stepSize = Math.max(...[Math.ceil((max - min) / 5) * 5, 20]);
|
||||||
const endDate = this.get("endDate") || moment();
|
|
||||||
const datesDifference = startDate.diff(endDate, "days");
|
|
||||||
let unit = "day";
|
|
||||||
if (datesDifference >= 366) {
|
|
||||||
unit = "quarter";
|
|
||||||
} else if (datesDifference >= 61) {
|
|
||||||
unit = "month";
|
|
||||||
} else if (datesDifference >= 14) {
|
|
||||||
unit = "week";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "line",
|
type: "line",
|
||||||
data,
|
data,
|
||||||
options: {
|
options: {
|
||||||
legend: { display: false },
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
responsive: true,
|
responsive: true,
|
||||||
layout: { padding: { left: 0, top: 0, right: 0, bottom: 0 } },
|
maintainAspectRatio: false,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
yAxes: [
|
yAxes: [{
|
||||||
{
|
display: true,
|
||||||
display: true,
|
ticks: {
|
||||||
ticks: { suggestedMin: 0, stepSize, suggestedMax: max + stepSize }
|
suggestedMin: 0,
|
||||||
|
stepSize,
|
||||||
|
suggestedMax: max + stepSize
|
||||||
}
|
}
|
||||||
],
|
}],
|
||||||
xAxes: [
|
xAxes: [{
|
||||||
{
|
display: true,
|
||||||
display: true,
|
type: "time",
|
||||||
type: "time",
|
time: {
|
||||||
time: {
|
parser: "YYYY-MM-DD"
|
||||||
parser: "YYYY-MM-DD",
|
|
||||||
unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
_computeTrend(total, prevTotal) {
|
|
||||||
const percentChange = ((total - prevTotal) / prevTotal) * 100;
|
|
||||||
|
|
||||||
if (percentChange > 50) return "double-up";
|
|
||||||
if (percentChange > 0) return "up";
|
|
||||||
if (percentChange === 0) return "stable";
|
|
||||||
if (percentChange < 50) return "double-down";
|
|
||||||
if (percentChange < 0) return "down";
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
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 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"],
|
||||||
@ -14,20 +9,17 @@ export default Ember.Controller.extend({
|
|||||||
dashboardFetchedAt: null,
|
dashboardFetchedAt: null,
|
||||||
exceptionController: Ember.inject.controller('exception'),
|
exceptionController: Ember.inject.controller('exception'),
|
||||||
|
|
||||||
|
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||||
|
|
||||||
fetchDashboard() {
|
fetchDashboard() {
|
||||||
if (this.get("isLoading")) return;
|
if (this.get("isLoading")) return;
|
||||||
|
|
||||||
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
|
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
|
||||||
this.set("isLoading", true);
|
this.set("isLoading", true);
|
||||||
|
|
||||||
AdminDashboardNext.find().then(d => {
|
AdminDashboardNext.find().then(adminDashboardNextModel => {
|
||||||
this.set("dashboardFetchedAt", new Date());
|
this.set("dashboardFetchedAt", new Date());
|
||||||
|
this.set("model", adminDashboardNextModel);
|
||||||
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 => {
|
}).catch(e => {
|
||||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||||
this.replaceRoute("exception");
|
this.replaceRoute("exception");
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { ajax } from 'discourse/lib/ajax';
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import Report from "admin/models/report";
|
||||||
|
|
||||||
|
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"];
|
||||||
|
|
||||||
|
const REPORTS = [ "global_reports", "user_reports" ];
|
||||||
|
|
||||||
const AdminDashboardNext = Discourse.Model.extend({});
|
const AdminDashboardNext = Discourse.Model.extend({});
|
||||||
|
|
||||||
AdminDashboardNext.reopenClass({
|
AdminDashboardNext.reopenClass({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||||
has expired and the server must collect the data again.
|
has expired and the server must collect the data again.
|
||||||
@ -11,13 +15,26 @@ AdminDashboardNext.reopenClass({
|
|||||||
@method find
|
@method find
|
||||||
@return {jqXHR} a jQuery Promise object
|
@return {jqXHR} a jQuery Promise object
|
||||||
**/
|
**/
|
||||||
find: function() {
|
find() {
|
||||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||||
var model = AdminDashboardNext.create(json);
|
var model = AdminDashboardNext.create();
|
||||||
model.set('loaded', true);
|
|
||||||
|
const reports = {};
|
||||||
|
REPORTS.forEach(name => json[name].forEach(r => {
|
||||||
|
if (!reports[name]) reports[name] = {};
|
||||||
|
reports[name][r.type] = Report.create(r);
|
||||||
|
}));
|
||||||
|
model.set("reports", reports);
|
||||||
|
|
||||||
|
const attributes = {};
|
||||||
|
ATTRIBUTES.forEach(a => attributes[a] = json[a]);
|
||||||
|
model.set("attributes", attributes);
|
||||||
|
|
||||||
|
model.set("loaded", true);
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AdminDashboardNext;
|
export default AdminDashboardNext;
|
||||||
|
@ -60,12 +60,18 @@ const Report = Discourse.Model.extend({
|
|||||||
sevenDayTrend() {
|
sevenDayTrend() {
|
||||||
const currentPeriod = this.valueFor(1, 7);
|
const currentPeriod = this.valueFor(1, 7);
|
||||||
const prevPeriod = this.valueFor(8, 14);
|
const prevPeriod = this.valueFor(8, 14);
|
||||||
if (currentPeriod > prevPeriod) {
|
const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
|
||||||
|
|
||||||
|
if (change > 50) {
|
||||||
|
return "high-trending-up";
|
||||||
|
} else if (change > 0) {
|
||||||
return "trending-up";
|
return "trending-up";
|
||||||
} else if (currentPeriod < prevPeriod) {
|
} else if (change === 0) {
|
||||||
return "trending-down";
|
|
||||||
} else {
|
|
||||||
return "no-change";
|
return "no-change";
|
||||||
|
} else if (change < -50) {
|
||||||
|
return "high-trending-down";
|
||||||
|
} else if (change < 0) {
|
||||||
|
return "trending-down";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
<h3>{{title}}</h3>
|
<h3>{{title}}</h3>
|
||||||
|
|
||||||
{{#if help}}
|
{{#if description}}
|
||||||
{{d-icon "question-circle" title=help}}
|
<span title={{description}}>
|
||||||
|
{{d-icon "question-circle"}}
|
||||||
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -14,7 +16,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="chart-trend {{trend}}">
|
<div class="chart-trend {{trend}}">
|
||||||
<span>{{number total}}</span>
|
<span>{{number prev30Days}}</span>
|
||||||
|
|
||||||
{{#if trendIcon}}
|
{{#if trendIcon}}
|
||||||
{{d-icon trendIcon}}
|
{{d-icon trendIcon}}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{{plugin-outlet name="admin-dashboard-top"}}
|
{{plugin-outlet name="admin-dashboard-top"}}
|
||||||
{{lastRefreshedAt}}
|
|
||||||
<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>
|
||||||
@ -9,31 +9,61 @@
|
|||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
{{dashboard-mini-chart
|
{{dashboard-mini-chart
|
||||||
model=global_reports_signups
|
model=model.reports.global_reports.signups
|
||||||
dataSourceName="signups"
|
dataSourceName="signups"
|
||||||
startDate=startDate
|
startDate=startDate
|
||||||
endDate=endDate
|
endDate=endDate}}
|
||||||
help="admin.dashboard.charts.signups.help"}}
|
|
||||||
|
|
||||||
{{dashboard-mini-chart
|
{{dashboard-mini-chart
|
||||||
model=global_reports_topics
|
model=model.reports.global_reports.topics
|
||||||
dataSourceName="topics"
|
dataSourceName="topics"
|
||||||
startDate=startDate
|
startDate=startDate
|
||||||
endDate=endDate
|
endDate=endDate}}
|
||||||
help="admin.dashboard.charts.topics.help"}}
|
|
||||||
|
{{dashboard-mini-chart
|
||||||
|
model=model.reports.global_reports.new_contributors
|
||||||
|
dataSourceName="new_contributors"
|
||||||
|
startDate=startDate
|
||||||
|
endDate=endDate}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-columns">
|
<div class="section-columns">
|
||||||
<div class="section-column">
|
<div class="section-column">
|
||||||
|
<div class="dashboard-table">
|
||||||
|
<div class="table-title">
|
||||||
|
<h3>{{i18n "admin.dashboard.activity_metrics"}}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
||||||
|
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
||||||
|
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
||||||
|
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
||||||
|
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{admin-report-counts report=model.reports.global_reports.topics}}
|
||||||
|
{{admin-report-counts report=model.reports.global_reports.signups}}
|
||||||
|
{{admin-report-counts report=model.reports.global_reports.new_contributors}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{dashboard-inline-table
|
{{dashboard-inline-table
|
||||||
model=user_reports_users_by_type
|
model=model.reports.user_reports.users_by_type
|
||||||
lastRefreshedAt=lastRefreshedAt
|
lastRefreshedAt=lastRefreshedAt
|
||||||
isLoading=isLoading}}
|
isLoading=isLoading}}
|
||||||
|
|
||||||
{{dashboard-inline-table
|
{{dashboard-inline-table
|
||||||
model=user_reports_users_by_trust_level
|
model=model.reports.user_reports.users_by_trust_level
|
||||||
lastRefreshedAt=lastRefreshedAt
|
lastRefreshedAt=lastRefreshedAt
|
||||||
isLoading=isLoading}}
|
isLoading=isLoading}}
|
||||||
|
|
||||||
@ -44,7 +74,7 @@
|
|||||||
<div class="backups">
|
<div class="backups">
|
||||||
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
||||||
<p>
|
<p>
|
||||||
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}})
|
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||||
<br />
|
<br />
|
||||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||||
</p>
|
</p>
|
||||||
@ -54,7 +84,7 @@
|
|||||||
<div class="uploads">
|
<div class="uploads">
|
||||||
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
|
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
|
||||||
<p>
|
<p>
|
||||||
{{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}})
|
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -965,13 +965,13 @@ table.api-keys {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.trending-up {
|
&.high-trending-up, &.trending-up {
|
||||||
i.up {
|
i.up {
|
||||||
color: $success;
|
color: $success;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.trending-down {
|
&.high-trending-down, &.trending-down {
|
||||||
i.down {
|
i.down {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -986,10 +986,10 @@ table.api-keys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr.reverse-colors {
|
tr.reverse-colors {
|
||||||
td.value.trending-down i.down {
|
td.value.high-trending-down i.down, td.value.trending-down i.down {
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
td.value.trending-up i.up {
|
td.value.high-trending-up i.up, td.value.trending-up i.up {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,10 @@
|
|||||||
.dashboard-table {
|
.dashboard-table {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
&.fixed table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-loading {
|
&.is-loading {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
@ -59,7 +63,6 @@
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
border: 1px solid $primary-low-mid;
|
border: 1px solid $primary-low-mid;
|
||||||
table-layout: fixed;
|
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
@ -67,6 +70,10 @@
|
|||||||
th {
|
th {
|
||||||
border: 1px solid $primary-low-mid;
|
border: 1px solid $primary-low-mid;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +84,31 @@
|
|||||||
border: 1px solid $primary-low-mid;
|
border: 1px solid $primary-low-mid;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.value {
|
||||||
|
i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.high-trending-up, &.trending-up {
|
||||||
|
i.up {
|
||||||
|
color: $success;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.high-trending-down, &.trending-down {
|
||||||
|
i.down {
|
||||||
|
color: $danger;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.no-change {
|
||||||
|
i.down {
|
||||||
|
display: inline;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,13 +142,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.double-up, &.up {
|
&.high-trending-up, &.trending-up {
|
||||||
.chart-trend, .data-point {
|
.chart-trend, .data-point {
|
||||||
color: rgb(17, 141, 0);
|
color: rgb(17, 141, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.double-down, &.down {
|
&.high-trending-down, &.trending-down {
|
||||||
.chart-trend, .data-point {
|
.chart-trend, .data-point {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
@ -145,13 +177,14 @@
|
|||||||
.chart-container {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-trend {
|
.chart-trend {
|
||||||
font-size: $font-up-5;
|
font-size: $font-up-5;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1.5em;
|
right: 40px;
|
||||||
top: .5em;
|
top: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -4,7 +4,8 @@ class AdminDashboardNextData
|
|||||||
GLOBAL_REPORTS ||= [
|
GLOBAL_REPORTS ||= [
|
||||||
'signups',
|
'signups',
|
||||||
'topics',
|
'topics',
|
||||||
'trending_search'
|
'trending_search',
|
||||||
|
'new_contributors'
|
||||||
]
|
]
|
||||||
|
|
||||||
USER_REPORTS ||= [
|
USER_REPORTS ||= [
|
||||||
|
@ -20,6 +20,7 @@ class Report
|
|||||||
title: I18n.t("reports.#{type}.title"),
|
title: I18n.t("reports.#{type}.title"),
|
||||||
xaxis: I18n.t("reports.#{type}.xaxis"),
|
xaxis: I18n.t("reports.#{type}.xaxis"),
|
||||||
yaxis: I18n.t("reports.#{type}.yaxis"),
|
yaxis: I18n.t("reports.#{type}.yaxis"),
|
||||||
|
description: I18n.t("reports.#{type}.description"),
|
||||||
data: data,
|
data: data,
|
||||||
total: total,
|
total: total,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
@ -109,6 +110,10 @@ class Report
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.report_new_contributors(report)
|
||||||
|
report_about report, User.real, :count_by_first_post
|
||||||
|
end
|
||||||
|
|
||||||
def self.report_profile_views(report)
|
def self.report_profile_views(report)
|
||||||
start_date = report.start_date.to_date
|
start_date = report.start_date.to_date
|
||||||
end_date = report.end_date.to_date
|
end_date = report.end_date.to_date
|
||||||
|
@ -21,6 +21,7 @@ class Topic < ActiveRecord::Base
|
|||||||
include Searchable
|
include Searchable
|
||||||
include LimitedEdit
|
include LimitedEdit
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
|
include DateGroupable
|
||||||
|
|
||||||
def_delegator :featured_users, :user_ids, :featured_user_ids
|
def_delegator :featured_users, :user_ids, :featured_user_ids
|
||||||
def_delegator :featured_users, :choose, :feature_topic_users
|
def_delegator :featured_users, :choose, :feature_topic_users
|
||||||
@ -458,9 +459,9 @@ class Topic < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.listable_count_per_day(start_date, end_date, category_id = nil)
|
def self.listable_count_per_day(start_date, end_date, category_id = nil)
|
||||||
result = listable_topics.where('created_at >= ? and created_at <= ?', start_date, end_date)
|
result = listable_topics.smart_group_by_date("topics.created_at", start_date, end_date)
|
||||||
result = result.where(category_id: category_id) if category_id
|
result = result.where(category_id: category_id) if category_id
|
||||||
result.group('date(created_at)').order('date(created_at)').count
|
result.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def private_message?
|
def private_message?
|
||||||
|
@ -19,6 +19,7 @@ class User < ActiveRecord::Base
|
|||||||
include Roleable
|
include Roleable
|
||||||
include HasCustomFields
|
include HasCustomFields
|
||||||
include SecondFactorManager
|
include SecondFactorManager
|
||||||
|
include DateGroupable
|
||||||
|
|
||||||
# TODO: Remove this after 7th Jan 2018
|
# TODO: Remove this after 7th Jan 2018
|
||||||
self.ignored_columns = %w{email}
|
self.ignored_columns = %w{email}
|
||||||
@ -829,13 +830,20 @@ class User < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.count_by_signup_date(start_date, end_date, group_id = nil)
|
def self.count_by_signup_date(start_date, end_date, group_id = nil)
|
||||||
result = where('users.created_at >= ? AND users.created_at <= ?', start_date, end_date)
|
result = smart_group_by_date("users.created_at", start_date, end_date)
|
||||||
|
|
||||||
if group_id
|
if group_id
|
||||||
result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id")
|
result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id")
|
||||||
result = result.where("group_users.group_id = ?", group_id)
|
result = result.where("group_users.group_id = ?", group_id)
|
||||||
end
|
end
|
||||||
result.group('date(users.created_at)').order('date(users.created_at)').count
|
|
||||||
|
result.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.count_by_first_post(start_date, end_date)
|
||||||
|
joins('INNER JOIN user_stats AS us ON us.user_id = users.id')
|
||||||
|
.smart_group_by_date("us.first_post_created_at", start_date, end_date)
|
||||||
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def secure_category_ids
|
def secure_category_ids
|
||||||
|
@ -2742,12 +2742,7 @@ en:
|
|||||||
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?
|
whats_new_in_discourse: What’s new in Discourse?
|
||||||
|
activity_metrics: Activity Metrics
|
||||||
charts:
|
|
||||||
signups:
|
|
||||||
help: Users created for this period
|
|
||||||
topics:
|
|
||||||
help: Topics created for this period
|
|
||||||
|
|
||||||
reports:
|
reports:
|
||||||
today: "Today"
|
today: "Today"
|
||||||
|
@ -840,6 +840,12 @@ en:
|
|||||||
title: "New Users"
|
title: "New Users"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
yaxis: "Number of new users"
|
yaxis: "Number of new users"
|
||||||
|
description: "Users created for this period"
|
||||||
|
new_contributors:
|
||||||
|
title: "New Contributors"
|
||||||
|
xaxis: "Day"
|
||||||
|
yaxis: "Number of new contributors"
|
||||||
|
description: "Number of users who made their first contribution"
|
||||||
profile_views:
|
profile_views:
|
||||||
title: "User Profile Views"
|
title: "User Profile Views"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
@ -848,6 +854,7 @@ en:
|
|||||||
title: "Topics"
|
title: "Topics"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
yaxis: "Number of new topics"
|
yaxis: "Number of new topics"
|
||||||
|
description: "Topics created for this period"
|
||||||
posts:
|
posts:
|
||||||
title: "Posts"
|
title: "Posts"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
|
@ -250,6 +250,36 @@ describe Report do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'new contributors report' do
|
||||||
|
let(:report) { Report.find('new_contributors') }
|
||||||
|
|
||||||
|
context "no contributors" do
|
||||||
|
it "returns an empty report" do
|
||||||
|
expect(report.data).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with contributors" do
|
||||||
|
before do
|
||||||
|
jeff = Fabricate(:user)
|
||||||
|
jeff.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 1.day.ago)
|
||||||
|
|
||||||
|
regis = Fabricate(:user)
|
||||||
|
regis.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 2.days.ago)
|
||||||
|
|
||||||
|
hawk = Fabricate(:user)
|
||||||
|
hawk.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 2.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a report with data" do
|
||||||
|
expect(report.data).to be_present
|
||||||
|
|
||||||
|
expect(report.data[0][:y]).to eq 2
|
||||||
|
expect(report.data[1][:y]).to eq 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'users by types level report' do
|
describe 'users by types level report' do
|
||||||
let(:report) { Report.find('users_by_type') }
|
let(:report) { Report.find('users_by_type') }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user