diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index 389adef695..d3cab9bf9d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -2398,6 +2398,7 @@ export default { level: 'Type', machine: 'Maskine', message: 'Besked', + messagesCount: 'logbeskeder', searchWithGoogle: 'Søg med Google', searchThisMessageWithGoogle: 'Søg efter denne besked på Google', searchWithBing: 'Søg med Bing', @@ -2441,6 +2442,7 @@ export default { totalUniqueMessageTypes: 'Samlet antal unikke beskedtyper: %0%', logTypes: 'Log typer', logTypesChartDescription: 'I det valgte datointerval har du dette antal logbeskeder af typen:', + viewLogsLabel: 'Vis %0% logs', }, clipboard: { labelForCopyAllEntries: 'Kopier %0%', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 146cac05ed..fa65c69959 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2469,6 +2469,7 @@ export default { level: 'Level', machine: 'Machine', message: 'Message', + messagesCount: 'log messages', searchWithGoogle: 'Search With Google', searchThisMessageWithGoogle: 'Search this message with Google', searchWithBing: 'Search With Bing', @@ -2511,7 +2512,8 @@ export default { commonLogMessages: 'Common Log Messages', totalUniqueMessageTypes: 'Total Unique Message types: %0%', logTypes: 'Log types', - logTypesChartDescription: 'In chosen date range you have this number of log message of type:', + logTypesChartDescription: 'In the chosen date range, you have this number of log messages grouped by type:', + viewLogsLabel: 'View %0% logs', }, clipboard: { labelForCopyAllEntries: 'Copy %0%', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts index 96872fa203..c91390b6b8 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts @@ -43,15 +43,23 @@ class UmbLogViewerMessagesData extends UmbMockDBBase { return this.data.slice(skip, take); } - getLevelCount() { - const levels = this.data.map((log) => log.level?.toLowerCase() ?? '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; + getLevelCount(): Record { + const levels = this.data.reduce( + (counts, log) => { + const level = log.level?.toLocaleLowerCase() ?? 'unknown'; + counts[level] = (counts[level] || 0) + 1; + return counts; + }, + {} as Record, + ); + + // Test 1k logs for the first level + levels[Object.keys(levels)[0]] += 1000; + + // Test 1m logs for the second level + levels[Object.keys(levels)[1]] += 1000000; + + return levels; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/util/anchor.ts b/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/util/anchor.ts index b0fff6303c..267ff0283d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/util/anchor.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/util/anchor.ts @@ -1,5 +1,5 @@ /** - * Hook up a click listener to the window that, for all anchor tags + * Hook up a click listener to the window that, for all anchor tags (HTML or SVG) * that has a relative HREF, uses the history API instead. */ export function ensureAnchorHistory() { @@ -10,37 +10,50 @@ export function ensureAnchorHistory() { if ((isWindows && e.ctrlKey) || (!isWindows && e.metaKey)) return; // Find the target by using the composed path to get the element through the shadow boundaries. + // Support both HTML anchor tags and SVG anchor tags const $anchor = (('composedPath' in e) as any) - ? e.composedPath().find(($elem) => $elem instanceof HTMLAnchorElement) + ? e.composedPath().find(($elem) => $elem instanceof HTMLAnchorElement || $elem instanceof SVGAElement) : e.target; - // Abort if the event is not about the anchor tag - if ($anchor == null || !($anchor instanceof HTMLAnchorElement)) { + // Abort if the event is not about an anchor tag (HTML or SVG) + if ($anchor == null || !($anchor instanceof HTMLAnchorElement || $anchor instanceof SVGAElement)) { return; } // Get the HREF value from the anchor tag - const href = $anchor.href; + // SVGAElement.href returns SVGAnimatedString, so we need to access .baseVal + const href = $anchor instanceof SVGAElement ? $anchor.href.baseVal : $anchor.href; + const target = $anchor instanceof SVGAElement ? $anchor.target.baseVal : $anchor.target; + + // For SVG anchors, we need to construct a full URL to extract pathname, search, and hash + // For HTML anchors, these properties are directly available + let fullUrl: URL; + try { + // Use the current document base URI as the base to resolve relative URLs + // This respects the tag and works the same as HTML anchors + // Note: This may resolve into an external URL, but we validate that later + fullUrl = new URL(href, document.baseURI); + } catch { + // Invalid URL, skip + return; + } // Only handle the anchor tag if the follow holds true: // - The HREF is relative to the origin of the current location. // - The target is targeting the current frame. // - The anchor doesn't have the attribute [data-router-slot]="disabled" if ( - !href.startsWith(location.origin) || - ($anchor.target !== '' && $anchor.target !== '_self') || + fullUrl.origin !== location.origin || + (target !== '' && target !== '_self') || $anchor.dataset['routerSlot'] === 'disabled' ) { return; } - // Remove the origin from the start of the HREF to get the path - const path = $anchor.pathname + $anchor.search + $anchor.hash; - // Prevent the default behavior e.preventDefault(); // Change the history! - history.pushState(null, '', path); + history.pushState(null, '', fullUrl); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-chart.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-chart.element.ts index 095b60c80e..839e06faea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-chart.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-chart.element.ts @@ -3,7 +3,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, - LitElement, svg, customElement, property, @@ -12,13 +11,16 @@ import { state, } from '@umbraco-cms/backoffice/external/lit'; import { clamp } from '@umbraco-cms/backoffice/utils'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; interface Circle { color: string; name: string; percent: number; + visualPercent: number; kind: string; number: number; + href: string; } interface CircleWithCommands extends Circle { @@ -32,11 +34,17 @@ interface CircleWithCommands extends Circle { * @augments {LitElement} */ @customElement('umb-donut-chart') -export class UmbDonutChartElement extends LitElement { +export class UmbDonutChartElement extends UmbLitElement { static percentToDegrees(percent: number): number { return percent * 3.6; } + /** + * Minimum visual percentage for rendering a slice. + * Slices below this percentage will be visually expanded to this size to remain visible. + */ + static MIN_SLICE_PERCENT = 5; + /** * Circle radius in pixels * @memberof UmbDonutChartElement @@ -72,6 +80,13 @@ export class UmbDonutChartElement extends LitElement { @property({ type: Boolean }) hideDetailBox = false; + /** + * Shows the description text below the chart + * @memberof UmbDonutChartElement + */ + @property({ type: Boolean, attribute: 'show-description' }) + showDescription = false; + @queryAssignedElements({ selector: 'umb-donut-slice' }) private _slices!: UmbDonutSliceElement[]; @@ -97,7 +112,7 @@ export class UmbDonutChartElement extends LitElement { private _detailName = ''; @state() - private _detailAmount = 0; + private _detailAmount = '0'; @state() private _detailColor = 'black'; @@ -126,24 +141,49 @@ export class UmbDonutChartElement extends LitElement { #calculatePercentage(partialValue: number) { if (this._totalAmount === 0) return 0; - const percent = Math.round((100 * partialValue) / this._totalAmount); + const percent = (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), - number: slice.amount, - color: slice.color, - name: slice.name, - kind: slice.kind, - }; - }), - ); + + // First pass: calculate actual percentages + const circles = this._slices.map((slice) => { + const percent = this.#calculatePercentage(slice.amount); + return { + percent, + visualPercent: percent, + number: slice.amount, + color: slice.color, + name: slice.name, + kind: slice.kind, + href: slice.href, + }; + }); + + // Second pass: apply minimum visual percentage and normalize to 100% + const totalActualPercent = circles.reduce((acc, c) => acc + c.percent, 0); + if (totalActualPercent > 0) { + const smallSlices = circles.filter((c) => c.percent > 0 && c.percent < UmbDonutChartElement.MIN_SLICE_PERCENT); + + // Expand small slices to minimum + smallSlices.forEach((c) => { + c.visualPercent = UmbDonutChartElement.MIN_SLICE_PERCENT; + }); + + // Calculate total and normalize to 100% + const totalVisualPercent = circles.reduce((acc, c) => acc + c.visualPercent, 0); + if (totalVisualPercent > 0 && totalVisualPercent !== 100) { + const scale = 100 / totalVisualPercent; + circles.forEach((c) => { + c.visualPercent = c.visualPercent * scale; + }); + } + } + + this._circles = this.#addCommands(circles); } #addCommands(Circles: Circle[]): CircleWithCommands[] { @@ -154,13 +194,13 @@ export class UmbDonutChartElement extends LitElement { commands: this.#getSliceCommands(slice, this.radius, this.svgSize, this.borderSize), offset: previousPercent * 3.6 * -1, }; - previousPercent += slice.percent; + previousPercent += slice.visualPercent; return sliceWithCommands; }); } #getSliceCommands(Circle: Circle, radius: number, svgSize: number, borderSize: number): string { - const degrees = UmbDonutChartElement.percentToDegrees(Circle.percent); + const degrees = UmbDonutChartElement.percentToDegrees(Circle.visualPercent); const longPathFlag = degrees > 180 ? 1 : 0; const innerRadius = radius - borderSize; @@ -181,10 +221,12 @@ export class UmbDonutChartElement extends LitElement { } #calculateDetailsBoxPosition = (event: MouseEvent) => { + // Recalculate bounds on each mouse move to handle window resize + this.#containerBounds = this._container.getBoundingClientRect(); 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; + this._posX = x + 10; + this._posY = y + 10; }; #setDetailsBoxData(event: MouseEvent) { @@ -192,7 +234,7 @@ export class UmbDonutChartElement extends LitElement { const index = target.dataset.index as unknown as number; const circle = this._circles[index]; this._detailName = circle.name; - this._detailAmount = circle.number; + this._detailAmount = this.localize.number(circle.number); this._detailColor = circle.color; this._detailKind = circle.kind; } @@ -231,11 +273,10 @@ export class UmbDonutChartElement extends LitElement { ${this.description} - ${this._circles.map( - (circle, i) => svg` + ${this._circles.map((circle, i) => { + const content = svg` - `, - )} + `; + + return circle.href + ? svg`${content}` + : content; + })} `; } override render() { return html`
- ${this.#renderCircles()} + + ${this.#renderCircles()} +
@@ -267,6 +314,7 @@ export class UmbDonutChartElement extends LitElement { ${this._detailAmount} ${this._detailKind}
+ ${this.showDescription && this.description ? html`

${this.description}

` : ''} `; } @@ -292,7 +340,9 @@ export class UmbDonutChartElement extends LitElement { #container { position: relative; - width: 200px; + width: 100%; + max-width: 200px; + aspect-ratio: 1; } #details-box { @@ -311,6 +361,7 @@ export class UmbDonutChartElement extends LitElement { 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; + pointer-events: none; } #details-box.show { @@ -328,6 +379,17 @@ export class UmbDonutChartElement extends LitElement { display: flex; align-items: center; } + + .slice-number { + user-select: none; + } + + .description { + text-align: center; + font-size: var(--uui-type-small-size); + color: var(--uui-color-text-alt); + margin: var(--uui-size-space-2) 0 0 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-slice.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-slice.element.ts index 36a2de1f8b..960ba07753 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-slice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/donut-chart/donut-slice.element.ts @@ -32,6 +32,13 @@ export class UmbDonutSliceElement extends LitElement { @property() kind = ''; + /** + * Optional href to make the slice clickable + * @memberof UmbDonutSliceElement + */ + @property() + href = ''; + override willUpdate() { this.dispatchEvent(new CustomEvent('slice-update', { composed: true, bubbles: true })); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts index 5f8d15c119..9420c86681 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts @@ -2,3 +2,4 @@ export * from './constants.js'; export * from './repository/index.js'; export * from './components/donut-chart/donut-chart.element.js'; export * from './components/donut-chart/donut-slice.element.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/index.ts index 347bd229d3..467436273e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/index.ts @@ -1,6 +1,6 @@ +import type { UmbLogLevelCounts } from '../../types.js'; import type { DirectionModel, - LogLevelCountsReponseModel, LogLevelModel, PagedLoggerResponseModel, PagedLogMessageResponseModel, @@ -37,7 +37,7 @@ export interface UmbLogMessagesDataSource { }: { startDate?: string; endDate?: string; - }): Promise>; + }): Promise>; getLogViewerLogs({ skip, take, diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/log-viewer.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/log-viewer.server.data.ts index 27dfae37b8..a5b3dfe9a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/log-viewer.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/log-viewer.server.data.ts @@ -1,12 +1,14 @@ import type { UmbLogMessagesDataSource, UmbLogSearchDataSource } from './index.js'; -import type { - DirectionModel, +import { LogLevelModel, - SavedLogSearchResponseModel, + type DirectionModel, + type SavedLogSearchResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { LogViewerService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; +import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; +import type { UmbLogLevelCounts } from '../../types.js'; /** * A data source for the log saved searches @@ -87,7 +89,7 @@ export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource * @memberof UmbLogMessagesServerDataSource */ async getLogViewerLevel({ skip = 0, take = 100 }: { skip?: number; take?: number }) { - return await tryExecute(this.#host, LogViewerService.getLogViewerLevel({ query: { skip, take } })); + return tryExecute(this.#host, LogViewerService.getLogViewerLevel({ query: { skip, take } })); } /** @@ -96,13 +98,45 @@ export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource * @returns {*} * @memberof UmbLogMessagesServerDataSource */ - async getLogViewerLevelCount({ startDate, endDate }: { startDate?: string; endDate?: string }) { - return await tryExecute( + async getLogViewerLevelCount({ + startDate, + endDate, + }: { + startDate?: string; + endDate?: string; + }): Promise> { + const data = await tryExecute( this.#host, LogViewerService.getLogViewerLevelCount({ query: { startDate, endDate }, }), ); + + if (data?.data) { + const normalizedData: UmbLogLevelCounts = { + [LogLevelModel.VERBOSE]: 0, + [LogLevelModel.DEBUG]: 0, + [LogLevelModel.INFORMATION]: 0, + [LogLevelModel.WARNING]: 0, + [LogLevelModel.ERROR]: 0, + [LogLevelModel.FATAL]: 0, + }; + + // Helper to normalize log level keys to PascalCase + const normalizeLogLevel = (level: string): LogLevelModel => { + const normalized = level.charAt(0).toUpperCase() + level.slice(1).toLowerCase(); + return normalized as LogLevelModel; + }; + + // Normalize keys to match LogLevelModel + for (const [level, count] of Object.entries(data.data)) { + normalizedData[normalizeLogLevel(level)] = count; + } + + return { data: normalizedData }; + } + + return {}; } /** * Grabs all the log messages from the server @@ -143,7 +177,7 @@ export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource startDate?: string; endDate?: string; }) { - return await tryExecute( + return tryExecute( this.#host, LogViewerService.getLogViewerLog({ query: { @@ -185,7 +219,7 @@ export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource startDate?: string; endDate?: string; }) { - return await tryExecute( + return tryExecute( this.#host, LogViewerService.getLogViewerMessageTemplate({ query: { skip, take, startDate, endDate }, @@ -194,7 +228,7 @@ export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource } async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) { - return await tryExecute( + return tryExecute( this.#host, LogViewerService.getLogViewerValidateLogsSize({ query: { startDate, endDate }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/types.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/types.ts new file mode 100644 index 0000000000..3a2ca64368 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/types.ts @@ -0,0 +1,5 @@ +import type { LogLevelModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export type UmbLogLevelCounts = { + [level in LogLevelModel]: number; +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts index 2b3c358849..3811877e6d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts @@ -1,8 +1,8 @@ import { UmbLogViewerRepository } from '../repository/log-viewer.repository.js'; +import type { UmbLogLevelCounts } from '../types.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from './logviewer-workspace.context-token.js'; import { UmbBasicState, UmbArrayState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import type { - LogLevelCountsReponseModel, PagedLoggerResponseModel, PagedLogMessageResponseModel, PagedLogTemplateResponseModel, @@ -38,10 +38,6 @@ export class UmbLogViewerWorkspaceContext extends UmbContextBase implements UmbW return 'log-viewer'; } - getEntityName() { - return 'Log Viewer'; - } - get today() { const today = new Date(); const dd = String(today.getDate()).padStart(2, '0'); @@ -68,7 +64,7 @@ export class UmbLogViewerWorkspaceContext extends UmbContextBase implements UmbW #savedSearches = new UmbObjectState(undefined); savedSearches = this.#savedSearches.asObservablePart((data) => data); - #logCount = new UmbObjectState(null); + #logCount = new UmbObjectState(null); logCount = this.#logCount.asObservable(); #dateRange = new UmbObjectState(this.defaultDateRange); @@ -236,10 +232,7 @@ export class UmbLogViewerWorkspaceContext extends UmbContextBase implements UmbW async getLogCount() { const { data } = await this.#repository.getLogCount({ ...this.#dateRange.getValue() }); - - if (data) { - this.#logCount.setValue(data); - } + this.#logCount.setValue(data ?? null); } async getMessageTemplates(skip: number, take: number) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts index 9a5dd12b39..bcefddf7a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts @@ -1,7 +1,7 @@ +import type { UmbLogLevelCounts } from '../../../../../log-viewer/types.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { LogLevelCountsReponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-log-types-chart') @@ -19,7 +19,10 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { } @state() - private _logLevelCountResponse: LogLevelCountsReponseModel | null = null; + private _dateRange = { startDate: '', endDate: '' }; + + @state() + private _logLevelCounts: UmbLogLevelCounts | null = null; @state() private _logLevelCount: [string, number][] = []; @@ -27,8 +30,11 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { @state() private _logLevelCountFilter: string[] = []; + @state() + private _logLevelKeys: [string, number][] = []; + protected override willUpdate(_changedProperties: Map): void { - if (_changedProperties.has('_logLevelCountFilter')) { + if (_changedProperties.has('_logLevelCountFilter') || _changedProperties.has('_logLevelCounts')) { this.setLogLevelCount(); } } @@ -43,56 +49,91 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { } setLogLevelCount() { - this._logLevelCount = this._logLevelCountResponse - ? Object.entries(this._logLevelCountResponse).filter(([level]) => !this._logLevelCountFilter.includes(level)) - : []; + if (this._logLevelCounts) { + const nonZeroEntries = Object.entries(this._logLevelCounts).filter(([, count]) => count > 0); + this._logLevelKeys = nonZeroEntries; + this._logLevelCount = nonZeroEntries.filter(([level]) => !this._logLevelCountFilter.includes(level)); + } else { + this._logLevelKeys = []; + this._logLevelCount = []; + } } #observeStuff() { this.observe(this._logViewerContext?.logCount, (logLevel) => { - this._logLevelCountResponse = logLevel ?? null; + this._logLevelCounts = logLevel ?? null; this.setLogLevelCount(); }); + + this.observe(this._logViewerContext?.dateRange, (dateRange) => { + if (dateRange) { + this._dateRange = dateRange; + } + }); + } + + #buildSearchUrl(level: string): string { + const params = new URLSearchParams(); + params.set('loglevels', level); + if (this._dateRange.startDate) { + params.set('startDate', this._dateRange.startDate); + } + if (this._dateRange.endDate) { + params.set('endDate', this._dateRange.endDate); + } + return `section/settings/workspace/logviewer/view/search/?${params.toString()}`; } - // TODO: Stop using this complex code in render methods, instead changes to _logLevelCount should trigger a state prop containing the keys. And then try to make use of the repeat LIT method: override render() { return html` +

+ + In the chosen date range, you have this number of log messages grouped by type: + +

+ + ${repeat( + this._logLevelCount, + ([level]) => level, + ([level, number]) => + html``, + )} +
    - ${this._logLevelCountResponse - ? Object.keys(this._logLevelCountResponse).map( - (level) => - html`
  • - -
  • `, - ) - : ''} + ${repeat( + this._logLevelKeys, + ([level]) => level, + ([level, count]) => + html`
  • + +
  • `, + )}
- - ${this._logLevelCountResponse - ? this._logLevelCount.map( - ([level, number]) => - html` `, - ) - : ''} -
`; @@ -100,12 +141,49 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { static override styles = [ css` + uui-box { + container-type: inline-size; + } + + #description { + text-align: center; + font-size: var(--uui-type-small-size); + color: var(--uui-color-text-alt); + margin: 0 0 var(--uui-size-space-4) 0; + } + #log-types-container { - display: flex; + display: grid; gap: var(--uui-size-space-4); - flex-direction: column-reverse; - align-items: center; - justify-content: space-between; + grid-template-columns: 1fr; + place-items: center; + } + + umb-donut-chart { + width: 100%; + max-width: 200px; + } + + #legend { + width: 100%; + display: flex; + justify-content: center; + } + + @container (min-width: 312px) { + #log-types-container { + grid-template-columns: auto 1fr; + place-items: start; + } + + umb-donut-chart { + max-width: 200px; + } + + #legend { + width: auto; + justify-content: flex-start; + } } button { @@ -158,6 +236,11 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { li uui-icon { margin-right: 1em; } + + .count { + margin-left: 0.3em; + color: var(--uui-color-text-alt); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts index 83dfeda1d1..0c74541853 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts @@ -29,7 +29,7 @@ export class UmbLogViewerOverviewViewElement extends UmbLitElement { #observeErrorCount() { this.observe(this._logViewerContext?.logCount, (logLevelCount) => { - this._errorCount = logLevelCount?.error; + this._errorCount = logLevelCount?.Error; }); }