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:
Joffrey JAFFEUX 2018-04-16 10:42:06 +02:00 committed by GitHub
parent 223379e21a
commit 0e15a575f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 454 additions and 3 deletions

View File

@ -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);
}
});

View File

@ -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);
})
}
});

View File

@ -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}`;
}
});

View File

@ -0,0 +1 @@
export default Discourse.Route.extend({});

View File

@ -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} );
}); });

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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";

View File

@ -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%;
}
}
}

View File

@ -0,0 +1,2 @@
class Admin::DashboardNextController < Admin::AdminController
end

View File

@ -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