EXPERIMENTAL: new dashboard UI
This is the first iteration of an effort towards making a very good dashboard. Until we feel confident this is good, this dashboard will only be accessible through /admin/dashboard_next
This commit is contained in:
parent
223379e21a
commit
0e15a575f4
|
@ -0,0 +1,129 @@
|
||||||
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
import loadScript from 'discourse/lib/load-script';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ["mini-chart"],
|
||||||
|
|
||||||
|
classNameBindings: ["trend", "oneDataPoint"],
|
||||||
|
|
||||||
|
isLoading: false,
|
||||||
|
total: null,
|
||||||
|
trend: null,
|
||||||
|
title: null,
|
||||||
|
chartData: null,
|
||||||
|
oneDataPoint: false,
|
||||||
|
|
||||||
|
backgroundColor: "rgba(200,220,240,0.3)",
|
||||||
|
borderColor: "#08C",
|
||||||
|
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||||
|
this.fetchReport.apply(this);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
didUpdateAttrs() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
this.fetchReport.apply(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("dataSourceName")
|
||||||
|
dataSource(dataSourceName) {
|
||||||
|
return `/admin/reports/${dataSourceName}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("trend")
|
||||||
|
trendIcon(trend) {
|
||||||
|
if (trend === "stable") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return `angle-${trend}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_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";
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchReport() {
|
||||||
|
let payload = {data: {}};
|
||||||
|
|
||||||
|
if (this.get("startDate")) {
|
||||||
|
payload.data.start_date = this.get("startDate").toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get("endDate")) {
|
||||||
|
payload.data.end_date = this.get("endDate").toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set("isLoading", true);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.set("isLoading", false);
|
||||||
|
|
||||||
|
Ember.run.schedule("afterRender", () => {
|
||||||
|
if (!this.get("oneDataPoint")) {
|
||||||
|
this.drawChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
drawChart() {
|
||||||
|
const ctx = this.$(".chart-canvas")[0].getContext("2d");
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
labels: this.get("chartData").map(r => r.x),
|
||||||
|
datasets: [{
|
||||||
|
data: this.get("chartData").map(r => r.y),
|
||||||
|
backgroundColor: this.get("backgroundColor"),
|
||||||
|
borderColor: this.get("borderColor")
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: "line",
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
legend: { display: false },
|
||||||
|
responsive: true,
|
||||||
|
layout: {
|
||||||
|
padding: { left: 0, top: 0, right: 0, bottom: 0 }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
display: true,
|
||||||
|
ticks: { suggestedMin: 0 }
|
||||||
|
}],
|
||||||
|
xAxes: [{ display: true }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this._chart = new window.Chart(ctx, config);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ["mini-table"],
|
||||||
|
|
||||||
|
total: null,
|
||||||
|
labels: null,
|
||||||
|
title: null,
|
||||||
|
chartData: null,
|
||||||
|
isLoading: false,
|
||||||
|
help: null,
|
||||||
|
helpPage: null,
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
this.fetchReport.apply(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("dataSourceName")
|
||||||
|
dataSource(dataSourceName) {
|
||||||
|
return `/admin/reports/${dataSourceName}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchReport() {
|
||||||
|
this.set("isLoading", true);
|
||||||
|
|
||||||
|
ajax(this.get("dataSource")).then((response) => {
|
||||||
|
const report = response.report;
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
labels: report.data.map(r => r.x),
|
||||||
|
dataset: report.data.map(r => r.y),
|
||||||
|
total: report.total,
|
||||||
|
title: report.title,
|
||||||
|
chartData: report.data
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.set("isLoading", false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
queryParams: ["period"],
|
||||||
|
|
||||||
|
period: "all",
|
||||||
|
|
||||||
|
@computed("period")
|
||||||
|
startDate(period) {
|
||||||
|
if (period === "all") return null;
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case "yearly":
|
||||||
|
return moment().subtract(1, "year").startOf("day");
|
||||||
|
break;
|
||||||
|
case "quarterly":
|
||||||
|
return moment().subtract(3, "month").startOf("day");
|
||||||
|
break;
|
||||||
|
case "weekly":
|
||||||
|
return moment().subtract(1, "week").startOf("day");
|
||||||
|
break;
|
||||||
|
case "monthly":
|
||||||
|
return moment().subtract(1, "month").startOf("day");
|
||||||
|
break;
|
||||||
|
case "daily":
|
||||||
|
return moment().startOf("day");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("period")
|
||||||
|
endDate(period) {
|
||||||
|
if (period === "all") return null;
|
||||||
|
|
||||||
|
return moment().endOf("day");
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
changePeriod(period) {
|
||||||
|
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_reportsForPeriodURL(period) {
|
||||||
|
return `/admin/dashboard_next?period=${period}`;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export default Discourse.Route.extend({});
|
|
@ -1,6 +1,7 @@
|
||||||
export default function() {
|
export default function() {
|
||||||
this.route('admin', { resetNamespace: true }, function() {
|
this.route('admin', { resetNamespace: true }, function() {
|
||||||
this.route('dashboard', { path: '/' });
|
this.route('dashboard', { path: '/' });
|
||||||
|
this.route('dashboard_next', { path: '/dashboard_next' });
|
||||||
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
|
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
|
||||||
this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} );
|
this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} );
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{#conditional-loading-spinner condition=isLoading}}
|
||||||
|
<div class="chart-title">
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
|
||||||
|
{{d-icon "question-circle"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
|
||||||
|
{{#if oneDataPoint}}
|
||||||
|
<span class="data-point">
|
||||||
|
{{chartData.lastObject.y}}
|
||||||
|
</span>
|
||||||
|
{{else}}
|
||||||
|
<div class="chart-trend {{trend}}">
|
||||||
|
<span>{{total}}</span>
|
||||||
|
|
||||||
|
{{#if trendIcon}}
|
||||||
|
{{d-icon trendIcon}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<canvas class="chart-canvas"></canvas>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{#conditional-loading-spinner condition=isLoading}}
|
||||||
|
<div class="table-title">
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
|
||||||
|
{{#if help}}
|
||||||
|
<a href="{{helpPage}}">{{i18n help}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{{#each labels as |label|}}
|
||||||
|
<th>{{label}}</th>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{{#each dataset as |data|}}
|
||||||
|
<td>{{data}}</td>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{plugin-outlet name="admin-dashboard-top"}}
|
||||||
|
|
||||||
|
{{#conditional-loading-spinner condition=loading}}
|
||||||
|
<div class="community-health section">
|
||||||
|
<div class="section-title">
|
||||||
|
<h2>Community health</h2>
|
||||||
|
{{period-chooser period=period action="changePeriod"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="charts">
|
||||||
|
{{mini-chart dataSourceName="signups" startDate=startDate endDate=endDate}}
|
||||||
|
{{mini-chart dataSourceName="topics" startDate=startDate endDate=endDate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-columns">
|
||||||
|
<div class="section-column">
|
||||||
|
{{mini-table dataSourceName="users_by_trust_level"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-column">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{/conditional-loading-spinner}}
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
@import "common/admin/customize";
|
@import "common/admin/customize";
|
||||||
@import "common/admin/flagging";
|
@import "common/admin/flagging";
|
||||||
|
@import "common/admin/dashboard_next";
|
||||||
@import "common/admin/moderation_history";
|
@import "common/admin/moderation_history";
|
||||||
@import "common/admin/suspend";
|
@import "common/admin/suspend";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
.dashboard-next {
|
||||||
|
|
||||||
|
&.admin-contents {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-columns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.section-column {
|
||||||
|
flex: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
.section-title {
|
||||||
|
h2 {
|
||||||
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid $primary-low-mid;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
padding-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-table {
|
||||||
|
.table-title {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin: .5em;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border: 1px solid $primary-low-mid;
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
background: $primary-low;
|
||||||
|
th {
|
||||||
|
border: 1px solid $primary-low-mid;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
border: 1px solid $primary-low-mid;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.mini-chart {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: calc(100% * (1/3) - 1px);
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin: .5em;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.double-up, &.up {
|
||||||
|
.chart-trend, .data-point {
|
||||||
|
color: rgb(17, 141, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.double-down, &.down {
|
||||||
|
.chart-trend, .data-point {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.one-data-point {
|
||||||
|
.chart-container {
|
||||||
|
height: 100px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-point {
|
||||||
|
font-size: $font-up-5;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(200,220,240,0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-trend {
|
||||||
|
font-size: $font-up-5;
|
||||||
|
position: absolute;
|
||||||
|
left: 1.5em;
|
||||||
|
top: .5em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
class Admin::DashboardNextController < Admin::AdminController
|
||||||
|
end
|
|
@ -230,6 +230,8 @@ Discourse::Application.routes.draw do
|
||||||
|
|
||||||
get "version_check" => "versions#show"
|
get "version_check" => "versions#show"
|
||||||
|
|
||||||
|
resources :dashboard_next, only: [:index]
|
||||||
|
|
||||||
resources :dashboard, only: [:index] do
|
resources :dashboard, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
get "problems"
|
get "problems"
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue