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