diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/health-check.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/health-check.models.ts
new file mode 100644
index 0000000000..110418e215
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/health-check.models.ts
@@ -0,0 +1,17 @@
+import type { ManifestElement } from './models';
+
+export interface ManifestHealthCheck extends ManifestElement {
+ type: 'healthCheck';
+ meta: MetaHealthCheck;
+}
+
+export interface MetaHealthCheck {
+ label: string;
+ api: any;
+}
+
+export interface HealthCheck {
+ alias: string;
+ name: string;
+ description: string;
+}
diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts
index 572ce8e8ab..d8762da1e0 100644
--- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts
+++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts
@@ -16,6 +16,7 @@ import type { ManifestPackageView } from './package-view.models';
import type { ManifestExternalLoginProvider } from './external-login-provider.models';
import type { ManifestCollectionBulkAction } from './collection-bulk-action.models';
import type { ManifestCollectionView } from './collection-view.models';
+import type { ManifestHealthCheck } from './health-check.models';
import type { ManifestSidebarMenuItem } from './sidebar-menu-item.models';
export * from './header-app.models';
@@ -36,6 +37,7 @@ export * from './package-view.models';
export * from './external-login-provider.models';
export * from './collection-bulk-action.models';
export * from './collection-view.models';
+export * from './health-check.models';
export * from './sidebar-menu-item.models';
export type ManifestTypes =
@@ -60,6 +62,7 @@ export type ManifestTypes =
| ManifestEntrypoint
| ManifestCollectionBulkAction
| ManifestCollectionView
+ | ManifestHealthCheck
| ManifestSidebarMenuItem;
export type ManifestStandardTypes = ManifestTypes['type'];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts
index 387d22bdf6..595da98b54 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts
@@ -1,17 +1,330 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
-import { css, html, LitElement } from 'lit';
-import { customElement } from 'lit/decorators.js';
+import { css, html, nothing } from 'lit';
+import { customElement, state, query, property } from 'lit/decorators.js';
+import { UUIButtonState, UUIPaginationElement, UUIPaginationEvent } from '@umbraco-ui/uui';
+import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal';
+import { UmbLitElement } from '@umbraco-cms/element';
+import { RedirectManagementResource, RedirectStatus, RedirectUrl } from '@umbraco-cms/backend-api';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
@customElement('umb-dashboard-redirect-management')
-export class UmbDashboardRedirectManagementElement extends LitElement {
- static styles = [UUITextStyles, css``];
+export class UmbDashboardRedirectManagementElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ .actions {
+ display: flex;
+ gap: 4px;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ }
+
+ .actions uui-icon {
+ transform: translateX(50%);
+ }
+
+ uui-table {
+ table-layout: fixed;
+ }
+
+ uui-table-head-cell:nth-child(2*n) {
+ width: 10%;
+ }
+
+ uui-table-head-cell:last-child,
+ uui-table-cell:last-child {
+ text-align: right;
+ }
+
+ uui-table uui-icon {
+ vertical-align: sub;
+ }
+ uui-pagination {
+ display: inline-block;
+ }
+ .pagination {
+ display: flex;
+ justify-content: center;
+ margin-top: var(--uui-size-space-5);
+ }
+
+ .trackerDisabled {
+ position: relative;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+ .trackerDisabled::after {
+ content: '';
+ background-color: rgba(250, 250, 250, 0.7);
+ position: absolute;
+ border-radius: 2px;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ a {
+ color: var(--uui-color-interactive);
+ }
+ a:hover,
+ a:focus {
+ color: var(--uui-color-interactive-emphasis);
+ }
+ `,
+ ];
+
+ @property({ type: Number, attribute: 'items-per-page' })
+ itemsPerPage = 20;
+
+ @state()
+ private _redirectData?: RedirectUrl[];
+
+ @state()
+ private _trackerStatus = true;
+
+ @state()
+ private _currentPage = 1;
+
+ @state()
+ private _total?: number;
+
+ @state()
+ private _buttonState: UUIButtonState;
+
+ @state()
+ private _filter?: string;
+
+ @query('#search-input')
+ private _searchField!: HTMLInputElement;
+
+ @query('uui-pagination')
+ private _pagination?: UUIPaginationElement;
+
+ private _modalService?: UmbModalService;
+
+ constructor() {
+ super();
+ this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (_instance) => {
+ this._modalService = _instance;
+ });
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._getTrackerStatus();
+ this._getRedirectData();
+ }
+
+ private async _getTrackerStatus() {
+ const { data } = await tryExecuteAndNotify(this, RedirectManagementResource.getRedirectManagementStatus());
+ if (data && data.status) this._trackerStatus = data.status === RedirectStatus.ENABLED ? true : false;
+ }
+
+ private _removeRedirectHandler(data: RedirectUrl) {
+ const modalHandler = this._modalService?.confirm({
+ headline: 'Delete',
+ content: html`
+
+
This will remove the redirect
+ Original URL:
${data.originalUrl}
+ Redirected To:
${data.destinationUrl}
+
Are you sure you want to delete?
+
+ `,
+ color: 'danger',
+ confirmLabel: 'Delete',
+ });
+ modalHandler?.onClose().then(({ confirmed }) => {
+ if (confirmed) this._removeRedirect(data);
+ });
+ }
+
+ private async _removeRedirect(r: RedirectUrl) {
+ if (!r.key) return;
+ const res = await tryExecuteAndNotify(
+ this,
+ RedirectManagementResource.deleteRedirectManagementByKey({ key: r.key })
+ );
+ if (!res.error) {
+ // or just run a this._getRedirectData() again?
+ this.shadowRoot?.getElementById(`redirect-key-${r.key}`)?.remove();
+ }
+ }
+
+ private _disableRedirectHandler() {
+ const modalHandler = this._modalService?.confirm({
+ headline: 'Disable URL tracker',
+ content: html`Are you sure you want to disable the URL tracker?`,
+ color: 'danger',
+ confirmLabel: 'Disable',
+ });
+ modalHandler?.onClose().then(({ confirmed }) => {
+ if (confirmed) this._toggleRedirect();
+ });
+ }
+
+ private async _toggleRedirect() {
+ const { error } = await tryExecuteAndNotify(
+ this,
+ RedirectManagementResource.postRedirectManagementStatus({ status: RedirectStatus.ENABLED })
+ );
+
+ if (!error) {
+ this._trackerStatus = !this._trackerStatus;
+ }
+ }
+
+ private _inputHandler(pressed: KeyboardEvent) {
+ if (pressed.key === 'Enter') this._searchHandler();
+ }
+
+ private async _searchHandler() {
+ this._filter = this._searchField.value;
+ if (this._pagination) this._pagination.current = 1;
+ this._currentPage = 1;
+ if (this._filter.length) {
+ this._buttonState = 'waiting';
+ }
+ this._getRedirectData();
+ }
+
+ private _onPageChange(event: UUIPaginationEvent) {
+ if (this._currentPage === event.target.current) return;
+ this._currentPage = event.target.current;
+ this._getRedirectData();
+ }
+
+ private async _getRedirectData() {
+ const skip = this._currentPage * this.itemsPerPage - this.itemsPerPage;
+ const { data } = await tryExecuteAndNotify(
+ this,
+ RedirectManagementResource.getRedirectManagement({ filter: this._filter, take: this.itemsPerPage, skip })
+ );
+ if (data) {
+ this._total = data?.total;
+ this._redirectData = data?.items;
+ if (this._filter?.length) this._buttonState = 'success';
+ }
+ }
render() {
- return html`
-
- Redirect Management
+ return html`
+ ${this._trackerStatus
+ ? html`
+
+
+
+ Search
+
+
+
+ Disable URL tracker
+ `
+ : html`
+ Enable URL tracker
+ `}
+
+
+ ${this._total && this._total > 0
+ ? html`
+ ${this.renderTable()}
+
`
+ : this._filter?.length
+ ? this._renderZeroResults()
+ : this.renderNoRedirects()} `;
+ }
+
+ private _renderZeroResults() {
+ return html`
+ No redirects matching this search criteria
+ Double check your search for any error or spelling mistakes.
+ `;
+ }
+
+ private renderNoRedirects() {
+ return html`
+ No redirects have been made
+ When a published page gets renamed or moved, a redirect will automatically be made to the new page.
+ `;
+ }
+
+ private renderTable() {
+ return html`
+
+
+ Culture
+ Original URL
+
+ Redirected To
+ Actions
+
+ ${this._redirectData?.map((data) => {
+ return html`
+ ${data.culture || '*'}
+
+ ${data.originalUrl}
+
+
+
+
+
+
+ ${data.destinationUrl}
+
+
+
+
+ this._removeRedirectHandler(data)}">
+
+
+
+
+ `;
+ })}
+
- `;
+ ${this._renderPagination()}
+ `;
+ }
+
+ private _renderPagination() {
+ if (!this._total) return nothing;
+
+ const totalPages = Math.ceil(this._total / this.itemsPerPage);
+
+ if (totalPages <= 1) return nothing;
+
+ return html``;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts
new file mode 100644
index 0000000000..2d960a1757
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts
@@ -0,0 +1,20 @@
+import { expect, fixture, html } from '@open-wc/testing';
+
+import { UmbDashboardRedirectManagementElement } from './dashboard-redirect-management.element';
+import { defaultA11yConfig } from '@umbraco-cms/test-utils';
+
+describe('UmbDashboardRedirectManagement', () => {
+ let element: UmbDashboardRedirectManagementElement;
+
+ beforeEach(async () => {
+ element = await fixture(html``);
+ });
+
+ it('is defined with its own instance', () => {
+ expect(element).to.be.instanceOf(UmbDashboardRedirectManagementElement);
+ });
+
+ it('passes the a11y audit', async () => {
+ await expect(element).to.be.accessible(defaultA11yConfig);
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts
index 7fb262b327..91fb021085 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts
@@ -1,17 +1,82 @@
-import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
-import { css, html, LitElement } from 'lit';
-import { customElement } from 'lit/decorators.js';
+import { html } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { IRoute, IRoutingInfo } from 'router-slot';
+import { UmbDashboardHealthCheckGroupElement } from './views/health-check-group.element';
+import {
+ UmbHealthCheckDashboardContext,
+ UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN,
+} from './health-check-dashboard.context';
+import { UmbHealthCheckContext } from './health-check.context';
+import { UmbLitElement } from '@umbraco-cms/element';
+import { ManifestHealthCheck } from '@umbraco-cms/extensions-registry';
+import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+import { HealthCheckGroup, HealthCheckResource } from '@umbraco-cms/backend-api';
@customElement('umb-dashboard-health-check')
-export class UmbDashboardHealthCheckElement extends LitElement {
- static styles = [UUITextStyles, css``];
+export class UmbDashboardHealthCheckElement extends UmbLitElement {
+ @state()
+ private _routes: IRoute[] = [
+ {
+ path: `/:groupName`,
+ component: () => import('./views/health-check-group.element'),
+ setup: (component: HTMLElement, info: IRoutingInfo) => {
+ const element = component as UmbDashboardHealthCheckGroupElement;
+ element.groupName = decodeURI(info.match.params.groupName);
+ },
+ },
+ {
+ path: ``,
+ component: () => import('./views/health-check-overview.element'),
+ },
+ ];
+
+ private _healthCheckDashboardContext = new UmbHealthCheckDashboardContext(this);
+
+ constructor() {
+ super();
+ this.provideContext(UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN, this._healthCheckDashboardContext);
+
+ this.observe(umbExtensionsRegistry.extensionsOfType('healthCheck'), (healthCheckManifests) => {
+ this._healthCheckDashboardContext.manifests = healthCheckManifests;
+ });
+ }
+
+ protected firstUpdated() {
+ this.#registerHealthChecks();
+ }
+
+ #registerHealthChecks = async () => {
+ const { data } = await tryExecuteAndNotify(this, HealthCheckResource.getHealthCheckGroup({ skip: 0, take: 9999 }));
+ if (!data) return;
+ const manifests = this.#createManifests(data.items);
+ this.#register(manifests);
+ };
+
+ #createManifests(groups: HealthCheckGroup[]): Array {
+ return groups.map((group) => {
+ return {
+ type: 'healthCheck',
+ alias: `Umb.HealthCheck.${group.name?.replace(/\s+/g, '') || ''}`,
+ name: `${group.name} Health Check`,
+ weight: 500,
+ meta: {
+ label: group.name || '',
+ api: UmbHealthCheckContext,
+ },
+ };
+ });
+ }
+
+ #register(manifests: Array) {
+ manifests.forEach((manifest) => {
+ if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
+ umbExtensionsRegistry.register(manifest);
+ });
+ }
render() {
- return html`
-
- Health Check
-
- `;
+ return html` `;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.stories.ts
index 2a49f5699b..e444c665c2 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.stories.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.stories.ts
@@ -1,15 +1,21 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
-import type { UmbDashboardHealthCheckElement } from './dashboard-health-check.element';
-import './dashboard-health-check.element';
+import type { UmbDashboardHealthCheckOverviewElement } from './views/health-check-overview.element';
+import './views/health-check-overview.element';
+
+import type { UmbDashboardHealthCheckGroupElement } from './views/health-check-group.element';
+import './views/health-check-group.element';
export default {
title: 'Dashboards/Health Check',
- component: 'umb-dashboard-health-check',
+ component: 'umb-dashboard-health-check-overview',
id: 'umb-dashboard-health-check',
} as Meta;
-export const AAAOverview: Story = () =>
- html` `;
+export const AAAOverview: Story = () =>
+ html` `;
AAAOverview.storyName = 'Overview';
+
+export const Group: Story = () =>
+ html` `;
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check-dashboard.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check-dashboard.context.ts
new file mode 100644
index 0000000000..f4c41f1cbc
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check-dashboard.context.ts
@@ -0,0 +1,39 @@
+import { UmbHealthCheckContext } from './health-check.context';
+import type { ManifestHealthCheck } from '@umbraco-cms/models';
+import { UmbContextToken } from '@umbraco-cms/context-api';
+
+export class UmbHealthCheckDashboardContext {
+ #manifests: ManifestHealthCheck[] = [];
+ set manifests(value: ManifestHealthCheck[]) {
+ this.#manifests = value;
+ this.#registerApis();
+ }
+ get manifests() {
+ return this.#manifests;
+ }
+
+ public apis = new Map();
+ public host: HTMLElement;
+
+ constructor(host: HTMLElement) {
+ this.host = host;
+ }
+
+ checkAll() {
+ for (const [label, api] of this.apis.entries()) {
+ api?.checkGroup?.(label);
+ }
+ }
+
+ #registerApis() {
+ this.apis.clear();
+ this.#manifests.forEach((manifest) => {
+ // the group name (label) is the unique key for a health check group
+ this.apis.set(manifest.meta.label, new manifest.meta.api(this.host));
+ });
+ }
+}
+
+export const UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN = new UmbContextToken(
+ UmbHealthCheckDashboardContext.name
+);
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check.context.ts
new file mode 100644
index 0000000000..d685f1e036
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/health-check.context.ts
@@ -0,0 +1,50 @@
+import { BehaviorSubject, Observable } from 'rxjs';
+import { HealthCheckResource, HealthCheckWithResult } from '@umbraco-cms/backend-api';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { UmbContextToken } from '@umbraco-cms/context-api';
+
+export class UmbHealthCheckContext {
+ private _checks: BehaviorSubject> = new BehaviorSubject(>[]);
+ public readonly checks: Observable> = this._checks.asObservable();
+
+ private _results: BehaviorSubject> = new BehaviorSubject(>[]);
+ public readonly results: Observable> = this._results.asObservable();
+
+ public host: UmbControllerHostInterface;
+
+ constructor(host: UmbControllerHostInterface) {
+ this.host = host;
+ }
+
+ //TODO: Is this how we want to it?
+
+ async getGroupChecks(name: string) {
+ const { data } = await tryExecuteAndNotify(this.host, HealthCheckResource.getHealthCheckGroupByName({ name }));
+
+ if (data) {
+ data.checks?.forEach((check) => {
+ delete check.results;
+ });
+ this._checks.next(data.checks as HealthCheckWithResult[]);
+ }
+ }
+
+ async checkGroup(name: string) {
+ const { data } = await tryExecuteAndNotify(this.host, HealthCheckResource.getHealthCheckGroupByName({ name }));
+
+ if (data) {
+ const results =
+ data.checks?.map((check) => {
+ return {
+ key: check.key,
+ results: check.results,
+ };
+ }) || [];
+
+ this._results.next(results);
+ }
+ }
+}
+
+export const UMB_HEALTHCHECK_CONTEXT_TOKEN = new UmbContextToken(UmbHealthCheckContext.name);
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-action.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-action.element.ts
new file mode 100644
index 0000000000..41c98164f8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-action.element.ts
@@ -0,0 +1,150 @@
+import { UUIButtonState } from '@umbraco-ui/uui';
+import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
+import { css, html, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
+
+import { HealthCheckAction, HealthCheckResource } from '@umbraco-cms/backend-api';
+import { UmbLitElement } from '@umbraco-cms/element';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+
+@customElement('umb-dashboard-health-check-action')
+export class UmbDashboardHealthCheckActionElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ :host {
+ margin: var(--uui-size-space-4) 0;
+ display: block;
+ border-radius: var(--uui-border-radius);
+ background-color: #eee;
+ }
+ form {
+ margin: 0;
+ padding: 0;
+ }
+
+ p {
+ padding-top: 0;
+ margin-top: 0;
+ }
+
+ .action {
+ padding: 20px 25px;
+ width: 100%;
+ }
+
+ .action uui-label {
+ display: block;
+ }
+
+ .action uui-button {
+ flex-shrink: 1;
+ }
+
+ .no-description {
+ color: var(--uui-color-border-emphasis);
+ font-style: italic;
+ }
+
+ .required-value {
+ margin: 0 0 var(--uui-size-space-4);
+ }
+ `,
+ ];
+
+ @property({ reflect: true })
+ action!: HealthCheckAction;
+
+ @state()
+ private _buttonState?: UUIButtonState;
+
+ private async _onActionClick(e: SubmitEvent) {
+ e.preventDefault();
+ this._buttonState = 'waiting';
+ const { error } = await tryExecuteAndNotify(
+ this,
+ HealthCheckResource.postHealthCheckExecuteAction({ requestBody: this.action })
+ );
+
+ if (error) {
+ this._buttonState = 'failed';
+ return;
+ }
+
+ this._buttonState = 'success';
+ this.dispatchEvent(new CustomEvent('action-executed'));
+ }
+
+ render() {
+ return html`
+
${this.action.description || html`This action has no description`}
+
+
+
+
`;
+ }
+
+ private _renderValueRequired() {
+ if (this.action.valueRequired) {
+ switch (this.action.providedValueValidation) {
+ case 'email':
+ return html`
+ Set new value:
+ (this.action.providedValue = e.target.value)}
+ placeholder="Value"
+ .value=${this.action.providedValue ?? ''}
+ required>
+
`;
+
+ case 'regex':
+ return html`
+ Set new value:
+ (this.action.providedValue = e.target.value)}
+ placeholder="Value"
+ .value=${this.action.providedValue ?? ''}
+ required>
+
`;
+
+ default:
+ return html`
+ Set new value:
+ (this.action.providedValue = e.target.value)}
+ placeholder="Value"
+ .value=${this.action.providedValue ?? ''}
+ required>
+
`;
+ }
+ }
+
+ return nothing;
+ }
+}
+
+export default UmbDashboardHealthCheckActionElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-dashboard-health-check-action': UmbDashboardHealthCheckActionElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group-box-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group-box-overview.element.ts
new file mode 100644
index 0000000000..3bfa9c7d81
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group-box-overview.element.ts
@@ -0,0 +1,162 @@
+import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
+import { css, html, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { ensureSlash, path } from 'router-slot';
+import { UmbHealthCheckContext } from '../health-check.context';
+import {
+ UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN,
+ UmbHealthCheckDashboardContext,
+} from '../health-check-dashboard.context';
+import type { ManifestHealthCheck } from '@umbraco-cms/models';
+import { StatusResultType } from '@umbraco-cms/backend-api';
+import { UmbLitElement } from '@umbraco-cms/element';
+
+@customElement('umb-health-check-group-box-overview')
+export class UmbHealthCheckGroupBoxOverviewElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ .group-box {
+ position: relative;
+ }
+
+ .group-box:hover::after {
+ content: '';
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: var(--uui-border-radius);
+ transition: opacity 100ms ease-out 0s;
+ opacity: 0.33;
+ outline-color: var(--uui-color-selected);
+ outline-width: 4px;
+ outline-style: solid;
+ }
+
+ a {
+ text-align: center;
+ font-weight: bold;
+ cursor: pointer;
+ text-decoration: none;
+ color: var(--uui-color-text);
+ margin-bottom: 10px;
+ display: block;
+ }
+
+ uui-icon {
+ padding-right: var(--uui-size-space-2);
+ }
+ `,
+ ];
+
+ @property({ type: Object })
+ manifest?: ManifestHealthCheck;
+
+ private _healthCheckContext?: UmbHealthCheckDashboardContext;
+
+ private _api?: UmbHealthCheckContext;
+
+ @state()
+ private _tagResults?: any = [];
+
+ @state()
+ private _keyResults?: any = [];
+
+ constructor() {
+ super();
+
+ this.consumeContext(UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN, (instance) => {
+ this._healthCheckContext = instance;
+ if (!this._healthCheckContext || !this.manifest?.meta.label) return;
+ this._api = this._healthCheckContext?.apis.get(this.manifest?.meta.label);
+
+ this._api?.results.subscribe((results) => {
+ this._keyResults = results;
+ });
+ });
+ }
+
+ render() {
+ return html`
+ ${this.manifest?.meta.label} ${this._renderStatus()}
+ `;
+ }
+
+ _renderStatus() {
+ const res: any = [];
+ this._keyResults.forEach((item: any) => {
+ item.results.forEach((result: any) => {
+ res.push(result.resultType);
+ });
+ });
+ this._tagResults = res;
+ return html`${this._renderCheckResults(this.filterResults(this._tagResults))}
`;
+ }
+
+ _renderCheckResults(resultObject: any) {
+ return html`${resultObject.success > 0
+ ? html`
+
+ ${resultObject.success}
+ `
+ : nothing}
+ ${resultObject.warning > 0
+ ? html`
+
+ ${resultObject.warning}
+ `
+ : nothing}
+ ${resultObject.error > 0
+ ? html`
+
+ ${resultObject.error}
+ `
+ : nothing}
+ ${resultObject.info > 0
+ ? html`
+
+ ${resultObject.info}
+ `
+ : nothing} `;
+ }
+
+ filterResults(results: any): any {
+ const tags = {
+ success: 0,
+ warning: 0,
+ error: 0,
+ info: 0,
+ };
+
+ results.forEach((result: any) => {
+ switch (result) {
+ case StatusResultType.SUCCESS:
+ tags.success += 1;
+ break;
+ case StatusResultType.WARNING:
+ tags.warning += 1;
+ break;
+ case StatusResultType.ERROR:
+ tags.error += 1;
+ break;
+ case StatusResultType.INFO:
+ tags.info += 1;
+ break;
+ default:
+ break;
+ }
+ });
+ return tags;
+ }
+}
+
+export default UmbHealthCheckGroupBoxOverviewElement;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-health-check--group-box-overview': UmbHealthCheckGroupBoxOverviewElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group.element.ts
new file mode 100644
index 0000000000..b27925fe84
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-group.element.ts
@@ -0,0 +1,211 @@
+import { UUIButtonState } from '@umbraco-ui/uui';
+import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
+import { css, html, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+
+import { UmbHealthCheckContext } from '../health-check.context';
+import {
+ UmbHealthCheckDashboardContext,
+ UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN,
+} from '../health-check-dashboard.context';
+import {
+ HealthCheckAction,
+ HealthCheckGroupWithResult,
+ HealthCheckResource,
+ HealthCheckWithResult,
+ StatusResultType,
+} from '@umbraco-cms/backend-api';
+import { UmbLitElement } from '@umbraco-cms/element';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+import './health-check-action.element';
+
+@customElement('umb-dashboard-health-check-group')
+export class UmbDashboardHealthCheckGroupElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ uui-box {
+ margin-bottom: var(--uui-size-space-5);
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .check-results-wrapper .check-result {
+ padding-top: var(--uui-size-space-5);
+ }
+
+ .check-results-wrapper .check-result:not(:last-child) {
+ border-bottom: 1px solid var(--uui-color-divider-standalone);
+ padding-bottom: var(--uui-size-space-5);
+ }
+
+ .check-results-wrapper uui-button {
+ margin-block-start: 1em;
+ }
+
+ .check-result-description {
+ display: flex;
+ }
+
+ .check-result-description span {
+ width: 36px;
+ }
+
+ uui-icon {
+ vertical-align: sub;
+ }
+ `,
+ ];
+
+ @property()
+ groupName!: string;
+
+ @state()
+ private _buttonState: UUIButtonState;
+
+ @state()
+ private _group?: HealthCheckGroupWithResult;
+
+ private _healthCheckContext?: UmbHealthCheckDashboardContext;
+
+ @state()
+ private _checks?: HealthCheckWithResult[] | null;
+
+ @state()
+ private _keyResults?: any;
+
+ private _api?: UmbHealthCheckContext;
+
+ constructor() {
+ super();
+ this.consumeContext(UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN, (instance) => {
+ this._healthCheckContext = instance;
+
+ this._api = this._healthCheckContext?.apis.get(this.groupName);
+
+ this._api?.getGroupChecks(this.groupName);
+
+ this._api?.checks.subscribe((checks) => {
+ this._checks = checks;
+ this._group = { name: this.groupName, checks: this._checks };
+ });
+
+ this._api?.results.subscribe((results) => {
+ this._keyResults = results;
+ });
+ });
+ }
+
+ private async _buttonHandler() {
+ this._buttonState = 'waiting';
+ this._api?.checkGroup(this.groupName);
+ this._buttonState = 'success';
+ }
+
+ private _onActionClick(action: HealthCheckAction) {
+ return tryExecuteAndNotify(this, HealthCheckResource.postHealthCheckExecuteAction({ requestBody: action }));
+ }
+
+ render() {
+ return html` ← Back to overview
+ ${this._group ? this.#renderGroup() : nothing}`;
+ }
+
+ #renderGroup() {
+ return html`
+
+
+ ${this._group?.checks?.map((check) => {
+ return html`
+ ${check.description}
+ ${check.key ? this.renderCheckResults(check.key) : nothing}
+ `;
+ })}
+
+ `;
+ }
+
+ renderCheckResults(key: string) {
+ const checkResults = this._keyResults?.find((result: any) => result.key === key);
+ return html`
+
+ ${checkResults?.results.map((result: any) => {
+ return html`
+
+
${this.renderIcon(result.resultType)}
+
${unsafeHTML(result.message)}
+
+
+ ${result.actions ? this.renderActions(result.actions) : nothing}
+ ${result.readMoreLink
+ ? html`
+ Read more
+
+ `
+ : nothing}
+
`;
+ })}
+
+ `;
+ }
+
+ private renderIcon(type?: StatusResultType) {
+ switch (type) {
+ case StatusResultType.SUCCESS:
+ return html``;
+ case StatusResultType.WARNING:
+ return html``;
+ case StatusResultType.ERROR:
+ return html``;
+ case StatusResultType.INFO:
+ return html``;
+ default:
+ return nothing;
+ }
+ }
+
+ private renderActions(actions: HealthCheckAction[]) {
+ if (actions.length)
+ return html`
+ ${actions.map(
+ (action) =>
+ html` this._buttonHandler()}>`
+ )}
+
`;
+ else return nothing;
+ }
+}
+
+export default UmbDashboardHealthCheckGroupElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-dashboard-health-check-group': UmbDashboardHealthCheckGroupElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-overview.element.ts
new file mode 100644
index 0000000000..cb1d908195
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/views/health-check-overview.element.ts
@@ -0,0 +1,84 @@
+import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
+import { css, html } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { UUIButtonState } from '@umbraco-ui/uui';
+
+import {
+ UmbHealthCheckDashboardContext,
+ UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN,
+} from '../health-check-dashboard.context';
+import { UmbLitElement } from '@umbraco-cms/element';
+
+import { ManifestHealthCheck } from '@umbraco-cms/extensions-registry';
+import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
+
+import './health-check-group-box-overview.element';
+
+@customElement('umb-dashboard-health-check-overview')
+export class UmbDashboardHealthCheckOverviewElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ uui-box + uui-box {
+ margin-top: var(--uui-size-space-5);
+ }
+
+ .flex {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .grid {
+ display: grid;
+ gap: var(--uui-size-space-4);
+ grid-template-columns: repeat(auto-fit, minmax(250px, auto));
+ }
+ `,
+ ];
+
+ @state()
+ private _buttonState: UUIButtonState;
+
+ private _healthCheckDashboardContext?: UmbHealthCheckDashboardContext;
+
+ constructor() {
+ super();
+ this.consumeContext(UMB_HEALTHCHECK_DASHBOARD_CONTEXT_TOKEN, (instance) => {
+ this._healthCheckDashboardContext = instance;
+ });
+ }
+
+ private async _onHealthCheckHandler() {
+ this._healthCheckDashboardContext?.checkAll();
+ }
+
+ render() {
+ return html`
+
+
+ Health Check
+
+ Perform all checks
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+export default UmbDashboardHealthCheckOverviewElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-dashboard-health-check-overview': UmbDashboardHealthCheckOverviewElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts
index d0e958b53c..16ca3c3f9b 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts
@@ -53,6 +53,19 @@ const dashboards: Array = [
pathname: 'published-status',
},
},
+ {
+ type: 'dashboard',
+ alias: 'Umb.Dashboard.HealthCheck',
+ name: 'Health Check',
+ elementName: 'umb-dashboard-health-check',
+ loader: () => import('./health-check/dashboard-health-check.element'),
+ weight: 102,
+ meta: {
+ label: 'Health Check',
+ sections: ['Umb.Section.Settings'],
+ pathname: 'health-check',
+ },
+ },
{
type: 'dashboard',
alias: 'Umb.Dashboard.Profiling',
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts
index 63771a52d3..074fa1a193 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts
@@ -30,6 +30,12 @@ export class UUICodeBlock extends LitElement {
overflow-y: auto;
overflow-wrap: anywhere;
}
+ pre {
+ max-width: 100%;
+ white-space: pre-line;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ }
`,
];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts
index 083a6a5181..eec5648e04 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts
@@ -1,9 +1,10 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { css, nothing } from 'lit';
import type { TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { map } from 'rxjs';
import { repeat } from 'lit/directives/repeat.js';
-import { umbExtensionsRegistry , createExtensionElement, isManifestElementableType } from '@umbraco-cms/extensions-api';
+import { createExtensionElement, isManifestElementableType, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import { UmbLitElement } from '@umbraco-cms/element';
export type InitializedExtension = { alias: string; weight: number; component: HTMLElement | null };
@@ -67,6 +68,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
};
this._extensions.push(extensionObject);
let component;
+
if (isManifestElementableType(extension)) {
component = await createExtensionElement(extension);
} else {
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts
index 2bd975f3c7..3b20551708 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts
@@ -4,12 +4,9 @@ import { customElement, state } from 'lit/decorators.js';
import { IRoutingInfo } from 'router-slot';
import { first, map } from 'rxjs';
import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section.context';
-import { createExtensionElement , umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
-import type {
- ManifestDashboard,
- ManifestDashboardCollection,
- ManifestWithMeta,
-} from '@umbraco-cms/models';
+import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
+import type { ManifestDashboard, ManifestDashboardCollection, ManifestWithMeta } from '@umbraco-cms/models';
+
import { UmbLitElement } from '@umbraco-cms/element';
@customElement('umb-section-dashboards')
@@ -29,7 +26,7 @@ export class UmbSectionDashboardsElement extends UmbLitElement {
}
#scroll-container {
- flex:1;
+ flex: 1;
}
#router-slot {
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts
index 8faab32740..11eec5f559 100644
--- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts
@@ -11,6 +11,7 @@ import { handlers as usersHandlers } from './domains/users.handlers';
import { handlers as userGroupsHandlers } from './domains/user-groups.handlers';
import { handlers as examineManagementHandlers } from './domains/examine-management.handlers';
import { handlers as modelsBuilderHandlers } from './domains/modelsbuilder.handlers';
+import { handlers as healthCheckHandlers } from './domains/health-check.handlers';
import { handlers as profilingHandlers } from './domains/performance-profiling.handlers';
import { handlers as documentHandlers } from './domains/document.handlers';
import { handlers as mediaHandlers } from './domains/media.handlers';
@@ -19,6 +20,7 @@ import { handlers as mediaTypeHandlers } from './domains/media-type.handlers';
import { handlers as memberGroupHandlers } from './domains/member-group.handlers';
import { handlers as memberTypeHandlers } from './domains/member-type.handlers';
import { handlers as templateHandlers } from './domains/template.handlers';
+import { handlers as redirectManagement } from './domains/redirect-management.handlers';
const handlers = [
serverHandlers.serverVersionHandler,
@@ -39,9 +41,11 @@ const handlers = [
...memberTypeHandlers,
...examineManagementHandlers,
...modelsBuilderHandlers,
+ ...healthCheckHandlers,
...profilingHandlers,
...dictionaryHandlers,
...templateHandlers,
+ ...redirectManagement,
];
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/health-check.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/health-check.data.ts
new file mode 100644
index 0000000000..2a9f438e6a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/health-check.data.ts
@@ -0,0 +1,367 @@
+import { HealthCheckGroup, HealthCheckGroupWithResult, StatusResultType } from '@umbraco-cms/backend-api';
+
+export function getGroupByName(name: string) {
+ return healthGroups.find((group) => group.name == name);
+}
+
+export const healthGroups: HealthCheckGroupWithResult[] = [
+ {
+ name: 'Configuration',
+ checks: [
+ {
+ key: 'd0f7599e-9b2a-4d9e-9883-81c7edc5616f',
+ name: 'Macro errors',
+ description:
+ 'Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.',
+ results: [
+ {
+ message: `MacroErrors are set to 'Throw' which will prevent some or all pages in your site from loading
+ completely if there are any errors in macros. Rectifying this will set the value to 'Inline'. `,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-macro-errors',
+ actions: [
+ {
+ healthCheckKey: 'key123',
+ name: 'Action name',
+ alias: 'Action alias',
+ description: 'Action description',
+ valueRequired: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ key: '3e2f7b14-4b41-452b-9a30-e67fbc8e1206',
+ name: 'Notification Email Settings',
+ description:
+ "If notifications are used, the 'from' email address should be specified and changed from the default value.",
+ results: [
+ {
+ message: `Notification email is still set to the default value of your@email.here.`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-notification-email',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'Data Integrity',
+ checks: [
+ {
+ key: '73dd0c1c-e0ca-4c31-9564-1dca509788af',
+ name: 'Database data integrity check',
+ description: 'Checks for various data integrity issues in the Umbraco database.',
+ //group: 'Data Integrity',
+ results: [
+ {
+ message: `All document paths are valid`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ { message: `All media paths are valid`, resultType: StatusResultType.SUCCESS },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'Live Environment',
+ checks: [
+ {
+ key: '61214ff3-fc57-4b31-b5cf-1d095c977d6d',
+ name: 'Debug Compilation Mode',
+ description:
+ 'Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.',
+ //group: 'Live Environment',
+ results: [
+ {
+ message: `Debug compilation mode is currently enabled. It is recommended to disable this setting before
+ go live.`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-compilation-debug',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'Permissions',
+ checks: [
+ {
+ key: '53dba282-4a79-4b67-b958-b29ec40fcc23',
+ name: 'Folder & File Permissions',
+ description: 'Checks that the web server folder and file permissions are set correctly for Umbraco to run.',
+ //group: 'Permissions',
+ results: [
+ {
+ message: `Folder creation`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ {
+ message: `File writing for packages`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ {
+ message: `File writing`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ {
+ message: `Media folder creation`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'Security',
+ checks: [
+ {
+ key: '6708ca45-e96e-40b8-a40a-0607c1ca7f28',
+ name: 'Application URL Configuration',
+ description: 'Checks if the Umbraco application URL is configured for your site.',
+ //group: 'Security',
+ results: [
+ {
+ message: `The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set`,
+ resultType: StatusResultType.WARNING,
+ readMoreLink: 'https://umbra.co/healthchecks-umbraco-application-url',
+ },
+ ],
+ },
+ {
+ key: 'ed0d7e40-971e-4be8-ab6d-8cc5d0a6a5b0',
+ name: 'Click-Jacking Protection',
+ description:
+ 'Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.',
+ //group: 'Security',
+ results: [
+ {
+ message: `Error pinging the URL https://localhost:44361 - 'The SSL connection could not be established,
+ see inner exception.'`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-click-jacking',
+ },
+ ],
+ },
+ {
+ key: '1cf27db3-efc0-41d7-a1bb-ea912064e071',
+ name: 'Content/MIME Sniffing Protection',
+ description: 'Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.',
+ //group: 'Security',
+ results: [
+ {
+ message: `Error pinging the URL https://localhost:44361 - 'The SSL connection could not be established,
+ see inner exception.'`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-no-sniff',
+ },
+ ],
+ },
+ {
+ key: 'e2048c48-21c5-4be1-a80b-8062162df124',
+ name: 'Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))',
+ description:
+ 'Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).',
+ //group: 'Security',
+ results: [
+ {
+ message: `Error pinging the URL https://localhost:44361 - 'The SSL connection could not be established,
+ see inner exception.'`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-hsts',
+ },
+ ],
+ },
+ {
+ key: 'f4d2b02e-28c5-4999-8463-05759fa15c3a',
+ name: 'Cross-site scripting Protection (X-XSS-Protection header)',
+ description:
+ 'This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.',
+ //group: 'Security',
+ results: [
+ {
+ message: `Error pinging the URL https://localhost:44361 - 'The SSL connection could not be established,
+ see inner exception.'`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-xss-protection',
+ },
+ ],
+ },
+ {
+ key: '92abbaa2-0586-4089-8ae2-9a843439d577',
+ name: 'Excessive Headers',
+ description:
+ 'Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.',
+ //group: 'Security',
+ results: [
+ {
+ message: `Error pinging the URL https://localhost:44361 - 'The SSL connection could not be established,
+ see inner exception.'`,
+ resultType: StatusResultType.WARNING,
+ readMoreLink: 'https://umbra.co/healthchecks-excessive-headers',
+ },
+ ],
+ },
+ {
+ key: 'eb66bb3b-1bcd-4314-9531-9da2c1d6d9a7',
+ name: 'HTTPS Configuration',
+ description:
+ 'Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.',
+ //group: 'Security',
+ results: [
+ {
+ message: `You are currently viewing the site using HTTPS scheme`,
+ resultType: StatusResultType.SUCCESS,
+ },
+ {
+ message: `The appSetting 'Umbraco:CMS:Global:UseHttps' is set to 'False' in your appSettings.json file,
+ your cookies are not marked as secure.`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-https-config',
+ },
+ {
+ message: `Error pinging the URL https://localhost:44361/ - 'The SSL connection could not be established,
+ see inner exception.'"`,
+ resultType: StatusResultType.ERROR,
+ readMoreLink: 'https://umbra.co/healthchecks-https-request',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'Services',
+ checks: [
+ {
+ key: '1b5d221b-ce99-4193-97cb-5f3261ec73df',
+ name: 'SMTP Settings',
+ description: 'Checks that valid settings for sending emails are in place.',
+ //group: 'Services',
+ results: [
+ {
+ message: `The 'Umbraco:CMS:Global:Smtp' configuration could not be found.`,
+ readMoreLink: 'https://umbra.co/healthchecks-smtp',
+ resultType: StatusResultType.ERROR,
+ },
+ ],
+ },
+ ],
+ },
+];
+export const healthGroupsWithoutResult: HealthCheckGroup[] = [
+ {
+ name: 'Configuration',
+ checks: [
+ {
+ key: 'd0f7599e-9b2a-4d9e-9883-81c7edc5616f',
+ name: 'Macro errors',
+ description:
+ 'Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.',
+ },
+ {
+ key: '3e2f7b14-4b41-452b-9a30-e67fbc8e1206',
+ name: 'Notification Email Settings',
+ description:
+ "If notifications are used, the 'from' email address should be specified and changed from the default value.",
+ },
+ ],
+ },
+ {
+ name: 'Data Integrity',
+ checks: [
+ {
+ key: '73dd0c1c-e0ca-4c31-9564-1dca509788af',
+ name: 'Database data integrity check',
+ description: 'Checks for various data integrity issues in the Umbraco database.',
+ //group: 'Data Integrity',
+ },
+ ],
+ },
+ {
+ name: 'Live Environment',
+ checks: [
+ {
+ key: '61214ff3-fc57-4b31-b5cf-1d095c977d6d',
+ name: 'Debug Compilation Mode',
+ description:
+ 'Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.',
+ //group: 'Live Environment',
+ },
+ ],
+ },
+ {
+ name: 'Permissions',
+ checks: [
+ {
+ key: '53dba282-4a79-4b67-b958-b29ec40fcc23',
+ name: 'Folder & File Permissions',
+ description: 'Checks that the web server folder and file permissions are set correctly for Umbraco to run.',
+ //group: 'Permissions',
+ },
+ ],
+ },
+ {
+ name: 'Security',
+ checks: [
+ {
+ key: '6708ca45-e96e-40b8-a40a-0607c1ca7f28',
+ name: 'Application URL Configuration',
+ description: 'Checks if the Umbraco application URL is configured for your site.',
+ //group: 'Security',
+ },
+ {
+ key: 'ed0d7e40-971e-4be8-ab6d-8cc5d0a6a5b0',
+ name: 'Click-Jacking Protection',
+ description:
+ 'Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.',
+ //group: 'Security',
+ },
+ {
+ key: '1cf27db3-efc0-41d7-a1bb-ea912064e071',
+ name: 'Content/MIME Sniffing Protection',
+ description: 'Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.',
+ //group: 'Security',
+ },
+ {
+ key: 'e2048c48-21c5-4be1-a80b-8062162df124',
+ name: 'Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))',
+ description:
+ 'Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).',
+ //group: 'Security',
+ },
+ {
+ key: 'f4d2b02e-28c5-4999-8463-05759fa15c3a',
+ name: 'Cross-site scripting Protection (X-XSS-Protection header)',
+ description:
+ 'This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.',
+ //group: 'Security',
+ },
+ {
+ key: '92abbaa2-0586-4089-8ae2-9a843439d577',
+ name: 'Excessive Headers',
+ description:
+ 'Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.',
+ //group: 'Security',
+ },
+ {
+ key: 'eb66bb3b-1bcd-4314-9531-9da2c1d6d9a7',
+ name: 'HTTPS Configuration',
+ description:
+ 'Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.',
+ //group: 'Security',
+ },
+ ],
+ },
+ {
+ name: 'Services',
+ checks: [
+ {
+ key: '1b5d221b-ce99-4193-97cb-5f3261ec73df',
+ name: 'SMTP Settings',
+ description: 'Checks that valid settings for sending emails are in place.',
+ //group: 'Services',
+ },
+ ],
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/health-check.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/health-check.handlers.ts
new file mode 100644
index 0000000000..f771dc68a9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/health-check.handlers.ts
@@ -0,0 +1,38 @@
+import { rest } from 'msw';
+
+import { getGroupByName, healthGroupsWithoutResult } from '../data/health-check.data';
+
+import { HealthCheckGroup, PagedHealthCheckGroup } from '@umbraco-cms/backend-api';
+import { umbracoPath } from '@umbraco-cms/utils';
+
+export const handlers = [
+ rest.get(umbracoPath('/health-check-group'), (_req, res, ctx) => {
+ return res(
+ // Respond with a 200 status code
+ ctx.status(200),
+ ctx.json({ total: 9999, items: healthGroupsWithoutResult })
+ );
+ }),
+
+ rest.get(umbracoPath('/health-check-group/:name'), (_req, res, ctx) => {
+ const name = _req.params.name as string;
+
+ if (!name) return;
+ const group = getGroupByName(name);
+
+ if (group) {
+ return res(ctx.status(200), ctx.json(group));
+ } else {
+ return res(ctx.status(404));
+ }
+ }),
+
+ rest.post(umbracoPath('/health-check/execute-action'), async (_req, res, ctx) => {
+ await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
+ return res(
+ // Respond with a 200 status code
+ ctx.status(200),
+ ctx.json(true)
+ );
+ }),
+];
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts
new file mode 100644
index 0000000000..b60e0430a4
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts
@@ -0,0 +1,173 @@
+import { rest } from 'msw';
+import { umbracoPath } from '@umbraco-cms/utils';
+import { PagedRedirectUrl, RedirectUrl, RedirectStatus, RedirectUrlStatus } from '@umbraco-cms/backend-api';
+
+export const handlers = [
+ rest.get(umbracoPath('/redirect-management'), (_req, res, ctx) => {
+ const filter = _req.url.searchParams.get('filter');
+ const skip = parseInt(_req.url.searchParams.get('skip') ?? '0', 10);
+ const take = parseInt(_req.url.searchParams.get('take') ?? '20', 10);
+
+ if (filter) {
+ const filtered: RedirectUrl[] = [];
+
+ PagedRedirectUrlData.items.forEach((item) => {
+ if (item.originalUrl?.includes(filter)) filtered.push(item);
+ });
+ const filteredPagedData: PagedRedirectUrl = {
+ total: filtered.length,
+ items: filtered.slice(skip, skip + take),
+ };
+ return res(ctx.status(200), ctx.json(filteredPagedData));
+ } else {
+ const items = PagedRedirectUrlData.items.slice(skip, skip + take);
+
+ const PagedData: PagedRedirectUrl = {
+ total: PagedRedirectUrlData.total,
+ items,
+ };
+ return res(ctx.status(200), ctx.json(PagedData));
+ }
+ }),
+
+ rest.get(umbracoPath('/redirect-management/:key'), async (_req, res, ctx) => {
+ const key = _req.params.key as string;
+ if (!key) return res(ctx.status(404));
+ if (key === 'status') return res(ctx.status(200), ctx.json(UrlTracker));
+
+ const PagedRedirectUrlObject = _getRedirectUrlByKey(key);
+
+ return res(ctx.status(200), ctx.json(PagedRedirectUrlObject));
+ }),
+
+ rest.delete(umbracoPath('/redirect-management/:key'), async (_req, res, ctx) => {
+ const key = _req.params.key as string;
+ if (!key) return res(ctx.status(404));
+
+ const PagedRedirectUrlObject = _deleteRedirectUrlByKey(key);
+
+ return res(ctx.status(200), ctx.json(PagedRedirectUrlObject));
+ }),
+
+ /*rest.get(umbracoPath('/redirect-management/status'), (_req, res, ctx) => {
+ return res(ctx.status(200), ctx.json(UrlTracker));
+ }),*/
+
+ rest.post(umbracoPath('/redirect-management/status'), async (_req, res, ctx) => {
+ UrlTracker.status = UrlTracker.status === RedirectStatus.ENABLED ? RedirectStatus.DISABLED : RedirectStatus.ENABLED;
+ return res(ctx.status(200), ctx.json(UrlTracker.status));
+ }),
+];
+
+// Mock Data
+
+const UrlTracker: RedirectUrlStatus = { status: RedirectStatus.ENABLED, userIsAdmin: true };
+
+const _getRedirectUrlByKey = (key: string) => {
+ const PagedResult: PagedRedirectUrl = {
+ total: 0,
+ items: [],
+ };
+ RedirectUrlData.forEach((data) => {
+ if (data.key?.includes(key)) {
+ PagedResult.items.push(data);
+ PagedResult.total++;
+ }
+ });
+ return PagedResult;
+};
+
+const _deleteRedirectUrlByKey = (key: string) => {
+ const index = RedirectUrlData.findIndex((data) => data.key === key);
+ if (index > -1) RedirectUrlData.splice(index, 1);
+ const PagedResult: PagedRedirectUrl = {
+ items: RedirectUrlData,
+ total: RedirectUrlData.length,
+ };
+ return PagedResult;
+};
+
+const RedirectUrlData: RedirectUrl[] = [
+ {
+ key: '1',
+ created: '2022-12-05T13:59:43.6827244',
+ destinationUrl: 'kitty.com',
+ originalUrl: 'kitty.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2',
+ },
+ {
+ key: '2',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'umbraco.com',
+ originalUrl: 'umbraco.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9f',
+ },
+ {
+ key: '3',
+ created: '2022-12-05T13:59:43.6827244',
+ destinationUrl: 'uui.umbraco.com',
+ originalUrl: 'uui.umbraco.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9f23',
+ },
+ {
+ key: '4',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'umbracoffee.com',
+ originalUrl: 'umbracoffee.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9fdsaa',
+ },
+ {
+ key: '5',
+ created: '2022-12-05T13:59:43.6827244',
+ destinationUrl: 'section/settings',
+ originalUrl: 'section/settings/123',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2e23',
+ },
+ {
+ key: '6',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'dxp.com',
+ originalUrl: 'dxp.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9fsafsfd',
+ },
+ {
+ key: '7',
+ created: '2022-12-05T13:59:43.6827244',
+ destinationUrl: 'google.com',
+ originalUrl: 'google.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2cxza',
+ },
+ {
+ key: '8',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'unicorns.com',
+ originalUrl: 'unicorns.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31d9fweds',
+ },
+ {
+ key: '9',
+ created: '2022-12-05T13:59:43.6827244',
+ destinationUrl: 'h5yr.com',
+ originalUrl: 'h5yr.dk',
+ contentKey: '7191c911-6747-4824-849e-5208e2b31ddsfsdsfadsfdx9f2',
+ },
+ {
+ key: '10',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'our.umbraco.com',
+ originalUrl: 'our.umbraco.dk',
+ contentKey: '7191c911-6747-4824-849e-52dsacx08e2b31d9dsafdsff',
+ },
+ {
+ key: '11',
+ created: '2022-13-05T13:59:43.6827244',
+ destinationUrl: 'your.umbraco.com',
+ originalUrl: 'your.umbraco.dk',
+ contentKey: '7191c911-6747-4824-849e-52dsacx08e2b31d9fsda',
+ },
+];
+
+const PagedRedirectUrlData: PagedRedirectUrl = {
+ total: RedirectUrlData.length,
+ items: RedirectUrlData,
+};
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts
index 7006b4c3f9..4f3575a236 100644
--- a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts
@@ -10,6 +10,8 @@ import { handlers as telemetryHandlers } from './domains/telemetry.handlers';
import { handlers as examineManagementHandlers } from './domains/examine-management.handlers';
import { handlers as modelsBuilderHandlers } from './domains/modelsbuilder.handlers';
import { handlers as profileHandlers } from './domains/performance-profiling.handlers';
+import { handlers as healthCheckHandlers } from './domains/health-check.handlers';
+import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers';
export const handlers = [
serverHandlers.serverRunningHandler,
@@ -26,4 +28,6 @@ export const handlers = [
...examineManagementHandlers,
...modelsBuilderHandlers,
...profileHandlers,
+ ...healthCheckHandlers,
+ ...redirectManagementHandlers,
];