Merge pull request #577 from umbraco/feature/log-viewer
Looks good, there is still a few things for Warren to get around. and something about the button-with-dropdown. I have added Azure board tasks and notified Warren.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"],
|
||||
"cSpell.words": ["combobox", "variantable"]
|
||||
"cSpell.words": ["combobox", "variantable"],
|
||||
"exportall.config.folderListener": [],
|
||||
"exportall.config.relExclusion": []
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { pushToUniqueArray } from './push-to-unique-array.function';
|
||||
*
|
||||
* The ArrayState provides methods to append data when the data is an Object.
|
||||
*/
|
||||
|
||||
export class ArrayState<T> extends DeepState<T[]> {
|
||||
constructor(initialData: T[], private _getUnique?: (entry: T) => unknown) {
|
||||
super(initialData);
|
||||
|
||||
@@ -8,7 +8,7 @@ const menuItem: ManifestMenuItem = {
|
||||
meta: {
|
||||
label: 'Log Viewer',
|
||||
icon: 'umb:box-alt',
|
||||
entityType: 'logviewer-root',
|
||||
entityType: 'logviewer',
|
||||
menus: ['Umb.Menu.Settings'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { UmbLogMessagesServerDataSource, UmbLogSearchesServerDataSource } from './sources/log-viewer.server.data';
|
||||
import { UmbContextConsumerController } from '@umbraco-cms/context-api';
|
||||
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
|
||||
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification';
|
||||
import { DirectionModel, LogLevelModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
// Move to documentation / JSdoc
|
||||
/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */
|
||||
// element -> context -> repository -> (store) -> data source
|
||||
// All methods should be async and return a promise. Some methods might return an observable as part of the promise response.
|
||||
export class UmbLogViewerRepository {
|
||||
#host: UmbControllerHostInterface;
|
||||
#searchDataSource: UmbLogSearchesServerDataSource;
|
||||
#messagesDataSource: UmbLogMessagesServerDataSource;
|
||||
#notificationService?: UmbNotificationContext;
|
||||
#initResolver?: () => void;
|
||||
#initialized = false;
|
||||
|
||||
constructor(host: UmbControllerHostInterface) {
|
||||
this.#host = host;
|
||||
this.#searchDataSource = new UmbLogSearchesServerDataSource(this.#host);
|
||||
this.#messagesDataSource = new UmbLogMessagesServerDataSource(this.#host);
|
||||
|
||||
new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
|
||||
this.#notificationService = instance;
|
||||
this.#checkIfInitialized();
|
||||
});
|
||||
}
|
||||
|
||||
#init() {
|
||||
// TODO: This would only works with one user of this method. If two, the first one would be forgotten, but maybe its alright for now as I guess this is temporary.
|
||||
return new Promise<void>((resolve) => {
|
||||
this.#initialized ? resolve() : (this.#initResolver = resolve);
|
||||
});
|
||||
}
|
||||
|
||||
#checkIfInitialized() {
|
||||
if (this.#notificationService) {
|
||||
this.#initialized = true;
|
||||
this.#initResolver?.();
|
||||
}
|
||||
}
|
||||
|
||||
async getSavedSearches({ skip, take }: { skip: number; take: number }) {
|
||||
await this.#init();
|
||||
|
||||
return this.#searchDataSource.getAllSavedSearches({ skip, take });
|
||||
}
|
||||
|
||||
async getMessageTemplates({ skip, take }: { skip: number; take: number }) {
|
||||
await this.#init();
|
||||
|
||||
return this.#messagesDataSource.getLogViewerMessageTemplate({ skip, take });
|
||||
}
|
||||
|
||||
async getLogCount({ startDate, endDate }: { startDate?: string; endDate?: string }) {
|
||||
await this.#init();
|
||||
|
||||
return this.#messagesDataSource.getLogViewerLevelCount({ startDate, endDate });
|
||||
}
|
||||
|
||||
async getLogs({
|
||||
skip = 0,
|
||||
take = 100,
|
||||
orderDirection,
|
||||
filterExpression,
|
||||
logLevel,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
orderDirection?: DirectionModel;
|
||||
filterExpression?: string;
|
||||
logLevel?: Array<LogLevelModel>;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
await this.#init();
|
||||
|
||||
return this.#messagesDataSource.getLogViewerLogs({
|
||||
skip,
|
||||
take,
|
||||
orderDirection,
|
||||
filterExpression,
|
||||
logLevel,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
|
||||
async getLogLevels({ skip = 0, take = 100 }: { skip: number; take: number }) {
|
||||
await this.#init();
|
||||
return this.#messagesDataSource.getLogViewerLevel({ skip, take });
|
||||
}
|
||||
|
||||
async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) {
|
||||
await this.#init();
|
||||
return this.#messagesDataSource.getLogViewerValidateLogsSize({ startDate, endDate });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
DirectionModel,
|
||||
LogLevelCountsModel,
|
||||
LogLevelModel,
|
||||
PagedLoggerModel,
|
||||
PagedLogMessageModel,
|
||||
PagedLogTemplateModel,
|
||||
PagedSavedLogSearchModel,
|
||||
SavedLogSearchModel,
|
||||
} from '@umbraco-cms/backend-api';
|
||||
import type { DataSourceResponse } from '@umbraco-cms/models';
|
||||
|
||||
|
||||
|
||||
export interface LogSearchDataSource {
|
||||
getAllSavedSearches({
|
||||
skip,
|
||||
take,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
}): Promise<DataSourceResponse<PagedSavedLogSearchModel>>;
|
||||
getSavedSearchByName({ name }: { name: string }): Promise<DataSourceResponse<SavedLogSearchModel>>;
|
||||
deleteSavedSearchByName({ name }: { name: string }): Promise<DataSourceResponse<unknown>>;
|
||||
postLogViewerSavedSearch({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody?: SavedLogSearchModel;
|
||||
}): Promise<DataSourceResponse<unknown>>;
|
||||
}
|
||||
|
||||
export interface LogMessagesDataSource {
|
||||
getLogViewerLevel({ skip, take }: { skip?: number; take?: number }): Promise<DataSourceResponse<PagedLoggerModel>>;
|
||||
getLogViewerLevelCount({
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<DataSourceResponse<LogLevelCountsModel>>;
|
||||
getLogViewerLogs({
|
||||
skip,
|
||||
take = 100,
|
||||
orderDirection,
|
||||
filterExpression,
|
||||
logLevel,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
orderDirection?: DirectionModel;
|
||||
filterExpression?: string;
|
||||
logLevel?: Array<LogLevelModel>;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<DataSourceResponse<PagedLogMessageModel>>;
|
||||
getLogViewerMessageTemplate({
|
||||
skip,
|
||||
take = 100,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<DataSourceResponse<PagedLogTemplateModel>>;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { LogMessagesDataSource, LogSearchDataSource } from '.';
|
||||
import { DirectionModel, LogLevelModel, LogViewerResource, SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
|
||||
|
||||
/**
|
||||
* A data source for the log saved searches
|
||||
* @export
|
||||
* @class UmbLogSearchesServerDataSource
|
||||
* @implements {TemplateDetailDataSource}
|
||||
*/
|
||||
export class UmbLogSearchesServerDataSource implements LogSearchDataSource {
|
||||
#host: UmbControllerHostInterface;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbLogSearchesServerDataSource.
|
||||
* @param {UmbControllerHostInterface} host
|
||||
* @memberof UmbLogSearchesServerDataSource
|
||||
*/
|
||||
constructor(host: UmbControllerHostInterface) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs all the log viewer saved searches from the server
|
||||
*
|
||||
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
|
||||
* @return {*}
|
||||
* @memberof UmbLogSearchesServerDataSource
|
||||
*/
|
||||
async getAllSavedSearches({ skip = 0, take = 100 }: { skip?: number; take?: number }) {
|
||||
return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerSavedSearch({ skip, take }));
|
||||
}
|
||||
/**
|
||||
* Get a log viewer saved search by name from the server
|
||||
*
|
||||
* @param {{ name: string }} { name }
|
||||
* @return {*}
|
||||
* @memberof UmbLogSearchesServerDataSource
|
||||
*/
|
||||
async getSavedSearchByName({ name }: { name: string }) {
|
||||
return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerSavedSearchByName({ name }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a new log viewer saved search to the server
|
||||
*
|
||||
* @param {{ requestBody?: SavedLogSearch }} { requestBody }
|
||||
* @return {*}
|
||||
* @memberof UmbLogSearchesServerDataSource
|
||||
*/
|
||||
async postLogViewerSavedSearch({ requestBody }: { requestBody?: SavedLogSearchModel }) {
|
||||
return await tryExecuteAndNotify(this.#host, LogViewerResource.postLogViewerSavedSearch({ requestBody }));
|
||||
}
|
||||
/**
|
||||
* Remove a log viewer saved search by name from the server
|
||||
*
|
||||
* @param {{ name: string }} { name }
|
||||
* @return {*}
|
||||
* @memberof UmbLogSearchesServerDataSource
|
||||
*/
|
||||
async deleteSavedSearchByName({ name }: { name: string }) {
|
||||
return await tryExecuteAndNotify(this.#host, LogViewerResource.deleteLogViewerSavedSearchByName({ name }));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A data source for the log messages and levels
|
||||
*
|
||||
* @export
|
||||
* @class UmbLogMessagesServerDataSource
|
||||
* @implements {LogMessagesDataSource}
|
||||
*/
|
||||
export class UmbLogMessagesServerDataSource implements LogMessagesDataSource {
|
||||
#host: UmbControllerHostInterface;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbLogMessagesServerDataSource.
|
||||
* @param {UmbControllerHostInterface} host
|
||||
* @memberof UmbLogMessagesServerDataSource
|
||||
*/
|
||||
constructor(host: UmbControllerHostInterface) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs all the loggers from the server
|
||||
*
|
||||
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
|
||||
* @return {*}
|
||||
* @memberof UmbLogMessagesServerDataSource
|
||||
*/
|
||||
async getLogViewerLevel({ skip = 0, take = 100 }: { skip?: number; take?: number }) {
|
||||
return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerLevel({ skip, take }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs all the number of different log messages from the server
|
||||
*
|
||||
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
|
||||
* @return {*}
|
||||
* @memberof UmbLogMessagesServerDataSource
|
||||
*/
|
||||
async getLogViewerLevelCount({ startDate, endDate }: { startDate?: string; endDate?: string }) {
|
||||
return await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
LogViewerResource.getLogViewerLevelCount({
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Grabs all the log messages from the server
|
||||
*
|
||||
* @param {{
|
||||
* skip?: number;
|
||||
* take?: number;
|
||||
* orderDirection?: DirectionModel;
|
||||
* filterExpression?: string;
|
||||
* logLevel?: Array<LogLevelModel>;
|
||||
* startDate?: string;
|
||||
* endDate?: string;
|
||||
* }} {
|
||||
* skip = 0,
|
||||
* take = 100,
|
||||
* orderDirection,
|
||||
* filterExpression,
|
||||
* logLevel,
|
||||
* startDate,
|
||||
* endDate,
|
||||
* }
|
||||
* @return {*}
|
||||
* @memberof UmbLogMessagesServerDataSource
|
||||
*/
|
||||
async getLogViewerLogs({
|
||||
skip = 0,
|
||||
take = 100,
|
||||
orderDirection,
|
||||
filterExpression,
|
||||
logLevel,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
orderDirection?: DirectionModel;
|
||||
filterExpression?: string;
|
||||
logLevel?: Array<LogLevelModel>;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
return await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
LogViewerResource.getLogViewerLog({
|
||||
skip,
|
||||
take,
|
||||
orderDirection,
|
||||
filterExpression,
|
||||
logLevel,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Grabs all the log message templates from the server
|
||||
*
|
||||
* @param {{
|
||||
* skip?: number;
|
||||
* take?: number;
|
||||
* startDate?: string;
|
||||
* endDate?: string;
|
||||
* }} {
|
||||
* skip,
|
||||
* take = 100,
|
||||
* startDate,
|
||||
* endDate,
|
||||
* }
|
||||
* @return {*}
|
||||
* @memberof UmbLogMessagesServerDataSource
|
||||
*/
|
||||
async getLogViewerMessageTemplate({
|
||||
skip,
|
||||
take = 100,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
return await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
LogViewerResource.getLogViewerMessageTemplate({
|
||||
skip,
|
||||
take,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) {
|
||||
return await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
LogViewerResource.getLogViewerValidateLogsSize({
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './log-viewer-date-range-selector.element';
|
||||
export * from './log-viewer-level-tag.element';
|
||||
export * from './log-viewer-to-many-logs-warning.element';
|
||||
@@ -0,0 +1,132 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, property, queryAll, state } from 'lit/decorators.js';
|
||||
import {
|
||||
LogViewerDateRange,
|
||||
UmbLogViewerWorkspaceContext,
|
||||
UMB_APP_LOG_VIEWER_CONTEXT_TOKEN,
|
||||
} from '../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { query } from 'router-slot';
|
||||
|
||||
@customElement('umb-log-viewer-date-range-selector')
|
||||
export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: inherit;
|
||||
padding: var(--uui-size-1) var(--uui-size-space-3);
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
position: relative;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* find out better validation for that */
|
||||
input:out-of-range {
|
||||
border-color: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
:host([horizontal]) .input-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _startDate = '';
|
||||
|
||||
@state()
|
||||
private _endDate = '';
|
||||
|
||||
@queryAll('input')
|
||||
private _inputs!: NodeListOf<HTMLInputElement>;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
horizontal = false;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('input', this.#setDates);
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getMessageTemplates(0, 10);
|
||||
this.#observeStuff();
|
||||
});
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.dateRange, (dateRange: LogViewerDateRange) => {
|
||||
this._startDate = dateRange?.startDate;
|
||||
this._endDate = dateRange?.endDate;
|
||||
});
|
||||
}
|
||||
|
||||
#setDates() {
|
||||
this._inputs.forEach((input) => {
|
||||
if (input.id === 'start-date') {
|
||||
this._startDate = input.value;
|
||||
} else if (input.id === 'end-date') {
|
||||
this._endDate = input.value;
|
||||
}
|
||||
});
|
||||
const newDateRange: LogViewerDateRange = { startDate: this._startDate, endDate: this._endDate };
|
||||
this.#logViewerContext?.setDateRange(newDateRange);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="input-container">
|
||||
<uui-label for="start-date">From:</uui-label>
|
||||
<input
|
||||
@click=${(e: Event) => {
|
||||
(e.target as HTMLInputElement).showPicker();
|
||||
}}
|
||||
|
||||
id="start-date"
|
||||
type="date"
|
||||
label="From"
|
||||
.max=${this.#logViewerContext?.today ?? ''}
|
||||
.value=${this._startDate}>
|
||||
</input>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<uui-label for="end-date">To: </uui-label>
|
||||
<input
|
||||
@click=${(e: Event) => {
|
||||
(e.target as HTMLInputElement).showPicker();
|
||||
}}
|
||||
id="end-date"
|
||||
type="date"
|
||||
label="To"
|
||||
.min=${this._startDate}
|
||||
.max=${this.#logViewerContext?.today ?? ''}
|
||||
.value=${this._endDate}>
|
||||
</input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-date-range-selector': UmbLogViewerDateRangeSelectorElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types';
|
||||
import { LogLevelModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
interface LevelMapStyles {
|
||||
look?: InterfaceLook;
|
||||
color?: InterfaceColor;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
@customElement('umb-log-viewer-level-tag')
|
||||
export class UmbLogViewerLevelTagElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
@property()
|
||||
level?: LogLevelModel;
|
||||
|
||||
levelMap: Record<LogLevelModel, LevelMapStyles> = {
|
||||
Verbose: { look: 'secondary' },
|
||||
Debug: {
|
||||
look: 'default',
|
||||
style: 'background-color: var(--umb-log-viewer-debug-color); color: var(--uui-color-surface)',
|
||||
},
|
||||
Information: { look: 'primary', color: 'positive' },
|
||||
Warning: { look: 'primary', color: 'warning' },
|
||||
Error: { look: 'primary', color: 'danger' },
|
||||
Fatal: { look: 'primary' },
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`<uui-tag
|
||||
look=${ifDefined(this.level ? this.levelMap[this.level]?.look : undefined)}
|
||||
color=${ifDefined(this.level ? this.levelMap[this.level]?.color : undefined)}
|
||||
style="${ifDefined(this.level ? this.levelMap[this.level]?.style : undefined)}"
|
||||
>${this.level}<slot></slot
|
||||
></uui-tag>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-level-tag': UmbLogViewerLevelTagElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-log-viewer-to-many-logs-warning')
|
||||
export class UmbLogViewerToManyLogsWarningElement extends LitElement {
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`<uui-box id="to-many-logs-warning">
|
||||
<h3>Unable to view logs</h3>
|
||||
<p>Today's log file is too large to be viewed and would cause performance problems.</p>
|
||||
<p>If you need to view the log files, narrow your date range or try opening them manually.</p>
|
||||
</uui-box>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-to-many-logs-warning': UmbLogViewerToManyLogsWarningElement;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,190 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import './components';
|
||||
import { map } from 'rxjs';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, state, property } from 'lit/decorators.js';
|
||||
import { IRoutingInfo } from 'router-slot';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { umbExtensionsRegistry, createExtensionElement } from '@umbraco-cms/extensions-api';
|
||||
import { ManifestWorkspaceView, ManifestWorkspaceViewCollection } from '@umbraco-cms/extensions-registry';
|
||||
import { UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/router';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../logviewer.context';
|
||||
import { repeat } from 'lit-html/directives/repeat.js';
|
||||
|
||||
//TODO make uui-input accept min and max values
|
||||
@customElement('umb-logviewer-workspace')
|
||||
export class UmbLogViewerWorkspaceElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--umb-log-viewer-debug-color: var(--uui-color-default-emphasis);
|
||||
--umb-log-viewer-information-color: var(--uui-color-positive);
|
||||
--umb-log-viewer-warning-color: var(--uui-color-warning);
|
||||
--umb-log-viewer-error-color: var(--uui-color-danger);
|
||||
--umb-log-viewer-fatal-color: var(--uui-color-default);
|
||||
--umb-log-viewer-verbose-color: var(--uui-color-current);
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
padding: 0 var(--uui-size-space-6);
|
||||
gap: var(--uui-size-space-4);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#router-slot {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
uui-tab-group {
|
||||
--uui-tab-divider: var(--uui-color-border);
|
||||
border-left: 1px solid var(--uui-color-border);
|
||||
border-right: 1px solid var(--uui-color-border);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private _alias = 'Umb.Workspace.LogviewerRoot';
|
||||
|
||||
@state()
|
||||
private _workspaceViews: Array<ManifestWorkspaceView | ManifestWorkspaceViewCollection> = [];
|
||||
|
||||
@state()
|
||||
private _routes: any[] = [];
|
||||
|
||||
@state()
|
||||
private _activePath?: string;
|
||||
|
||||
@state()
|
||||
private _routerPath?: string;
|
||||
|
||||
#logViewerContext = new UmbLogViewerWorkspaceContext(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#logViewerContext.init();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._observeWorkspaceViews();
|
||||
this.provideContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, this.#logViewerContext);
|
||||
}
|
||||
|
||||
load(): void {
|
||||
// Not relevant for this workspace -added to prevent the error from popping up
|
||||
}
|
||||
|
||||
private _observeWorkspaceViews() {
|
||||
this.observe(
|
||||
umbExtensionsRegistry
|
||||
.extensionsOfTypes<ManifestWorkspaceView>(['workspaceView'])
|
||||
.pipe(map((extensions) => extensions.filter((extension) => extension.meta.workspaces.includes(this._alias)))),
|
||||
(workspaceViews) => {
|
||||
this._workspaceViews = workspaceViews;
|
||||
this._createRoutes();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// Not relevant for this workspace
|
||||
}
|
||||
|
||||
private _createRoutes() {
|
||||
this._routes = [];
|
||||
|
||||
if (this._workspaceViews.length > 0) {
|
||||
this._routes = this._workspaceViews.map((view) => {
|
||||
return {
|
||||
path: `${view.meta.pathname}`,
|
||||
component: () => {
|
||||
return createExtensionElement(view);
|
||||
},
|
||||
setup: (component: Promise<HTMLElement> | HTMLElement, info: IRoutingInfo) => {
|
||||
// When its using import, we get an element, when using createExtensionElement we get a Promise.
|
||||
if ((component as any).then) {
|
||||
(component as any).then((el: any) => (el.manifest = view));
|
||||
} else {
|
||||
(component as any).manifest = view;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: `${this._workspaceViews[0].meta.pathname}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#renderRoutes() {
|
||||
return html`
|
||||
${this._routes.length > 0
|
||||
? html`
|
||||
<umb-router-slot
|
||||
id="router-slot"
|
||||
.routes="${this._routes}"
|
||||
@init=${(event: UmbRouterSlotInitEvent) => {
|
||||
this._routerPath = event.target.absoluteRouterPath;
|
||||
}}
|
||||
@change=${(event: UmbRouterSlotChangeEvent) => {
|
||||
this._activePath = event.target.localActiveViewPath;
|
||||
}}></umb-router-slot>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderViews() {
|
||||
return html`
|
||||
${this._workspaceViews.length > 1
|
||||
? html`
|
||||
<uui-tab-group slot="tabs">
|
||||
${repeat(
|
||||
this._workspaceViews,
|
||||
(view) => view.alias,
|
||||
(view) => html`
|
||||
<uui-tab
|
||||
.label="${view.meta.label || view.name}"
|
||||
href="${this._routerPath}/${view.meta.pathname}"
|
||||
?active="${view.meta.pathname === this._activePath}">
|
||||
<uui-icon slot="icon" name="${view.meta.icon}"></uui-icon>
|
||||
${view.meta.label || view.name}
|
||||
</uui-tab>
|
||||
`
|
||||
)}
|
||||
</uui-tab-group>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@customElement('umb-logviewer-root-workspace')
|
||||
export class UmbLogViewerRootWorkspaceElement extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<h1>LogViewer Root Workspace</h1>
|
||||
</div>
|
||||
<umb-body-layout>
|
||||
<div id="header" slot="header">
|
||||
<h3 id="headline">
|
||||
${this._activePath === 'overview' ? 'Log Overview for Selected Time Period' : 'Log search'}
|
||||
</h3>
|
||||
</div>
|
||||
${this.#renderViews()} ${this.#renderRoutes()}
|
||||
<slot></slot>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbLogViewerRootWorkspaceElement;
|
||||
export default UmbLogViewerWorkspaceElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-logviewer-root-workspace': UmbLogViewerRootWorkspaceElement;
|
||||
'umb-logviewer-workspace': UmbLogViewerWorkspaceElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models';
|
||||
|
||||
const workspaceAlias = 'Umb.Workspace.LogviewerRoot';
|
||||
|
||||
const workspace: ManifestWorkspace = {
|
||||
type: 'workspace',
|
||||
alias: 'Umb.Workspace.LogviewerRoot',
|
||||
alias: workspaceAlias,
|
||||
name: 'LogViewer Root Workspace',
|
||||
loader: () => import('./logviewer-root-workspace.element'),
|
||||
meta: {
|
||||
entityType: 'logviewer-root',
|
||||
entityType: 'logviewer',
|
||||
},
|
||||
};
|
||||
|
||||
const workspaceViews: Array<ManifestWorkspaceView> = [];
|
||||
const workspaceViews: Array<ManifestWorkspaceView> = [
|
||||
{
|
||||
type: 'workspaceView',
|
||||
alias: 'Umb.WorkspaceView.Logviewer.Overview',
|
||||
name: 'LogViewer Root Workspace Overview View',
|
||||
loader: () => import('../views/overview/index'),
|
||||
weight: 300,
|
||||
meta: {
|
||||
workspaces: [workspaceAlias],
|
||||
label: 'Overview',
|
||||
pathname: 'overview',
|
||||
icon: 'umb:box-alt',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'workspaceView',
|
||||
alias: 'Umb.WorkspaceView.Logviewer.Search',
|
||||
name: 'LogViewer Root Workspace Search View',
|
||||
loader: () => import('../views/search/index'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
workspaces: [workspaceAlias],
|
||||
label: 'Search',
|
||||
pathname: 'search',
|
||||
icon: 'umb:search',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const workspaceActions: Array<ManifestWorkspaceAction> = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { UmbLogViewerRepository } from '../repository/log-viewer.repository';
|
||||
import { ArrayState, createObservablePart, DeepState, ObjectState, StringState } from '@umbraco-cms/observable-api';
|
||||
import {
|
||||
DirectionModel,
|
||||
LogLevelCountsModel,
|
||||
LogLevelModel,
|
||||
PagedLoggerModel,
|
||||
PagedLogMessageModel,
|
||||
PagedLogTemplateModel,
|
||||
PagedSavedLogSearchModel,
|
||||
} from '@umbraco-cms/backend-api';
|
||||
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
|
||||
import { UmbContextToken } from '@umbraco-cms/context-api';
|
||||
import { BasicState } from 'libs/observable-api/basic-state';
|
||||
|
||||
export type PoolingInterval = 0 | 2000 | 5000 | 10000 | 20000 | 30000;
|
||||
export interface PoolingCOnfig {
|
||||
enabled: boolean;
|
||||
interval: PoolingInterval;
|
||||
}
|
||||
export interface LogViewerDateRange {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export class UmbLogViewerWorkspaceContext {
|
||||
#host: UmbControllerHostInterface;
|
||||
#repository: UmbLogViewerRepository;
|
||||
|
||||
get today() {
|
||||
const today = new Date();
|
||||
const dd = String(today.getDate()).padStart(2, '0');
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = today.getFullYear();
|
||||
|
||||
return yyyy + '-' + mm + '-' + dd;
|
||||
}
|
||||
|
||||
get yesterday() {
|
||||
const yesterday = new Date(new Date().setDate(new Date().getDate() - 1));
|
||||
const dd = String(yesterday.getDate()).padStart(2, '0');
|
||||
const mm = String(yesterday.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = yesterday.getFullYear();
|
||||
|
||||
return yyyy + '-' + mm + '-' + dd;
|
||||
}
|
||||
|
||||
defaultDateRange: LogViewerDateRange = {
|
||||
startDate: this.yesterday,
|
||||
endDate: this.today,
|
||||
};
|
||||
|
||||
#savedSearches = new DeepState<PagedSavedLogSearchModel | undefined>(undefined);
|
||||
savedSearches = createObservablePart(this.#savedSearches, (data) => data?.items);
|
||||
|
||||
#logCount = new DeepState<LogLevelCountsModel | null>(null);
|
||||
logCount = createObservablePart(this.#logCount, (data) => data);
|
||||
|
||||
#dateRange = new DeepState<LogViewerDateRange>(this.defaultDateRange);
|
||||
dateRange = createObservablePart(this.#dateRange, (data) => data);
|
||||
|
||||
#loggers = new DeepState<PagedLoggerModel | null>(null);
|
||||
loggers = createObservablePart(this.#loggers, (data) => data?.items);
|
||||
|
||||
#canShowLogs = new BasicState<boolean | null>(null);
|
||||
canShowLogs = createObservablePart(this.#canShowLogs, (data) => data);
|
||||
|
||||
#filterExpression = new StringState<string>('');
|
||||
filterExpression = createObservablePart(this.#filterExpression, (data) => data);
|
||||
|
||||
#messageTemplates = new DeepState<PagedLogTemplateModel | null>(null);
|
||||
messageTemplates = createObservablePart(this.#messageTemplates, (data) => data);
|
||||
|
||||
#logLevelsFilter = new ArrayState<LogLevelModel>([]);
|
||||
logLevelsFilter = createObservablePart(this.#logLevelsFilter, (data) => data);
|
||||
|
||||
#logs = new DeepState<PagedLogMessageModel | null>(null);
|
||||
logs = createObservablePart(this.#logs, (data) => data?.items);
|
||||
logsTotal = createObservablePart(this.#logs, (data) => data?.total);
|
||||
|
||||
#polling = new ObjectState<PoolingCOnfig>({ enabled: false, interval: 2000 });
|
||||
polling = createObservablePart(this.#polling, (data) => data);
|
||||
|
||||
#sortingDirection = new BasicState<DirectionModel>(DirectionModel.ASCENDING);
|
||||
sortingDirection = createObservablePart(this.#sortingDirection, (data) => data);
|
||||
|
||||
#intervalID: number | null = null;
|
||||
|
||||
currentPage = 1;
|
||||
|
||||
constructor(host: UmbControllerHostInterface) {
|
||||
this.#host = host;
|
||||
this.#repository = new UmbLogViewerRepository(this.#host);
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.validateLogSize();
|
||||
}
|
||||
|
||||
setDateRange(dateRange: LogViewerDateRange) {
|
||||
const { startDate, endDate } = dateRange;
|
||||
|
||||
const isAnyDateInTheFuture = new Date(startDate) > new Date() || new Date(endDate) > new Date();
|
||||
const isStartDateBiggerThenEndDate = new Date(startDate) > new Date(endDate);
|
||||
if (isAnyDateInTheFuture || isStartDateBiggerThenEndDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#dateRange.next(dateRange);
|
||||
this.validateLogSize();
|
||||
this.getLogCount();
|
||||
}
|
||||
|
||||
async getSavedSearches() {
|
||||
const { data } = await this.#repository.getSavedSearches({ skip: 0, take: 100 });
|
||||
if (data) {
|
||||
this.#savedSearches.next(data);
|
||||
} else {
|
||||
//falback to some default searches like in the old backoffice
|
||||
this.#savedSearches.next({
|
||||
items: [
|
||||
{
|
||||
name: 'Find all logs where the Level is NOT Verbose and NOT Debug',
|
||||
query: "Not(@Level='Verbose') and Not(@Level='Debug')",
|
||||
},
|
||||
{
|
||||
name: 'Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)',
|
||||
query: 'Has(@Exception)',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that have the property 'Duration'",
|
||||
query: 'Has(Duration)',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
|
||||
query: 'Has(Duration) and Duration > 1000',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that are from the namespace 'Umbraco.Core'",
|
||||
query: "StartsWith(SourceContext, 'Umbraco.Core')",
|
||||
},
|
||||
{
|
||||
name: 'Find all logs that use a specific log message template',
|
||||
query: "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'",
|
||||
},
|
||||
],
|
||||
total: 6,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getLogCount() {
|
||||
const { data } = await this.#repository.getLogCount({ ...this.#dateRange.getValue() });
|
||||
|
||||
if (data) {
|
||||
this.#logCount.next(data);
|
||||
}
|
||||
}
|
||||
|
||||
async getMessageTemplates(skip: number, take: number) {
|
||||
const { data } = await this.#repository.getMessageTemplates({ skip, take });
|
||||
|
||||
if (data) {
|
||||
this.#messageTemplates.next(data);
|
||||
}
|
||||
}
|
||||
|
||||
async getLogLevels(skip: number, take: number) {
|
||||
const { data } = await this.#repository.getLogLevels({ skip, take });
|
||||
|
||||
if (data) {
|
||||
this.#loggers.next(data);
|
||||
}
|
||||
}
|
||||
|
||||
async validateLogSize() {
|
||||
const { data, error } = await this.#repository.getLogViewerValidateLogsSize({ ...this.#dateRange.getValue() });
|
||||
if (error) {
|
||||
this.#canShowLogs.next(false);
|
||||
console.info('LogViewer: ', error);
|
||||
return;
|
||||
}
|
||||
this.#canShowLogs.next(true);
|
||||
console.info('LogViewer:showinfg logs');
|
||||
}
|
||||
|
||||
setCurrentPage(page: number) {
|
||||
this.currentPage = page;
|
||||
}
|
||||
|
||||
getLogs = async () => {
|
||||
if (!this.#canShowLogs.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skip = (this.currentPage - 1) * 100;
|
||||
const take = 100;
|
||||
|
||||
const options = {
|
||||
skip,
|
||||
take,
|
||||
orderDirection: this.#sortingDirection.getValue(),
|
||||
filterExpression: this.#filterExpression.getValue(),
|
||||
logLevel: this.#logLevelsFilter.getValue(),
|
||||
...this.#dateRange.getValue(),
|
||||
};
|
||||
|
||||
const { data } = await this.#repository.getLogs(options);
|
||||
|
||||
if (data) {
|
||||
this.#logs.next(data);
|
||||
}
|
||||
};
|
||||
|
||||
setFilterExpression(query: string) {
|
||||
this.#filterExpression.next(query);
|
||||
}
|
||||
|
||||
setLogLevelsFilter(logLevels: LogLevelModel[]) {
|
||||
this.#logLevelsFilter.next(logLevels);
|
||||
}
|
||||
|
||||
togglePolling() {
|
||||
const isEnabled = !this.#polling.getValue().enabled;
|
||||
this.#polling.update({
|
||||
enabled: isEnabled,
|
||||
});
|
||||
|
||||
if (isEnabled) {
|
||||
this.#intervalID = setInterval(() => {
|
||||
this.currentPage = 1;
|
||||
this.getLogs();
|
||||
}, this.#polling.getValue().interval) as unknown as number;
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.#intervalID as number);
|
||||
}
|
||||
|
||||
setPollingInterval(interval: PoolingInterval) {
|
||||
this.#polling.update({ interval, enabled: true });
|
||||
}
|
||||
|
||||
toggleSortOrder() {
|
||||
const direction = this.#sortingDirection.getValue();
|
||||
const newDirection = direction === DirectionModel.ASCENDING ? DirectionModel.DESCENDING : DirectionModel.ASCENDING;
|
||||
this.#sortingDirection.next(newDirection);
|
||||
}
|
||||
}
|
||||
|
||||
export const UMB_APP_LOG_VIEWER_CONTEXT_TOKEN = new UmbContextToken<UmbLogViewerWorkspaceContext>(
|
||||
UmbLogViewerWorkspaceContext.name
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { manifests as logviewerRootManifests } from './logviewer-root/manifests';
|
||||
|
||||
|
||||
export const manifests = [...logviewerRootManifests];
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './log-viewer-saved-searches-overview.element';
|
||||
export * from './log-viewer-message-templates-overview.element';
|
||||
export * from './log-viewer-log-types-chart.element';
|
||||
export * from './log-viewer-log-level-overview.element';
|
||||
@@ -0,0 +1,48 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { LoggerModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
//TODO: implement the saved searches pagination when the API total bug is fixed
|
||||
@customElement('umb-log-viewer-log-level-overview')
|
||||
export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
this.#observeLogLevels();
|
||||
});
|
||||
}
|
||||
|
||||
@state()
|
||||
private _loggers: LoggerModel[] = [];
|
||||
/**
|
||||
* The name of the logger to get the level for. Defaults to 'Global'.
|
||||
*
|
||||
* @memberof UmbLogViewerLogLevelOverviewElement
|
||||
*/
|
||||
@property()
|
||||
loggerName = 'Global';
|
||||
|
||||
#observeLogLevels() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.loggers, (loggers) => {
|
||||
this._loggers = loggers ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`${this._loggers.length > 0
|
||||
? this._loggers.find((logger) => logger.name === this.loggerName)?.level
|
||||
: ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-log-level-overview': UmbLogViewerLogLevelOverviewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { LogLevelCountsModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
@customElement('umb-log-viewer-log-types-chart')
|
||||
export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
||||
static styles = [
|
||||
css`
|
||||
#log-types-container {
|
||||
display: flex;
|
||||
gap: var(--uui-size-space-4);
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px solid var(--uui-color-focus);
|
||||
}
|
||||
|
||||
button.active {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#chart {
|
||||
width: 150px;
|
||||
aspect-ratio: 1;
|
||||
background: radial-gradient(white 40%, transparent 41%),
|
||||
conic-gradient(
|
||||
var(--umb-log-viewer-debug-color) 0% 20%,
|
||||
var(--umb-log-viewer-information-color) 20% 40%,
|
||||
var(--umb-log-viewer-warning-color) 40% 60%,
|
||||
var(--umb-log-viewer-error-color) 60% 80%,
|
||||
var(--umb-log-viewer-fatal-color) 80% 100%
|
||||
);
|
||||
margin: 10px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
li uui-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getLogCount();
|
||||
this.#observeStuff();
|
||||
});
|
||||
}
|
||||
|
||||
@state()
|
||||
private _logLevelCount: LogLevelCountsModel | null = null;
|
||||
|
||||
@state()
|
||||
private logLevelCount: [string, number][] = [];
|
||||
|
||||
@state()
|
||||
private _logLevelCountFilter: string[] = [];
|
||||
|
||||
protected willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
|
||||
if (_changedProperties.has('_logLevelCountFilter')) {
|
||||
this.setLogLevelCount();
|
||||
}
|
||||
}
|
||||
|
||||
#setCountFilter(level: string) {
|
||||
if (this._logLevelCountFilter.includes(level)) {
|
||||
this._logLevelCountFilter = this._logLevelCountFilter.filter((item) => item !== level);
|
||||
return;
|
||||
}
|
||||
|
||||
this._logLevelCountFilter = [...this._logLevelCountFilter, level];
|
||||
}
|
||||
|
||||
setLogLevelCount() {
|
||||
this.logLevelCount = this._logLevelCount
|
||||
? Object.entries(this._logLevelCount).filter(([level, number]) => !this._logLevelCountFilter.includes(level))
|
||||
: [];
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.logCount, (logLevel) => {
|
||||
this._logLevelCount = logLevel ?? null;
|
||||
this.setLogLevelCount();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box id="types" headline="Log types">
|
||||
<div id="log-types-container">
|
||||
<div id="legend">
|
||||
<ul>
|
||||
${this._logLevelCount
|
||||
? Object.keys(this._logLevelCount).map(
|
||||
(level) =>
|
||||
html`<li>
|
||||
<button
|
||||
@click=${(e: Event) => {
|
||||
(e.target as HTMLElement)?.classList.toggle('active');
|
||||
this.#setCountFilter(level);
|
||||
}}>
|
||||
<uui-icon
|
||||
name="umb:record"
|
||||
style="color: var(--umb-log-viewer-${level.toLowerCase()}-color);"></uui-icon
|
||||
>${level}
|
||||
</button>
|
||||
</li>`
|
||||
)
|
||||
: ''}
|
||||
</ul>
|
||||
</div>
|
||||
<umb-donut-chart .description=${'In chosen date range you have this number of log message of type:'}>
|
||||
${this._logLevelCount
|
||||
? this.logLevelCount.map(
|
||||
([level, number]) =>
|
||||
html`<umb-donut-slice
|
||||
.name=${level}
|
||||
.amount=${number}
|
||||
.kind=${'messages'}
|
||||
.color="${`var(--umb-log-viewer-${level.toLowerCase()}-color)`}"></umb-donut-slice> `
|
||||
)
|
||||
: ''}
|
||||
</umb-donut-chart>
|
||||
</div>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-log-types-chart': UmbLogViewerLogTypesChartElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { PagedLogTemplateModel, SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
//TODO: fix pagination bug when API is fixed
|
||||
@customElement('umb-log-viewer-message-templates-overview')
|
||||
export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#show-more-templates-btn {
|
||||
margin-top: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
uui-table-cell {
|
||||
padding: 10px 20px;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
uui-table-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
uui-table-row:hover > uui-table-cell {
|
||||
background-color: var(--uui-color-surface-alt);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _messageTemplates: PagedLogTemplateModel | null = null;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getMessageTemplates(0, 10);
|
||||
this.#observeStuff();
|
||||
});
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.messageTemplates, (templates) => {
|
||||
this._messageTemplates = templates ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
#getMessageTemplates() {
|
||||
const take = this._messageTemplates?.items?.length ?? 0;
|
||||
this.#logViewerContext?.getMessageTemplates(0, take + 10);
|
||||
}
|
||||
|
||||
#renderSearchItem = (searchListItem: SavedLogSearchModel) => {
|
||||
return html` <li>
|
||||
<uui-button
|
||||
@click=${() => {
|
||||
this.#setCurrentQuery(searchListItem.query ?? '');
|
||||
}}
|
||||
label="${searchListItem.name ?? ''}"
|
||||
title="${searchListItem.name ?? ''}"
|
||||
href=${'/section/settings/logviewer/search?lq=' + searchListItem.query}
|
||||
><uui-icon name="umb:search"></uui-icon>${searchListItem.name}</uui-button
|
||||
>
|
||||
</li>`;
|
||||
};
|
||||
|
||||
#setCurrentQuery = (query: string) => {
|
||||
this.#logViewerContext?.setFilterExpression(query);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box headline="Common Log Messages" id="saved-searches">
|
||||
<p style="font-style: italic;">Total Unique Message types: ${this._messageTemplates?.total}</p>
|
||||
|
||||
<uui-table>
|
||||
${this._messageTemplates
|
||||
? this._messageTemplates.items.map(
|
||||
(template) =>
|
||||
html`<uui-table-row
|
||||
><uui-table-cell>
|
||||
<a
|
||||
@click=${() => {
|
||||
this.#setCurrentQuery(`@MessageTemplate='${template.messageTemplate}'` ?? '');
|
||||
}}
|
||||
href=${'/section/settings/logviewer/search?lg=@MessageTemplate%3D' + template.messageTemplate}>
|
||||
<span>${template.messageTemplate}</span> <span>${template.count}</span>
|
||||
</a>
|
||||
</uui-table-cell>
|
||||
</uui-table-row>`
|
||||
)
|
||||
: ''}
|
||||
</uui-table>
|
||||
|
||||
<uui-button
|
||||
id="show-more-templates-btn"
|
||||
look="primary"
|
||||
@click=${this.#getMessageTemplates}
|
||||
label="Show more templates"
|
||||
>Show more</uui-button
|
||||
>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-message-templates-overview': UmbLogViewerMessageTemplatesOverviewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
//TODO: implement the saved searches pagination when the API total bug is fixed
|
||||
@customElement('umb-log-viewer-saved-searches-overview')
|
||||
export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
uui-box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
li uui-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _savedSearches: SavedLogSearchModel[] = [];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
this.#observeStuff();
|
||||
});
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
||||
this._savedSearches = savedSearches ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
#setCurrentQuery(query: string) {
|
||||
this.#logViewerContext?.setFilterExpression(query);
|
||||
}
|
||||
|
||||
#renderSearchItem = (searchListItem: SavedLogSearchModel) => {
|
||||
return html` <li>
|
||||
<uui-button
|
||||
@click=${() => {
|
||||
this.#setCurrentQuery(searchListItem.query ?? '');
|
||||
}}
|
||||
label="${searchListItem.name ?? ''}"
|
||||
title="${searchListItem.name ?? ''}"
|
||||
href=${'/section/settings/logviewer/search?lq=' + searchListItem.query}
|
||||
><uui-icon name="umb:search"></uui-icon>${searchListItem.name}</uui-button
|
||||
>
|
||||
</li>`;
|
||||
};
|
||||
|
||||
render() {
|
||||
return html` <uui-box id="saved-searches" headline="Saved searches">
|
||||
<ul>
|
||||
<li>
|
||||
<uui-button
|
||||
@click=${() => {
|
||||
this.#setCurrentQuery('');
|
||||
}}
|
||||
label="All logs"
|
||||
title="All logs"
|
||||
href="/section/settings/logviewer/search"
|
||||
><uui-icon name="umb:search"></uui-icon>All logs</uui-button
|
||||
>
|
||||
</li>
|
||||
${this._savedSearches.map(this.#renderSearchItem)}
|
||||
</ul>
|
||||
</uui-box>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-saved-searches-overview': UmbLogViewerSavedSearchesOverviewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import './components';
|
||||
import { UmbLogViewerOverviewViewElement } from './log-overview-view.element';
|
||||
|
||||
export default UmbLogViewerOverviewViewElement;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../logviewer.context';
|
||||
import { LogLevelCountsModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
//TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint
|
||||
@customElement('umb-log-viewer-overview-view')
|
||||
export class UmbLogViewerOverviewViewElement extends UmbLitElement {
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#logviewer-layout {
|
||||
margin: 20px;
|
||||
height: calc(100vh - 160px);
|
||||
display: grid;
|
||||
grid-template-columns: 7fr 2fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 20px 20px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'saved-searches info'
|
||||
'common-messages info';
|
||||
}
|
||||
|
||||
#info {
|
||||
grid-area: info;
|
||||
align-self: start;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 20px 20px;
|
||||
}
|
||||
|
||||
#time-period {
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
}
|
||||
|
||||
#errors {
|
||||
grid-area: 2 / 1 / 3 / 2;
|
||||
}
|
||||
|
||||
#level {
|
||||
grid-area: 2 / 2 / 3 / 3;
|
||||
}
|
||||
|
||||
#log-lever {
|
||||
color: var(--uui-color-positive);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#types {
|
||||
grid-area: 3 / 1 / 5 / 3;
|
||||
}
|
||||
|
||||
#saved-searches-container,
|
||||
to-many-logs-warning {
|
||||
grid-area: saved-searches;
|
||||
}
|
||||
|
||||
#common-messages-container {
|
||||
grid-area: common-messages;
|
||||
--uui-box-default-padding: 0 var(--uui-size-space-5, 18px) var(--uui-size-space-5, 18px)
|
||||
var(--uui-size-space-5, 18px);
|
||||
}
|
||||
|
||||
#common-messages-container > uui-box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
uui-label:nth-of-type(2) {
|
||||
display: block;
|
||||
margin-top: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
#error-count {
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
color: var(--uui-color-danger);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _errorCount = 0;
|
||||
|
||||
@state()
|
||||
private _logLevelCount: LogLevelCountsModel | null = null;
|
||||
|
||||
@state()
|
||||
private _canShowLogs = false;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeErrorCount();
|
||||
this.#observeCanShowLogs();
|
||||
this.#logViewerContext?.getLogLevels(0, 100);
|
||||
});
|
||||
}
|
||||
|
||||
#observeErrorCount() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logCount, () => {
|
||||
this._errorCount = this._logLevelCount?.error ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
#observeCanShowLogs() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
||||
this._canShowLogs = canShowLogs ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="logviewer-layout">
|
||||
<div id="info">
|
||||
<uui-box id="time-period" headline="Time Period">
|
||||
<umb-log-viewer-date-range-selector></umb-log-viewer-date-range-selector>
|
||||
</uui-box>
|
||||
|
||||
<uui-box id="errors" headline="Number of Errors">
|
||||
<h1 id="error-count">${this._errorCount}</h1>
|
||||
</uui-box>
|
||||
|
||||
<uui-box id="level" headline="Log level">
|
||||
<h1 id="log-lever"><umb-log-viewer-log-level-overview></umb-log-viewer-log-level-overview></h1>
|
||||
</uui-box>
|
||||
|
||||
<umb-log-viewer-log-types-chart id="types"></umb-log-viewer-log-types-chart>
|
||||
</div>
|
||||
|
||||
${this._canShowLogs
|
||||
? html`<div id="saved-searches-container">
|
||||
<umb-log-viewer-saved-searches-overview></umb-log-viewer-saved-searches-overview>
|
||||
</div>
|
||||
|
||||
<div id="common-messages-container">
|
||||
<umb-log-viewer-message-templates-overview></umb-log-viewer-message-templates-overview>
|
||||
</div>`
|
||||
: html`<umb-log-viewer-to-many-logs-warning id="to-many-logs-warning"></umb-log-viewer-to-many-logs-warning>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-overview-view': UmbLogViewerOverviewViewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './log-viewer-log-level-filter-menu.element';
|
||||
export * from './log-viewer-message.element';
|
||||
export * from './log-viewer-messages-list.element';
|
||||
export * from './log-viewer-polling-button.element';
|
||||
export * from './log-viewer-search-input.element';
|
||||
@@ -0,0 +1,118 @@
|
||||
import { UUICheckboxElement } from '@umbraco-ui/uui';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, queryAll, state } from 'lit/decorators.js';
|
||||
import _ from 'lodash';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { LogLevelModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-log-viewer-log-level-filter-menu')
|
||||
export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#log-level-selector {
|
||||
padding: var(--uui-box-default-padding, var(--uui-size-space-5, 18px));
|
||||
width: 150px;
|
||||
background-color: var(--uui-color-surface);
|
||||
box-shadow: var(--uui-shadow-depth-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
.log-level-button-indicator {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-level-button-indicator:not(:last-of-type)::after {
|
||||
content: ', ';
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@queryAll('#log-level-selector > uui-checkbox')
|
||||
private _logLevelSelectorCheckboxes!: NodeListOf<UUICheckboxElement>;
|
||||
|
||||
@state()
|
||||
private _logLevelFilter: LogLevelModel[] = [];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeLogLevelFilter();
|
||||
});
|
||||
}
|
||||
|
||||
#observeLogLevelFilter() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logLevelsFilter, (levelsFilter) => {
|
||||
this._logLevelFilter = levelsFilter ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
#setLogLevel() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
|
||||
const logLevels = Array.from(this._logLevelSelectorCheckboxes)
|
||||
.filter((checkbox) => checkbox.checked)
|
||||
.map((checkbox) => checkbox.value as LogLevelModel);
|
||||
this.#logViewerContext?.setLogLevelsFilter(logLevels);
|
||||
this.#logViewerContext.getLogs();
|
||||
}
|
||||
|
||||
setLogLevelDebounce = _.debounce(this.#setLogLevel, 300);
|
||||
|
||||
#selectAllLogLevels() {
|
||||
this._logLevelSelectorCheckboxes.forEach((checkbox) => (checkbox.checked = true));
|
||||
this.#setLogLevel();
|
||||
}
|
||||
|
||||
#deselectAllLogLevels() {
|
||||
this._logLevelSelectorCheckboxes.forEach((checkbox) => (checkbox.checked = false));
|
||||
this.#setLogLevel();
|
||||
}
|
||||
|
||||
#renderLogLevelSelector() {
|
||||
return html`
|
||||
<div slot="dropdown" id="log-level-selector" @change=${this.setLogLevelDebounce}>
|
||||
${Object.values(LogLevelModel).map(
|
||||
(logLevel) =>
|
||||
html`<uui-checkbox class="log-level-menu-item" .value=${logLevel} label="${logLevel}"
|
||||
><umb-log-viewer-level-tag .level=${logLevel}></umb-log-viewer-level-tag
|
||||
></uui-checkbox>`
|
||||
)}
|
||||
<uui-button class="log-level-menu-item" @click=${this.#selectAllLogLevels} label="Select all"
|
||||
>Select all</uui-button
|
||||
>
|
||||
<uui-button class="log-level-menu-item" @click=${this.#deselectAllLogLevels} label="Deselect all"
|
||||
>Deselect all</uui-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-button-with-dropdown label="Select log levels"
|
||||
>Log Level:
|
||||
${this._logLevelFilter.length > 0
|
||||
? this._logLevelFilter.map((level) => html`<span class="log-level-button-indicator">${level}</span>`)
|
||||
: 'All'}
|
||||
${this.#renderLogLevelSelector()}
|
||||
</umb-button-with-dropdown>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-log-level-filter-menu': UmbLogViewerLogLevelFilterMenuElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, PropertyValueMap } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { LogLevelModel, LogMessagePropertyModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
//TODO: check how to display EventId field in the message properties
|
||||
@customElement('umb-log-viewer-message')
|
||||
export class UmbLogViewerMessageElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host > details {
|
||||
border-top: 1px solid var(--uui-color-border);
|
||||
}
|
||||
|
||||
:host(:last-child) > details {
|
||||
border-bottom: 1px solid var(--uui-color-border);
|
||||
}
|
||||
|
||||
summary {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
margin-bottom: var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
summary:hover,
|
||||
#properties-list {
|
||||
background-color: var(--uui-color-background);
|
||||
}
|
||||
|
||||
#properties-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin-bottom: var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
.property {
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
border-top: 1px solid var(--uui-color-border);
|
||||
}
|
||||
|
||||
summary > div {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#timestamp {
|
||||
flex: 1 0 14ch;
|
||||
}
|
||||
|
||||
#level,
|
||||
#machine {
|
||||
flex: 1 0 14ch;
|
||||
}
|
||||
|
||||
#message {
|
||||
flex: 6 0 14ch;
|
||||
}
|
||||
|
||||
.property-name,
|
||||
.property-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.property-name {
|
||||
font-weight: 600;
|
||||
flex: 1 1 20ch;
|
||||
}
|
||||
|
||||
.property-value {
|
||||
flex: 3 0 20ch;
|
||||
}
|
||||
|
||||
#search-menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: var(--uui-size-space-3);
|
||||
background-color: var(--uui-color-surface);
|
||||
box-shadow: var(--uui-shadow-depth-3);
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
#search-menu > li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--uui-color-background);
|
||||
border-top: 1px solid #d8d7d9;
|
||||
border-left: 4px solid #d42054;
|
||||
color: #303033;
|
||||
display: block;
|
||||
font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
padding: 9.5px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('details')
|
||||
details!: HTMLDetailsElement;
|
||||
|
||||
@property()
|
||||
timestamp = '';
|
||||
|
||||
@state()
|
||||
date?: Date;
|
||||
|
||||
@property()
|
||||
level: LogLevelModel | '' = '';
|
||||
|
||||
@property()
|
||||
messageTemplate = '';
|
||||
|
||||
@property()
|
||||
renderedMessage = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
properties: Array<LogMessagePropertyModel> = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
@property()
|
||||
exception = '';
|
||||
|
||||
willUpdate(changedProperties: Map<string | number | symbol, unknown>) {
|
||||
if (changedProperties.has('timestamp')) {
|
||||
this.date = new Date(this.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
if (_changedProperties.has('open')) {
|
||||
this.open ? this.details.setAttribute('open', 'true') : this.details.removeAttribute('open');
|
||||
}
|
||||
}
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
private _searchMenuData: Array<{ label: string; href: () => string; icon: string; title: string }> = [
|
||||
{
|
||||
label: 'Search in Google',
|
||||
title: '@logViewer_searchThisMessageWithGoogle',
|
||||
href: () => `https://www.google.com/search?q=${this.renderedMessage}`,
|
||||
icon: 'https://www.google.com/favicon.ico',
|
||||
},
|
||||
{
|
||||
label: 'Search in Bing',
|
||||
title: 'Search this message with Bing',
|
||||
href: () => `https://www.bing.com/search?q=${this.renderedMessage}`,
|
||||
icon: 'https://www.bing.com/favicon.ico',
|
||||
},
|
||||
{
|
||||
label: 'Search in OurUmbraco',
|
||||
title: 'Search this message on Our Umbraco forums and docs',
|
||||
href: () => `https://our.umbraco.com/search?q=${this.renderedMessage}&content=wiki,forum,documentation`,
|
||||
icon: 'https://our.umbraco.com/assets/images/app-icons/favicon.png',
|
||||
},
|
||||
{
|
||||
label: 'Search in OurUmbraco with Google',
|
||||
title: 'Search Our Umbraco forums using Google',
|
||||
href: () =>
|
||||
`https://www.google.co.uk/?q=site:our.umbraco.com ${this.renderedMessage}&safe=off#q=site:our.umbraco.com ${
|
||||
this.renderedMessage
|
||||
} ${this.properties.find((property) => property.name === 'SourceContext')?.value}&safe=off"`,
|
||||
icon: 'https://www.google.com/favicon.ico',
|
||||
},
|
||||
{
|
||||
label: 'Search Umbraco Source',
|
||||
title: 'Search within Umbraco source code on Github',
|
||||
href: () =>
|
||||
`https://github.com/umbraco/Umbraco-CMS/search?q=${
|
||||
this.properties.find((property) => property.name === 'SourceContext')?.value
|
||||
}`,
|
||||
icon: 'https://github.githubassets.com/favicon.ico',
|
||||
},
|
||||
{
|
||||
label: 'Search Umbraco Issues',
|
||||
title: 'Search Umbraco Issues on Github',
|
||||
href: () =>
|
||||
`https://github.com/umbraco/Umbraco-CMS/issues?q=${
|
||||
this.properties.find((property) => property.name === 'SourceContext')?.value
|
||||
}`,
|
||||
icon: 'https://github.githubassets.com/favicon.ico',
|
||||
},
|
||||
];
|
||||
|
||||
private _propertiesWithSearchMenu: Array<string> = ['HttpRequestNumber', 'SourceContext', 'MachineName'];
|
||||
|
||||
private _findLogsWithProperty({ name, value }: LogMessagePropertyModel) {
|
||||
let queryString = '';
|
||||
|
||||
if (isNaN(+(value ?? ''))) {
|
||||
queryString = name + "='" + value + "'";
|
||||
} else {
|
||||
queryString = name + '=' + value;
|
||||
}
|
||||
|
||||
this.#logViewerContext?.setFilterExpression(queryString);
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
this.details.removeAttribute('open');
|
||||
this.#logViewerContext?.getLogs();
|
||||
}
|
||||
|
||||
#setOpen(event: Event) {
|
||||
this.open = (event.target as HTMLDetailsElement).open;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<details @open=${this.#setOpen}>
|
||||
<summary>
|
||||
<div id="timestamp">${this.date?.toLocaleString()}</div>
|
||||
<div id="level">
|
||||
<umb-log-viewer-level-tag .level=${this.level ? this.level : 'Information'}></umb-log-viewer-level-tag>
|
||||
</div>
|
||||
<div id="machine">${this.properties.find((property) => property.name === 'MachineName')?.value}</div>
|
||||
<div id="message">${this.renderedMessage}</div>
|
||||
</summary>
|
||||
${this.exception ? html`<pre id="exception">${this.exception}</pre>` : ''}
|
||||
<ul id="properties-list">
|
||||
<li class="property">
|
||||
<div class="property-name">Timestamp</div>
|
||||
<div class="property-value">${this.date?.toLocaleString()}</div>
|
||||
</li>
|
||||
<li class="property">
|
||||
<div class="property-name">@MessageTemplate</div>
|
||||
<div class="property-value">${this.messageTemplate}</div>
|
||||
</li>
|
||||
${this.properties.map(
|
||||
(property) =>
|
||||
html`<li class="property">
|
||||
<div class="property-name">${property.name}:</div>
|
||||
<div class="property-value">
|
||||
${property.value}
|
||||
${this._propertiesWithSearchMenu.includes(property.name ?? '')
|
||||
? html`<uui-button
|
||||
compact
|
||||
@click=${() => {
|
||||
this._findLogsWithProperty(property);
|
||||
}}
|
||||
look="secondary"
|
||||
label="Find logs with ${property.name}"
|
||||
title="Find logs with ${property.name}"
|
||||
><uui-icon name="umb:search"></uui-icon
|
||||
></uui-button>`
|
||||
: ''}
|
||||
</div>
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
<umb-button-with-dropdown look="secondary" placement="bottom-start" id="search-button" label="Search">
|
||||
<uui-icon name="umb:search"></uui-icon>Search
|
||||
<ul id="search-menu" slot="dropdown">
|
||||
${this._searchMenuData.map(
|
||||
(menuItem) => html`
|
||||
<li>
|
||||
<uui-menu-item
|
||||
class="search-item"
|
||||
href="${menuItem.href()}"
|
||||
target="_blank"
|
||||
label="${menuItem.label}"
|
||||
title="${menuItem.title}">
|
||||
<img slot="icon" src="${menuItem.icon}" width="16" height="16" alt="" />
|
||||
</uui-menu-item>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
</umb-button-with-dropdown>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-message': UmbLogViewerMessageElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { DirectionModel, LogMessageModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-ui/uui';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, query, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
|
||||
@customElement('umb-log-viewer-messages-list')
|
||||
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#message-list-header {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#message-list-header > div {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#timestamp {
|
||||
flex: 1 0 14ch;
|
||||
}
|
||||
|
||||
#level,
|
||||
#machine {
|
||||
flex: 1 0 14ch;
|
||||
}
|
||||
|
||||
#message {
|
||||
flex: 6 0 14ch;
|
||||
}
|
||||
|
||||
#empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
#pagination {
|
||||
margin: var(--uui-size-space-5, 18px) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('#logs-scroll-container')
|
||||
private _logsScrollContainer!: UUIScrollContainerElement;
|
||||
|
||||
@state()
|
||||
private _sortingDirection: DirectionModel = DirectionModel.ASCENDING;
|
||||
|
||||
@state()
|
||||
private _logs: LogMessageModel[] = [];
|
||||
|
||||
@state()
|
||||
private _logsTotal = 0;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeLogs();
|
||||
this.#logViewerContext.getLogs();
|
||||
});
|
||||
}
|
||||
|
||||
#observeLogs() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logs, (logs) => {
|
||||
this._logs = logs ?? [];
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.logsTotal, (total) => {
|
||||
this._logsTotal = total ?? 0;
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.sortingDirection, (direction) => {
|
||||
this._sortingDirection = direction;
|
||||
});
|
||||
}
|
||||
|
||||
#sortLogs() {
|
||||
this.#logViewerContext?.toggleSortOrder();
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
this.#logViewerContext?.getLogs();
|
||||
}
|
||||
|
||||
_onPageChange(event: Event): void {
|
||||
const current = (event.target as UUIPaginationElement).current;
|
||||
this.#logViewerContext?.setCurrentPage(current);
|
||||
this.#logViewerContext?.getLogs();
|
||||
this._logsScrollContainer.scrollTop = 0;
|
||||
}
|
||||
|
||||
private _renderPagination() {
|
||||
if (!this._logsTotal) return '';
|
||||
|
||||
const totalPages = Math.ceil(this._logsTotal / 100);
|
||||
|
||||
if (totalPages <= 1) return '';
|
||||
|
||||
return html`<div id="pagination">
|
||||
<uui-pagination .total=${totalPages} @change="${this._onPageChange}"></uui-pagination>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-box>
|
||||
<p style="font-weight: bold;">Total items: ${this._logsTotal}</p>
|
||||
<div id="message-list-header">
|
||||
<div id="timestamp">
|
||||
Timestamp
|
||||
<uui-button compact @click=${this.#sortLogs} label="Sort logs">
|
||||
<uui-symbol-sort
|
||||
?descending=${this._sortingDirection === DirectionModel.DESCENDING}
|
||||
active></uui-symbol-sort>
|
||||
</uui-button>
|
||||
</div>
|
||||
<div id="level">Level</div>
|
||||
<div id="machine">Machine name</div>
|
||||
<div id="message">Message</div>
|
||||
</div>
|
||||
<uui-scroll-container id="logs-scroll-container" style="max-height: calc(100vh - 490px)">
|
||||
${this._logs.length > 0
|
||||
? html` ${this._logs.map(
|
||||
(log) => html`<umb-log-viewer-message
|
||||
.timestamp=${log.timestamp ?? ''}
|
||||
.level=${log.level ?? ''}
|
||||
.renderedMessage=${log.renderedMessage ?? ''}
|
||||
.properties=${log.properties ?? []}
|
||||
.exception=${log.exception ?? ''}
|
||||
.messageTemplate=${log.messageTemplate ?? ''}></umb-log-viewer-message>`
|
||||
)}`
|
||||
: html`<umb-empty-state size="small"
|
||||
><span id="empty">
|
||||
<uui-icon name="umb:search"></uui-icon>Sorry, we cannot find what you are looking for.
|
||||
</span></umb-empty-state
|
||||
>`}
|
||||
</uui-scroll-container>
|
||||
${this._renderPagination()}
|
||||
</uui-box>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-messages-list': UmbLogViewerMessagesListElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, query, state } from 'lit/decorators.js';
|
||||
import {
|
||||
PoolingCOnfig,
|
||||
PoolingInterval,
|
||||
UmbLogViewerWorkspaceContext,
|
||||
UMB_APP_LOG_VIEWER_CONTEXT_TOKEN,
|
||||
} from '../../../logviewer.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-log-viewer-polling-button')
|
||||
export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#polling-interval-menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 20ch;
|
||||
background-color: var(--uui-color-surface);
|
||||
box-shadow: var(--uui-shadow-depth-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(calc((100% - 33px) * -1));
|
||||
}
|
||||
|
||||
#polling-enabled-icon {
|
||||
margin-right: var(--uui-size-space-3);
|
||||
margin-bottom: 1px;
|
||||
-webkit-animation: rotate-center 0.8s ease-in-out infinite both;
|
||||
animation: rotate-center 0.8s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotate-center {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotate-center {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('#polling-popover')
|
||||
private _pollingPopover!: UUIPopoverElement;
|
||||
|
||||
@query('#polling-expand-symbol')
|
||||
private _polingExpandSymbol!: UUISymbolExpandElement;
|
||||
|
||||
@state()
|
||||
private _poolingConfig: PoolingCOnfig = { enabled: false, interval: 0 };
|
||||
|
||||
#pollingIntervals: PoolingInterval[] = [2000, 5000, 10000, 20000, 30000];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observePoolingConfig();
|
||||
this.#logViewerContext.getLogs();
|
||||
});
|
||||
}
|
||||
|
||||
#observePoolingConfig() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.polling, (poolingConfig) => {
|
||||
this._poolingConfig = { ...poolingConfig };
|
||||
});
|
||||
}
|
||||
|
||||
#togglePolling() {
|
||||
this.#logViewerContext?.togglePolling();
|
||||
}
|
||||
|
||||
#setPolingInterval(interval: PoolingInterval) {
|
||||
this.#logViewerContext?.setPollingInterval(interval);
|
||||
this.#closePoolingPopover();
|
||||
}
|
||||
|
||||
#openPoolingPopover() {
|
||||
this._pollingPopover.open = true;
|
||||
this._polingExpandSymbol.open = true;
|
||||
}
|
||||
|
||||
#closePoolingPopover() {
|
||||
this._pollingPopover.open = false;
|
||||
this._polingExpandSymbol.open = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <uui-button-group>
|
||||
<uui-button label="Start pooling" @click=${this.#togglePolling}
|
||||
>${this._poolingConfig.enabled
|
||||
? html`<uui-icon name="umb:axis-rotation" id="polling-enabled-icon"></uui-icon>Polling
|
||||
${this._poolingConfig.interval / 1000} seconds`
|
||||
: 'Pooling'}</uui-button
|
||||
>
|
||||
<uui-popover placement="bottom-end" id="polling-popover" @close=${() => (this._polingExpandSymbol.open = false)}>
|
||||
<uui-button slot="trigger" compact label="Choose pooling time" @click=${this.#openPoolingPopover}>
|
||||
<uui-symbol-expand id="polling-expand-symbol"></uui-symbol-expand>
|
||||
</uui-button>
|
||||
|
||||
<ul id="polling-interval-menu" slot="popover">
|
||||
${this.#pollingIntervals.map(
|
||||
(interval: PoolingInterval) =>
|
||||
html`<uui-menu-item
|
||||
label="Every ${interval / 1000} seconds"
|
||||
@click-label=${() => this.#setPolingInterval(interval)}></uui-menu-item>`
|
||||
)}
|
||||
</ul>
|
||||
</uui-popover>
|
||||
</uui-button-group>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-polling-button': UmbLogViewerPollingButtonElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { UUIInputElement, UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, query, state } from 'lit/decorators.js';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context';
|
||||
import { SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-log-viewer-search-input')
|
||||
export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
#search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#saved-searches-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#saved-searches-popover {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#saved-searches-container {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
background-color: var(--uui-color-surface);
|
||||
box-shadow: var(--uui-shadow-depth-1);
|
||||
}
|
||||
|
||||
.saved-search-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
border-bottom: 1px solid #e9e9eb;
|
||||
}
|
||||
|
||||
.saved-search-item-button {
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
flex: 1;
|
||||
background: 0 0;
|
||||
padding: 0 0;
|
||||
border: 0;
|
||||
clear: both;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
color: var(--uui-color-interactive);
|
||||
}
|
||||
|
||||
.saved-search-item-button:hover {
|
||||
background-color: var(--uui-color-surface-emphasis, rgb(250, 250, 250));
|
||||
color: var(--color-standalone);
|
||||
}
|
||||
|
||||
.saved-search-item-name {
|
||||
font-weight: 600;
|
||||
margin: 0 var(--uui-size-space-3);
|
||||
}
|
||||
|
||||
#polling-symbol-expand,
|
||||
#saved-search-expand-symbol,
|
||||
uui-symbol-sort {
|
||||
margin-left: var(--uui-size-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('#saved-searches-popover')
|
||||
private _savedSearchesPopover!: UUIPopoverElement;
|
||||
|
||||
@query('#saved-search-expand-symbol')
|
||||
private _savedSearchesExpandSymbol!: UUISymbolExpandElement;
|
||||
|
||||
@state()
|
||||
private _savedSearches: SavedLogSearchModel[] = [];
|
||||
|
||||
@state()
|
||||
private _inputQuery = '';
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeStuff();
|
||||
this.#logViewerContext.getLogs();
|
||||
});
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
||||
this._savedSearches = savedSearches ?? [];
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.filterExpression, (query) => {
|
||||
this._inputQuery = query;
|
||||
});
|
||||
}
|
||||
|
||||
#toggleSavedSearchesPopover() {
|
||||
this._savedSearchesPopover.open = !this._savedSearchesPopover.open;
|
||||
}
|
||||
|
||||
#toggleSavedSearchesExpandSymbol() {
|
||||
this._savedSearchesExpandSymbol.open = !this._savedSearchesExpandSymbol.open;
|
||||
}
|
||||
|
||||
#openSavedSearchesPopover() {
|
||||
this.#toggleSavedSearchesPopover();
|
||||
this.#toggleSavedSearchesExpandSymbol();
|
||||
}
|
||||
|
||||
#setQuery(event: Event) {
|
||||
const target = event.target as UUIInputElement;
|
||||
this._inputQuery = target.value as string;
|
||||
this.#logViewerContext?.setFilterExpression(this._inputQuery);
|
||||
}
|
||||
|
||||
#setQueryFromSavedSearch(query: string) {
|
||||
this._inputQuery = query;
|
||||
this.#logViewerContext?.setFilterExpression(query);
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
|
||||
this.#logViewerContext?.getLogs();
|
||||
this._savedSearchesPopover.open = false;
|
||||
}
|
||||
|
||||
#clearQuery() {
|
||||
this._inputQuery = '';
|
||||
this.#logViewerContext?.setFilterExpression('');
|
||||
this.#logViewerContext?.getLogs();
|
||||
}
|
||||
|
||||
#search() {
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
|
||||
this.#logViewerContext?.getLogs();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <uui-popover
|
||||
placement="bottom-start"
|
||||
id="saved-searches-popover"
|
||||
@close=${this.#toggleSavedSearchesExpandSymbol}>
|
||||
<uui-input
|
||||
id="search-input"
|
||||
label="Search logs"
|
||||
.placeholder=${'Search logs...'}
|
||||
slot="trigger"
|
||||
@input=${this.#setQuery}
|
||||
.value=${this._inputQuery}>
|
||||
${this._inputQuery
|
||||
? html`<uui-button compact slot="append" label="Save search"
|
||||
><uui-icon name="umb:favorite"></uui-icon></uui-button
|
||||
><uui-button compact slot="append" label="Clear" @click=${this.#clearQuery}
|
||||
><uui-icon name="umb:delete"></uui-icon
|
||||
></uui-button>`
|
||||
: html``}
|
||||
<uui-button
|
||||
compact
|
||||
slot="append"
|
||||
id="saved-searches-button"
|
||||
@click=${this.#openSavedSearchesPopover}
|
||||
label="Saved searches"
|
||||
>Saved searches <uui-symbol-expand id="saved-search-expand-symbol"></uui-symbol-expand
|
||||
></uui-button>
|
||||
</uui-input>
|
||||
|
||||
<uui-scroll-container slot="popover" id="saved-searches-container" role="list">
|
||||
${this._savedSearches.map(
|
||||
(search) =>
|
||||
html`<li class="saved-search-item">
|
||||
<button
|
||||
label="Search for ${search.name}"
|
||||
class="saved-search-item-button"
|
||||
@click=${() => this.#setQueryFromSavedSearch(search.query ?? '')}>
|
||||
<span class="saved-search-item-name">${search.name}</span>
|
||||
<span class="saved-search-item-query">${search.query}</span></button
|
||||
><uui-button label="Remove saved search" color="danger"
|
||||
><uui-icon name="umb:trash"></uui-icon
|
||||
></uui-button>
|
||||
</li>`
|
||||
)}
|
||||
</uui-scroll-container>
|
||||
</uui-popover>
|
||||
<uui-button look="primary" @click=${this.#search} label="Search">Search</uui-button>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-search-input': UmbLogViewerSearchInputElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import './components';
|
||||
import { UmbLogViewerSearchViewElement } from './log-search-view.element';
|
||||
|
||||
export default UmbLogViewerSearchViewElement;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../logviewer.context';
|
||||
|
||||
@customElement('umb-log-viewer-search-view')
|
||||
export class UmbLogViewerSearchViewElement extends UmbLitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#layout {
|
||||
margin: 20px;
|
||||
}
|
||||
#levels-container,
|
||||
#input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--uui-size-space-4);
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#levels-container {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#dates-polling-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
umb-log-viewer-search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
umb-log-viewer-date-range-selector {
|
||||
flex-direction: row;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _canShowLogs = false;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeCanShowLogs();
|
||||
});
|
||||
}
|
||||
|
||||
#observeCanShowLogs() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
||||
this._canShowLogs = canShowLogs ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="layout">
|
||||
<div id="levels-container">
|
||||
<umb-log-viewer-log-level-filter-menu></umb-log-viewer-log-level-filter-menu>
|
||||
<div id="dates-polling-container">
|
||||
<umb-log-viewer-date-range-selector horizontal></umb-log-viewer-date-range-selector>
|
||||
<umb-log-viewer-polling-button> </umb-log-viewer-polling-button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="input-container">
|
||||
<umb-log-viewer-search-input></umb-log-viewer-search-input>
|
||||
</div>
|
||||
${this._canShowLogs
|
||||
? html`<umb-log-viewer-messages-list></umb-log-viewer-messages-list>`
|
||||
: html`<umb-log-viewer-to-many-logs-warning id="to-many-logs-warning"></umb-log-viewer-to-many-logs-warning>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-log-viewer-search-view': UmbLogViewerSearchViewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { PopoverPlacement, UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui';
|
||||
import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types';
|
||||
|
||||
// TODO: maybe this should go to UI library? It's a common pattern
|
||||
// TODO: consider not using this, but instead use dropdown, which is more generic shared component of backoffice. (this is at the movement only used in Log Viewer)
|
||||
@customElement('umb-button-with-dropdown')
|
||||
export class UmbButtonWithDropdownElement extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
uui-symbol-expand {
|
||||
margin-left: var(--uui-size-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property()
|
||||
label = '';
|
||||
|
||||
@property()
|
||||
open = false;
|
||||
|
||||
@property()
|
||||
look: InterfaceLook = 'default';
|
||||
|
||||
@property()
|
||||
color: InterfaceColor = 'default';
|
||||
|
||||
@property()
|
||||
placement: PopoverPlacement = 'bottom-start';
|
||||
|
||||
@query('#symbol-expand')
|
||||
symbolExpand!: UUISymbolExpandElement;
|
||||
|
||||
@query('#popover')
|
||||
popover!: UUIPopoverElement;
|
||||
|
||||
#openPopover() {
|
||||
this.open = true;
|
||||
this.popover.open = true;
|
||||
this.symbolExpand.open = true;
|
||||
}
|
||||
|
||||
#closePopover() {
|
||||
this.open = false;
|
||||
this.popover.open = false;
|
||||
this.symbolExpand.open = false;
|
||||
}
|
||||
|
||||
#togglePopover() {
|
||||
this.open ? this.#closePopover() : this.#openPopover();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-popover placement=${this.placement} id="popover" @close=${this.#closePopover}>
|
||||
<uui-button
|
||||
slot="trigger"
|
||||
.look=${this.look}
|
||||
.color=${this.color}
|
||||
.label=${this.label}
|
||||
id="myPopoverBtn"
|
||||
@click=${this.#togglePopover}>
|
||||
<slot></slot>
|
||||
<uui-symbol-expand id="symbol-expand" .open=${this.open}></uui-symbol-expand>
|
||||
</uui-button>
|
||||
<div slot="popover">
|
||||
<slot name="dropdown"></slot>
|
||||
</div>
|
||||
</uui-popover>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-button-with-dropdown': UmbButtonWithDropdownElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import './button-with-dropdown.element';
|
||||
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
import { UmbButtonWithDropdownElement } from './button-with-dropdown.element';
|
||||
|
||||
export default {
|
||||
title: 'Components/Button with dropdown',
|
||||
component: 'umb-button-with-dropdown',
|
||||
id: 'umb-button-with-dropdown',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbButtonWithDropdownElement> = () => html` <umb-button-with-dropdown>
|
||||
Open me
|
||||
<div slot="dropdown" style="background: pink; height: 300px">I am a dropdown</div>
|
||||
</umb-button-with-dropdown>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -0,0 +1,20 @@
|
||||
import './donut-slice';
|
||||
import './donut-chart';
|
||||
|
||||
import { Meta } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
export default {
|
||||
title: 'Components/Donut chart',
|
||||
component: 'umb-donut-chart',
|
||||
id: 'umb-donut-chart',
|
||||
tags: ['autodocs'],
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview = () => html` <umb-donut-chart description="Colors of fruits">
|
||||
<umb-donut-slice color="red" name="Red" amount="10" kind="apples"></umb-donut-slice>
|
||||
<umb-donut-slice color="green" name="Green" amount="20" kind="apples"></umb-donut-slice>
|
||||
<umb-donut-slice color="yellow" name="Yellow" amount="10" kind="bananas"></umb-donut-slice>
|
||||
<umb-donut-slice color="purple" name="Purple" amount="69" kind="plums"></umb-donut-slice>
|
||||
</umb-donut-chart>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -0,0 +1,337 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, LitElement, svg } from 'lit';
|
||||
import { customElement, property, query, queryAssignedElements, state } from 'lit/decorators.js';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { UmbDonutSliceElement } from './donut-slice';
|
||||
|
||||
export interface Circle {
|
||||
color: string;
|
||||
name: string;
|
||||
percent: number;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface CircleWithCommands extends Circle {
|
||||
offset: number;
|
||||
commands: string;
|
||||
}
|
||||
//TODO: maybe move to UI Library
|
||||
/**
|
||||
* This is a donut chart component that can be used to display data in a circular way.
|
||||
*
|
||||
* @export
|
||||
* @class UmbDonutChartElement
|
||||
* @extends {LitElement}
|
||||
*/
|
||||
@customElement('umb-donut-chart')
|
||||
export class UmbDonutChartElement extends LitElement {
|
||||
static percentToDegrees(percent: number): number {
|
||||
return percent * 3.6;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
path {
|
||||
pointer-events: visibleFill;
|
||||
}
|
||||
.circle {
|
||||
filter: url(#erode);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
transition: opacity 200ms linear;
|
||||
filter: url(#filter);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.highlight:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#details-box {
|
||||
background: #ffffffe6;
|
||||
border: 1px solid var(--uui-color-border-standalone);
|
||||
border-radius: var(--uui-border-radius);
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
padding: 0.5em;
|
||||
line-height: 1.5;
|
||||
font-size: var(--uui-type-small-size);
|
||||
box-shadow: var(--uui-shadow-depth-1);
|
||||
transform: translate3d(var(--pos-x), var(--pos-y), 0);
|
||||
transition: transform 0.2s cubic-bezier(0.02, 1.23, 0.79, 1.08);
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
#details-box.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#details-box uui-icon {
|
||||
/* optically correct alignment */
|
||||
color: var(--umb-donut-detail-color);
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
#details-title {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* Circle radius in pixels
|
||||
*
|
||||
* @memberof UmbDonutChartElement
|
||||
*/
|
||||
@property({ type: Number })
|
||||
radius = 45;
|
||||
|
||||
/**
|
||||
* The circle thickness in pixels
|
||||
*
|
||||
* @memberof UmbDonutChartElement
|
||||
*/
|
||||
@property({ type: Number, attribute: 'border-size' })
|
||||
borderSize = 20;
|
||||
|
||||
/**
|
||||
* The size of SVG element in pixels
|
||||
*
|
||||
* @memberof UmbDonutChartElement
|
||||
*/
|
||||
@property({ type: Number, attribute: 'svg-size' })
|
||||
svgSize = 100;
|
||||
|
||||
/**
|
||||
* Description of the graph, added for accessibility purposes
|
||||
*
|
||||
* @memberof UmbDonutChartElement
|
||||
*/
|
||||
@property()
|
||||
description = '';
|
||||
|
||||
/**
|
||||
* Hides the box that appears oh hover with the details of the slice
|
||||
*
|
||||
* @memberof UmbDonutChartElement
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
hideDetailBox = false;
|
||||
|
||||
@queryAssignedElements({ selector: 'umb-donut-slice' })
|
||||
private _slices!: UmbDonutSliceElement[];
|
||||
|
||||
@query('#container')
|
||||
private _container!: HTMLDivElement;
|
||||
|
||||
@query('#details-box')
|
||||
private _detailsBox!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
private circles: CircleWithCommands[] = [];
|
||||
|
||||
@state()
|
||||
private viewBox = 100;
|
||||
|
||||
@state()
|
||||
private _posY = 0;
|
||||
|
||||
@state()
|
||||
private _posX = 0;
|
||||
|
||||
@state()
|
||||
private _detailName = '';
|
||||
|
||||
@state()
|
||||
private _detailAmount = 0;
|
||||
|
||||
@state()
|
||||
private _detailColor = 'black';
|
||||
|
||||
@state()
|
||||
private _totalAmount = 0;
|
||||
|
||||
@state()
|
||||
private _detailKind = '';
|
||||
|
||||
#containerBounds: DOMRect | undefined;
|
||||
|
||||
firstUpdated() {
|
||||
this.#containerBounds = this._container.getBoundingClientRect();
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
|
||||
if (_changedProperties.has('radius') || _changedProperties.has('borderSize') || _changedProperties.has('svgSize')) {
|
||||
if (this.borderSize > this.radius) {
|
||||
throw new Error('Border size cannot be bigger than radius');
|
||||
}
|
||||
|
||||
this.#printCircles();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#calculatePercentage(partialValue: number) {
|
||||
if (this._totalAmount === 0) return 0;
|
||||
const percent = Math.round((100 * partialValue) / this._totalAmount);
|
||||
return clamp(percent, 0, 99);
|
||||
}
|
||||
|
||||
#printCircles(event: Event | null = null) {
|
||||
this._totalAmount = this._slices.reduce((acc, slice) => acc + slice.amount, 0);
|
||||
event?.stopPropagation();
|
||||
this.circles = this.#addCommands(
|
||||
this._slices.map((slice) => {
|
||||
return {
|
||||
percent: this.#calculatePercentage(slice.amount),
|
||||
color: slice.color,
|
||||
name: slice.name,
|
||||
kind: slice.kind,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#addCommands(Circles: Circle[]): CircleWithCommands[] {
|
||||
let previousPercent = 0;
|
||||
return Circles.map((slice) => {
|
||||
const sliceWithCommands: CircleWithCommands = {
|
||||
...slice,
|
||||
commands: this.#getSliceCommands(slice, this.radius, this.svgSize, this.borderSize),
|
||||
offset: previousPercent * 3.6 * -1,
|
||||
};
|
||||
previousPercent += slice.percent;
|
||||
return sliceWithCommands;
|
||||
});
|
||||
}
|
||||
|
||||
#getSliceCommands(Circle: Circle, radius: number, svgSize: number, borderSize: number): string {
|
||||
const degrees = UmbDonutChartElement.percentToDegrees(Circle.percent);
|
||||
const longPathFlag = degrees > 180 ? 1 : 0;
|
||||
const innerRadius = radius - borderSize;
|
||||
|
||||
const commands: string[] = [];
|
||||
commands.push(`M ${svgSize / 2 + radius} ${svgSize / 2}`);
|
||||
commands.push(`A ${radius} ${radius} 0 ${longPathFlag} 0 ${this.#getCoordFromDegrees(degrees, radius, svgSize)}`);
|
||||
commands.push(`L ${this.#getCoordFromDegrees(degrees, innerRadius, svgSize)}`);
|
||||
commands.push(`A ${innerRadius} ${innerRadius} 0 ${longPathFlag} 1 ${svgSize / 2 + innerRadius} ${svgSize / 2}`);
|
||||
return commands.join(' ');
|
||||
}
|
||||
|
||||
#getCoordFromDegrees(angle: number, radius: number, svgSize: number): string {
|
||||
const x = Math.cos((angle * Math.PI) / 180);
|
||||
const y = Math.sin((angle * Math.PI) / 180);
|
||||
const coordX = x * radius + svgSize / 2;
|
||||
const coordY = y * -radius + svgSize / 2;
|
||||
return [coordX, coordY].join(' ');
|
||||
}
|
||||
|
||||
#calculateDetailsBoxPosition = (event: MouseEvent) => {
|
||||
const x = this.#containerBounds ? event.clientX - this.#containerBounds?.left : 0;
|
||||
const y = this.#containerBounds ? event.clientY - this.#containerBounds?.top : 0;
|
||||
this._posX = x - 10;
|
||||
this._posY = y - 70;
|
||||
};
|
||||
|
||||
#setDetailsBoxData(event: MouseEvent) {
|
||||
const target = event.target as SVGPathElement;
|
||||
const index = target.dataset.index as unknown as number;
|
||||
const circle = this.circles[index];
|
||||
this._detailName = circle.name;
|
||||
this._detailAmount = circle.percent;
|
||||
this._detailColor = circle.color;
|
||||
this._detailKind = circle.kind;
|
||||
}
|
||||
|
||||
#showDetailsBox(event: MouseEvent) {
|
||||
if (this.hideDetailBox) return;
|
||||
this.#setDetailsBoxData(event);
|
||||
this._detailsBox.classList.add('show');
|
||||
}
|
||||
|
||||
#hideDetailsBox() {
|
||||
if (this.hideDetailBox) return;
|
||||
this._detailsBox.classList.remove('show');
|
||||
}
|
||||
|
||||
#renderCircles() {
|
||||
return svg`
|
||||
<filter id="erode" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="linearRGB">
|
||||
<feMorphology operator="erode" radius="0.5 0.5" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" result="morphology"/>
|
||||
</filter>
|
||||
<filter id="filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="linearRGB">
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="1.8 0 0 0 0
|
||||
0 1.8 0 0 0
|
||||
0 0 1.8 0 0
|
||||
0 0 0 500 -20" x="0%" y="0%" width="100%" height="100%" in="merge1" result="colormatrix2"/>
|
||||
<feMorphology operator="erode" radius="0.5 0.5" x="0%" y="0%" width="100%" height="100%" in="colormatrix2" result="morphology2"/>
|
||||
<feFlood flood-color="#ffffff" flood-opacity="0.3" x="0%" y="0%" width="100%" height="100%" result="flood3"/>
|
||||
<feComposite in="flood3" in2="SourceAlpha" operator="in" x="0%" y="0%" width="100%" height="100%" result="composite3"/>
|
||||
<feMorphology operator="erode" radius="1 1" x="0%" y="0%" width="100%" height="100%" in="composite3" result="morphology1"/>
|
||||
<feMerge x="0%" y="0%" width="100%" height="100%" result="merge1">
|
||||
<feMergeNode in="morphology2"/>
|
||||
<feMergeNode in="morphology1"/>
|
||||
</feMerge>
|
||||
<feDropShadow stdDeviation="1 1" in="merge1" dx="0" dy="0" flood-color="#000" flood-opacity="0.8" x="0%" y="0%" width="100%" height="100%" result="dropShadow1"/>
|
||||
</filter>
|
||||
<desc>${this.description}</desc>
|
||||
${this.circles.map(
|
||||
(circle, i) => svg`
|
||||
<path
|
||||
class="circle"
|
||||
|
||||
data-index="${i}"
|
||||
fill="${circle.color}"
|
||||
role="listitem"
|
||||
d="${circle.commands}"
|
||||
transform="rotate(${circle.offset} ${this.viewBox / 2} ${this.viewBox / 2})">
|
||||
</path>
|
||||
<path
|
||||
data-index="${i}"
|
||||
@mouseenter=${this.#showDetailsBox}
|
||||
@mouseleave=${this.#hideDetailsBox}
|
||||
class="highlight"
|
||||
fill="${circle.color}"
|
||||
role="listitem"
|
||||
d="${circle.commands}"
|
||||
transform="rotate(${circle.offset} ${this.viewBox / 2} ${this.viewBox / 2})">
|
||||
</path>`
|
||||
)}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div id="container" @mousemove=${this.#calculateDetailsBoxPosition}>
|
||||
<svg viewBox="0 0 ${this.viewBox} ${this.viewBox}" role="list">${this.#renderCircles()}</svg>
|
||||
<div
|
||||
id="details-box"
|
||||
style="--pos-y: ${this._posY}px; --pos-x: ${this._posX}px; --umb-donut-detail-color: ${this._detailColor}">
|
||||
<div id="details-title"><uui-icon name="umb:record"></uui-icon>${this._detailName}</div>
|
||||
<span>${this._detailAmount} ${this._detailKind}</span>
|
||||
</div>
|
||||
</div>
|
||||
<slot @slotchange=${this.#printCircles} @slice-update=${this.#printCircles}></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-donut-chart': UmbDonutChartElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
/**
|
||||
* This component is used to display a single slice of a donut chart. It only makes sense insice the donut chart
|
||||
*
|
||||
* @export
|
||||
* @class UmbDonutSliceElement
|
||||
* @fires slice-update - This event is fired when the slice is updated
|
||||
* @extends {LitElement}
|
||||
*/
|
||||
@customElement('umb-donut-slice')
|
||||
export class UmbDonutSliceElement extends LitElement {
|
||||
/**
|
||||
* Number of items that this slice represents
|
||||
*
|
||||
* @memberof UmbDonutSliceElement
|
||||
*/
|
||||
@property({ type: Number })
|
||||
amount = 0;
|
||||
/**
|
||||
* Color of the slice. Any valid css color is accepted, custom properties are also supported
|
||||
*
|
||||
* @memberof UmbDonutSliceElement
|
||||
*/
|
||||
@property()
|
||||
color = 'red';
|
||||
/**
|
||||
* Name of the slice. This is used to display the name of the slice in the donut chart
|
||||
*
|
||||
* @memberof UmbDonutSliceElement
|
||||
*/
|
||||
@property()
|
||||
name = '';
|
||||
/**
|
||||
* Kind of the slice. This is shown on a details box when hovering over the slice
|
||||
*
|
||||
* @memberof UmbDonutSliceElement
|
||||
*/
|
||||
@property()
|
||||
kind = '';
|
||||
|
||||
willUpdate() {
|
||||
this.dispatchEvent(new CustomEvent('slice-update', { composed: true, bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-donut-slice': UmbDonutSliceElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './donut-chart';
|
||||
export * from './donut-slice';
|
||||
@@ -3,6 +3,7 @@ import { css, html, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
// TODO: maybe move this to UI Library.
|
||||
@customElement('umb-dropdown')
|
||||
export class UmbDropdownElement extends UmbLitElement {
|
||||
static styles = [
|
||||
|
||||
@@ -7,8 +7,10 @@ import './backoffice-frame/backoffice-header.element';
|
||||
import './backoffice-frame/backoffice-main.element';
|
||||
import './backoffice-frame/backoffice-modal-container.element';
|
||||
import './backoffice-frame/backoffice-notification-container.element';
|
||||
import './button-with-dropdown/button-with-dropdown.element';
|
||||
import './code-block/code-block.element';
|
||||
import './debug/debug.element';
|
||||
import './donut-chart';
|
||||
import './dropdown/dropdown.element';
|
||||
import './empty-state/empty-state.element';
|
||||
import './extension-slot/extension-slot.element';
|
||||
|
||||
@@ -24,6 +24,7 @@ import { handlers as templateHandlers } from './domains/template.handlers';
|
||||
import { handlers as languageHandlers } from './domains/language.handlers';
|
||||
import { handlers as cultureHandlers } from './domains/culture.handlers';
|
||||
import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers';
|
||||
import { handlers as logViewerHandlers } from './domains/log-viewer.handlers';
|
||||
import { handlers as packageHandlers } from './domains/package.handlers';
|
||||
|
||||
const handlers = [
|
||||
@@ -52,6 +53,7 @@ const handlers = [
|
||||
...languageHandlers,
|
||||
...cultureHandlers,
|
||||
...redirectManagementHandlers,
|
||||
...logViewerHandlers,
|
||||
...packageHandlers,
|
||||
];
|
||||
|
||||
|
||||
@@ -5,4 +5,8 @@ export class UmbData<T> {
|
||||
constructor(data: Array<T>) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get total() {
|
||||
return this.data.length;
|
||||
}
|
||||
}
|
||||
|
||||
413
src/Umbraco.Web.UI.Client/src/core/mocks/data/log-viewer.data.ts
Normal file
413
src/Umbraco.Web.UI.Client/src/core/mocks/data/log-viewer.data.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import { logs } from './logs.data';
|
||||
import { UmbData } from './data';
|
||||
import { LogMessageModel, LogTemplateModel, SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
// Temp mocked database
|
||||
class UmbLogviewerSearchesData extends UmbData<SavedLogSearchModel> {
|
||||
constructor(data: SavedLogSearchModel[]) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
// skip can be number or null
|
||||
getSavedSearches(skip = 0, take = this.data.length): Array<SavedLogSearchModel> {
|
||||
return this.data.slice(skip, take);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.data.find((search) => search.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
class UmbLogviewerTemplatesData extends UmbData<LogTemplateModel> {
|
||||
constructor(data: LogTemplateModel[]) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
// skip can be number or null
|
||||
getTemplates(skip = 0, take = this.data.length): Array<LogTemplateModel> {
|
||||
return this.data.slice(skip, take);
|
||||
}
|
||||
}
|
||||
|
||||
class UmbLogviewerMessagesData extends UmbData<LogMessageModel> {
|
||||
constructor(data: LogTemplateModel[]) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
// skip can be number or null
|
||||
getLogs(skip = 0, take = this.data.length): Array<LogMessageModel> {
|
||||
return this.data.slice(skip, take);
|
||||
}
|
||||
|
||||
getLevelCount() {
|
||||
const levels = this.data.map((log) => log.level ?? 'unknown');
|
||||
const counts = {};
|
||||
levels.forEach((level: string) => {
|
||||
//eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
counts[level ?? 'unknown'] = (counts[level] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
}
|
||||
|
||||
export const savedSearches: Array<SavedLogSearchModel> = [
|
||||
{
|
||||
name: 'Find all logs where the Level is NOT Verbose and NOT Debug',
|
||||
query: "Not(@Level='Verbose') and Not(@Level='Debug')",
|
||||
},
|
||||
{
|
||||
name: 'Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)',
|
||||
query: 'Has(@Exception)',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that have the property 'Duration'",
|
||||
query: 'Has(Duration)',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
|
||||
query: 'Has(Duration) and Duration > 1000',
|
||||
},
|
||||
{
|
||||
name: "Find all logs that are from the namespace 'Umbraco.Core'",
|
||||
query: "StartsWith(SourceContext, 'Umbraco.Core')",
|
||||
},
|
||||
{
|
||||
name: 'Find all logs that use a specific log message template',
|
||||
query: "@messageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'",
|
||||
},
|
||||
{
|
||||
name: 'Find logs where one of the items in the SortedComponentTypes property array is equal to',
|
||||
query: "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'",
|
||||
},
|
||||
{
|
||||
name: 'Find logs where one of the items in the SortedComponentTypes property array contains',
|
||||
query: "Contains(SortedComponentTypes[?], 'DatabaseServer')",
|
||||
},
|
||||
{
|
||||
name: 'Find all logs that the message has localhost in it with SQL like',
|
||||
query: "@Message like '%localhost%'",
|
||||
},
|
||||
{
|
||||
name: "Find all logs that the message that starts with 'end' in it with SQL like",
|
||||
query: "@Message like 'end%'",
|
||||
},
|
||||
{
|
||||
name: 'bla',
|
||||
query: 'bla bla',
|
||||
},
|
||||
];
|
||||
|
||||
export const messageTemplates: LogTemplateModel[] = [
|
||||
{
|
||||
messageTemplate: 'Create Foreign Key:\n {Sql}',
|
||||
count: 90,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Create Index:\n {Sql}',
|
||||
count: 86,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Create table:\n {Sql}',
|
||||
count: 82,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Create Primary Key:\n {Sql}',
|
||||
count: 78,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Creating data in {TableName}',
|
||||
count: 58,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Completed creating data in {TableName}',
|
||||
count: 58,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'New table {TableName} was created',
|
||||
count: 58,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'At {OrigState}',
|
||||
count: 18,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'SQL [{ContextIndex}]: {Sql}',
|
||||
count: 15,
|
||||
},
|
||||
{
|
||||
messageTemplate: '{StartMessage} [Timing {TimingId}]',
|
||||
count: 14,
|
||||
},
|
||||
{
|
||||
messageTemplate: '{EndMessage} ({Duration}ms) [Timing {TimingId}]',
|
||||
count: 14,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Execute {MigrationType}',
|
||||
count: 13,
|
||||
},
|
||||
{
|
||||
messageTemplate: "Assigned Deploy permission letter '{Permission}' to user group '{UserGroupAlias}'",
|
||||
count: 6,
|
||||
},
|
||||
{
|
||||
messageTemplate: "Starting '{MigrationName}'...",
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Done (pending scope completion).',
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
"Umbraco Forms scheduled record deletion task will not run as it has been not enabled via configuration. To enable, set the configuration value at 'Umbraco:Forms:Options:ScheduledRecordDeletion:Enabled' to true.",
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Mapped Umbraco.Cloud.Deployment.SiteExtension.Messages.External.Git.ApplyChangesFromWwwRootToRepositoryCommand -> "siteext-input-queue"',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Bus "Rebus 1" started',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Acquiring MainDom.',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Acquired MainDom.',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Profiler is VoidProfiler, not profiling (must run debug mode to profile).',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
"Found single permission letter for '{LegacyPermission}' on user group '{UserGroupAlias}', assuming this is the 'Notifications' permission instead of the Deploy 'Queue for transfer' permission",
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Started :: Running {edition} edition',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
"File system watcher for deploy events started with filter 'deploy*' and notify filter 'FileName'.",
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Application started. Press Ctrl+C to shut down.',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Hosting environment: {envName}',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Content root path: {contentRoot}',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFRecordDataLongString as a primary key already exists.',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'No last synced Id found, this generally means this is a new server/install. A cold boot will be triggered.',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Deploy permissions updated and saved',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Starting :: Running on Umbraco Cloud',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Creating the content store, localContentDbExists? {LocalContentDbExists}',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Creating the media store, localMediaDbExists? {LocalMediaDbExists}',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Stopping ({SignalSource})',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Released ({SignalSource})',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Application is shutting down...',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Bus "Rebus 1" stopped',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Queued Hosted Service is stopping.',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Starting unattended install.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Unattended install completed.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Configured with Azure database.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Initialized the SqlServer database schema.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFRecordDataBit as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFRecordDataDateTime as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFRecordDataInteger as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFUserFormSecurity as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFUserGroupSecurity as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database migration step not completed: could not create primary key constraint on UFUserGroupFormSecurity as a primary key already exists.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate:
|
||||
'Database NuCache was serialized using {CurrentSerializer}. Currently configured NuCache serializer {Serializer}. Rebuilding Nucache',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Starting :: Running locally',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Started :: Transitioned from azure init marker to deploy marker',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Found {diskReadTrigger} or {diskReadOnStartTrigger} trigger file when starting, processing...',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Beginning deployment {id}.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Suspend scheduled publishing.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Preparing',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Reading state',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'No artifacts',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Restore caches and indexes',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Resume scheduled publishing.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Complete',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Deployment {id} completed.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Work Status {WorkStatus}.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Released from MainDom',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: "Keep alive failed (at '{keepAlivePingUrl}').",
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Adding examine event handlers for {RegisteredIndexers} index providers.',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
messageTemplate: 'Document {ContentName} (id={ContentId}) has been published.',
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const logLevels = {
|
||||
total: 2,
|
||||
items: [
|
||||
{
|
||||
name: 'Global',
|
||||
level: 'Information',
|
||||
},
|
||||
{
|
||||
name: 'UmbracoFile',
|
||||
level: 'Verbose',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const umbLogviewerData = {
|
||||
searches: new UmbLogviewerSearchesData(savedSearches),
|
||||
templates: new UmbLogviewerTemplatesData(messageTemplates),
|
||||
logs: new UmbLogviewerMessagesData(logs),
|
||||
logLevels: logLevels,
|
||||
};
|
||||
7350
src/Umbraco.Web.UI.Client/src/core/mocks/data/logs.data.ts
Normal file
7350
src/Umbraco.Web.UI.Client/src/core/mocks/data/logs.data.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import { rest } from 'msw';
|
||||
import { umbLogviewerData } from '../data/log-viewer.data';
|
||||
import { umbracoPath } from '@umbraco-cms/utils';
|
||||
import { SavedLogSearchModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
export const handlers = [
|
||||
//#region Searches
|
||||
rest.get(umbracoPath('/log-viewer/saved-search'), (req, res, ctx) => {
|
||||
const skip = req.url.searchParams.get('skip');
|
||||
const skipNumber = skip ? Number.parseInt(skip) : undefined;
|
||||
const take = req.url.searchParams.get('take');
|
||||
const takeNumber = take ? Number.parseInt(take) : undefined;
|
||||
|
||||
const items = umbLogviewerData.searches.getSavedSearches(skipNumber, takeNumber);
|
||||
|
||||
const response = {
|
||||
total: items.length,
|
||||
items,
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
|
||||
rest.get(umbracoPath('/log-viewer/saved-search/:name'), (req, res, ctx) => {
|
||||
const name = req.params.key as string;
|
||||
|
||||
if (!name) return;
|
||||
|
||||
const item = umbLogviewerData.searches.getByName(name);
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(item));
|
||||
}),
|
||||
|
||||
rest.post<SavedLogSearchModel>(umbracoPath('/log-viewer/saved-search'), async (req, res, ctx) => {
|
||||
return res(ctx.delay(), ctx.status(200));
|
||||
}),
|
||||
|
||||
rest.delete(umbracoPath('/log-viewer/saved-search/:name'), async (req, res, ctx) => {
|
||||
return res(ctx.status(200));
|
||||
}),
|
||||
//#endregion
|
||||
|
||||
//#region Temaplates
|
||||
rest.get(umbracoPath('/log-viewer/message-template'), (req, res, ctx) => {
|
||||
const skip = req.url.searchParams.get('skip');
|
||||
const skipNumber = skip ? Number.parseInt(skip) : undefined;
|
||||
const take = req.url.searchParams.get('take');
|
||||
const takeNumber = take ? Number.parseInt(take) : undefined;
|
||||
|
||||
const items = umbLogviewerData.templates.getTemplates(skipNumber, takeNumber);
|
||||
|
||||
const response = {
|
||||
total: umbLogviewerData.templates.total,
|
||||
items,
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
//#endregion
|
||||
//#region Logs
|
||||
rest.get(umbracoPath('/log-viewer/level'), (req, res, ctx) => {
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(umbLogviewerData.logLevels));
|
||||
}),
|
||||
|
||||
rest.get(umbracoPath('/log-viewer/level-count'), (req, res, ctx) => {
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(umbLogviewerData.logs.getLevelCount()));
|
||||
}),
|
||||
|
||||
rest.get(umbracoPath('/log-viewer/log'), (req, res, ctx) => {
|
||||
const skip = req.url.searchParams.get('skip');
|
||||
const skipNumber = skip ? Number.parseInt(skip) : undefined;
|
||||
const take = req.url.searchParams.get('take');
|
||||
const takeNumber = take ? Number.parseInt(take) : undefined;
|
||||
|
||||
const items = umbLogviewerData.logs.getLogs(skipNumber, takeNumber);
|
||||
const response = {
|
||||
total: umbLogviewerData.logs.total,
|
||||
items,
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
];
|
||||
Reference in New Issue
Block a user