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() {
|
||||
this.route('admin', { resetNamespace: true }, function() {
|
||||
this.route('dashboard', { path: '/' });
|
||||
this.route('dashboard_next', { path: '/dashboard_next' });
|
||||
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
|
||||
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/flagging";
|
||||
@import "common/admin/dashboard_next";
|
||||
@import "common/admin/moderation_history";
|
||||
@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"
|
||||
|
||||
resources :dashboard_next, only: [:index]
|
||||
|
||||
resources :dashboard, only: [:index] do
|
||||
collection do
|
||||
get "problems"
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue