diff --git a/src/Umbraco.Web.UI.Client/schemas/api/api.yml b/src/Umbraco.Web.UI.Client/schemas/api/api.yml index 3c4e98e020..cf8864266f 100644 --- a/src/Umbraco.Web.UI.Client/schemas/api/api.yml +++ b/src/Umbraco.Web.UI.Client/schemas/api/api.yml @@ -105,6 +105,35 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + /published-cache/status: + get: + operationId: PublishedCacheStatus + responses: + '200': + description: 200 response + content: + application/json: + schema: + type: string + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /published-cache/reload: + post: + operationId: PublishedCacheReload + parameters: [] + responses: + '201': + description: 201 response + '400': + description: 400 response + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /server/status: get: operationId: GetStatus diff --git a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts index fdc1e66117..2c4423664d 100644 --- a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts +++ b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts @@ -22,6 +22,12 @@ export interface paths { "/manifests/packages/installed": { get: operations["ManifestsPackagesInstalled"]; }; + "/published-cache/status": { + get: operations["PublishedCacheStatus"]; + }; + "/published-cache/reload": { + post: operations["PublishedCacheReload"]; + }; "/server/status": { get: operations["GetStatus"]; }; @@ -418,6 +424,35 @@ export interface operations { }; }; }; + PublishedCacheStatus: { + responses: { + /** 200 response */ + 200: { + content: { + "application/json": string; + }; + }; + /** default response */ + default: { + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; + PublishedCacheReload: { + parameters: {}; + responses: { + /** 201 response */ + 201: unknown; + /** 400 response */ + 400: { + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; GetStatus: { responses: { /** 200 response */ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.element.ts index 04d478f2af..1ccad9f1eb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.element.ts @@ -1,15 +1,107 @@ +import { UUIButtonState } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; + +import { getPublishedCacheStatus, postPublishedCacheReload } from '../../../core/api/fetcher'; +import { UmbContextConsumerMixin } from '../../../core/context'; +import { UmbNotificationService } from '../../../core/services/notification'; +import { UmbNotificationDefaultData } from '../../../core/services/notification/layouts/default'; @customElement('umb-dashboard-published-status') -export class UmbDashboardPublishedStatusElement extends LitElement { - static styles = [UUITextStyles, css``]; +export class UmbDashboardPublishedStatusElement extends UmbContextConsumerMixin(LitElement) { + static styles = [UUITextStyles, css` + uui-box + uui-box { + margin-top: var(--uui-size-space-5); + } + uui-box p:first-child { + margin-block-start: 0; + } + `]; + + @state() + private _publishedStatusText = ''; + + @state() + private _buttonState: UUIButtonState = undefined; + + private _notificationService?: UmbNotificationService; + + constructor() { + super(); + + this.consumeContext('umbNotificationService', (notificationService: UmbNotificationService) => { + this._notificationService = notificationService; + }); + } + + connectedCallback() { + super.connectedCallback(); + + this._getPublishedStatus(); + } + + private async _getPublishedStatus() { + try { + const { data } = await getPublishedCacheStatus({}); + this._publishedStatusText = data; + } catch (e) { + if (e instanceof getPublishedCacheStatus.Error) { + const error = e.getActualType(); + const data: UmbNotificationDefaultData = { message: error.data.detail ?? 'Something went wrong' }; + this._notificationService?.peek('danger', { data }); + } + } + } + + private async _onRefreshCacheHandler() { + this._buttonState = 'waiting'; + try { + await postPublishedCacheReload({}); + this._getPublishedStatus(); + this._buttonState = 'success'; + } catch (e) { + this._buttonState = 'failed'; + if (e instanceof postPublishedCacheReload.Error) { + const error = e.getActualType(); + const data: UmbNotificationDefaultData = { message: error.data.detail ?? 'Something went wrong' }; + this._notificationService?.peek('danger', { data }); + } + } + } + + private async _onReloadCacheHandler() { + await undefined; + } + + private async _onRebuildCacheHandler() { + await undefined; + } + + private async _onSnapshotCacheHandler() { + await undefined; + } render() { return html` - -

Published Status

+ +

${this._publishedStatusText}

+ Refresh Status +
+ + +

This button lets you reload the in-memory cache, by entirely reloading it from the database cache (but it does not rebuild that database cache). This is relatively fast. Use it when you think that the memory cache has not been properly refreshed, after some events triggered—which would indicate a minor Umbraco issue. (note: triggers the reload on all servers in an LB environment).

+ Reload Memory Cache +
+ + +

This button lets you rebuild the database cache, ie the content of the cmsContentNu table. Rebuilding can be expensive. Use it when reloading is not enough, and you think that the database cache has not been properly generated—which would indicate some critical Umbraco issue.

+ Rebuild Database Cache +
+ + +

This button lets you trigger a NuCache snapshots collection (after running a fullCLR GC). Unless you know what that means, you probably do not need to use it.

+ Snapshot Internal Cache
`; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.test.ts new file mode 100644 index 0000000000..64cd1bc0ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/published-status/dashboard-published-status.test.ts @@ -0,0 +1,20 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import { defaultA11yConfig } from '../../../core/helpers/chai'; +import { UmbDashboardPublishedStatusElement } from './dashboard-published-status.element'; + +describe('UmbDashboardPublishedStatus', () => { + let element: UmbDashboardPublishedStatusElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbDashboardPublishedStatusElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts index fd4662b525..ed247e1079 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts @@ -31,6 +31,10 @@ export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { padding: var(--uui-size-space-5); display: block; } + + #scroll-container { + height: 500px; + } `, ]; @@ -146,7 +150,9 @@ export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { render() { return html` ${this._renderNavigation()} - + + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts b/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts index 034bd7851c..4d8899883f 100644 --- a/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts +++ b/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts @@ -21,3 +21,5 @@ export const getUpgradeSettings = fetcher.path('/upgrade/settings').method('get' export const PostUpgradeAuthorize = fetcher.path('/upgrade/authorize').method('post').create(); export const getManifests = fetcher.path('/manifests').method('get').create(); export const getPackagesInstalled = fetcher.path('/manifests/packages/installed').method('get').create(); +export const getPublishedCacheStatus = fetcher.path('/published-cache/status').method('get').create(); +export const postPublishedCacheReload = fetcher.path('/published-cache/reload').method('post').create(); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts index edd2b8e1ca..c2c7df7f6c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts @@ -1,12 +1,13 @@ -import { handlers as contentHandlers } from './domains/node.handlers'; import { handlers as dataTypeHandlers } from './domains/data-type.handlers'; import { handlers as documentTypeHandlers } from './domains/document-type.handlers'; +import { handlers as treeHandlers } from './domains/entity.handlers'; import { handlers as installHandlers } from './domains/install.handlers'; import * as manifestsHandlers from './domains/manifests.handlers'; +import { handlers as contentHandlers } from './domains/node.handlers'; +import { handlers as publishedStatusHandlers } from './domains/published-status.handlers'; import * as serverHandlers from './domains/server.handlers'; import { handlers as upgradeHandlers } from './domains/upgrade.handlers'; import { handlers as userHandlers } from './domains/user.handlers'; -import { handlers as treeHandlers } from './domains/entity.handlers'; const handlers = [ serverHandlers.serverVersionHandler, @@ -18,6 +19,7 @@ const handlers = [ ...documentTypeHandlers, ...treeHandlers, ...manifestsHandlers.default, + ...publishedStatusHandlers, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/domains/published-status.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/domains/published-status.handlers.ts new file mode 100644 index 0000000000..1cef7b9433 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/domains/published-status.handlers.ts @@ -0,0 +1,24 @@ +import { rest } from 'msw'; + +import umbracoPath from '../../core/helpers/umbraco-path'; + +export const handlers = [ + rest.get(umbracoPath('/published-cache/status'), (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json( + 'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.' + ) + ); + }), + + rest.post(umbracoPath('/published-cache/reload'), 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(201) + ); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts index 09e9121f40..1f9cf1acc3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts @@ -1,8 +1,9 @@ -import { handlers as contentHandlers } from './domains/node.handlers'; import { handlers as dataTypeHandlers } from './domains/data-type.handlers'; import { handlers as documentTypeHandlers } from './domains/document-type.handlers'; import { handlers as installHandlers } from './domains/install.handlers'; import * as manifestsHandlers from './domains/manifests.handlers'; +import { handlers as contentHandlers } from './domains/node.handlers'; +import { handlers as publishedStatusHandlers } from './domains/published-status.handlers'; import * as serverHandlers from './domains/server.handlers'; import { handlers as upgradeHandlers } from './domains/upgrade.handlers'; import { handlers as userHandlers } from './domains/user.handlers'; @@ -18,4 +19,5 @@ export const handlers = [ ...dataTypeHandlers, ...documentTypeHandlers, ...manifestsHandlers.default, + ...publishedStatusHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts b/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts index d24a1ab104..a6655933fe 100644 --- a/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts @@ -118,6 +118,18 @@ export const internalManifests: Array Promise import('./backoffice/dashboards/published-status/dashboard-published-status.element'), + meta: { + sections: ['Umb.Section.Settings'], + pathname: 'published-status', // TODO: how to we want to support pretty urls? + weight: 9, + }, + }, { type: 'dashboard', alias: 'Umb.Dashboard.MediaManagement', diff --git a/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts b/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts index c7eb465583..551eb45cec 100644 --- a/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts +++ b/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts @@ -1,5 +1,6 @@ import './installer'; import './manifests'; +import './publishedstatus'; import './server'; import './upgrader'; import './user'; diff --git a/src/Umbraco.Web.UI.Client/temp-schema-generator/publishedstatus.ts b/src/Umbraco.Web.UI.Client/temp-schema-generator/publishedstatus.ts new file mode 100644 index 0000000000..7079625166 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/temp-schema-generator/publishedstatus.ts @@ -0,0 +1,30 @@ +import { body, defaultResponse, endpoint, request, response } from '@airtasker/spot'; + +import { ProblemDetails } from './models'; + +@endpoint({ + method: 'GET', + path: '/published-cache/status', +}) +export class PublishedCacheStatus { + @response({ status: 200 }) + success(@body body: string) {} + + @defaultResponse + default(@body body: ProblemDetails) {} +} + +@endpoint({ + method: 'POST', + path: '/published-cache/reload', +}) +export class PublishedCacheReload { + @request + request() {} + + @response({ status: 201 }) + success() {} + + @response({ status: 400 }) + badRequest(@body body: ProblemDetails) {} +}