audit log and info workspace

This commit is contained in:
Lone Iversen
2024-01-05 15:48:27 +01:00
parent 0b43637f86
commit cc852446b0
18 changed files with 432 additions and 141 deletions

View File

@@ -49,6 +49,7 @@
"./events": "./dist-cms/packages/core/umb-events/index.js",
"./repository": "./dist-cms/packages/core/repository/index.js",
"./temporary-file": "./dist-cms/packages/core/temporary-file/index.js",
"./audit-log": "./dist-cms/packages/audit-log/index.js",
"./dictionary": "./dist-cms/packages/dictionary/dictionary/index.js",
"./document": "./dist-cms/packages/documents/documents/index.js",
"./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.js",

View File

@@ -12,6 +12,7 @@ import './components/index.js';
// TODO: temp solution to load core packages
const CORE_PACKAGES = [
import('../../packages/audit-log/umbraco-package.js'),
import('../../packages/core/umbraco-package.js'),
import('../../packages/settings/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),

View File

@@ -0,0 +1 @@
export * from './repository/index.js';

View File

@@ -0,0 +1,3 @@
import { manifests as repositoryManifests } from './repository/manifests.js';
export const manifests = [...repositoryManifests];

View File

@@ -0,0 +1,67 @@
import { UmbAuditLogServerDataSource } from './audit-log.server.data.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import { AuditTypeModel, DirectionModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
export class UmbAuditLogRepository extends UmbBaseController {
#dataSource: UmbAuditLogServerDataSource;
#notificationService?: UmbNotificationContext;
#init;
constructor(host: UmbControllerHostElement) {
super(host);
this.#dataSource = new UmbAuditLogServerDataSource(host);
this.#init = new UmbContextConsumerController(host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationService = instance;
}).asPromise();
}
async getLog({
orderDirection,
sinceDate,
skip = 0,
take = 100,
}: {
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLog({ orderDirection, sinceDate, skip, take });
}
async getAuditLogByUnique({
id,
orderDirection,
skip = 0,
take = 100,
}: {
id: string;
orderDirection?: DirectionModel;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLogById({ id, orderDirection, skip, take });
}
async getAuditLogTypeByLogType({
logType,
sinceDate,
skip,
take,
}: {
logType: AuditTypeModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLogTypeByLogType({ logType, sinceDate, skip, take });
}
}

View File

@@ -0,0 +1,80 @@
import { AuditLogResource, DirectionModel, AuditTypeModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for Data Type items that fetches data from the server
* @export
* @class UmbAuditLogServerDataSource
*/
export class UmbAuditLogServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbAuditLogServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbAuditLogServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof UmbAuditLogServerDataSource
*/
async getAuditLog({
orderDirection,
sinceDate,
skip,
take,
}: {
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLog({ orderDirection, sinceDate, skip, take }),
);
}
async getAuditLogById({
id,
orderDirection,
sinceDate,
skip,
take,
}: {
id: string;
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLogById({ id, orderDirection, sinceDate, skip, take }),
);
}
async getAuditLogTypeByLogType({
logType,
sinceDate,
skip,
take,
}: {
logType: AuditTypeModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLogTypeByLogType({ logType, sinceDate, skip, take }),
);
}
}

View File

@@ -0,0 +1,2 @@
export { UMB_AUDIT_LOG_REPOSITORY_ALIAS as AUDIT_LOG_REPOSITORY_ALIAS } from './manifests.js';
export { UmbAuditLogRepository } from './audit-log.repository.js';

View File

@@ -0,0 +1,13 @@
import { UmbAuditLogRepository } from './audit-log.repository.js';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_AUDIT_LOG_REPOSITORY_ALIAS = 'Umb.Repository.AuditLog';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_AUDIT_LOG_REPOSITORY_ALIAS,
name: 'AuditLog Repository',
api: UmbAuditLogRepository,
};
export const manifests = [repository];

View File

@@ -0,0 +1,9 @@
export const name = 'Umbraco.Core.AuditLog';
export const extensions = [
{
name: 'Audit Log Bundle',
alias: 'Umb.Bundle.AuditLog',
type: 'bundle',
js: () => import('./manifests.js'),
},
];

View File

@@ -43,11 +43,11 @@ export class UmbHistoryItemElement extends UmbLitElement {
gap: calc(2 * var(--uui-size-space-5));
align-items: center;
}
.slots-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
}
slot[name='actions'] {

View File

@@ -1,27 +1,15 @@
import { css, html, nothing, repeat, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
interface HistoryNode {
userId?: number;
userAvatars?: [];
userName?: string;
timestamp?: string;
comment?: string;
entityType?: string;
logType?: HistoryLogType;
nodeId?: string;
parameters?: string;
}
type HistoryLogType = 'Publish' | 'Save' | 'Unpublish' | 'ContentVersionEnableCleanup' | 'ContentVersionPreventCleanup';
import './history/document-info-history-workspace-view.element.js';
@customElement('umb-document-info-workspace-view')
export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
@state()
private _historyList: HistoryNode[] = [
/** Dont delete, need for mock data */
/*@state()
private _historyList: [] = [
{
userId: -1,
userAvatars: [],
@@ -79,13 +67,7 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
nodeId: '1058',
parameters: undefined,
},
];
@state()
private _total?: number;
@state()
private _currentPage = 1;
];*/
@state()
private _nodeName = '';
@@ -94,7 +76,6 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
private _documentTypeId = '';
private _workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE;
private itemsPerPage = 10;
@state()
private _editDocumentTypePath = '';
@@ -130,27 +111,14 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
*/
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
//TODO: Run endpoint to get next history parts
}
render() {
return html`<div class="container">
<uui-box headline=${this.localize.term('general_links')} style="--uui-box-default-padding: 0;">
${this.#renderLinksSection()}
</uui-box>
<uui-box headline=${this.localize.term('general_history')}>
<umb-history-list>
${repeat(
this._historyList,
(item) => item.timestamp,
(item) => this.#renderHistory(item),
)}
</umb-history-list>
${this.#renderHistoryPagination()}
</uui-box>
<umb-document-info-history-workspace-view
.documentUnique=${this._documentTypeId}></umb-document-info-history-workspace-view>
</div>
<div class="container">
<uui-box headline="General" id="general-section">${this.#renderGeneralSection()}</uui-box>
@@ -158,7 +126,6 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
}
#renderLinksSection() {
//repeat
return html`<div id="link-section">
<a href="http://google.com" target="_blank" class="link-item with-href">
<span class="link-language">da-DK</span>
@@ -175,104 +142,34 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
return html`
<div class="general-item">
<strong>${this.localize.term('content_publishStatus')}</strong>
<span
><uui-tag color="positive" look="primary" label=${this.localize.term('content_published')}
><umb-localize key="content_published"></umb-localize></uui-tag
></span>
<span>
<uui-tag color="positive" look="primary" label=${this.localize.term('content_published')}>
<umb-localize key="content_published"></umb-localize>
</uui-tag>
</span>
</div>
<div class="general-item">
<strong><umb-localize key="content_createDate"></umb-localize></strong>
<span><umb-localize-date date="${new Date()}"></umb-localize-date></span>
<span><umb-localize-date .date="${new Date()}"></umb-localize-date></span>
</div>
<div class="general-item">
<strong><umb-localize key="content_documentType"></umb-localize></strong>
<uui-button
look="secondary"
href=${this._editDocumentTypePath + 'edit/' + this._documentTypeId}
label=${this.localize.term('general_edit')}></uui-button>
</div>
<div class="general-item">
<strong><umb-localize key="template_template"></umb-localize></strong>
<span>IMPLEMENT template picker?</span>
<uui-button look="secondary" label="Template picker TODO"></uui-button>
</div>
<div class="general-item">
<strong><umb-localize key="template_id"></umb-localize></strong>
<span>...</span>
<span>${this._documentTypeId}</span>
</div>
`;
}
#renderHistory(history: HistoryNode) {
return html` <umb-history-item .name="${history.userName}" .detail="${this.localize.date(history.timestamp!)}">
<span class="log-type"
>${this.#renderTag(history.logType)} ${this.#renderTagDescription(history.logType, history)}</span
>
<uui-button label=${this.localize.term('actions_rollback')} look="secondary" slot="actions">
<uui-icon name="icon-undo"></uui-icon>
<umb-localize key="actions_rollback"></umb-localize>
</uui-button>
</umb-history-item>`;
}
#renderHistoryPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
#renderTag(type?: HistoryLogType) {
switch (type) {
case 'Publish':
return html`<uui-tag look="primary" color="positive" label=${this.localize.term('content_publish')}
><umb-localize key="content_publish"></umb-localize
></uui-tag>`;
case 'Unpublish':
return html`<uui-tag look="primary" color="warning" label=${this.localize.term('content_unpublish')}
><umb-localize key="content_unpublish"></umb-localize
></uui-tag>`;
case 'Save':
return html`<uui-tag look="primary" label=${this.localize.term('auditTrails_smallSave')}
><umb-localize key="auditTrails_smallSave"></umb-localize
></uui-tag>`;
case 'ContentVersionEnableCleanup':
return html`<uui-tag
look="secondary"
label=${this.localize.term('contentTypeEditor_historyCleanupEnableCleanup')}
><umb-localize key="contentTypeEditor_historyCleanupEnableCleanup"></umb-localize
></uui-tag>`;
case 'ContentVersionPreventCleanup':
return html`<uui-tag
look="secondary"
label=${this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}
><umb-localize key="contentTypeEditor_historyCleanupPreventCleanup"></umb-localize
></uui-tag>`;
default:
return 'Could not detect log type';
}
}
#renderTagDescription(type?: HistoryLogType, params?: HistoryNode) {
switch (type) {
case 'Publish':
return this.localize.term('auditTrails_publish');
case 'Unpublish':
return this.localize.term('auditTrails_unpublish');
case 'Save':
return this.localize.term('auditTrails_save');
case 'ContentVersionEnableCleanup':
return this.localize.term('auditTrails_contentversionenablecleanup', [params?.nodeId]);
case 'ContentVersionPreventCleanup':
return this.localize.term('auditTrails_contentversionpreventcleanup', [params?.nodeId]);
default:
return 'Could not detect log type';
}
}
static styles = [
UmbTextStyles,
css`
@@ -342,25 +239,6 @@ export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
.link-item.with-href:hover {
background: var(--uui-color-divider);
}
//History section
uui-tag uui-icon {
margin-right: var(--uui-size-space-1);
}
.log-type {
display: flex;
gap: var(--uui-size-space-2);
}
uui-pagination {
display: inline-block;
}
.pagination {
display: flex;
justify-content: center;
margin-top: var(--uui-size-space-4);
}
`,
];
}

View File

@@ -0,0 +1,170 @@
import { HistoricTagAndDescription } from './history-utils.js';
import { css, html, customElement, state, property, nothing, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbAuditLogRepository } from '@umbraco-cms/backoffice/audit-log';
import { AuditLogBaseModel, DirectionModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-document-info-history-workspace-view')
export class UmbDocumentInfoHistoryWorkspaceViewElement extends UmbLitElement {
#logRepository: UmbAuditLogRepository;
#itemsPerPage = 10;
@property()
documentUnique = '';
@state()
private _total?: number;
@state()
private _items?: Array<AuditLogBaseModel>;
@state()
private _currentPage = 1;
constructor() {
super();
this.#logRepository = new UmbAuditLogRepository(this);
}
protected firstUpdated(): void {
this.#getLogs();
}
async #getLogs() {
this._items = undefined;
if (!this.documentUnique) return;
/*const { data } = await this.#logRepository.getAuditLogByUnique({
id: this.documentUnique,
orderDirection: DirectionModel.DESCENDING,
skip: (this._currentPage - 1) * this.#itemsPerPage,
take: this.#itemsPerPage,
});
if (!data) return;
this._total = data.total;
this._items = data.items;
*/
//TODO: I think there is an issue with the API (backend). Hacking for now.
// Uncomment previous code and delete the following when issue fixed.
const { data } = await this.#logRepository.getAuditLogByUnique({
id: '',
orderDirection: DirectionModel.DESCENDING,
skip: 0,
take: 99999,
});
if (!data) return;
// Hack list to only get the items for the current document
const list = data.items.filter((item) => item.entityId === this.documentUnique);
this._total = list.length;
// Hack list to only get the items for the current page
this._items = list.slice(
(this._currentPage - 1) * this.#itemsPerPage,
(this._currentPage - 1) * this.#itemsPerPage + this.#itemsPerPage,
);
}
#localizeTimestamp(timestamp: string) {
//TODO In what format should we show the timestamps.. Server based? Localized? Other?
//What about user's current culture? (American vs European date format) based on their user setting or windows setting?
return new Date(timestamp).toLocaleString();
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
this.#getLogs();
}
render() {
return html`<uui-box headline=${this.localize.term('general_history')}>
${this._items ? this.#renderHistory() : html`<uui-loader-circle></uui-loader-circle> `}
${this.#renderHistoryPagination()}
</uui-box>`;
}
#renderHistory() {
if (this._items && this._items.length) {
return html`
<umb-history-list>
${repeat(
this._items,
(item) => item.timestamp,
(item) => {
const { text, style } = HistoricTagAndDescription(item.logType);
return html`<umb-history-item name="TODO Username" detail=${this.#localizeTimestamp(item.timestamp)}>
<span class="log-type">
<uui-tag look=${style.look} color=${style.color}> ${this.localize.term(text.label)} </uui-tag>
${this.localize.term(text.desc, item.parameters)}
</span>
<uui-button label=${this.localize.term('actions_rollback')} look="secondary" slot="actions">
<uui-icon name="icon-undo"></uui-icon>
<umb-localize key="actions_rollback"></umb-localize>
</uui-button>
</umb-history-item>`;
},
)}
</umb-history-list>
`;
} else {
return html`No items found`;
}
}
#renderHistoryPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.#itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
static styles = [
UmbTextStyles,
css`
uui-loader-circle {
font-size: 2rem;
}
uui-tag uui-icon {
margin-right: var(--uui-size-space-1);
}
.log-type {
flex-grow: 1;
display: flex;
gap: var(--uui-size-space-2);
}
uui-pagination {
flex: 1;
display: inline-block;
}
.pagination {
display: flex;
justify-content: center;
margin-top: var(--uui-size-space-4);
}
`,
];
}
export default UmbDocumentInfoHistoryWorkspaceViewElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-info-history-workspace-view': UmbDocumentInfoHistoryWorkspaceViewElement;
}
}

View File

@@ -0,0 +1,64 @@
import { AuditTypeModel } from '@umbraco-cms/backoffice/backend-api';
interface HistoricStyleMap {
look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder';
color: 'default' | 'danger' | 'warning' | 'positive';
}
interface HistoricText {
label: string;
desc: string;
}
interface HistoricData {
style: HistoricStyleMap;
text: HistoricText;
}
// Return label, color, look, desc
export function HistoricTagAndDescription(type: AuditTypeModel): HistoricData {
switch (type) {
case AuditTypeModel.SAVE:
return {
style: { look: 'primary', color: 'default' },
text: { label: 'auditTrails_smallSave', desc: 'auditTrails_save' },
};
case AuditTypeModel.PUBLISH:
return {
style: { look: 'primary', color: 'positive' },
text: { label: 'content_publish', desc: 'auditTrails_publish' },
};
case AuditTypeModel.UNPUBLISH:
return {
style: { look: 'primary', color: 'warning' },
text: { label: 'content_unpublish', desc: 'auditTrails_unpublish' },
};
case AuditTypeModel.CONTENT_VERSION_ENABLE_CLEANUP:
return {
style: { look: 'secondary', color: 'default' },
text: {
label: 'contentTypeEditor_historyCleanupEnableCleanup',
desc: 'auditTrails_contentversionenablecleanup',
},
};
case AuditTypeModel.CONTENT_VERSION_PREVENT_CLEANUP:
return {
style: { look: 'secondary', color: 'default' },
text: {
label: 'contentTypeEditor_historyCleanupPreventCleanup',
desc: 'auditTrails_contentversionpreventcleanup',
},
};
default:
return {
style: { look: 'placeholder', color: 'danger' },
text: { label: type, desc: 'TODO' },
};
}
}

View File

@@ -87,6 +87,7 @@
"@umbraco-cms/backoffice/data-type": ["./src/packages/core/data-type/index.ts"],
"@umbraco-cms/backoffice/language": ["./src/packages/settings/languages/index.ts"],
"@umbraco-cms/backoffice/logviewer": ["src/packages/log-viewer/index.ts"],
"@umbraco-cms/backoffice/audit-log": ["src/packages/audit-log/index.ts"],
"@umbraco-cms/backoffice/relation-type": ["./src/packages/settings/relation-types/index.ts"],
"@umbraco-cms/backoffice/tags": ["./src/packages/tags/index.ts"],
"@umbraco-cms/backoffice/static-file": ["./src/packages/static-file/index.ts"],

View File

@@ -90,6 +90,7 @@ export default {
'@umbraco-cms/backoffice/event': './src/packages/core/event/index.ts',
'@umbraco-cms/backoffice/repository': './src/packages/core/repository/index.ts',
'@umbraco-cms/backoffice/temporary-file': './src/packages/core/temporary-file/index.ts',
'@umbraco-cms/backoffice/audit-log': './src/packages/core/audit-log/index.ts',
'@umbraco-cms/backoffice/dictionary': './src/packages/dictionary/dictionary/index.ts',