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._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`}

+ +
this._onActionClick(e)}> + ${this._renderValueRequired()} + + ${this.action.name || 'Action'} + +
+
+
`; + } + + 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?.name}

+ + Perform checks + +
+
+ ${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, ];