From 98c93b66c81c1ea5eea1c94afe839256aa64a7b0 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 23 Mar 2022 09:51:53 +0100 Subject: [PATCH] Added dashboard 5.0 stats service and days dropdown refs https://github.com/TryGhost/Team/issues/1432 - Added very basic state selection at the bottom of dashboard 5.0 - Added a dashboard stats service, who is responsible for fetching and returning stats data - Added POC for days dropdown with communication and reload between ember components - Added proper automatic number and plural formatting for member counts --- .../app/components/dashboard/dashboard-v5.hbs | 30 +++++- .../app/components/dashboard/dashboard-v5.js | 90 +++++++++++++++++ .../dashboard/v5/chart-members-counts.hbs | 12 +-- .../dashboard/v5/chart-members-counts.js | 24 +++-- .../dashboard/v5/chart-total-members.hbs | 2 +- .../dashboard/v5/chart-total-members.js | 67 ++++++++++++- ghost/admin/app/services/dashboard-stats.js | 96 +++++++++++++++++++ ghost/admin/app/styles/layouts/dashboard.css | 12 +++ 8 files changed, 314 insertions(+), 19 deletions(-) create mode 100644 ghost/admin/app/services/dashboard-stats.js diff --git a/ghost/admin/app/components/dashboard/dashboard-v5.hbs b/ghost/admin/app/components/dashboard/dashboard-v5.hbs index def3ce7c25..9c91e76a71 100644 --- a/ghost/admin/app/components/dashboard/dashboard-v5.hbs +++ b/ghost/admin/app/components/dashboard/dashboard-v5.hbs @@ -5,12 +5,24 @@
-
- Dropdown: 7d/30d/90d/all time -
+
+ + {{#if option.name}}{{option.name}}{{else}}Unknown option{{/if}} + +
- +
{{#if this.hasPaidTiers}} @@ -74,6 +86,7 @@

Prototype control panel

+
@@ -143,4 +156,13 @@
+ +
+ + + + + + +
\ No newline at end of file diff --git a/ghost/admin/app/components/dashboard/dashboard-v5.js b/ghost/admin/app/components/dashboard/dashboard-v5.js index 4b2fcfbc67..67320f7b4d 100644 --- a/ghost/admin/app/components/dashboard/dashboard-v5.js +++ b/ghost/admin/app/components/dashboard/dashboard-v5.js @@ -1,11 +1,37 @@ import Component from '@glimmer/component'; +import {action} from '@ember/object'; +import {inject as service} from '@ember/service'; import {tracked} from '@glimmer/tracking'; +const DAYS_OPTIONS = [{ + name: '7 days', + value: 7 +}, { + name: '30 days', + value: 30 +}, { + name: '90 days', + value: 90 +}, { + name: 'All time', + value: 365 // todo: add support for all time (not important for prototype) +}]; + export default class DashboardDashboardV5Component extends Component { + @service dashboardStats; + @tracked mockPaidTiers = true; @tracked mockStripeEnabled = true; @tracked mockNewslettersEnabled = true; @tracked mockMembersEnabled = true; + + @tracked days = 30; + + daysOptions = DAYS_OPTIONS; + + get selectedDaysOption() { + return this.daysOptions.find(d => d.value === this.days); + } get hasPaidTiers() { return this.mockPaidTiers; @@ -22,4 +48,68 @@ export default class DashboardDashboardV5Component extends Component { get areMembersEnabled() { return this.mockMembersEnabled; } + + @action + onDaysChange(selected) { + this.days = selected.value; + } + + /** + * This method generates new data and forces a reload for all the charts + * Might be better to move this code to a temporary mocking service + */ + @action + updateMockedData(days) { + const generateDays = days; + const startDate = new Date(); + startDate.setDate(startDate.getDate() - generateDays + 1); + + const stats = []; + let growPeriod = true; + let growCount = 0; + let growLength = Math.floor(Math.random() * 14); + for (let index = 0; index < generateDays; index++) { + const date = new Date(startDate.getTime()); + date.setDate(date.getDate() + index); + + const previous = stats.length ? stats[stats.length - 1] : {free: 0, paid: 0, comped: 0}; + + stats.push({ + date: date.toISOString().split('T')[0], + free: previous.free + (growPeriod ? (index + Math.floor(Math.random() * (previous.free) * 0.01)) : 0), + paid: previous.paid + Math.floor(Math.random() * (previous.free) * 0.005), + comped: previous.comped + Math.floor(Math.random() * 1) + }); + + if (growPeriod) { + growCount += 1; + if (growCount > growLength) { + growPeriod = false; + growCount = 0; + growLength = Math.floor(Math.random() * 14); + } + } else { + growCount += 1; + if (growCount > growLength) { + growPeriod = true; + growCount = 0; + growLength = Math.floor(Math.random() * 14); + } + } + } + + this.dashboardStats.mockedMemberCountStats = stats; + + this.dashboardStats.mockedMemberCounts = { + total: stats[stats.length - 1].paid + stats[stats.length - 1].free + stats[stats.length - 1].comped, + paid: stats[stats.length - 1].paid, + free: stats[stats.length - 1].free + stats[stats.length - 1].comped + }; + + // Force update all data + this.dashboardStats.loadMembersCounts(); + this.dashboardStats.loadMrrStats(this.days); + this.dashboardStats.loadMemberCountStats(this.days); + this.dashboardStats.loadLastSeen(); + } } diff --git a/ghost/admin/app/components/dashboard/v5/chart-members-counts.hbs b/ghost/admin/app/components/dashboard/v5/chart-members-counts.hbs index c64440b145..aa26b845d2 100644 --- a/ghost/admin/app/components/dashboard/v5/chart-members-counts.hbs +++ b/ghost/admin/app/components/dashboard/v5/chart-members-counts.hbs @@ -1,16 +1,16 @@
-
{{this.dataTotalMembers}}
-
Total members
+
{{format-number this.totalMembers}}
+
{{gh-pluralize this.totalMembers "Total member" without-count=true}}
-
{{this.dataPaidMembers}}
-
Paid members
+
{{format-number this.paidMembers}}
+
{{gh-pluralize this.paidMembers "Paid member" without-count=true}}
-
{{this.dataFreeMembers}}
-
Free members
+
{{format-number this.freeMembers}}
+
{{gh-pluralize this.freeMembers "Free member" without-count=true}}
\ No newline at end of file diff --git a/ghost/admin/app/components/dashboard/v5/chart-members-counts.js b/ghost/admin/app/components/dashboard/v5/chart-members-counts.js index b392fc0258..1164b2ea00 100644 --- a/ghost/admin/app/components/dashboard/v5/chart-members-counts.js +++ b/ghost/admin/app/components/dashboard/v5/chart-members-counts.js @@ -1,15 +1,27 @@ import Component from '@glimmer/component'; +import {inject as service} from '@ember/service'; export default class ChartMembersCounts extends Component { - get dataTotalMembers() { - return '10,000'; + @service dashboardStats; + + constructor() { + super(...arguments); + this.loadCharts(); } - get dataPaidMembers() { - return '1,500'; + loadCharts() { + this.dashboardStats.loadMembersCounts(); } - get dataFreeMembers() { - return '8,500'; + get totalMembers() { + return this.dashboardStats.memberCounts?.total ?? 0; + } + + get paidMembers() { + return this.dashboardStats.memberCounts?.paid ?? 0; + } + + get freeMembers() { + return this.dashboardStats.memberCounts?.free ?? 0; } } diff --git a/ghost/admin/app/components/dashboard/v5/chart-total-members.hbs b/ghost/admin/app/components/dashboard/v5/chart-total-members.hbs index 654331aeac..3d3f0c5e09 100644 --- a/ghost/admin/app/components/dashboard/v5/chart-total-members.hbs +++ b/ghost/admin/app/components/dashboard/v5/chart-total-members.hbs @@ -1,5 +1,5 @@

Total members

- stat.total); + return { - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June'], + labels: labels, datasets: [{ - data: [65, 59, 80, 81, 56, 55, 40], + data: data, fill: false, borderColor: '#14b8ff', tension: 0.1 @@ -28,4 +52,43 @@ export default class ChartTotalMembers extends Component { get chartHeight() { return 300; } + + /** + * This method is borrowed from the members stats service and would need an update + */ + fillCountDates(data = {}, days) { + let currentRangeDate = moment().subtract(days, 'days'); + + let endDate = moment().add(1, 'hour'); + const output = {}; + const firstDateInRangeIndex = data.findIndex((val) => { + return moment(val.date).isAfter(currentRangeDate); + }); + let initialDateInRangeVal = firstDateInRangeIndex > 0 ? data[firstDateInRangeIndex - 1] : null; + if (firstDateInRangeIndex === 0 && !initialDateInRangeVal) { + initialDateInRangeVal = data[firstDateInRangeIndex]; + } + if (data.length > 0 && !initialDateInRangeVal && firstDateInRangeIndex !== 0) { + initialDateInRangeVal = data[data.length - 1]; + } + let lastVal = { + paid: initialDateInRangeVal ? initialDateInRangeVal.paid : 0, + free: initialDateInRangeVal ? initialDateInRangeVal.free : 0, + comped: initialDateInRangeVal ? initialDateInRangeVal.comped : 0, + total: initialDateInRangeVal ? (initialDateInRangeVal.paid + initialDateInRangeVal.free + initialDateInRangeVal.comped) : 0 + }; + while (currentRangeDate.isBefore(endDate)) { + let dateStr = currentRangeDate.format('YYYY-MM-DD'); + const dataOnDate = data.find(d => d.date === dateStr); + output[dateStr] = dataOnDate ? { + paid: dataOnDate.paid, + free: dataOnDate.free, + comped: dataOnDate.comped, + total: dataOnDate.paid + dataOnDate.free + dataOnDate.comped + } : lastVal; + lastVal = output[dateStr]; + currentRangeDate = currentRangeDate.add(1, 'day'); + } + return output; + } } diff --git a/ghost/admin/app/services/dashboard-stats.js b/ghost/admin/app/services/dashboard-stats.js new file mode 100644 index 0000000000..a5c6613104 --- /dev/null +++ b/ghost/admin/app/services/dashboard-stats.js @@ -0,0 +1,96 @@ +import Service from '@ember/service'; +import {tracked} from '@glimmer/tracking'; + +export default class DashboardStatsService extends Service { + @tracked + useMocks = true; + + @tracked + memberCounts = null; + + @tracked + memberCountStats = []; + + @tracked + mrrStats = []; + + @tracked + membersLastSeen30d = null; + + @tracked + membersLastSeen7d = null; + + @tracked + newsletterSubscribers = null; + + @tracked + emailsSent30d = null; + + @tracked + emailOpenRateStats = null; + + loadMembersCounts() { + if (this.useMocks) { + this.memberCounts = this.mockedMemberCounts; + return; + } + // Normal implementation + // @todo + } + + /** + * Loads the members graphs + * - total paid + * - total members + * for each day in the last {{days}} days + * @param {number} days The number of days to fetch data for + */ + loadMemberCountStats(days) { + if (this.useMocks) { + this.memberCountStats = this.mockedMemberCountStats.slice(-days); + return; + } + + // Normal implementation + // @todo + } + + /** + * Loads the mrr graphs + * @param {number} days The number of days to fetch data for + */ + loadMrrStats(days) { + if (this.useMocks) { + this.mmrStats = this.mockedMrrStats.slice(-days); + return; + } + + // Normal implementation + // @todo + } + + loadLastSeen() { + if (this.useMocks) { + this.membersLastSeen30d = 620; + this.membersLastSeen7d = 320; + return; + } + // Normal implementation + // @todo + } + + // Mocked data (move this to a mocking service?) + + @tracked + mockedMemberCountStats = []; + + @tracked + mockedMrrStats = []; + + @tracked + mockedMemberCounts = { + total: 0, + paid: 0, + free: 0 + }; +} diff --git a/ghost/admin/app/styles/layouts/dashboard.css b/ghost/admin/app/styles/layouts/dashboard.css index 7e71242be8..4e375d6110 100644 --- a/ghost/admin/app/styles/layouts/dashboard.css +++ b/ghost/admin/app/styles/layouts/dashboard.css @@ -1081,6 +1081,18 @@ a.gh-dashboard-container { margin-top: 50vh; } +.prototype-states-buttons { + margin-top: 15px; + display: flex; + flex-direction: row; + gap: 10px; +} + +.prototype-day-selection { + display: flex; + justify-content: flex-end; +} + .prototype-counts { display: flex; flex-direction: row;