Merge branch 'main' into feature/templates-scaffold

This commit is contained in:
Mads Rasmussen
2023-01-30 08:42:08 +01:00
21 changed files with 1755 additions and 31 deletions

View File

@@ -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;
}

View File

@@ -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'];

View File

@@ -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`
<div style="width:300px">
<p>This will remove the redirect</p>
Original URL: <strong>${data.originalUrl}</strong><br />
Redirected To: <strong>${data.destinationUrl}</strong>
<p>Are you sure you want to delete?</p>
</div>
`,
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`
<uui-box>
<h1>Redirect Management</h1>
return html`<div class="actions">
${this._trackerStatus
? html`<div>
<uui-input
id="search-input"
placeholder="Original URL"
label="input for search"
@keypress="${this._inputHandler}">
</uui-input>
<uui-button
id="search-button"
look="primary"
color="positive"
label="search"
.state="${this._buttonState}"
@click="${this._searchHandler}">
Search<uui-icon name="umb:search"></uui-icon>
</uui-button>
</div>
<uui-button
label="Disable URL tracker"
look="outline"
color="danger"
@click="${this._disableRedirectHandler}">
Disable URL tracker
</uui-button> `
: html`<uui-button
label="Enable URL tracker"
look="primary"
color="positive"
@click="${this._toggleRedirect}">
Enable URL tracker
</uui-button>`}
</div>
${this._total && this._total > 0
? html`<div class="wrapper ${this._trackerStatus ? 'trackerEnabled' : 'trackerDisabled'}">
${this.renderTable()}
</div>`
: this._filter?.length
? this._renderZeroResults()
: this.renderNoRedirects()} `;
}
private _renderZeroResults() {
return html`<uui-box>
<strong>No redirects matching this search criteria</strong>
<p>Double check your search for any error or spelling mistakes.</p>
</uui-box>`;
}
private renderNoRedirects() {
return html`<uui-box>
<strong>No redirects have been made</strong>
<p>When a published page gets renamed or moved, a redirect will automatically be made to the new page.</p>
</uui-box>`;
}
private renderTable() {
return html`<uui-box style="--uui-box-default-padding: 0;">
<uui-table>
<uui-table-head>
<uui-table-head-cell style="width:10%;">Culture</uui-table-head-cell>
<uui-table-head-cell>Original URL</uui-table-head-cell>
<uui-table-head-cell style="width:10%;"></uui-table-head-cell>
<uui-table-head-cell>Redirected To</uui-table-head-cell>
<uui-table-head-cell style="width:10%;">Actions</uui-table-head-cell>
</uui-table-head>
${this._redirectData?.map((data) => {
return html` <uui-table-row id="redirect-key-${data.key}">
<uui-table-cell> ${data.culture || '*'} </uui-table-cell>
<uui-table-cell>
<a href="${data.originalUrl || '#'}" target="_blank"> ${data.originalUrl}</a>
<uui-icon name="umb:out"></uui-icon>
</uui-table-cell>
<uui-table-cell>
<uui-icon name="umb:arrow-right"></uui-icon>
</uui-table-cell>
<uui-table-cell>
<a href="${data.destinationUrl || '#'}" target="_blank"> ${data.destinationUrl}</a>
<uui-icon name="umb:out"></uui-icon>
</uui-table-cell>
<uui-table-cell>
<uui-action-bar style="justify-self: left;">
<uui-button
label="Delete"
look="secondary"
.disabled=${!this._trackerStatus}
@click="${() => this._removeRedirectHandler(data)}">
<uui-icon name="delete"></uui-icon>
</uui-button>
</uui-action-bar>
</uui-table-cell>
</uui-table-row>`;
})}
</uui-table>
</uui-box>
`;
${this._renderPagination()}
</uui-scroll-container
>`;
}
private _renderPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this._onPageChange}"></uui-pagination>
</div>`;
}
}

View File

@@ -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`<umb-dashboard-redirect-management></umb-dashboard-redirect-management>`);
});
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);
});
});

View File

@@ -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<ManifestHealthCheck> {
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<ManifestHealthCheck>) {
manifests.forEach((manifest) => {
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
umbExtensionsRegistry.register(manifest);
});
}
render() {
return html`
<uui-box>
<h1>Health Check</h1>
</uui-box>
`;
return html` <router-slot .routes=${this._routes}></router-slot>`;
}
}

View File

@@ -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<UmbDashboardHealthCheckElement> = () =>
html` <umb-dashboard-health-check></umb-dashboard-health-check>`;
export const AAAOverview: Story<UmbDashboardHealthCheckOverviewElement> = () =>
html` <umb-dashboard-health-check-overview></umb-dashboard-health-check-overview>`;
AAAOverview.storyName = 'Overview';
export const Group: Story<UmbDashboardHealthCheckGroupElement> = () =>
html` <umb-dashboard-health-check-group></umb-dashboard-health-check-group>`;

View File

@@ -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<string, UmbHealthCheckContext>();
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>(
UmbHealthCheckDashboardContext.name
);

View File

@@ -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<Array<any>> = new BehaviorSubject(<Array<any>>[]);
public readonly checks: Observable<Array<any>> = this._checks.asObservable();
private _results: BehaviorSubject<Array<any>> = new BehaviorSubject(<Array<any>>[]);
public readonly results: Observable<Array<any>> = 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>(UmbHealthCheckContext.name);

View File

@@ -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` <div class="action">
<p>${this.action.description || html`<span class="no-description">This action has no description</span>`}</p>
<uui-form>
<form @submit=${(e: SubmitEvent) => this._onActionClick(e)}>
${this._renderValueRequired()}
<uui-button
type="submit"
look="primary"
color="positive"
label="${this.action.name || 'Action'}"
.state=${this._buttonState}>
${this.action.name || 'Action'}
</uui-button>
</form>
</uui-form>
</div>`;
}
private _renderValueRequired() {
if (this.action.valueRequired) {
switch (this.action.providedValueValidation) {
case 'email':
return html` <div class="required-value">
<uui-label for="action">Set new value:</uui-label>
<uui-input
id="action"
type="email"
@input=${(e: any) => (this.action.providedValue = e.target.value)}
placeholder="Value"
.value=${this.action.providedValue ?? ''}
required></uui-input>
</div>`;
case 'regex':
return html`<div class="required-value">
<uui-label for="action">Set new value:</uui-label>
<uui-input
id="action"
type="text"
pattern="${ifDefined(this.action.providedValueValidationRegex ?? undefined)}"
@input=${(e: any) => (this.action.providedValue = e.target.value)}
placeholder="Value"
.value=${this.action.providedValue ?? ''}
required></uui-input>
</div>`;
default:
return html`<div class="required-value">
<uui-label for="action">Set new value:</uui-label>
<uui-input
id="action"
type="text"
@input=${(e: any) => (this.action.providedValue = e.target.value)}
placeholder="Value"
.value=${this.action.providedValue ?? ''}
required></uui-input>
</div>`;
}
}
return nothing;
}
}
export default UmbDashboardHealthCheckActionElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-health-check-action': UmbDashboardHealthCheckActionElement;
}
}

View File

@@ -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`<a href="${ensureSlash(path()) + this.manifest?.meta.label}">
<uui-box class="group-box"> ${this.manifest?.meta.label} ${this._renderStatus()} </uui-box>
</a>`;
}
_renderStatus() {
const res: any = [];
this._keyResults.forEach((item: any) => {
item.results.forEach((result: any) => {
res.push(result.resultType);
});
});
this._tagResults = res;
return html`<div>${this._renderCheckResults(this.filterResults(this._tagResults))}</div>`;
}
_renderCheckResults(resultObject: any) {
return html`${resultObject.success > 0
? html`<uui-tag look="secondary" color="positive">
<uui-icon name="umb:check"></uui-icon>
${resultObject.success}
</uui-tag> `
: nothing}
${resultObject.warning > 0
? html`<uui-tag look="secondary" color="warning">
<uui-icon name="umb:alert"></uui-icon>
${resultObject.warning}
</uui-tag>`
: nothing}
${resultObject.error > 0
? html`<uui-tag look="secondary" color="danger">
<uui-icon name="umb:wrong"></uui-icon>
${resultObject.error}
</uui-tag>`
: nothing}
${resultObject.info > 0
? html`<uui-tag look="secondary">
<uui-icon name="umb:info"></uui-icon>
${resultObject.info}
</uui-tag>`
: 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;
}
}

View File

@@ -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` <a href="/section/settings/dashboard/health-check"> &larr; Back to overview </a>
${this._group ? this.#renderGroup() : nothing}`;
}
#renderGroup() {
return html`
<div class="header">
<h2>${this._group?.name}</h2>
<uui-button
label="Perform checks"
color="positive"
look="primary"
.state="${this._buttonState}"
@click="${this._buttonHandler}">
Perform checks
</uui-button>
</div>
<div class="checks-wrapper">
${this._group?.checks?.map((check) => {
return html`<uui-box headline="${check.name || '?'}">
<p>${check.description}</p>
${check.key ? this.renderCheckResults(check.key) : nothing}
</uui-box>`;
})}
</div>
`;
}
renderCheckResults(key: string) {
const checkResults = this._keyResults?.find((result: any) => result.key === key);
return html`<uui-icon-registry-essential>
<div class="check-results-wrapper">
${checkResults?.results.map((result: any) => {
return html`<div class="check-result">
<div class="check-result-description">
<span>${this.renderIcon(result.resultType)}</span>
<p>${unsafeHTML(result.message)}</p>
</div>
${result.actions ? this.renderActions(result.actions) : nothing}
${result.readMoreLink
? html`<uui-button
label="Read more"
color="default"
look="primary"
target="_blank"
href="${result.readMoreLink}">
Read more
<uui-icon name="umb:out"></uui-icon>
</uui-button>`
: nothing}
</div>`;
})}
</div>
</uui-icon-registry-essential>`;
}
private renderIcon(type?: StatusResultType) {
switch (type) {
case StatusResultType.SUCCESS:
return html`<uui-icon style="color: var(--uui-color-positive);" name="check"></uui-icon>`;
case StatusResultType.WARNING:
return html`<uui-icon style="color: var(--uui-color-warning);" name="alert"></uui-icon>`;
case StatusResultType.ERROR:
return html`<uui-icon style="color: var(--uui-color-danger);" name="remove"></uui-icon>`;
case StatusResultType.INFO:
return html`<uui-icon style="color:black;" name="info"></uui-icon>`;
default:
return nothing;
}
}
private renderActions(actions: HealthCheckAction[]) {
if (actions.length)
return html` <div class="action-wrapper">
${actions.map(
(action) =>
html`<umb-dashboard-health-check-action
.action=${action}
@action-executed=${() => this._buttonHandler()}></umb-dashboard-health-check-action>`
)}
</div>`;
else return nothing;
}
}
export default UmbDashboardHealthCheckGroupElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-health-check-group': UmbDashboardHealthCheckGroupElement;
}
}

View File

@@ -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`
<uui-box>
<div slot="headline" class="flex">
Health Check
<uui-button
label="Perform all checks"
color="positive"
look="primary"
.state="${this._buttonState}"
@click="${this._onHealthCheckHandler}">
Perform all checks
</uui-button>
</div>
<div class="grid">
<umb-extension-slot type="healthCheck" default-element="umb-health-check-group-box-overview">
</umb-extension-slot>
</div>
</uui-box>
`;
}
}
export default UmbDashboardHealthCheckOverviewElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-health-check-overview': UmbDashboardHealthCheckOverviewElement;
}
}

View File

@@ -53,6 +53,19 @@ const dashboards: Array<ManifestDashboard> = [
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',

View File

@@ -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;
}
`,
];

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 <strong>your@email.here</strong>.`,
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',
},
],
},
];

View File

@@ -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<PagedHealthCheckGroup>({ 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<HealthCheckGroup>(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<boolean>(true)
);
}),
];

View File

@@ -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<PagedRedirectUrl>(filteredPagedData));
} else {
const items = PagedRedirectUrlData.items.slice(skip, skip + take);
const PagedData: PagedRedirectUrl = {
total: PagedRedirectUrlData.total,
items,
};
return res(ctx.status(200), ctx.json<PagedRedirectUrl>(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<RedirectUrlStatus>(UrlTracker));
const PagedRedirectUrlObject = _getRedirectUrlByKey(key);
return res(ctx.status(200), ctx.json<PagedRedirectUrl>(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<any>(PagedRedirectUrlObject));
}),
/*rest.get(umbracoPath('/redirect-management/status'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<RedirectUrlStatus>(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<any>(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,
};

View File

@@ -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,
];