Files
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/log-viewer/repository/sources/log-viewer.server.data.ts
Jacob Overgaard ce98184178 Log Viewer: Enhances the donut chart to be responsive, link to log search, and show numbers directly (#20928)
* Log Viewer: Refactor log types chart to use Lit repeat directive

- Import and use repeat directive for better performance
- Add _logLevelKeys state property to track log level keys
- Update setLogLevelCount() to populate _logLevelKeys
- Replace .map() with repeat() in render method for legend and donut slices
- Update willUpdate to observe both filter and response changes
- Resolves TODO comment about using repeat directive

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Donut Chart: Add inline numbers and fix tooltip positioning

- Add showInlineNumbers property to optionally display numbers inside slices
- Implement #getTextPosition() method to calculate text position at slice center
- Render SVG text elements when showInlineNumbers is enabled
- Fix tooltip positioning to appear near cursor (changed from x-10, y-70 to x+10, y+10)
- Recalculate container bounds on each mouse move to handle window resize
- Add pointer-events: none to tooltip to prevent mouse interference
- Add CSS styling for slice numbers (user-select: none)
- Enable inline numbers by default in log viewer log types chart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Donut Chart: Add clickable slices and visible description

- Add href property to donut-slice element for clickable slices
- Wrap SVG paths in <a> tags when href is provided
- Update Circle interface to include href property
- Add showDescription property to optionally display description text
- Render description as visible text below the chart
- Add CSS styling for description text
- Update log-types-chart to build search URLs with log level and date range
- Observe dateRange from context to build accurate search URLs
- Enable clickable slices and visible description in log-types-chart

Now clicking on a donut slice navigates to the search view filtered by that log level and the current date range.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: uses whole link

* Log Viewer: Fix log types chart layout for larger screens

Add media query to switch from column to row layout on screens wider than 768px. This ensures the legend and donut chart are displayed side by side on desktop resolutions instead of stacked vertically.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: improves mock function

* chore: formatting

* fix: ensures the donut chart works responsively

* feat: adds support for SVGAElement in the router

* adds key for description

* chore: adds test data

* feat: displays numbers in the legend instead of the chart

* chore: restores functionality with lower-cased keys

* fix: adds translation to 'log messages'

* chore: removes unused method

* feat: ensures that the log levels follow the generated LogLevelModel enum from the server, which requires to map the keys as JSON camelCase's the keys

* fix: uses correct property

* fix: reverts back to the original behavior to calculate a relative URL (rather than the automatic .toString() that gets a qualified URL)

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: uses fullUrl for router

* fix: properly translates new aria-label

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 09:28:44 +00:00

239 lines
6.5 KiB
TypeScript

import type { UmbLogMessagesDataSource, UmbLogSearchDataSource } from './index.js';
import {
LogLevelModel,
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
* @class UmbLogSearchesServerDataSource
* @implements {TemplateDetailDataSource}
*/
export class UmbLogSearchesServerDataSource implements UmbLogSearchDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbLogSearchesServerDataSource.
* @param {UmbControllerHost} host - The controller host for this controller to be appended to
* @memberof UmbLogSearchesServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Grabs all the log viewer saved searches from the server
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
* @returns {*}
* @memberof UmbLogSearchesServerDataSource
*/
async getAllSavedSearches({ skip = 0, take = 100 }: { skip?: number; take?: number }) {
return await tryExecute(this.#host, LogViewerService.getLogViewerSavedSearch({ query: { skip, take } }));
}
/**
* Get a log viewer saved search by name from the server
* @param {{ name: string }} { name }
* @returns {*}
* @memberof UmbLogSearchesServerDataSource
*/
async getSavedSearchByName({ name }: { name: string }) {
return await tryExecute(this.#host, LogViewerService.getLogViewerSavedSearchByName({ path: { name } }));
}
/**
* Post a new log viewer saved search to the server
* @param {{ body?: SavedLogSearch }} { body }
* @returns {*}
* @memberof UmbLogSearchesServerDataSource
*/
async postLogViewerSavedSearch({ name, query }: SavedLogSearchResponseModel) {
return await tryExecute(this.#host, LogViewerService.postLogViewerSavedSearch({ body: { name, query } }));
}
/**
* Remove a log viewer saved search by name from the server
* @param {{ name: string }} { name }
* @returns {*}
* @memberof UmbLogSearchesServerDataSource
*/
async deleteSavedSearchByName({ name }: { name: string }) {
return await tryExecute(this.#host, LogViewerService.deleteLogViewerSavedSearchByName({ path: { name } }));
}
}
/**
* A data source for the log messages and levels
* @class UmbLogMessagesServerDataSource
* @implements {UmbLogMessagesDataSource}
*/
export class UmbLogMessagesServerDataSource implements UmbLogMessagesDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbLogMessagesServerDataSource.
* @param {UmbControllerHost} host - The controller host for this controller to be appended to
* @memberof UmbLogMessagesServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Grabs all the loggers from the server
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
* @returns {*}
* @memberof UmbLogMessagesServerDataSource
*/
async getLogViewerLevel({ skip = 0, take = 100 }: { skip?: number; take?: number }) {
return tryExecute(this.#host, LogViewerService.getLogViewerLevel({ query: { skip, take } }));
}
/**
* Grabs all the number of different log messages from the server
* @param {{ skip?: number; take?: number }} { skip = 0, take = 100 }
* @returns {*}
* @memberof UmbLogMessagesServerDataSource
*/
async getLogViewerLevelCount({
startDate,
endDate,
}: {
startDate?: string;
endDate?: string;
}): Promise<UmbDataSourceResponse<UmbLogLevelCounts>> {
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
* @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,
* }
* @returns {*}
* @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 tryExecute(
this.#host,
LogViewerService.getLogViewerLog({
query: {
skip,
take,
orderDirection,
filterExpression,
logLevel: logLevel?.length ? logLevel : undefined,
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,
* }
* @returns {*}
* @memberof UmbLogMessagesServerDataSource
*/
async getLogViewerMessageTemplate({
skip,
take = 100,
startDate,
endDate,
}: {
skip?: number;
take?: number;
startDate?: string;
endDate?: string;
}) {
return tryExecute(
this.#host,
LogViewerService.getLogViewerMessageTemplate({
query: { skip, take, startDate, endDate },
}),
);
}
async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) {
return tryExecute(
this.#host,
LogViewerService.getLogViewerValidateLogsSize({
query: { startDate, endDate },
}),
);
}
}