diff --git a/ghost/admin/app/components/settings/audit-log/event-filter.hbs b/ghost/admin/app/components/settings/audit-log/event-filter.hbs
new file mode 100644
index 0000000000..5b4eb98592
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/event-filter.hbs
@@ -0,0 +1,29 @@
+
+
+
+ {{svg-jar "filter"}}
+ Filter events
+
+
+
+
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/settings/audit-log/event-filter.js b/ghost/admin/app/components/settings/audit-log/event-filter.js
new file mode 100644
index 0000000000..e45e5a8368
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/event-filter.js
@@ -0,0 +1,56 @@
+import Component from '@glimmer/component';
+import {action} from '@ember/object';
+import {inject as service} from '@ember/service';
+
+const ALL_EVENT_TYPES = [
+ {event: 'created', icon: 'event-filter-signup', name: 'Created'},
+ {event: 'edited', icon: 'event-filter-login', name: 'Edited'},
+ {event: 'deleted', icon: 'event-filter-subscription', name: 'Deleted'},
+ {event: 'post', icon: 'event-filter-payment', name: 'Posts'},
+ {event: 'pages', icon: 'event-filter-payment', name: 'Pages'},
+ {event: 'tag', icon: 'event-filter-newsletter', name: 'Tags'},
+ {event: 'integration', icon: 'event-filter-newsletter', name: 'Integrations'},
+ {event: 'api_key', icon: 'event-filter-newsletter', name: 'API keys'},
+ {event: 'label', icon: 'event-filter-newsletter', name: 'Labels'}
+];
+
+export default class AuditLogEventFilter extends Component {
+ @service settings;
+ @service feature;
+
+ get availableEventTypes() {
+ const extended = [...ALL_EVENT_TYPES];
+
+ if (this.args.hiddenEvents?.length) {
+ return extended.filter(t => !this.args.hiddenEvents.includes(t.event));
+ } else {
+ return extended;
+ }
+ }
+
+ get eventTypes() {
+ const excludedEvents = (this.args.excludedEvents || '').split(',');
+
+ return this.availableEventTypes.map(type => ({
+ event: type.event,
+ icon: type.icon,
+ name: type.name,
+ isSelected: !excludedEvents.includes(type.event)
+ }));
+ }
+
+ @action
+ toggleEventType(eventType) {
+ const excludedEvents = new Set(this.eventTypes.reject(type => type.isSelected).map(type => type.event));
+
+ if (excludedEvents.has(eventType)) {
+ excludedEvents.delete(eventType);
+ } else {
+ excludedEvents.add(eventType);
+ }
+
+ const excludeString = Array.from(excludedEvents).join(',');
+
+ this.args.onChange(excludeString || null);
+ }
+}
diff --git a/ghost/admin/app/components/settings/audit-log/no-events.hbs b/ghost/admin/app/components/settings/audit-log/no-events.hbs
new file mode 100644
index 0000000000..8ef96b7b17
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/no-events.hbs
@@ -0,0 +1,13 @@
+
+
+ {{svg-jar "activity-placeholder" class="gh-members-placeholder"}}
+ {{#if @filter}}
+
No actions match the current filter
+
+ Show all actions
+
+ {{else}}
+ No actions yet
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/settings/audit-log/search-trigger.hbs b/ghost/admin/app/components/settings/audit-log/search-trigger.hbs
new file mode 100644
index 0000000000..0c7005fb90
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/search-trigger.hbs
@@ -0,0 +1 @@
+Filter member {{svg-jar "arrow-down-small"}}
diff --git a/ghost/admin/app/components/settings/audit-log/search.hbs b/ghost/admin/app/components/settings/audit-log/search.hbs
new file mode 100644
index 0000000000..984887f6d6
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/search.hbs
@@ -0,0 +1,23 @@
+{{#if @selected}}
+
+
+ Clear search ×
+
+
+{{else}}
+
+ {{#if member.name}}{{member.name}} {{/if}}
+ {{member.email}}
+
+{{/if}}
diff --git a/ghost/admin/app/components/settings/audit-log/search.js b/ghost/admin/app/components/settings/audit-log/search.js
new file mode 100644
index 0000000000..97acb8b445
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/search.js
@@ -0,0 +1,20 @@
+import Component from '@glimmer/component';
+import {action} from '@ember/object';
+import {inject as service} from '@ember/service';
+import {task, timeout} from 'ember-concurrency';
+
+export default class AuditLogSearch extends Component {
+ @service store;
+
+ @action
+ clear() {
+ this.args.onChange(null);
+ }
+
+ @task
+ *searchUsersTask(term) {
+ yield timeout(300); // debounce
+
+ return yield this.store.query('user', {search: term, limit: 20});
+ }
+}
diff --git a/ghost/admin/app/components/settings/audit-log/table.hbs b/ghost/admin/app/components/settings/audit-log/table.hbs
new file mode 100644
index 0000000000..e408ed0891
--- /dev/null
+++ b/ghost/admin/app/components/settings/audit-log/table.hbs
@@ -0,0 +1,41 @@
+
+
+
+ User
+ Action
+ When
+
+
+
+ {{#each @events as |event|}}
+ {{#let (parse-audit-log-event event) as |ev|}}
+
+
+
+
+ Photo of {{ev.actor.name}}
+
+
{{ev.actor.name}}
+
+
+
+
+
{{svg-jar ev.actionIcon}}
+
+
+ {{capitalize-first-letter ev.action}}
+ {{#if (or ev.resource.title ev.resource.name)}}
+
+ {{if ev.resource.title ev.resource.title ev.resource.name}}
+
+ {{/if}}
+
+
+
+
+ {{moment-format ev.original.created_at "DD MMM YYYY HH:mm:ss"}}
+
+ {{/let}}
+ {{/each}}
+
+
diff --git a/ghost/admin/app/controllers/settings/audit-log.js b/ghost/admin/app/controllers/settings/audit-log.js
index 8a3f944f26..400e38b193 100644
--- a/ghost/admin/app/controllers/settings/audit-log.js
+++ b/ghost/admin/app/controllers/settings/audit-log.js
@@ -1,4 +1,39 @@
import Controller from '@ember/controller';
+import {action} from '@ember/object';
+import {inject as service} from '@ember/service';
+import {tracked} from '@glimmer/tracking';
export default class AuditLogController extends Controller {
+ @service router;
+ @service settings;
+ @service store;
+
+ queryParams = ['excludedResources', 'user'];
+
+ @tracked excludedResources = null;
+ @tracked user = null;
+
+ get fullExcludedResources() {
+ return (this.excludedResources || '').split(',');
+ }
+
+ get userRecord() {
+ if (!this.user) {
+ return null;
+ }
+
+ // TODO: {reload: true} here shouldn't be needed but without it
+ // the template renders nothing if the record is already in the store
+ return this.store.findRecord('user', this.user, {reload: true});
+ }
+
+ @action
+ changeExcludedResources(newList) {
+ this.router.transitionTo({queryParams: {excludedResources: newList}});
+ }
+
+ @action
+ changeUser(user) {
+ this.router.transitionTo({queryParams: {user: user?.id}});
+ }
}
diff --git a/ghost/admin/app/helpers/audit-log-event-fetcher.js b/ghost/admin/app/helpers/audit-log-event-fetcher.js
index e6b73dfcca..8a0af9f53e 100644
--- a/ghost/admin/app/helpers/audit-log-event-fetcher.js
+++ b/ghost/admin/app/helpers/audit-log-event-fetcher.js
@@ -34,6 +34,10 @@ export default class AuditLogEventFetcher extends Resource {
this.cursor = moment.utc().format('YYYY-MM-DD HH:mm:ss');
let filter = `created_at:<'${this.cursor}'`;
+ if (this.args.named.filter) {
+ filter += `+${this.args.named.filter}`;
+ }
+
// Can't get this working with Promise.all, somehow results in an infinite loop
await this.loadEventsTask.perform({filter});
}
@@ -58,6 +62,10 @@ export default class AuditLogEventFetcher extends Resource {
this.cursor = cursor;
let filter = `created_at:<'${this.cursor}'`;
+ if (this.args.named.filter) {
+ filter += `+${this.args.named.filter}`;
+ }
+
this.loadEventsTask.perform({filter});
}
diff --git a/ghost/admin/app/helpers/audit-log-event-filter.js b/ghost/admin/app/helpers/audit-log-event-filter.js
new file mode 100644
index 0000000000..e37a895776
--- /dev/null
+++ b/ghost/admin/app/helpers/audit-log-event-filter.js
@@ -0,0 +1,34 @@
+import Helper from '@ember/component/helper';
+import classic from 'ember-classic-decorator';
+import {isBlank} from '@ember/utils';
+import {inject as service} from '@ember/service';
+
+@classic
+export default class AuditLogEventFilter extends Helper {
+ @service settings;
+ @service feature;
+
+ compute(
+ positionalParams,
+ {excludedEvents = [], user = ''}
+ ) {
+ const excludedEventsSet = new Set();
+
+ if (excludedEvents.length) {
+ excludedEvents.forEach(type => excludedEventsSet.add(type));
+ }
+
+ let filterParts = [];
+
+ const excludedEventsArray = Array.from(excludedEventsSet).reject(isBlank);
+ if (excludedEventsArray.length > 0) {
+ filterParts.push(`resource_type:-[${excludedEventsArray.join(',')}]`);
+ }
+
+ if (user) {
+ filterParts.push(`actor_id:${user}`);
+ }
+
+ return filterParts.join('+');
+ }
+}
diff --git a/ghost/admin/app/templates/settings/audit-log.hbs b/ghost/admin/app/templates/settings/audit-log.hbs
index 9331d45a2c..c112b73cf3 100644
--- a/ghost/admin/app/templates/settings/audit-log.hbs
+++ b/ghost/admin/app/templates/settings/audit-log.hbs
@@ -4,74 +4,41 @@
Settings
{{svg-jar "arrow-right"}}
Audit log
+ {{#if this.userRecord}}
+ {{svg-jar "arrow-right"}}
+ {{or this.userRecord.name this.userRecord.email}}
+ {{/if}}
+
+
+
+
+
- {{#let (audit-log-event-fetcher pageSize=50) as |eventsFetcher|}}
- {{#if eventsFetcher.data}}
-