diff --git a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 index 365df161f9d..82eceb66915 100644 --- a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 @@ -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)); }); })); } diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 index 75029937c65..f95738df61e 100644 --- a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 @@ -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 }, diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 index ac359b8cd78..2f24cf9edd0 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 @@ -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"); } }, diff --git a/app/assets/javascripts/admin/mixins/async-report.js.es6 b/app/assets/javascripts/admin/mixins/async-report.js.es6 index 481d92d37db..12c5d9543f3 100644 --- a/app/assets/javascripts/admin/mixins/async-report.js.es6 +++ b/app/assets/javascripts/admin/mixins/async-report.js.es6 @@ -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")); }, }); diff --git a/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs index 90cba2318b5..f6a44b7b53e 100644 --- a/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs +++ b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs @@ -1,4 +1,4 @@ -{{#conditional-loading-section isLoading=isLoading title=loadingTitle}} +{{#conditional-loading-section isLoading=isLoading}}

{{title}}

@@ -7,7 +7,7 @@ {{/if}}
- {{#each-in reports as |key report|}} + {{#each reportsForPeriod as |report|}}
@@ -36,5 +36,5 @@
- {{/each-in}} + {{/each}} {{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs index 2ad937b70ae..328e0829ff5 100644 --- a/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs +++ b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs @@ -1,31 +1,31 @@ -{{#conditional-loading-section isLoading=isLoading title=loadingTitle}} -
- {{#each-in reports as |key report|}} -
- -
-

- - {{report.title}} - -

+{{#conditional-loading-section isLoading=isLoading}} + {{#each reportsForPeriod as |report|}} +
+

+ + {{report.title}} + -
- - {{#if report.average}} - {{report.currentAverage}} - {{else}} - {{number report.currentTotal noTitle="true"}} - {{/if}} - - {{#if report.trendIcon}} - {{d-icon report.trendIcon}} - {{/if}} -
-

+ + {{d-icon "question-circle"}} + + + +
+ + {{#if report.average}} + {{number report.currentAverage}}{{#if report.percent}}%{{/if}} + {{else}} + {{number report.currentTotal noTitle="true"}}{{#if report.percent}}%{{/if}} + {{/if}} + + + {{#if report.trendIcon}} + {{d-icon report.trendIcon class="trend-icon"}} + {{/if}}
- {{/each-in}} -
+
+ {{/each}}
diff --git a/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 index 084513463f7..dd8f321635c 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 @@ -3,5 +3,7 @@ export default Ember.Component.extend({ classNameBindings: ["isLoading"], - isLoading: false + isLoading: false, + + title: I18n.t("conditional_loading_section.loading") }); diff --git a/app/assets/javascripts/discourse/lib/tooltip.js.es6 b/app/assets/javascripts/discourse/lib/tooltip.js.es6 new file mode 100644 index 00000000000..f1df4b3c9e6 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/tooltip.js.es6 @@ -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(` +
+
+
${content}
+
+ `); + + $(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'); + } +} diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index 7a6ad48bdb2..7285453b905 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -1,9 +1,12 @@ .dashboard-next { - &.admin-contents { margin: 0; } + .section-top { + margin-bottom: 1em; + } + .section-columns { display: flex; justify-content: space-between; @@ -62,223 +65,26 @@ } } - .dashboard-table { - margin-bottom: 1em; - - &.is-disabled { - background: $primary-low; - padding: 1em; - } - - @include small-width { - table { - tbody tr td { - font-size: $font-down-2; - } - } - } - - &.is-loading { - height: 150px; - } - - .table-title { - align-items: center; - display: flex; - justify-content: space-between; - - h3 { - margin: .5em 0; - } - } - - table { - table-layout: fixed; - - thead { - border: 1px solid $primary-low; - tr { - background: $primary-low; - th { - text-align: center; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - } - } - - tbody { - tr { - td:first-child { - text-overflow: ellipsis; - overflow: hidden; - white-space: normal; - } - - td { - border: 1px solid $primary-low; - text-align: center; - } - td.left { - text-align: left; - } - - td.value { - i { - display: none; - } - - &.high-trending-up, &.trending-up { - i.up { - color: $success; - display: inline; - } - } - &.high-trending-down, &.trending-down { - i.down { - color: $danger; - display: inline; - } - } - &.no-change { - i.down { - display: inline; - visibility: hidden; - } - } - } - } - } - } - } - .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 { + .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 { + max-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; @@ -300,4 +106,204 @@ } } } + + .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 { + background: $primary-low; + padding: 1em; + } + + @include small-width { + table { + tbody tr td { + font-size: $font-down-2; + } + } + } + + &.is-loading { + height: 150px; + } + + .table-title { + align-items: center; + display: flex; + justify-content: space-between; + + h3 { + margin: .5em 0; + } + } + + table { + table-layout: fixed; + + thead { + border: 1px solid $primary-low; + tr { + background: $primary-low; + th { + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + + tbody { + tr { + td:first-child { + text-overflow: ellipsis; + overflow: hidden; + white-space: normal; + } + + td { + border: 1px solid $primary-low; + text-align: center; + } + td.left { + text-align: left; + } + + td.value { + i { + display: none; + } + + &.high-trending-up, &.trending-up { + i.up { + color: $success; + display: inline; + } + } + &.high-trending-down, &.trending-down { + i.down { + color: $danger; + display: inline; + } + } + &.no-change { + i.down { + display: inline; + visibility: hidden; + } + } + } + } + } + } } diff --git a/app/assets/stylesheets/common/base/tooltip.scss b/app/assets/stylesheets/common/base/tooltip.scss new file mode 100644 index 00000000000..0469599ba7b --- /dev/null +++ b/app/assets/stylesheets/common/base/tooltip.scss @@ -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; + } +} diff --git a/test/javascripts/fixtures/daily-engaged-users.js.es6 b/test/javascripts/fixtures/daily-engaged-users.js.es6 index 902beb981cb..4787c458f37 100644 --- a/test/javascripts/fixtures/daily-engaged-users.js.es6 +++ b/test/javascripts/fixtures/daily-engaged-users.js.es6 @@ -13,7 +13,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": null, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/dau-by-mau.js.es6 b/test/javascripts/fixtures/dau-by-mau.js.es6 index d7ab39d3a0c..78a125d9522 100644 --- a/test/javascripts/fixtures/dau-by-mau.js.es6 +++ b/test/javascripts/fixtures/dau-by-mau.js.es6 @@ -13,7 +13,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": 46, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/new-contributors.js.es6 b/test/javascripts/fixtures/new-contributors.js.es6 index 2d23b143803..fc3d6f86c9e 100644 --- a/test/javascripts/fixtures/new-contributors.js.es6 +++ b/test/javascripts/fixtures/new-contributors.js.es6 @@ -42,7 +42,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": null, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/posts.js.es6 b/test/javascripts/fixtures/posts.js.es6 index 904ba874324..dea89045210 100644 --- a/test/javascripts/fixtures/posts.js.es6 +++ b/test/javascripts/fixtures/posts.js.es6 @@ -12,7 +12,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": 0, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/signups.js.es6 b/test/javascripts/fixtures/signups.js.es6 index a93d26fe06c..dafe3909d00 100644 --- a/test/javascripts/fixtures/signups.js.es6 +++ b/test/javascripts/fixtures/signups.js.es6 @@ -39,7 +39,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": 0, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/topics.js.es6 b/test/javascripts/fixtures/topics.js.es6 index 86e888af4fc..c5a5f9b2451 100644 --- a/test/javascripts/fixtures/topics.js.es6 +++ b/test/javascripts/fixtures/topics.js.es6 @@ -12,7 +12,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": 0, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/trending-search.js.es6 b/test/javascripts/fixtures/trending-search.js.es6 index a8255e1adec..c02721d8eca 100644 --- a/test/javascripts/fixtures/trending-search.js.es6 +++ b/test/javascripts/fixtures/trending-search.js.es6 @@ -12,7 +12,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": null, - "labels": ["Term", "Searches", "Unique"] + "labels": ["Term", "Searches", "Unique"], + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/users-by-trust-level.js.es6 b/test/javascripts/fixtures/users-by-trust-level.js.es6 index 11d227e8b19..6af6b12c74c 100644 --- a/test/javascripts/fixtures/users-by-trust-level.js.es6 +++ b/test/javascripts/fixtures/users-by-trust-level.js.es6 @@ -13,7 +13,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": null, - "labels": null + "labels": null, + "report_key": "" } } }; diff --git a/test/javascripts/fixtures/users-by-type.js.es6 b/test/javascripts/fixtures/users-by-type.js.es6 index e72d5cbf498..1da202a59e2 100644 --- a/test/javascripts/fixtures/users-by-type.js.es6 +++ b/test/javascripts/fixtures/users-by-type.js.es6 @@ -13,7 +13,8 @@ export default { "category_id": null, "group_id": null, "prev30Days": null, - "labels": null + "labels": null, + "report_key": "" } } };