new dashboard quality pass (code, tests and UI)
This commit is contained in:
parent
0639b902dc
commit
af548c23c4
|
@ -1,4 +1,5 @@
|
|||
import loadScript from 'discourse/lib/load-script';
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'canvas',
|
||||
|
@ -22,10 +23,16 @@ export default Ember.Component.extend({
|
|||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
callback: (label) => number(label),
|
||||
suggestedMin: 0
|
||||
}
|
||||
}]
|
||||
|
|
|
@ -4,6 +4,7 @@ import Report from 'admin/models/report';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
classNames: ["admin-reports"],
|
||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
||||
viewMode: 'graph',
|
||||
viewingTable: Em.computed.equal('viewMode', 'table'),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: ["isLoading"],
|
||||
classNameBindings: ["isLoading", "dataSourceNames"],
|
||||
reports: null,
|
||||
isLoading: false,
|
||||
dataSourceNames: "",
|
||||
|
@ -25,7 +25,6 @@ export default Ember.Mixin.create({
|
|||
// the array contains only unique values
|
||||
reports = reports.uniqBy("report_key");
|
||||
|
||||
|
||||
const sort = (r) => {
|
||||
if (r.length > 1) {
|
||||
return dataSourceNames
|
||||
|
@ -40,7 +39,6 @@ export default Ember.Mixin.create({
|
|||
return sort(reports);
|
||||
}
|
||||
|
||||
|
||||
return sort(reports.filter(report => {
|
||||
return report.report_key.includes(startDate.format("YYYYMMDD")) &&
|
||||
report.report_key.includes(endDate.format("YYYYMMDD"));
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fillMissingDates } from 'discourse/lib/utilities';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { fillMissingDates } from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("type", "start_date", "end_date")
|
||||
reportUrl(type, start_date, end_date) {
|
||||
start_date = moment(start_date).locale('en').format("YYYY-MM-DD");
|
||||
end_date = moment(end_date).locale('en').format("YYYY-MM-DD");
|
||||
start_date = moment(start_date).locale("en").format("YYYY-MM-DD");
|
||||
end_date = moment(end_date).locale("en").format("YYYY-MM-DD");
|
||||
return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`);
|
||||
},
|
||||
|
||||
valueAt(numDaysAgo) {
|
||||
if (this.data) {
|
||||
const wantedDate = moment().subtract(numDaysAgo, "days").locale('en').format("YYYY-MM-DD");
|
||||
const wantedDate = moment().subtract(numDaysAgo, "days").locale("en").format("YYYY-MM-DD");
|
||||
const item = this.data.find(d => d.x === wantedDate);
|
||||
if (item) {
|
||||
return item.y;
|
||||
|
@ -29,7 +31,7 @@ const Report = Discourse.Model.extend({
|
|||
if (this.data) {
|
||||
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
|
||||
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
|
||||
var d, sum = 0, count = 0;
|
||||
let d, sum = 0, count = 0;
|
||||
_.each(this.data, datum => {
|
||||
d = moment(datum.x);
|
||||
if (d >= earliestDate && d <= latestDate) {
|
||||
|
@ -46,6 +48,7 @@ const Report = Discourse.Model.extend({
|
|||
yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"),
|
||||
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"),
|
||||
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"),
|
||||
|
||||
lastSevenDaysCount: function() {
|
||||
return this.averageCount(7, this.valueFor(1, 7));
|
||||
}.property("data", "average"),
|
||||
|
@ -57,50 +60,22 @@ const Report = Discourse.Model.extend({
|
|||
return this.get("average") ? value / count : value;
|
||||
},
|
||||
|
||||
@computed('yesterdayCount')
|
||||
@computed("yesterdayCount")
|
||||
yesterdayTrend(yesterdayCount) {
|
||||
const yesterdayVal = yesterdayCount;
|
||||
const twoDaysAgoVal = this.valueAt(2);
|
||||
const change = ((yesterdayVal - twoDaysAgoVal) / yesterdayVal) * 100;
|
||||
|
||||
if (change > 50) {
|
||||
return "high-trending-up";
|
||||
} else if (change > 0) {
|
||||
return "trending-up";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
return this._computeTrend(this.valueAt(2), yesterdayCount);
|
||||
},
|
||||
|
||||
@computed('lastSevenDaysCount')
|
||||
sevenDayTrend(lastSevenDaysCount) {
|
||||
const currentPeriod = lastSevenDaysCount;
|
||||
const prevPeriod = this.valueFor(8, 14);
|
||||
const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
|
||||
|
||||
if (change > 50) {
|
||||
return "high-trending-up";
|
||||
} else if (change > 0) {
|
||||
return "trending-up";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
@computed("lastSevenDaysCount")
|
||||
sevenDaysTrend(lastSevenDaysCount) {
|
||||
return this._computeTrend(this.valueFor(8, 14), lastSevenDaysCount);
|
||||
},
|
||||
|
||||
@computed('data')
|
||||
@computed("data")
|
||||
currentTotal(data){
|
||||
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
|
||||
},
|
||||
|
||||
@computed('data', 'currentTotal')
|
||||
@computed("data", "currentTotal")
|
||||
currentAverage(data, total) {
|
||||
return Ember.makeArray(data).length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1));
|
||||
},
|
||||
|
@ -121,43 +96,18 @@ const Report = Discourse.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed('prev_period', 'currentTotal', 'currentAverage')
|
||||
@computed("prev_period", "currentTotal", "currentAverage")
|
||||
trend(prev, currentTotal, currentAverage) {
|
||||
const total = this.get('average') ? currentAverage : currentTotal;
|
||||
const change = ((total - prev) / total) * 100;
|
||||
|
||||
if (change > 50) {
|
||||
return "high-trending-up";
|
||||
} else if (change > 0) {
|
||||
return "trending-up";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
const total = this.get("average") ? currentAverage : currentTotal;
|
||||
return this._computeTrend(prev, total);
|
||||
},
|
||||
|
||||
@computed('prev30Days', 'lastThirtyDaysCount')
|
||||
thirtyDayTrend(prev30Days, lastThirtyDaysCount) {
|
||||
const currentPeriod = lastThirtyDaysCount;
|
||||
const change = ((currentPeriod - prev30Days) / currentPeriod) * 100;
|
||||
|
||||
if (change > 50) {
|
||||
return "high-trending-up";
|
||||
} else if (change > 0) {
|
||||
return "trending-up";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
@computed("prev30Days", "lastThirtyDaysCount")
|
||||
thirtyDaysTrend(prev30Days, lastThirtyDaysCount) {
|
||||
return this._computeTrend(prev30Days, lastThirtyDaysCount);
|
||||
},
|
||||
|
||||
@computed('type')
|
||||
@computed("type")
|
||||
icon(type) {
|
||||
if (type.indexOf("message") > -1) {
|
||||
return "envelope";
|
||||
|
@ -170,7 +120,7 @@ const Report = Discourse.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed('type')
|
||||
@computed("type")
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
return "average";
|
||||
|
@ -180,75 +130,98 @@ const Report = Discourse.Model.extend({
|
|||
},
|
||||
|
||||
percentChangeString(val1, val2) {
|
||||
const val = ((val1 - val2) / val2) * 100;
|
||||
if (isNaN(val) || !isFinite(val)) {
|
||||
const change = this._computeChange(val1, val2);
|
||||
|
||||
if (isNaN(change) || !isFinite(change)) {
|
||||
return null;
|
||||
} else if (val > 0) {
|
||||
return "+" + val.toFixed(0) + "%";
|
||||
} else if (change > 0) {
|
||||
return "+" + change.toFixed(0) + "%";
|
||||
} else {
|
||||
return val.toFixed(0) + "%";
|
||||
return change.toFixed(0) + "%";
|
||||
}
|
||||
},
|
||||
|
||||
@computed('prev_period', 'currentTotal', 'currentAverage')
|
||||
@computed("prev_period", "currentTotal", "currentAverage")
|
||||
trendTitle(prev, currentTotal, currentAverage) {
|
||||
let current = this.get('average') ? currentAverage : currentTotal;
|
||||
let percent = this.percentChangeString(current, prev);
|
||||
let current = this.get("average") ? currentAverage : currentTotal;
|
||||
let percent = this.percentChangeString(prev, current);
|
||||
|
||||
if (this.get('average')) {
|
||||
if (this.get("average")) {
|
||||
prev = prev ? prev.toFixed(1) : "0";
|
||||
if (this.get('percent')) {
|
||||
current += '%';
|
||||
prev += '%';
|
||||
if (this.get("percent")) {
|
||||
current += "%";
|
||||
prev += "%";
|
||||
}
|
||||
} else {
|
||||
prev = number(prev);
|
||||
current = number(current);
|
||||
}
|
||||
|
||||
return I18n.t('admin.dashboard.reports.trend_title', {percent: percent, prev: prev, current: current});
|
||||
return I18n.t("admin.dashboard.reports.trend_title", {percent, prev, current});
|
||||
},
|
||||
|
||||
changeTitle(val1, val2, prevPeriodString) {
|
||||
const percentChange = this.percentChangeString(val1, val2);
|
||||
var title = "";
|
||||
if (percentChange) { title += percentChange + " change. "; }
|
||||
title += "Was " + val2 + " " + prevPeriodString + ".";
|
||||
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
||||
const change = this.percentChangeString(valAtT1, valAtT2);
|
||||
let title = "";
|
||||
if (change) { title += `${change} change. `; }
|
||||
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
||||
return title;
|
||||
},
|
||||
|
||||
@computed('yesterdayCount')
|
||||
@computed("yesterdayCount")
|
||||
yesterdayCountTitle(yesterdayCount) {
|
||||
return this.changeTitle(yesterdayCount, this.valueAt(2), "two days ago");
|
||||
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
||||
},
|
||||
|
||||
@computed('lastSevenDaysCount')
|
||||
sevenDayCountTitle(lastSevenDaysCount) {
|
||||
return this.changeTitle(lastSevenDaysCount, this.valueFor(8, 14), "two weeks ago");
|
||||
@computed("lastSevenDaysCount")
|
||||
sevenDaysCountTitle(lastSevenDaysCount) {
|
||||
return this.changeTitle(this.valueFor(8, 14), lastSevenDaysCount, "two weeks ago");
|
||||
},
|
||||
|
||||
@computed('prev30Days', 'lastThirtyDaysCount')
|
||||
thirtyDayCountTitle(prev30Days, lastThirtyDaysCount) {
|
||||
return this.changeTitle(lastThirtyDaysCount, prev30Days, "in the previous 30 day period");
|
||||
@computed("prev30Days", "lastThirtyDaysCount")
|
||||
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
|
||||
return this.changeTitle(prev30Days, lastThirtyDaysCount, "in the previous 30 day period");
|
||||
},
|
||||
|
||||
@computed('data')
|
||||
@computed("data")
|
||||
sortedData(data) {
|
||||
return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray();
|
||||
return this.get("xAxisIsDate") ? data.toArray().reverse() : data.toArray();
|
||||
},
|
||||
|
||||
@computed('data')
|
||||
@computed("data")
|
||||
xAxisIsDate() {
|
||||
if (!this.data[0]) return false;
|
||||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||
}
|
||||
},
|
||||
|
||||
_computeChange(valAtT1, valAtT2) {
|
||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||
},
|
||||
|
||||
_computeTrend(valAtT1, valAtT2) {
|
||||
const change = this._computeChange(valAtT1, valAtT2);
|
||||
const higherIsBetter = this.get("higher_is_better");
|
||||
|
||||
if (change > 50) {
|
||||
return higherIsBetter ? "high-trending-up" : "high-trending-down";
|
||||
} else if (change > 0) {
|
||||
return higherIsBetter ? "trending-up" : "trending-down";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return higherIsBetter ? "high-trending-down" : "high-trending-up";
|
||||
} else if (change < 0) {
|
||||
return higherIsBetter ? "trending-down" : "trending-up";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Report.reopenClass({
|
||||
|
||||
fillMissingDates(report) {
|
||||
if (_.isArray(report.data)) {
|
||||
|
||||
const startDateFormatted = moment.utc(report.start_date).locale('en').format('YYYY-MM-DD');
|
||||
const endDateFormatted = moment.utc(report.end_date).locale('en').format('YYYY-MM-DD');
|
||||
const startDateFormatted = moment.utc(report.start_date).locale("en").format("YYYY-MM-DD");
|
||||
const endDateFormatted = moment.utc(report.end_date).locale("en").format("YYYY-MM-DD");
|
||||
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
|
||||
}
|
||||
},
|
||||
|
@ -272,7 +245,7 @@ Report.reopenClass({
|
|||
// TODO: fillMissingDates if xaxis is date
|
||||
const related = Report.create({ type: json.report.related_report.type });
|
||||
related.setProperties(json.report.related_report);
|
||||
model.set('relatedReport', related);
|
||||
model.set("relatedReport", related);
|
||||
}
|
||||
|
||||
return model;
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
{{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>
|
||||
<td class="value {{report.sevenDaysTrend}}" title={{report.sevenDaysCountTitle}}>
|
||||
{{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>
|
||||
<td class="value {{report.thirtyDaysTrend}}" title={{report.thirtyDaysCountTitle}}>
|
||||
{{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
</td>
|
||||
|
||||
|
|
|
@ -4,41 +4,49 @@
|
|||
<p>{{model.description}}</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="admin-reports-filter">
|
||||
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
|
||||
{{#if showCategoryOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{/if}}
|
||||
{{#if showGroupOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
|
||||
{{/if}}
|
||||
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
|
||||
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
<div class="report-container">
|
||||
<div class="visualization">
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
<div class='view-options'>
|
||||
{{#if viewingTable}}
|
||||
{{i18n 'admin.dashboard.reports.view_table'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if viewingGraph}}
|
||||
{{i18n 'admin.dashboard.reports.view_graph'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if viewingGraph}}
|
||||
{{admin-graph model=model}}
|
||||
{{else}}
|
||||
{{admin-table-report model=model}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-table-report model=model.relatedReport}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
|
||||
</span>
|
||||
<span>
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
|
||||
</span>
|
||||
{{#if showCategoryOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{/if}}
|
||||
{{#if showGroupOptions}}
|
||||
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
|
||||
{{/if}}
|
||||
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
|
||||
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='view-options'>
|
||||
{{#if viewingTable}}
|
||||
{{i18n 'admin.dashboard.reports.view_table'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if viewingGraph}}
|
||||
{{i18n 'admin.dashboard.reports.view_graph'}}
|
||||
{{else}}
|
||||
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
{{#if viewingGraph}}
|
||||
{{admin-graph model=model}}
|
||||
{{else}}
|
||||
{{admin-table-report model=model}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-table-report model=model.relatedReport}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "common/admin/customize";
|
||||
@import "common/admin/flagging";
|
||||
@import "common/admin/dashboard_next";
|
||||
@import "common/admin/admin_reports";
|
||||
@import "common/admin/moderation_history";
|
||||
@import "common/admin/suspend";
|
||||
|
||||
|
@ -1968,6 +1969,12 @@ table#user-badges {
|
|||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.admin-reports, .dashboard-next {
|
||||
&.admin-contents {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cbox0 { background: blend-primary-secondary(0%); }
|
||||
.cbox10 { background: blend-primary-secondary(10%); }
|
||||
.cbox20 { background: blend-primary-secondary(20%); }
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
.admin-reports {
|
||||
h3 {
|
||||
border-bottom: 1px solid $primary-low;
|
||||
margin-bottom: .5em;
|
||||
padding-bottom: .5em;
|
||||
}
|
||||
|
||||
.report-container {
|
||||
display: flex;
|
||||
|
||||
.loading-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.visualization {
|
||||
display: flex;
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-left: 2em;
|
||||
|
||||
.date-picker {
|
||||
margin: 0;
|
||||
width: 195px;
|
||||
}
|
||||
|
||||
.combo-box, .date-picker-wrapper, .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@include small-width {
|
||||
flex-direction: column;
|
||||
min-width: 100%;
|
||||
|
||||
.visualization {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.filters {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
.dashboard-next {
|
||||
&.admin-contents {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-top {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
@ -19,6 +15,14 @@
|
|||
min-width: calc(50% - .5em);
|
||||
max-width: 100%;
|
||||
|
||||
&:last-child, {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
@include small-width {
|
||||
min-width: 100%;
|
||||
|
||||
|
@ -32,16 +36,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section-column:last-child, {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.section-column:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
@include small-width {
|
||||
.section-column:last-child, .section-column:first-child {
|
||||
.section-column:last-child,
|
||||
.section-column:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
@ -107,16 +104,17 @@
|
|||
.durability, .last-dashboard-update {
|
||||
flex: 1 1 50%;
|
||||
box-sizing: border-box;
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
margin: 1em 0;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.durability {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.backups, .uploads {
|
||||
flex: 1 1 100%;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.uploads p:last-of-type {
|
||||
|
@ -146,7 +144,7 @@
|
|||
border-left: 1px solid $primary-low;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: center;
|
||||
div {
|
||||
align-self: center;
|
||||
h4 {
|
||||
|
@ -156,7 +154,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.top-referred-topics, .trending-search {
|
||||
th:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.top-referred-topics {
|
||||
.dashboard-table table {
|
||||
table-layout: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.community-health {
|
||||
.period-chooser .period-chooser-header {
|
||||
|
@ -171,7 +179,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.dashboard-mini-chart {
|
||||
.status {
|
||||
display: flex;
|
||||
|
@ -239,7 +246,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.d-icon-question-circle {
|
||||
.tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -255,41 +262,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-up, &.trending-up {
|
||||
.chart-trend, .data-point {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-down, &.trending-down {
|
||||
.chart-trend, .data-point {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table.activity-metrics {
|
||||
table {
|
||||
@media screen and (min-width: 400px) {
|
||||
table-layout: auto;
|
||||
}
|
||||
tr th {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-referred-topics, .trending-search {
|
||||
th:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.top-referred-topics {
|
||||
.dashboard-table table {
|
||||
table-layout: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
|
@ -351,6 +323,7 @@
|
|||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
td.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -396,3 +369,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table.activity-metrics {
|
||||
table {
|
||||
@media screen and (min-width: 400px) {
|
||||
table-layout: auto;
|
||||
}
|
||||
tr th {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.conditional-loading-section {
|
||||
|
||||
&.is-loading {
|
||||
padding: 2em;
|
||||
margin: 1em;
|
||||
|
|
|
@ -4,7 +4,8 @@ class Report
|
|||
|
||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||
:end_date, :category_id, :group_id, :labels, :async,
|
||||
:prev_period, :facets, :limit, :processing, :average, :percent
|
||||
:prev_period, :facets, :limit, :processing, :average, :percent,
|
||||
:higher_is_better
|
||||
|
||||
def self.default_days
|
||||
30
|
||||
|
@ -14,6 +15,9 @@ class Report
|
|||
@type = type
|
||||
@start_date ||= Report.default_days.days.ago.beginning_of_day
|
||||
@end_date ||= Time.zone.now.end_of_day
|
||||
@average = false
|
||||
@percent = false
|
||||
@higher_is_better = true
|
||||
end
|
||||
|
||||
def self.cache_key(report)
|
||||
|
@ -54,7 +58,8 @@ class Report
|
|||
labels: labels,
|
||||
processing: self.processing,
|
||||
average: self.average,
|
||||
percent: self.percent
|
||||
percent: self.percent,
|
||||
higher_is_better: self.higher_is_better
|
||||
}.tap do |json|
|
||||
json[:total] = total if total
|
||||
json[:prev_period] = prev_period if prev_period
|
||||
|
@ -83,8 +88,9 @@ class Report
|
|||
report.facets = opts[:facets] || [:total, :prev30Days]
|
||||
report.limit = opts[:limit] if opts[:limit]
|
||||
report.processing = false
|
||||
report.average = opts[:average] || false
|
||||
report.percent = opts[:percent] || false
|
||||
report.average = opts[:average] if opts[:average]
|
||||
report.percent = opts[:percent] if opts[:percent]
|
||||
report.higher_is_better = opts[:higher_is_better] if opts[:higher_is_better]
|
||||
|
||||
report
|
||||
end
|
||||
|
@ -278,6 +284,7 @@ class Report
|
|||
end
|
||||
|
||||
def self.report_time_to_first_response(report)
|
||||
report.higher_is_better = false
|
||||
report.data = []
|
||||
Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r|
|
||||
report.data << { x: Date.parse(r["date"]), y: r["hours"].to_f.round(2) }
|
||||
|
|
|
@ -12,5 +12,11 @@ QUnit.test("Visit dashboard next page", assert => {
|
|||
|
||||
andThen(() => {
|
||||
assert.ok($('.dashboard-next').length, "has dashboard-next class");
|
||||
|
||||
assert.ok($('.dashboard-mini-chart.signups').length, "has a signups chart");
|
||||
assert.ok($('.dashboard-mini-chart.posts').length, "has a posts chart");
|
||||
assert.ok($('.dashboard-mini-chart.dau_by_mau').length, "has a dau_by_mau chart");
|
||||
assert.ok($('.dashboard-mini-chart.daily_engaged_users').length, "has a daily_engaged_users chart");
|
||||
assert.ok($('.dashboard-mini-chart.new_contributors').length, "has a new_contributors chart");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,63 +1,180 @@
|
|||
import Report from 'admin/models/report';
|
||||
import Report from "admin/models/report";
|
||||
|
||||
QUnit.module("Report");
|
||||
|
||||
function reportWithData(data) {
|
||||
return Report.create({
|
||||
type: 'topics',
|
||||
data: _.map(data, function(val, index) {
|
||||
return { x: moment().subtract(index, "days").format('YYYY-MM-DD'), y: val };
|
||||
type: "topics",
|
||||
data: _.map(data, (val, index) => {
|
||||
return { x: moment().subtract(index, "days").format("YYYY-MM-DD"), y: val };
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.test("counts", assert => {
|
||||
var report = reportWithData([5, 4, 3, 2, 1, 100, 99, 98, 1000]);
|
||||
const report = reportWithData([5, 4, 3, 2, 1, 100, 99, 98, 1000]);
|
||||
|
||||
assert.equal(report.get('todayCount'), 5);
|
||||
assert.equal(report.get('yesterdayCount'), 4);
|
||||
assert.equal(report.get("todayCount"), 5);
|
||||
assert.equal(report.get("yesterdayCount"), 4);
|
||||
assert.equal(report.valueFor(2, 4), 6, "adds the values for the given range of days, inclusive");
|
||||
assert.equal(report.get('lastSevenDaysCount'), 307, "sums 7 days excluding today");
|
||||
assert.equal(report.get("lastSevenDaysCount"), 307, "sums 7 days excluding today");
|
||||
|
||||
report.set("method", "average");
|
||||
assert.equal(report.valueFor(2, 4), 2, "averages the values for the given range of days");
|
||||
});
|
||||
|
||||
QUnit.test("percentChangeString", assert => {
|
||||
var report = reportWithData([]);
|
||||
const report = reportWithData([]);
|
||||
|
||||
assert.equal(report.percentChangeString(8, 5), "+60%", "value increased");
|
||||
assert.equal(report.percentChangeString(2, 8), "-75%", "value decreased");
|
||||
assert.equal(report.percentChangeString(5, 8), "+60%", "value increased");
|
||||
assert.equal(report.percentChangeString(8, 2), "-75%", "value decreased");
|
||||
assert.equal(report.percentChangeString(8, 8), "0%", "value unchanged");
|
||||
assert.blank(report.percentChangeString(8, 0), "returns blank when previous value was 0");
|
||||
assert.equal(report.percentChangeString(0, 8), "-100%", "yesterday was 0");
|
||||
assert.blank(report.percentChangeString(0, 8), "returns blank when previous value was 0");
|
||||
assert.equal(report.percentChangeString(8, 0), "-100%", "yesterday was 0");
|
||||
assert.blank(report.percentChangeString(0, 0), "returns blank when both were 0");
|
||||
});
|
||||
|
||||
QUnit.test("yesterdayCountTitle with valid values", assert => {
|
||||
var title = reportWithData([6,8,5,2,1]).get('yesterdayCountTitle');
|
||||
assert.ok(title.indexOf('+60%') !== -1);
|
||||
const title = reportWithData([6,8,5,2,1]).get("yesterdayCountTitle");
|
||||
assert.ok(title.indexOf("+60%") !== -1);
|
||||
assert.ok(title.match(/Was 5/));
|
||||
});
|
||||
|
||||
QUnit.test("yesterdayCountTitle when two days ago was 0", assert => {
|
||||
var title = reportWithData([6,8,0,2,1]).get('yesterdayCountTitle');
|
||||
assert.equal(title.indexOf('%'), -1);
|
||||
const title = reportWithData([6,8,0,2,1]).get("yesterdayCountTitle");
|
||||
assert.equal(title.indexOf("%"), -1);
|
||||
assert.ok(title.match(/Was 0/));
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("sevenDayCountTitle", assert => {
|
||||
var title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get('sevenDayCountTitle');
|
||||
QUnit.test("sevenDaysCountTitle", assert => {
|
||||
const title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get("sevenDaysCountTitle");
|
||||
assert.ok(title.match(/-50%/));
|
||||
assert.ok(title.match(/Was 14/));
|
||||
});
|
||||
|
||||
QUnit.test("thirtyDayCountTitle", assert => {
|
||||
var report = reportWithData([5,5,5,5]);
|
||||
report.set('prev30Days', 10);
|
||||
var title = report.get('thirtyDayCountTitle');
|
||||
QUnit.test("thirtyDaysCountTitle", assert => {
|
||||
const report = reportWithData([5,5,5,5]);
|
||||
report.set("prev30Days", 10);
|
||||
const title = report.get("thirtyDaysCountTitle");
|
||||
|
||||
assert.ok(title.indexOf('+50%') !== -1);
|
||||
assert.ok(title.indexOf("+50%") !== -1);
|
||||
assert.ok(title.match(/Was 10/));
|
||||
});
|
||||
|
||||
QUnit.test("sevenDaysTrend", assert => {
|
||||
let report;
|
||||
let trend;
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1, 1,1,1,1,1,1,1]);
|
||||
trend = report.get("sevenDaysTrend");
|
||||
assert.ok(trend === "no-change");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1, 0,0,0,0,0,0,0]);
|
||||
trend = report.get("sevenDaysTrend");
|
||||
assert.ok(trend === "high-trending-up");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1, 1,1,1,1,1,1,0]);
|
||||
trend = report.get("sevenDaysTrend");
|
||||
assert.ok(trend === "trending-up");
|
||||
|
||||
report = reportWithData([0, 0,0,0,0,0,0,0, 1,1,1,1,1,1,1]);
|
||||
trend = report.get("sevenDaysTrend");
|
||||
assert.ok(trend === "high-trending-down");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,0, 1,1,1,1,1,1,1]);
|
||||
trend = report.get("sevenDaysTrend");
|
||||
assert.ok(trend === "trending-down");;
|
||||
});
|
||||
|
||||
QUnit.test("yesterdayTrend", assert => {
|
||||
let report;
|
||||
let trend;
|
||||
|
||||
report = reportWithData([0, 1, 1]);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "no-change");
|
||||
|
||||
report = reportWithData([0, 1, 0]);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "high-trending-up");
|
||||
|
||||
report = reportWithData([0, 1.1, 1]);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "trending-up");
|
||||
|
||||
report = reportWithData([0, 0, 1]);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "high-trending-down");
|
||||
|
||||
report = reportWithData([0, 1, 1.1]);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "trending-down");;
|
||||
});
|
||||
|
||||
QUnit.test("thirtyDaysTrend", assert => {
|
||||
let report;
|
||||
let trend;
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);
|
||||
report.set("prev30Days", 30);
|
||||
trend = report.get("thirtyDaysTrend");
|
||||
assert.ok(trend === "no-change");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);
|
||||
report.set("prev30Days", 0);
|
||||
trend = report.get("thirtyDaysTrend");
|
||||
assert.ok(trend === "high-trending-up");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);
|
||||
report.set("prev30Days", 25);
|
||||
trend = report.get("thirtyDaysTrend");
|
||||
assert.ok(trend === "trending-up");
|
||||
|
||||
report = reportWithData([0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
|
||||
report.set("prev30Days", 60);
|
||||
trend = report.get("thirtyDaysTrend");
|
||||
assert.ok(trend === "high-trending-down");
|
||||
|
||||
report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0]);
|
||||
report.set("prev30Days", 35);
|
||||
trend = report.get("thirtyDaysTrend");
|
||||
assert.ok(trend === "trending-down");;
|
||||
});
|
||||
|
||||
QUnit.test("higher is better false", assert => {
|
||||
let report;
|
||||
let trend;
|
||||
|
||||
report = reportWithData([0, 1, 0]);
|
||||
report.set("higher_is_better", false);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "high-trending-down");
|
||||
|
||||
report = reportWithData([0, 1.1, 1]);
|
||||
report.set("higher_is_better", false);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "trending-down");
|
||||
|
||||
report = reportWithData([0, 0, 1]);
|
||||
report.set("higher_is_better", false);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "high-trending-up");
|
||||
|
||||
report = reportWithData([0, 1, 1.1]);
|
||||
report.set("higher_is_better", false);
|
||||
trend = report.get("yesterdayTrend");
|
||||
assert.ok(trend === "trending-up");;
|
||||
});
|
||||
|
||||
QUnit.test("average", assert => {
|
||||
let report;
|
||||
|
||||
report = reportWithData([5, 5, 5, 5, 5, 5, 5, 5]);
|
||||
|
||||
report.set("average", true);
|
||||
assert.ok(report.get("lastSevenDaysCount") === 5);
|
||||
|
||||
report.set("average", false);
|
||||
assert.ok(report.get("lastSevenDaysCount") === 35);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue