diff --git a/src/Umbraco.Web.UI.Client/schemas/api/api.yml b/src/Umbraco.Web.UI.Client/schemas/api/api.yml index 0668c60071..6aa6d277f7 100644 --- a/src/Umbraco.Web.UI.Client/schemas/api/api.yml +++ b/src/Umbraco.Web.UI.Client/schemas/api/api.yml @@ -347,6 +347,58 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + /telemetry/ConsentLevel: + get: + operationId: GetConsentLevel + responses: + '200': + description: 200 response + content: + application/json: + schema: + $ref: '#/components/schemas/ConsentLevelSettings' + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + post: + operationId: PostConsentLevel + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConsentLevelSettings' + required: true + responses: + '201': + description: 201 response + '400': + description: 400 response + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /telemetry/ConsentLevels: + get: + operationId: ConsentLevels + responses: + '200': + description: 200 response + content: + application/json: + schema: + type: array + items: + type: string + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' components: schemas: ConsentLevel: @@ -1135,3 +1187,10 @@ components: type: string required: - sections + ConsentLevelSettings: + type: object + properties: + telemetryLevel: + $ref: '#/components/schemas/ConsentLevel' + required: + - telemetryLevel diff --git a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts index 4aed7c754a..3216149c08 100644 --- a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts +++ b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts @@ -67,6 +67,13 @@ export interface paths { "/user/sections": { get: operations["GetAllowedSections"]; }; + "/telemetry/ConsentLevel": { + get: operations["GetConsentLevel"]; + post: operations["PostConsentLevel"]; + }; + "/telemetry/ConsentLevels": { + get: operations["ConsentLevels"]; + }; } export interface components { @@ -380,6 +387,9 @@ export interface components { AllowedSectionsResponse: { sections: string[]; }; + ConsentLevelSettings: { + telemetryLevel: components["schemas"]["ConsentLevel"]; + }; }; } @@ -723,6 +733,56 @@ export interface operations { }; }; }; + GetConsentLevel: { + responses: { + /** 200 response */ + 200: { + content: { + "application/json": components["schemas"]["ConsentLevelSettings"]; + }; + }; + /** default response */ + default: { + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; + PostConsentLevel: { + parameters: {}; + responses: { + /** 201 response */ + 201: unknown; + /** 400 response */ + 400: { + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConsentLevelSettings"]; + }; + }; + }; + ConsentLevels: { + responses: { + /** 200 response */ + 200: { + content: { + "application/json": string[]; + }; + }; + /** default response */ + default: { + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; } export interface external {} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.element.ts index ef57dea972..dc9fc75d7d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.element.ts @@ -1,15 +1,150 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; + +import { customElement, state } from 'lit/decorators.js'; + +import type { TelemetryModel } from '../../../core/models'; +import { getConsentLevel, getConsentLevels, postConsentLevel } from '../../../core/api/fetcher'; + +export type SettingOption = 'Minimal' | 'Basic' | 'Detailed'; @customElement('umb-dashboard-telemetry') export class UmbDashboardTelemetryElement extends LitElement { - static styles = [UUITextStyles, css``]; + static styles = [ + UUITextStyles, + css` + .italic { + font-style: italic; + } + `, + ]; + + @state() + private _telemetryFormData: TelemetryModel['level'] = 'Basic'; + + @state() + private _telemetryLevels: TelemetryModel['level'][] = []; + + @state() + private _errorMessage = ''; + + constructor() { + super(); + } + + connectedCallback(): void { + super.connectedCallback(); + this._setup(); + } + + private async _setup() { + try { + const consentLevels = await getConsentLevels({}); + this._telemetryLevels = consentLevels.data as TelemetryModel['level'][]; + } catch (e) { + this._errorMessage; + } + try { + const consentSetting = await getConsentLevel({}); + this._telemetryFormData = consentSetting.data.telemetryLevel as TelemetryModel['level']; + } catch (e) { + if (e instanceof getConsentLevel.Error) { + this._errorMessage = e.data.detail; + } + } + } + + private _handleSubmit = async (e: CustomEvent) => { + e.stopPropagation(); + try { + await postConsentLevel({ telemetryLevel: this._telemetryFormData }); + } catch (e) { + if (e instanceof postConsentLevel.Error) { + const error = e.getActualType(); + if (error.status === 400) { + this._errorMessage = error.data.detail || 'Unknown error, please try again'; + } + } else { + this._errorMessage = 'Unknown error, please try again'; + } + } + }; + + disconnectedCallback(): void { + super.disconnectedCallback(); + } + + private _handleChange(e: InputEvent) { + const target = e.target as HTMLInputElement; + this._telemetryFormData = this._telemetryLevels[parseInt(target.value) - 1]; + } + + private get _selectedTelemetryIndex() { + return this._telemetryLevels?.findIndex((x) => x === this._telemetryFormData) ?? 0; + } + + private get _selectedTelemetry() { + return this._telemetryLevels?.find((x) => x === this._telemetryFormData) ?? this._telemetryLevels[0]; + } + + private get _selectedTelemetryDescription() { + switch (this._selectedTelemetry) { + case 'Minimal': + return 'We will only send an anonymized site ID to let us know that the site exists.'; + case 'Basic': + return 'We will send an anonymized site ID, Umbraco version, and packages installed.'; + case 'Detailed': + return `We will send: + + We might change what we send on the Detailed level in the future. If so, it will be listed above. + By choosing "Detailed" you agree to current and future anonymized information being collected.`; + default: + return 'Could not find description for this setting'; + } + } + + private _renderSettingSlider() { + if (!this._telemetryLevels || this._telemetryLevels.length < 1) return; + + return html` + +

${this._selectedTelemetry}

+

${unsafeHTML(this._selectedTelemetryDescription)}

+ `; + } render() { return html` -

Telemetry

+

Consent for telemetry data

+
+

+ In order to improve Umbraco and add new functionality based on as relevant information as possible, we would + like to collect system- and usage information from your installation. Aggregate data will be shared on a + regular basis as well as learnings from these metrics. Hopefully, you will help us collect some valuable + data. +

+ We WILL NOT collect any personal data such as content, code, user information, and all data + will be fully anonymized. +

+ ${this._renderSettingSlider()} + + Save + +
`; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.test.ts new file mode 100644 index 0000000000..01b34283e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/telemetry/dashboard-telemetry.test.ts @@ -0,0 +1,19 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { defaultA11yConfig } from '../../../core/helpers/chai'; +import { UmbDashboardTelemetryElement } from './dashboard-telemetry.element'; + +describe('UmbDashboardTelemetryElement', () => { + let element: UmbDashboardTelemetryElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbDashboardTelemetryElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).to.be.accessible(defaultA11yConfig); + }); +}); 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 c8efc59d06..9a45dc0f8e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts +++ b/src/Umbraco.Web.UI.Client/src/core/api/fetcher.ts @@ -21,6 +21,10 @@ 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 getConsentLevels = fetcher.path('/telemetry/ConsentLevels').method('get').create(); +export const getConsentLevel = fetcher.path('/telemetry/ConsentLevel').method('get').create(); +export const postConsentLevel = fetcher.path('/telemetry/ConsentLevel').method('post').create(); + // Property Editors export const getPropertyEditorsList = fetcher.path('/property-editors/list').method('get').create(); @@ -36,3 +40,4 @@ export const getPublishedCacheStatus = fetcher.path('/published-cache/status').m export const postPublishedCacheReload = fetcher.path('/published-cache/reload').method('post').create(); export const postPublishedCacheRebuild = fetcher.path('/published-cache/rebuild').method('post').create(); export const getPublishedCacheCollect = fetcher.path('/published-cache/collect').method('get').create(); + diff --git a/src/Umbraco.Web.UI.Client/src/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/core/models/index.ts index 0c64b04e82..be343c04ad 100644 --- a/src/Umbraco.Web.UI.Client/src/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/models/index.ts @@ -30,6 +30,7 @@ export type ManifestEntrypoint = components['schemas']['IManifestEntrypoint']; export type ManifestCustom = components['schemas']['IManifestCustom']; export type ManifestPackageView = components['schemas']['IManifestPackageView']; export type PackageInstalled = components['schemas']['PackageInstalled']; +export type ConsentLevelSettings = components['schemas']['ConsentLevelSettings']; // Property Editors export type PropertyEditorsListResponse = components['schemas']['PropertyEditorsListResponse']; 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 866364821d..2f04f670b8 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts @@ -8,6 +8,7 @@ import { handlers as publishedStatusHandlers } from './domains/published-status. 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 telemetryHandlers } from './domains/telemetry.handlers'; import { handlers as propertyEditorHandlers } from './domains/property-editor.handlers'; const handlers = [ @@ -21,6 +22,7 @@ const handlers = [ ...treeHandlers, ...propertyEditorHandlers, ...manifestsHandlers.default, + ...telemetryHandlers, ...publishedStatusHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/domains/telemetry.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/domains/telemetry.handlers.ts new file mode 100644 index 0000000000..84555c27a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/domains/telemetry.handlers.ts @@ -0,0 +1,33 @@ +import { rest } from 'msw'; + +import umbracoPath from '../../core/helpers/umbraco-path'; +import type { ConsentLevelSettings, TelemetryModel } from '../../core/models'; + +let telemetryLevel: TelemetryModel['level'] = 'Basic'; + +export const handlers = [ + rest.get(umbracoPath('/telemetry/ConsentLevel'), (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json({ + telemetryLevel, + }) + ); + }), + rest.get(umbracoPath('/telemetry/ConsentLevels'), (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json(['Minimal', 'Basic', 'Detailed']) + ); + }), + + rest.post(umbracoPath('/telemetry/ConsentLevel'), async (_req, res, ctx) => { + telemetryLevel = (await _req.json()).telemetryLevel; + 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 4de1be3966..53252b4f1b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/e2e-handlers.ts @@ -7,6 +7,7 @@ import { handlers as publishedStatusHandlers } from './domains/published-status. 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 telemetryHandlers } from './domains/telemetry.handlers'; import { handlers as treeHandlers } from './domains/entity.handlers'; import { handlers as propertyEditorHandlers } from './domains/property-editor.handlers'; @@ -21,6 +22,7 @@ export const handlers = [ ...dataTypeHandlers, ...documentTypeHandlers, ...manifestsHandlers.default, + ...telemetryHandlers, ...publishedStatusHandlers, ...treeHandlers, ...propertyEditorHandlers, 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 e3e2cbb32d..bc579440a0 100644 --- a/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts @@ -100,6 +100,19 @@ export const internalManifests: Array Promise import('./backoffice/dashboards/telemetry/dashboard-telemetry.element'), + meta: { + label: 'Telemetry Data', + sections: ['Umb.Section.Settings'], + pathname: 'telemetry', // TODO: how do we want to support pretty urls? + weight: 0, + }, + }, { type: 'dashboard', alias: 'Umb.Dashboard.ExamineManagement', 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 4248d94e49..7a52baaf5d 100644 --- a/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts +++ b/src/Umbraco.Web.UI.Client/temp-schema-generator/api.ts @@ -4,6 +4,7 @@ import './publishedstatus'; import './server'; import './upgrader'; import './user'; +import './telemetry'; import './property-editors'; import { api } from '@airtasker/spot'; diff --git a/src/Umbraco.Web.UI.Client/temp-schema-generator/telemetry.ts b/src/Umbraco.Web.UI.Client/temp-schema-generator/telemetry.ts new file mode 100644 index 0000000000..3ebd5f85da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/temp-schema-generator/telemetry.ts @@ -0,0 +1,47 @@ +import { body, defaultResponse, endpoint, request, response } from '@airtasker/spot'; +import { TelemetryModel } from './installer'; + +import { ProblemDetails } from './models'; + +@endpoint({ + method: 'GET', + path: '/telemetry/ConsentLevel', +}) +export class GetConsentLevel { + @response({ status: 200 }) + success(@body body: ConsentLevelSettings) {} + + @defaultResponse + default(@body body: ProblemDetails) {} +} + +@endpoint({ + method: 'GET', + path: '/telemetry/ConsentLevels', +}) +export class ConsentLevels { + @response({ status: 200 }) + success(@body body: string[]) {} + + @defaultResponse + default(@body body: ProblemDetails) {} +} + +@endpoint({ + method: 'POST', + path: '/telemetry/ConsentLevel', +}) +export class PostConsentLevel { + @request + request(@body body: ConsentLevelSettings) {} + + @response({ status: 201 }) + success() {} + + @response({ status: 400 }) + badRequest(@body body: ProblemDetails) {} +} + +export interface ConsentLevelSettings { + telemetryLevel: TelemetryModel['level']; +}