diff --git a/ghost/admin/app/components/gh-event-timeline.hbs b/ghost/admin/app/components/gh-event-timeline.hbs new file mode 100644 index 0000000000..2cc9e05d73 --- /dev/null +++ b/ghost/admin/app/components/gh-event-timeline.hbs @@ -0,0 +1,14 @@ +
+ +
diff --git a/ghost/admin/app/components/gh-event-timeline.js b/ghost/admin/app/components/gh-event-timeline.js new file mode 100644 index 0000000000..65e416a2cd --- /dev/null +++ b/ghost/admin/app/components/gh-event-timeline.js @@ -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 + }; + }); + } +} diff --git a/ghost/admin/app/controllers/dashboard.js b/ghost/admin/app/controllers/dashboard.js index 5c74c33fe2..604812e261 100644 --- a/ghost/admin/app/controllers/dashboard.js +++ b/ghost/admin/app/controllers/dashboard.js @@ -1,7 +1,32 @@ import Controller from '@ember/controller'; import {inject as service} from '@ember/service'; +import {tracked} from '@glimmer/tracking'; export default class DashboardController extends Controller { @service feature; @service session; -} \ No newline at end of file + @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; + }); + } +} diff --git a/ghost/admin/app/services/members-stats.js b/ghost/admin/app/services/members-stats.js index e24be5d600..4a94e4c35a 100644 --- a/ghost/admin/app/services/members-stats.js +++ b/ghost/admin/app/services/members-stats.js @@ -9,6 +9,7 @@ export default class MembersStatsService extends Service { @tracked days = '30'; @tracked stats = null; + @tracked events = null; fetch() { let daysChanged = this._lastFetchedDays !== this.days; @@ -27,6 +28,20 @@ export default class MembersStatsService extends Service { 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() { this._forceRefresh = true; } @@ -44,4 +59,13 @@ export default class MembersStatsService extends Service { this.stats = 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; + } } diff --git a/ghost/admin/app/templates/dashboard.hbs b/ghost/admin/app/templates/dashboard.hbs index 3e35fcc7e2..6e3ced217f 100644 --- a/ghost/admin/app/templates/dashboard.hbs +++ b/ghost/admin/app/templates/dashboard.hbs @@ -140,32 +140,18 @@

Activity feed

- + {{#if this.events.loading}} + Loading... + {{else}} + {{#if this.events.error}} +

+ There was an error loading events + {{this.events.error.message}} +

+ {{else}} + + {{/if}} + {{/if}}
diff --git a/ghost/admin/app/utils/currency.js b/ghost/admin/app/utils/currency.js new file mode 100644 index 0000000000..083517e70b --- /dev/null +++ b/ghost/admin/app/utils/currency.js @@ -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; +}