Added member count trends in dashboard 5.0

refs https://github.com/TryGhost/Team/issues/1443

- Fetch member counts from 30 days ago
- Added Percentage component to show +1%, -1% or = changes
- Fixed: emails sent number was not formatted
- Fixed: wrong dates were used for some stats
This commit is contained in:
Simon Backx 2022-03-28 16:15:35 +02:00
parent 7d104b2b44
commit 8d3c1dacc2
5 changed files with 83 additions and 11 deletions

View File

@ -12,7 +12,7 @@
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">Emails sent in the past 30 days</h4>
<div class="gh-dashboard5-number is-small">{{this.dataEmailsSent}}</div>
<div class="gh-dashboard5-number is-small">{{format-number this.dataEmailsSent}}</div>
<div class="gh-dashboard5-percentage is-positive">+2.2%</div>
</div>

View File

@ -5,16 +5,22 @@
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.totalMembers "Total member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.totalMembers}}</div>
<div class="gh-dashboard5-percentage is-positive">+2.2%</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.totalMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.paidMembers "Paid member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.paidMembers}}</div>
<div class="gh-dashboard5-percentage is-positive">+3.3%</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.paidMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.freeMembers "Free member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.freeMembers}}</div>
<div class="gh-dashboard5-percentage is-negative">-1.1%</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.freeMembersTrend}}/>
{{/if}}
</div>
</div>

View File

@ -21,4 +21,31 @@ export default class ChartMembersCounts extends Component {
get freeMembers() {
return this.dashboardStats.memberCounts?.free ?? 0;
}
get hasTrends() {
return this.dashboardStats.memberCounts !== null && this.dashboardStats.memberCountsTrend !== null;
}
get totalMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
}
get paidMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
}
get freeMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
return Math.round((to - from) / from * 100);
}
}

View File

@ -0,0 +1,7 @@
{{#if (gt @percentage 0) }}
<div class="gh-dashboard5-percentage is-positive">+{{ @percentage }}%</div>
{{else if (lt @percentage 0)}}
<div class="gh-dashboard5-percentage is-negative">{{ @percentage }}%</div>
{{else}}
<div class="gh-dashboard5-percentage">=</div>
{{/if}}

View File

@ -77,6 +77,12 @@ export default class DashboardStatsService extends Service {
@tracked
memberCounts = null;
/**
* @type {?MemberCounts} Member counts to compare against for trends (30 days ago)
*/
@tracked
memberCountsTrend = null;
/**
* @type {?MemberCountStat[]}
*/
@ -171,27 +177,53 @@ export default class DashboardStatsService extends Service {
@task({restartable: true})
*_loadMembersCounts() {
this.memberCounts = null;
this.memberCountsTrend = null;
if (this.dashboardMocks.enabled) {
yield this.dashboardMocks.waitRandom();
if (this.dashboardMocks.memberCounts === null) {
return null;
}
this.memberCounts = {...this.dashboardMocks.memberCounts};
this.memberCountsTrend = {
// One percentage up
total: Math.round(this.dashboardMocks.memberCounts.total * 1.06),
// One percentage down
free: Math.round(this.dashboardMocks.memberCounts.free * 0.96),
// One percentage =
paid: this.dashboardMocks.memberCounts.paid
};
return;
}
// @todo We need to have way to reduce the total number of API requests
const paidResult = yield this.store.query('member', {limit: 1, filter: 'status:paid'});
const paid = paidResult.meta.pagination.total;
let paidResult = yield this.store.query('member', {limit: 1, filter: 'status:paid'});
let paid = paidResult.meta.pagination.total;
const freeResult = yield this.store.query('member', {limit: 1, filter: 'status:-paid'});
const free = freeResult.meta.pagination.total;
let freeResult = yield this.store.query('member', {limit: 1, filter: 'status:-paid'});
let free = freeResult.meta.pagination.total;
this.memberCounts = {
total: paid + free,
paid,
free
};
// Now fetch trends (30 days ago)
const trendDate = new Date(Date.now() - 30 * 60 * 60 * 24 * 1000);
paidResult = yield this.store.query('member', {limit: 1, filter: `status:paid+created_at:<'${trendDate.toISOString()}'`});
paid = paidResult.meta.pagination.total;
freeResult = yield this.store.query('member', {limit: 1, filter: `status:-paid+created_at:<'${trendDate.toISOString()}'`});
free = freeResult.meta.pagination.total;
this.memberCountsTrend = {
total: paid + free,
paid,
free
};
}
loadMemberCountStats() {
@ -286,8 +318,8 @@ export default class DashboardStatsService extends Service {
// @todo We need to have way to reduce the total number of API requests
const start30d = new Date(Date.now() - 30 * 3600 * 1000);
const start7d = new Date(Date.now() - 7 * 3600 * 1000);
const start30d = new Date(Date.now() - 30 * 86400 * 1000);
const start7d = new Date(Date.now() - 7 * 86400 * 1000);
let extraFilter = '';
if (this.lastSeenFilterStatus === 'paid') {
@ -379,7 +411,7 @@ export default class DashboardStatsService extends Service {
return;
}
const start30d = new Date(Date.now() - 30 * 3600 * 1000);
const start30d = new Date(Date.now() - 30 * 86400 * 1000);
const result = yield this.store.query('email', {limit: 100, filter: 'submitted_at:>' + start30d.toISOString()});
this.emailsSent30d = result.reduce((c, email) => c + email.emailCount, 0);
}