Added initial event timeline to dashboard (#1840)

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

* Updated stats service to fetch event timeline
* Updated dashboard controller to load events
* Added currency utils
* Added GhEventTimeline component
* Updated dashboard to render GhEventTimeline
This commit is contained in:
Fabien 'egg' O'Carroll 2021-02-18 14:17:10 +00:00 committed by GitHub
parent f9bafa740a
commit c56ba0c71e
6 changed files with 202 additions and 27 deletions

View File

@ -0,0 +1,14 @@
<div class="gh-event-timeline">
<ul class="gh-dashboard-activity-feed">
{{#each this.parsedEvents as |event|}}
<li>
<div class="activity">
<div>
<span class="member">{{event.subject}}</span> {{event.action}} {{event.object}} <span class="highlight">{{event.info}}</span>
</div>
</div>
<span class="time">{{event.timestamp}}</span>
</li>
{{/each}}
</ul>
</div>

View File

@ -0,0 +1,98 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {getNonDecimal, getSymbol} from 'ghost-admin/utils/currency';
import {tracked} from '@glimmer/tracking';
export default class EventTimeline extends Component {
@tracked
parsedEvents = null;
constructor(...args) {
super(...args);
this.parseEvents(this.args.events);
}
getIcon(event) {
return event.type;
}
getAction(event) {
if (event.type === 'login_event') {
return 'logged in';
}
if (event.type === 'payment_event') {
return 'made a payment';
}
if (event.type === 'newsletter_event') {
if (event.data.subscribed) {
return 'subscribed to';
} else {
return 'unsubscribed from';
}
}
if (event.type === 'subscription_event') {
if (event.data.from_plan === null) {
return 'started';
}
if (event.data.to_plan === null) {
return 'cancelled';
}
return 'changed';
}
}
getObject(event) {
if (event.type === 'login_event') {
return '';
}
if (event.type === 'payment_event') {
return '';
}
if (event.type === 'newsletter_event') {
return 'emails';
}
if (event.type === 'subscription_event') {
return 'their subscription';
}
}
getInfo(event) {
if (event.type === 'subscription_event') {
let mrrDelta = getNonDecimal(event.data.mrr_delta, event.data.currency);
if (mrrDelta === 0) {
return;
}
let sign = mrrDelta > 0 ? '+' : '-';
let symbol = getSymbol(event.data.currency);
return `(MRR ${sign}${symbol}${mrrDelta})`;
}
return;
}
parseEvents(events) {
this.parsedEvents = events.map((event) => {
let subject = event.data.member.name;
let icon = this.getIcon(event);
let action = this.getAction(event);
let object = this.getObject(event);
let info = this.getInfo(event);
let timestamp = moment(event.data.created_at).fromNow();
return {
icon,
subject,
action,
object,
info,
timestamp
};
});
}
}

View File

@ -1,7 +1,32 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import {inject as service} from '@ember/service'; import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class DashboardController extends Controller { export default class DashboardController extends Controller {
@service feature; @service feature;
@service session; @service session;
@service membersStats;
@tracked
events = {
data: null,
error: null,
loading: false
};
constructor(...args) {
super(...args);
this.loadEvents();
}
loadEvents() {
this.events.loading = true;
this.membersStats.fetchTimeline().then(({events}) => {
this.events.data = events;
this.events.loading = false;
}, (error) => {
this.events.error = error;
this.events.loading = false;
});
}
} }

View File

@ -9,6 +9,7 @@ export default class MembersStatsService extends Service {
@tracked days = '30'; @tracked days = '30';
@tracked stats = null; @tracked stats = null;
@tracked events = null;
fetch() { fetch() {
let daysChanged = this._lastFetchedDays !== this.days; let daysChanged = this._lastFetchedDays !== this.days;
@ -27,6 +28,20 @@ export default class MembersStatsService extends Service {
return this._fetchTask.perform(); return this._fetchTask.perform();
} }
fetchTimeline() {
let staleData = this._lastFetchedTimeline && this._lastFetchedTimeline - new Date() > 1 * 60 * 1000;
if (this._fetchTimelineTask.isRunning) {
return this._fetchTask.last;
}
if (this.events && !this._forceRefresh && !staleData) {
return Promise.resolve(this.events);
}
return this._fetchTimelineTask.perform();
}
invalidate() { invalidate() {
this._forceRefresh = true; this._forceRefresh = true;
} }
@ -44,4 +59,13 @@ export default class MembersStatsService extends Service {
this.stats = stats; this.stats = stats;
return stats; return stats;
} }
@task
*_fetchTimelineTask() {
this._lastFetchedTimeline = new Date();
let eventsUrl = this.ghostPaths.url.api('members/events');
let events = yield this.ajax.request(eventsUrl);
this.events = events;
return events;
}
} }

View File

@ -140,32 +140,18 @@
<div class="gh-dashboard-box grey activity-feed"> <div class="gh-dashboard-box grey activity-feed">
<h4 class="gh-dashboard-header">Activity feed</h4> <h4 class="gh-dashboard-header">Activity feed</h4>
<div class="content"> <div class="content">
<ul class="gh-dashboard-activity-feed"> {{#if this.events.loading}}
<li> Loading...
<div class="activity"> {{else}}
<div> {{#if this.events.error}}
<span class="member">Kadin Levin</span> subscribed <span class="highlight">(MRR +$38)</span> <p class="error">
</div> There was an error loading events
</div> <code>{{this.events.error.message}}</code>
<span class="time">29 mins ago</span> </p>
</li> {{else}}
<li> <GhEventTimeline @events={{this.events.data}}/>
<div class="activity"> {{/if}}
<div> {{/if}}
<span class="member">Terry Stanton</span> subscribed <span class="highlight">(MRR +$38)</span>
</div>
</div>
<span class="time">1 day ago</span>
</li>
<li>
<div class="activity">
<div>
<span class="member">Angel Rhiel Madsen</span> subscribed <span class="highlight">(MRR +$38)</span>
</div>
</div>
<span class="time">3 days ago</span>
</li>
</ul>
</div> </div>
</div> </div>
<div class="gh-dashboard-box"> <div class="gh-dashboard-box">

View File

@ -0,0 +1,28 @@
export function getSymbol(currency) {
switch (currency) {
case 'usd':
case 'aud':
case 'cad':
return '$';
case 'eur':
return '€';
case 'gbp':
return '£';
case 'inr':
return '₹';
}
return null;
}
export function getNonDecimal(amount, currency) {
switch (currency) {
case 'usd':
case 'aud':
case 'cad':
case 'eur':
case 'gbp':
case 'inr':
return amount / 100;
}
return null;
}