mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Added first version of Audit Log UI
refs https://github.com/TryGhost/Toolbox/issues/356 - skateboard version w/ loading data from API, settings button guarded by labs, and a basic UI populating a table with info
This commit is contained in:
parent
e77fedf9b2
commit
6c2aebbf0c
7 changed files with 219 additions and 0 deletions
ghost/admin/app
4
ghost/admin/app/controllers/settings/audit-log.js
Normal file
4
ghost/admin/app/controllers/settings/audit-log.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class AuditLogController extends Controller {
|
||||
}
|
92
ghost/admin/app/helpers/audit-log-event-fetcher.js
Normal file
92
ghost/admin/app/helpers/audit-log-event-fetcher.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
import moment from 'moment';
|
||||
import {Resource} from 'ember-could-get-used-to-this';
|
||||
import {TrackedArray} from 'tracked-built-ins';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class AuditLogEventFetcher extends Resource {
|
||||
@service ajax;
|
||||
@service ghostPaths;
|
||||
@service store;
|
||||
|
||||
@tracked data = new TrackedArray([]);
|
||||
@tracked isLoading = false;
|
||||
@tracked isError = false;
|
||||
@tracked errorMessage = null;
|
||||
@tracked hasReachedEnd = false;
|
||||
|
||||
cursor = null;
|
||||
|
||||
get value() {
|
||||
return {
|
||||
isLoading: this.isLoading,
|
||||
isError: this.isError,
|
||||
errorMessage: this.errorMessage,
|
||||
data: this.data,
|
||||
loadNextPage: this.loadNextPage,
|
||||
hasReachedEnd: this.hasReachedEnd
|
||||
};
|
||||
}
|
||||
|
||||
async setup() {
|
||||
this.cursor = moment.utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
let filter = `created_at:<'${this.cursor}'`;
|
||||
|
||||
// Can't get this working with Promise.all, somehow results in an infinite loop
|
||||
await this.loadEventsTask.perform({filter});
|
||||
}
|
||||
|
||||
@action
|
||||
loadNextPage() {
|
||||
// NOTE: assumes data is always ordered by created_at desc
|
||||
const lastEvent = this.data[this.data.length - 1];
|
||||
|
||||
if (!lastEvent?.data?.created_at) {
|
||||
this.hasReachedEnd = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = moment.utc(lastEvent.data.created_at).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
if (cursor === this.cursor) {
|
||||
this.hasReachedEnd = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.cursor = cursor;
|
||||
let filter = `created_at:<'${this.cursor}'`;
|
||||
|
||||
this.loadEventsTask.perform({filter});
|
||||
}
|
||||
|
||||
@task
|
||||
*loadEventsTask(queryParams) {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
|
||||
const url = this.ghostPaths.url.api('actions');
|
||||
const data = Object.assign({}, queryParams, {limit: this.args.named.pageSize});
|
||||
const {actions} = yield this.ajax.request(url, {data});
|
||||
|
||||
if (actions.length < data.limit) {
|
||||
this.hasReachedEnd = true;
|
||||
}
|
||||
|
||||
this.data.push(...actions);
|
||||
} catch (e) {
|
||||
this.isError = true;
|
||||
|
||||
const errorMessage = e.payload?.errors?.[0]?.message;
|
||||
if (errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
// TODO: log to Sentry
|
||||
console.error(e); // eslint-disable-line
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
31
ghost/admin/app/helpers/parse-audit-log-event.js
Normal file
31
ghost/admin/app/helpers/parse-audit-log-event.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
export default function parseAuditLogEvent(ev) {
|
||||
const actorName = getActorName(ev);
|
||||
const action = getAction(ev);
|
||||
const actionIcon = getActionIcon(ev);
|
||||
|
||||
return {
|
||||
actorName,
|
||||
actionIcon,
|
||||
action,
|
||||
timestamp: ev.created_at
|
||||
};
|
||||
}
|
||||
|
||||
function getActionIcon(ev) {
|
||||
switch (ev.event) {
|
||||
case 'added':
|
||||
return 'add-stroke';
|
||||
case 'edited':
|
||||
return 'content';
|
||||
case 'deleted':
|
||||
return 'cross-circle';
|
||||
}
|
||||
}
|
||||
|
||||
function getActorName(ev) {
|
||||
return ev.actor_id;
|
||||
}
|
||||
|
||||
function getAction(ev) {
|
||||
return `${ev.event} ${ev.resource_type}`;
|
||||
}
|
|
@ -49,6 +49,7 @@ Router.map(function () {
|
|||
this.route('settings.general', {path: '/settings/general'});
|
||||
this.route('settings.membership', {path: '/settings/members'});
|
||||
this.route('settings.code-injection', {path: '/settings/code-injection'});
|
||||
this.route('settings.audit-log', {path: '/settings/audit-log'});
|
||||
|
||||
// redirect from old /settings/members-email to /settings/newsletters
|
||||
this.route('settings.members-email', {path: '/settings/members-email'});
|
||||
|
|
9
ghost/admin/app/routes/settings/audit-log.js
Normal file
9
ghost/admin/app/routes/settings/audit-log.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import AdminRoute from 'ghost-admin/routes/admin';
|
||||
|
||||
export default class AuditLogRoute extends AdminRoute {
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Audit log'
|
||||
};
|
||||
}
|
||||
}
|
|
@ -82,6 +82,16 @@
|
|||
<p>Testing ground for new features</p>
|
||||
</div>
|
||||
</LinkTo>
|
||||
|
||||
{{#if (feature 'auditLog')}}
|
||||
<LinkTo class="gh-setting-group" @route="settings.audit-log" data-test-nav="audit-log">
|
||||
<span class="blue">{{svg-jar "TODO"}}</span>
|
||||
<div>
|
||||
<h4>Audit log</h4>
|
||||
<p>View staff user actions</p>
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
|
72
ghost/admin/app/templates/settings/audit-log.hbs
Normal file
72
ghost/admin/app/templates/settings/audit-log.hbs
Normal file
|
@ -0,0 +1,72 @@
|
|||
<section class="gh-canvas gh-members-activity">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.audit-log" data-test-link="audit-log-back">Audit log</LinkTo>
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
<div class="view-container">
|
||||
{{#let (audit-log-event-fetcher pageSize=50) as |eventsFetcher|}}
|
||||
{{#if eventsFetcher.data}}
|
||||
<div class="gh-list-scrolling">
|
||||
<table class="gh-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each eventsFetcher.data as |event|}}
|
||||
{{#let (parse-audit-log-event event) as |ev|}}
|
||||
<tr>
|
||||
<div class="gh-list-data">
|
||||
<div class="flex items-center">
|
||||
<div class="w-80">
|
||||
<h3 class="ma0 pa0 gh-members-list-name">{{ev.actorName}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-list-data">
|
||||
<div class="gh-members-activity-container">
|
||||
<div class="gh-members-activity-icon">{{svg-jar ev.actionIcon}}</div>
|
||||
<div class="gh-members-activity-event">
|
||||
<span class="gh-members-activity-description">
|
||||
{{capitalize-first-letter event.action}}
|
||||
{{ev.action}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-list-data">{{moment-format ev.timestamp "DD MMM YYYY HH:mm:ss"}}</div>
|
||||
</tr>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{#if (not (or eventsFetcher.isLoading eventsFetcher.hasReachedEnd))}}
|
||||
<GhScrollTrigger @enter={{eventsFetcher.loadNextPage}} @triggerOffset={{250}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#unless eventsFetcher.isLoading}}
|
||||
<div class="no-posts-box">
|
||||
<div class="no-posts">
|
||||
{{svg-jar "activity-placeholder" class="gh-members-placeholder"}}
|
||||
<h4>No staff activity yet</h4>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if eventsFetcher.isLoading}}
|
||||
<div class="no-posts-box"><GhLoadingSpinner /></div>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{outlet}}
|
Loading…
Add table
Reference in a new issue