UX: tooltips and improvements to new dashboard

- tooltips
- revert chart title UI
- reduce period chooser font-size
- localize dates of data points
- fix a bug where multiple reports were loaded at the same time
- fix a bug where % was not showing anymore
- remove spacing at the top
- remove loadingTitle feature (Loading...%report name%) incompatible with new hijack design
This commit is contained in:
Joffrey JAFFEUX 2018-05-16 16:45:21 +02:00 committed by GitHub
parent 131b7f5da5
commit 9554d9c56a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 438 additions and 270 deletions

View File

@ -6,8 +6,6 @@ export default Ember.Component.extend(AsyncReport, {
classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
help: null,
helpPage: null,
title: null,
loadingTitle: null,
loadReport(report_json) {
return Report.create(report_json);
@ -30,12 +28,10 @@ export default Ember.Component.extend(AsyncReport, {
payload.data.limit = this.get("limit");
}
this.set("reports", Ember.Object.create());
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
return ajax(dataSource, payload)
.then(response => {
this.set(`reports.${response.report.report_key}`, this.loadReport(response.report));
this.get("reports").pushObject(this.loadReport(response.report));
});
}));
}

View File

@ -3,6 +3,7 @@ import AsyncReport from "admin/mixins/async-report";
import Report from "admin/models/report";
import { number } from 'discourse/lib/formatter';
import loadScript from "discourse/lib/load-script";
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
function collapseWeekly(data, average) {
let aggregate = [];
@ -25,7 +26,7 @@ function collapseWeekly(data, average) {
}
export default Ember.Component.extend(AsyncReport, {
classNames: ["dashboard-mini-chart"],
classNames: ["chart", "dashboard-mini-chart"],
total: 0,
init() {
@ -34,6 +35,18 @@ export default Ember.Component.extend(AsyncReport, {
this._colorsPool = ["rgb(0,136,204)", "rgb(235,83,148)"];
},
didRender() {
this._super();
registerTooltip($(this.element).find("[data-tooltip]"));
},
willDestroyElement() {
this._super();
unregisterTooltip($(this.element).find("[data-tooltip]"));
},
pickColorAtIndex(index) {
return this._colorsPool[index] || this._colorsPool[0];
},
@ -58,12 +71,10 @@ export default Ember.Component.extend(AsyncReport, {
this._chart = null;
}
this.set("reports", Ember.Object.create());
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
return ajax(dataSource, payload)
.then(response => {
this.set(`reports.${response.report.report_key}`, this.loadReport(response.report));
this.get("reports").pushObject(this.loadReport(response.report));
});
}));
},
@ -93,13 +104,13 @@ export default Ember.Component.extend(AsyncReport, {
if (!$chartCanvas.length) return;
const context = $chartCanvas[0].getContext("2d");
const reports = _.values(this.get("reports"));
const reportsForPeriod = this.get("reportsForPeriod");
const labels = Ember.makeArray(reports.get("firstObject.data")).map(d => d.x);
const labels = Ember.makeArray(reportsForPeriod.get("firstObject.data")).map(d => d.x);
const data = {
labels,
datasets: reports.map(report => {
datasets: reportsForPeriod.map(report => {
return {
data: Ember.makeArray(report.data).map(d => d.y),
backgroundColor: "rgba(200,220,240,0.3)",
@ -127,6 +138,11 @@ export default Ember.Component.extend(AsyncReport, {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: {
display: false
},

View File

@ -100,7 +100,7 @@ export default Ember.Controller.extend({
return fullDay.subtract(1, "month").startOf("day");
break;
default:
return null;
return fullDay.subtract(1, "month").startOf("day");
}
},

View File

@ -2,14 +2,14 @@ import computed from "ember-addons/ember-computed-decorators";
export default Ember.Mixin.create({
classNameBindings: ["isLoading"],
reports: null,
isLoading: false,
dataSourceNames: "",
title: null,
init() {
this._super();
this.set("reports", Ember.Object.create());
this.set("reports", []);
},
@computed("dataSourceNames")
@ -17,8 +17,27 @@ export default Ember.Mixin.create({
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
},
@computed("reports.[]", "startDate", "endDate")
reportsForPeriod(reports, startDate, endDate) {
// on a slow network fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
reports = reports.uniqBy("report_key");
if (!startDate || !endDate) {
return reports;
}
return reports.filter(report => {
return report.report_key.includes(startDate.format("YYYYMMDD")) &&
report.report_key.includes(endDate.format("YYYYMMDD"));
});
},
didInsertElement() {
this._super();
this.fetchReport()
.finally(() => {
this.renderReport();
@ -27,6 +46,7 @@ export default Ember.Mixin.create({
didUpdateAttrs() {
this._super();
this.fetchReport()
.finally(() => {
this.renderReport();
@ -35,26 +55,14 @@ export default Ember.Mixin.create({
renderReport() {
if (!this.element || this.isDestroying || this.isDestroyed) return;
const reports = _.values(this.get("reports"));
if (!reports.length) return;
const title = reports.map(report => report.title).join(", ");
if (reports.map(report => report.processing).includes(true)) {
const loading = I18n.t("conditional_loading_section.loading");
this.set("loadingTitle", `${loading}\n\n${title}`);
return;
}
this.setProperties({ title, isLoading: false});
this.set("title", this.get("reportsForPeriod").map(r => r.title).join(", "));
this.set("isLoading", false);
},
loadReport() {},
fetchReport() {
this.set("reports", []);
this.set("isLoading", true);
this.set("loadingTitle", I18n.t("conditional_loading_section.loading"));
},
});

View File

@ -1,4 +1,4 @@
{{#conditional-loading-section isLoading=isLoading title=loadingTitle}}
{{#conditional-loading-section isLoading=isLoading}}
<div class="table-title">
<h3>{{title}}</h3>
@ -7,7 +7,7 @@
{{/if}}
</div>
{{#each-in reports as |key report|}}
{{#each reportsForPeriod as |report|}}
<div class="table-container">
<table>
<thead>
@ -36,5 +36,5 @@
</tbody>
</table>
</div>
{{/each-in}}
{{/each}}
{{/conditional-loading-section}}

View File

@ -1,31 +1,31 @@
{{#conditional-loading-section isLoading=isLoading title=loadingTitle}}
<div class="dashboard-mini-statuses">
{{#each-in reports as |key report|}}
<div class="dashboard-mini-status" title="{{report.trendTitle}}">
<span class="indicator" style="background-color:{{report.color}}"></span>
<div class="legend">
{{#conditional-loading-section isLoading=isLoading}}
{{#each reportsForPeriod as |report|}}
<div class="status">
<h4 class="title">
<a href="{{report.reportUrl}}">
{{report.title}}
</a>
<span class="info" data-tooltip="{{report.description}}">
{{d-icon "question-circle"}}
</span>
</h4>
<div class="trend">
<span class="value" title="{{report.trendTitle}}">
<div class="trend {{report.trend}}">
<span class="trend-value" title="{{report.trendTitle}}">
{{#if report.average}}
{{report.currentAverage}}
{{number report.currentAverage}}{{#if report.percent}}%{{/if}}
{{else}}
{{number report.currentTotal noTitle="true"}}
{{number report.currentTotal noTitle="true"}}{{#if report.percent}}%{{/if}}
{{/if}}
</span>
{{#if report.trendIcon}}
{{d-icon report.trendIcon}}
{{d-icon report.trendIcon class="trend-icon"}}
{{/if}}
</div>
</div>
</div>
{{/each-in}}
</div>
{{/each}}
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>

View File

@ -3,5 +3,7 @@ export default Ember.Component.extend({
classNameBindings: ["isLoading"],
isLoading: false
isLoading: false,
title: I18n.t("conditional_loading_section.loading")
});

View File

@ -0,0 +1,73 @@
export function showTooltip() {
const fadeSpeed = 300;
const tooltipID = "#discourse-tooltip";
const $this = $(this);
const $parent = $this.offsetParent();
const content = $this.attr("data-tooltip");
const retina = window.devicePixelRatio && window.devicePixelRatio > 1 ? "class='retina'" : "";
let pos = $this.offset();
const delta = $parent.offset();
pos.top -= delta.top;
pos.left -= delta.left;
$(tooltipID).fadeOut(fadeSpeed).remove();
$(this).after(`
<div id="discourse-tooltip" ${retina}>
<div class="tooltip-pointer"></div>
<div class="tooltip-content">${content}</div>
</div>
`);
$(window).on("click.discourse", (event) => {
if ($(event.target).closest(tooltipID).length === 0) {
$(tooltipID).remove();
$(window).off("click.discourse");
}
return true;
});
const $tooltip = $(tooltipID);
$tooltip.css({top: 0, left: 0});
let left = (pos.left - ($tooltip.width() / 2) + $this.width()/2);
if (left < 0) {
$tooltip.find(".tooltip-pointer").css({
"margin-left": left*2 + "px"
});
left = 0;
}
// also do a right margin fix
const parentWidth = $parent.width();
if (left + $tooltip.width() > parentWidth) {
let oldLeft = left;
left = parentWidth - $tooltip.width();
$tooltip.find(".tooltip-pointer").css({
"margin-left": (oldLeft - left) * 2 + "px"
});
}
$tooltip.css({
top: pos.top + 5 + "px",
left: left + "px"
});
$tooltip.fadeIn(fadeSpeed);
return false;
}
export function registerTooltip(jqueryContext) {
if (jqueryContext.length) {
jqueryContext.on('click', showTooltip);
}
}
export function unregisterTooltip(jqueryContext) {
if (jqueryContext.length) {
jqueryContext.off('click');
}
}

View File

@ -1,9 +1,12 @@
.dashboard-next {
&.admin-contents {
margin: 0;
}
.section-top {
margin-bottom: 1em;
}
.section-columns {
display: flex;
justify-content: space-between;
@ -62,7 +65,159 @@
}
}
.dashboard-table {
.charts {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
.chart {
max-width: calc(100% * 1/3);
width: 100%;
flex-grow: 1;
flex-basis: 100%;
display: flex;
margin-bottom: 1em;
}
@include small-width {
.chart {
max-width: 100%;
}
}
.chart-canvas-container {
position: relative;
padding: 0 1em 0 0;
}
.chart-canvas {
width: 100%;
height: 100%;
}
}
.misc {
.durability {
display: flex;
justify-content: space-between;
.durability-title {
text-transform: capitalize;
}
}
}
.community-health {
.period-chooser .period-chooser-header {
.selected-name, .d-icon {
font-size: $font-up-1;
}
.d-icon {
margin: 0;
}
}
}
}
.dashboard-mini-chart {
.status {
display: flex;
justify-content: space-between;
margin-bottom: 1em;
.title {
font-size: $font-0;
font-weight: 700;
margin: 0;
a { color: $primary; }
.info {
cursor: pointer;
margin-left: .25em;
color: $primary-medium;
}
}
.trend {
margin-right: 1em;
align-items: center;
&.trending-down, &.high-trending-down {
color: $danger;
}
&.trending-up, &.high-trending-up {
color: $success;
}
.trend-value {
font-size: $font-up-1;
font-weight: 700;
}
.trend-icon {
font-size: $font-up-1;
font-weight: 700;
}
}
}
.conditional-loading-section {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
@include small-width {
max-width: 100%;
}
&.is-loading {
height: 200px;
}
.loading-container.visible {
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
.d-icon-question-circle {
cursor: pointer;
}
.chart-title {
align-items: center;
display: flex;
justify-content: space-between;
h3 {
margin: 1em 0;
a, a:visited {
color: $primary;
}
}
}
&.high-trending-up, &.trending-up {
.chart-trend, .data-point {
color: $success;
}
}
&.high-trending-down, &.trending-down {
.chart-trend, .data-point {
color: $danger;
}
}
}
.dashboard-table {
margin-bottom: 1em;
&.is-disabled {
@ -151,153 +306,4 @@
}
}
}
}
.charts {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
.dashboard-mini-statuses {
margin-bottom: 1em;
display: inline-flex;
}
.dashboard-mini-status {
flex-direction: row;
margin-right: 1em;
display: flex;
.indicator {
margin-right: .5em;
width: .33em;
height: 35px;
}
.legend {
display: flex;
flex-direction: column;
.title {
a {color: black;}
font-size: $font-down-2;
font-weight: 700;
margin: 0;
}
.trend {
flex-direction: row;
.d-icon {
font-weight: 700;
&.d-icon-angle-down, &.d-icon-angle-double-down {
color: $danger;
}
&.d-icon-angle-up, &.d-icon-angle-double-up {
color: rgb(17, 141, 0);
}
}
}
}
}
.dashboard-mini-chart {
max-width: calc(100% * 1/3);
width: 100%;
flex-grow: 1;
flex-basis: 100%;
display: flex;
margin-bottom: 1em;
.conditional-loading-section {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
@include small-width {
max-width: 100%;
}
&.is-loading {
height: 200px;
}
.loading-container.visible {
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
.d-icon-question-circle {
cursor: pointer;
}
.chart-title {
align-items: center;
display: flex;
justify-content: space-between;
h3 {
margin: 1em 0;
a, a:visited {
color: $primary;
}
}
}
&.high-trending-up, &.trending-up {
.chart-trend, .data-point {
color: rgb(17, 141, 0);
}
}
&.high-trending-down, &.trending-down {
.chart-trend, .data-point {
color: $danger;
}
}
}
@include small-width {
.dashboard-mini-chart {
width: 100%;
}
}
.chart-trend {
font-size: $font-up-3;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
margin-right: 1em;
}
.chart-canvas-container {
position: relative;
padding: 0 1em 0 0;
}
.chart-canvas {
width: 100%;
height: 100%;
}
}
.misc {
.durability {
display: flex;
justify-content: space-between;
.durability-title {
text-transform: capitalize;
}
}
}
}

View File

@ -0,0 +1,58 @@
$discourse-tooltip-background: $secondary;
$discourse-tooltip-border: $primary-medium;
#discourse-tooltip {
background-color: $discourse-tooltip-background;
position: absolute;
z-index: 1000;
border: 1px solid $discourse-tooltip-border;
max-width: 400px;
margin-top: 25px;
overflow-wrap: break-word;
display: none;
font-size: $font-0;
font-weight: 500;
&.retina {
border: 0.5px solid $discourse-tooltip-border;
}
.tooltip-pointer {
position: relative;
background: $discourse-tooltip-background;
}
.tooltip-pointer:before, .tooltip-pointer:after {
position: absolute;
pointer-events: none;
border: solid transparent;
bottom: 100%;
content: "";
height: 0;
width: 0;
}
.tooltip-pointer:after
{
border-bottom-color: $discourse-tooltip-background;
border-width: 8px;
left: 50%;
margin-left: -8px;
margin-bottom: -0.5px;
}
.tooltip-pointer:before {
border-bottom-color: $discourse-tooltip-border;
border-width: 9px;
left: 50%;
margin-left: -9px;
margin-bottom: -0.5px;
}
.tooltip-content {
padding: 0 0.5em;
font-size: $font-down-1;
color: $primary-medium;
line-height: 1.4em;
}
}

View File

@ -13,7 +13,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": null,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -13,7 +13,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": 46,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -42,7 +42,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": null,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -12,7 +12,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": 0,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -39,7 +39,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": 0,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -12,7 +12,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": 0,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -12,7 +12,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": null,
"labels": ["Term", "Searches", "Unique"]
"labels": ["Term", "Searches", "Unique"],
"report_key": ""
}
}
};

View File

@ -13,7 +13,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": null,
"labels": null
"labels": null,
"report_key": ""
}
}
};

View File

@ -13,7 +13,8 @@ export default {
"category_id": null,
"group_id": null,
"prev30Days": null,
"labels": null
"labels": null,
"report_key": ""
}
}
};