From 0db991ccb8fc987eae9dd35c60d86070701db09e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 24 Aug 2022 11:00:23 +0200 Subject: [PATCH 1/2] add call to validate database endpoint if database requires it --- src/Umbraco.Web.UI.Client/schemas/api/api.yml | 9 +- .../schemas/generated-schema.ts | 5 +- .../installer/installer-database.element.ts | 58 ++++++++- .../src/installer/installer.stories.ts | 4 +- .../src/mocks/domains/install.handlers.ts | 27 ++++- .../temp-schema-generator/installer.ts | 114 +++++++++--------- 6 files changed, 137 insertions(+), 80 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/schemas/api/api.yml b/src/Umbraco.Web.UI.Client/schemas/api/api.yml index 59e42377b1..974f5dc74a 100644 --- a/src/Umbraco.Web.UI.Client/schemas/api/api.yml +++ b/src/Umbraco.Web.UI.Client/schemas/api/api.yml @@ -46,7 +46,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/InstallValidateDatabaseRequest' + $ref: '#/components/schemas/InstallSetupDatabaseConfiguration' required: true responses: '201': @@ -341,13 +341,6 @@ components: required: - user - telemetryLevel - InstallValidateDatabaseRequest: - type: object - properties: - database: - $ref: '#/components/schemas/InstallSetupDatabaseConfiguration' - required: - - database ServerStatus: type: string enum: diff --git a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts index 664b9b012f..4c9f3aa57f 100644 --- a/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts +++ b/src/Umbraco.Web.UI.Client/schemas/generated-schema.ts @@ -102,9 +102,6 @@ export interface components { telemetryLevel: components["schemas"]["ConsentLevel"]; database?: components["schemas"]["InstallSetupDatabaseConfiguration"]; }; - InstallValidateDatabaseRequest: { - database: components["schemas"]["InstallSetupDatabaseConfiguration"]; - }; /** @enum {string} */ ServerStatus: "running" | "must-install" | "must-upgrade"; StatusResponse: { @@ -184,7 +181,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["InstallValidateDatabaseRequest"]; + "application/json": components["schemas"]["InstallSetupDatabaseConfiguration"]; }; }; }; diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts b/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts index f3ccdab29d..ab9f21e11b 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts +++ b/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts @@ -1,8 +1,9 @@ import { UUIButtonElement } from '@umbraco-ui/uui'; -import { css, CSSResultGroup, html, LitElement } from 'lit'; +import { css, CSSResultGroup, html, LitElement, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { Subscription } from 'rxjs'; +import { postInstallSetup, postInstallValidateDatabase } from '../core/api/fetcher'; import { UmbContextConsumerMixin } from '../core/context'; import { UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models'; import { UmbInstallerContext } from './installer-context'; @@ -69,6 +70,11 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { margin-left: auto; min-width: 120px; } + + .error { + color: var(--uui-color-danger); + padding: var(--uui-size-space-2) 0; + } `, ]; @@ -90,6 +96,9 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { @state() private _installerStore?: UmbInstallerContext; + @state() + private _validationErrorMessage = ''; + private storeDataSubscription?: Subscription; private storeSettingsSubscription?: Subscription; @@ -136,15 +145,19 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { this._installerStore?.appendData({ database }); } - private _handleSubmit = (e: SubmitEvent) => { - e.preventDefault(); + private _handleSubmit = async (evt: SubmitEvent) => { + evt.preventDefault(); - const form = e.target as HTMLFormElement; + const form = evt.target as HTMLFormElement; if (!form) return; const isValid = form.checkValidity(); if (!isValid) return; + if (!this._installerStore) return; + + this._installButton.state = 'waiting'; + // Only append the database if it's not pre-configured if (!this._preConfiguredDatabase) { const formData = new FormData(form); @@ -154,6 +167,39 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { const server = formData.get('server') as string; const name = formData.get('name') as string; const useIntegratedAuthentication = formData.has('useIntegratedAuthentication'); + const connectionString = formData.get('connectionString') as string; + + // Validate connection + const selectedDatabase = this._databases.find((x) => x.id === id); + if (selectedDatabase?.requiresConnectionTest) { + try { + let databaseDetails: UmbracoPerformInstallDatabaseConfiguration = {}; + + if (connectionString) { + databaseDetails.connectionString = connectionString; + } else { + databaseDetails = { + id, + username, + password, + server, + useIntegratedAuthentication, + name, + }; + } + await postInstallValidateDatabase(databaseDetails); + } catch (e) { + if (e instanceof postInstallSetup.Error) { + const error = e.getActualType(); + console.warn('Database validation failed', error.data); + this._validationErrorMessage = error.data.detail ?? 'Could not verify database connection'; + } else { + this._validationErrorMessage = 'A server error happened when trying to validate the database'; + } + this._installButton.state = 'failed'; + return; + } + } const database = { ...this._installerStore?.getData().database, @@ -163,14 +209,13 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { server, name, useIntegratedAuthentication, + connectionString, } as UmbracoPerformInstallDatabaseConfiguration; this._installerStore?.appendData({ database }); } this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true })); - - this._installButton.state = 'waiting'; }; private _onBack() { @@ -321,6 +366,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { ${this._preConfiguredDatabase ? this._renderPreConfiguredDatabase(this._preConfiguredDatabase) : this._renderDatabaseSelection()} + ${this._validationErrorMessage ? html`
${this._validationErrorMessage}
` : nothing}
diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts b/src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts index 6d44af66fc..12d25358d6 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts @@ -48,7 +48,7 @@ export const Step3Database: Story = () => html` html` { @@ -73,6 +78,26 @@ export const handlers = [ ); }), + rest.post(umbracoPath('/install/validateDatabase'), async (req, res, ctx) => { + const body = await req.json(); + + if (body.name === 'validate') { + return res( + ctx.status(400), + ctx.json({ + type: 'connection', + status: 400, + detail: 'Database connection failed', + }) + ); + } + + return res( + // Respond with a 200 status code + ctx.status(201) + ); + }), + rest.post(umbracoPath('/install/setup'), async (req, res, ctx) => { await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds const body = await req.json(); diff --git a/src/Umbraco.Web.UI.Client/temp-schema-generator/installer.ts b/src/Umbraco.Web.UI.Client/temp-schema-generator/installer.ts index f4d0f4df6b..81a2b060aa 100644 --- a/src/Umbraco.Web.UI.Client/temp-schema-generator/installer.ts +++ b/src/Umbraco.Web.UI.Client/temp-schema-generator/installer.ts @@ -3,103 +3,99 @@ import { body, defaultResponse, endpoint, request, response } from '@airtasker/s import { ProblemDetails } from './models'; @endpoint({ - method: 'GET', - path: '/install/settings', + method: 'GET', + path: '/install/settings', }) export class GetInstallSettings { - @response({ status: 200 }) - success(@body body: InstallSettingsResponse) {} + @response({ status: 200 }) + success(@body body: InstallSettingsResponse) {} - @defaultResponse - default(@body body: ProblemDetails) {} + @defaultResponse + default(@body body: ProblemDetails) {} } @endpoint({ - method: 'POST', - path: '/install/setup', + method: 'POST', + path: '/install/setup', }) export class PostInstallSetup { - @request - request(@body body: InstallSetupRequest) {} + @request + request(@body body: InstallSetupRequest) {} - @response({ status: 201 }) - success() {} + @response({ status: 201 }) + success() {} - @response({ status: 400 }) - badRequest(@body body: ProblemDetails) {} + @response({ status: 400 }) + badRequest(@body body: ProblemDetails) {} } @endpoint({ - method: 'POST', - path: '/install/validateDatabase', + method: 'POST', + path: '/install/validateDatabase', }) export class PostInstallValidateDatabase { - @request - request(@body body: InstallValidateDatabaseRequest) {} + @request + request(@body body: InstallSetupDatabaseConfiguration) {} - @response({ status: 201 }) - success() {} + @response({ status: 201 }) + success() {} - @response({ status: 400 }) - badRequest(@body body: ProblemDetails) {} + @response({ status: 400 }) + badRequest(@body body: ProblemDetails) {} } export interface InstallSetupRequest { - user: InstallSetupUserConfiguration; - telemetryLevel: ConsentLevel; - database?: InstallSetupDatabaseConfiguration; -} - -export interface InstallValidateDatabaseRequest { - database: InstallSetupDatabaseConfiguration; + user: InstallSetupUserConfiguration; + telemetryLevel: ConsentLevel; + database?: InstallSetupDatabaseConfiguration; } export interface InstallSettingsResponse { - user: InstallUserModel; - databases: InstallDatabaseModel[]; + user: InstallUserModel; + databases: InstallDatabaseModel[]; } export interface InstallUserModel { - minCharLength: number; - minNonAlphaNumericLength: number; - consentLevels: TelemetryModel[]; + minCharLength: number; + minNonAlphaNumericLength: number; + consentLevels: TelemetryModel[]; } export interface InstallSetupUserConfiguration { - name: string; - email: string; - password: string; - subscribeToNewsletter: boolean; + name: string; + email: string; + password: string; + subscribeToNewsletter: boolean; } export interface InstallSetupDatabaseConfiguration { - id?: string; - server?: string | null; - password?: string | null; - username?: string | null; - name?: string | null; - providerName?: string | null; - useIntegratedAuthentication?: boolean | null; - connectionString?: string | null; + id?: string; + server?: string | null; + password?: string | null; + username?: string | null; + name?: string | null; + providerName?: string | null; + useIntegratedAuthentication?: boolean | null; + connectionString?: string | null; } export interface TelemetryModel { - level: ConsentLevel; - description: string; + level: ConsentLevel; + description: string; } export interface InstallDatabaseModel { - id: string; - sortOrder: number; - displayName: string; - defaultDatabaseName: string; - providerName: null | string; - isConfigured: boolean; - requiresServer: boolean; - serverPlaceholder: null | string; - requiresCredentials: boolean; - supportsIntegratedAuthentication: boolean; - requiresConnectionTest: boolean; + id: string; + sortOrder: number; + displayName: string; + defaultDatabaseName: string; + providerName: null | string; + isConfigured: boolean; + requiresServer: boolean; + serverPlaceholder: null | string; + requiresCredentials: boolean; + supportsIntegratedAuthentication: boolean; + requiresConnectionTest: boolean; } export type ConsentLevel = 'Minimal' | 'Basic' | 'Detailed'; From d6b34bf1e3be7cc8c16bee4a3544933518a1ca2e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 24 Aug 2022 11:08:12 +0200 Subject: [PATCH 2/2] fix a11y warnings --- .../installer/installer-database.element.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts b/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts index ab9f21e11b..4e3614bcbe 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts +++ b/src/Umbraco.Web.UI.Client/src/installer/installer-database.element.ts @@ -57,7 +57,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { margin-bottom: var(--uui-size-layout-3); } - h4 { + h2 { margin: 0; } @@ -250,14 +250,15 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { } private _renderServer = () => html` -

Connection

+

Connection


- Server + Server address `; private _renderCredentials = () => html` -

Credentials

+

Credentials


Use integrated authentication + .checked=${this.databaseFormData.useIntegratedAuthentication || false}> + Use integrated authentication + ${!this.databaseFormData.useIntegratedAuthentication @@ -300,6 +302,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { .value=${this.databaseFormData.username ?? ''} id="username" name="username" + label="Username" @input=${this._handleChange} required required-message="Username is required">
@@ -312,6 +315,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) { .value=${this.databaseFormData.password ?? ''} id="password" name="password" + label="Password" @input=${this._handleChange} autocomplete="new-password" required