mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
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:
parent
f9bafa740a
commit
c56ba0c71e
14
ghost/admin/app/components/gh-event-timeline.hbs
Normal file
14
ghost/admin/app/components/gh-event-timeline.hbs
Normal 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>
|
98
ghost/admin/app/components/gh-event-timeline.js
Normal file
98
ghost/admin/app/components/gh-event-timeline.js
Normal 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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
28
ghost/admin/app/utils/currency.js
Normal file
28
ghost/admin/app/utils/currency.js
Normal 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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user