From 7e5b864299799c2f4509fd1ba118500bf41852f6 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Sat, 19 Oct 2019 15:42:49 -0700 Subject: [PATCH] BDC Dashboard connection retry (#7784) * Open cluster dashboard * Remove old translated strings and update var name * Add exported auth type * Add newline * Add connection retry for dashboard * Change getMainSectionComponent to return multiple (no undefined) * Move try/catch to withConnectRetry * Add connection retry for dashboard * Change getMainSectionComponent to return multiple (no undefined) * Move try/catch to withConnectRetry --- .../controller/clusterControllerApi.ts | 233 +++++++----- .../dialog/bdcDashboardModel.ts | 4 +- .../dialog/connectControllerDialog.ts | 50 +++ .../bigDataCluster/dialog/hdfsDialogBase.ts | 239 +++++++++++++ .../bigDataCluster/dialog/mountHdfsDialog.ts | 334 ++++-------------- .../big-data-cluster/src/common/promise.ts | 25 ++ 6 files changed, 528 insertions(+), 357 deletions(-) create mode 100644 extensions/big-data-cluster/src/bigDataCluster/dialog/connectControllerDialog.ts create mode 100644 extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts create mode 100644 extensions/big-data-cluster/src/common/promise.ts diff --git a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts index 1467503944..c238513f29 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts @@ -10,6 +10,7 @@ import { BdcRouterApi, Authentication, EndpointModel, BdcStatusModel, DefaultApi import { TokenRouterApi } from './clusterApiGenerated2'; import { AuthType } from '../constants'; import * as nls from 'vscode-nls'; +import { ConnectControllerDialog, ConnectControllerModel } from '../dialog/connectControllerDialog'; const localize = nls.loadMessageBundle(); @@ -89,6 +90,8 @@ export class ClusterController { private authPromise: Promise; private _url: string; + private readonly dialog: ConnectControllerDialog; + private connectionPromise: Promise; constructor(url: string, private authType: AuthType, @@ -105,6 +108,13 @@ export class ClusterController { } else { this.authPromise = this.requestTokenUsingKerberos(ignoreSslVerification); } + this.dialog = new ConnectControllerDialog(new ConnectControllerModel( + { + url: this._url, + auth: this.authType, + username: this.username, + password: this.password + })); } private async requestTokenUsingKerberos(ignoreSslVerification?: boolean): Promise { @@ -134,8 +144,6 @@ export class ClusterController { } } - - private async verifyKerberosSupported(ignoreSslVerification: boolean): Promise { let tokenApi = new TokenRouterApi(this._url); tokenApi.setDefaultAuthentication(new SslAuth(!!ignoreSslVerification)); @@ -150,102 +158,161 @@ export class ClusterController { } } - public async getEndPoints(): Promise { - let auth = await this.authPromise; - let endPointApi = new BdcApiWrapper(this.username, this.password, this._url, auth); + public async getEndPoints(promptConnect: boolean = false): Promise { + return await this.withConnectRetry( + this.getEndpointsImpl, + promptConnect, + localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", this._url)); + } + + private async getEndpointsImpl(self: ClusterController): Promise { + let auth = await self.authPromise; + let endPointApi = new BdcApiWrapper(self.username, self.password, self._url, auth); let options: any = {}; - try { - let result = await endPointApi.endpointsGet(options); - return { - response: result.response as IHttpResponse, - endPoints: result.body as EndpointModel[] - }; - } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", this._url)); - } + + let result = await endPointApi.endpointsGet(options); + return { + response: result.response as IHttpResponse, + endPoints: result.body as EndpointModel[] + }; } - public async getBdcStatus(): Promise { - let auth = await this.authPromise; - const bdcApi = new BdcApiWrapper(this.username, this.password, this._url, auth); - - try { - const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true); - return { - response: bdcStatus.response, - bdcStatus: bdcStatus.body - }; - } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", this._url)); - } + public async getBdcStatus(promptConnect: boolean = false): Promise { + return await this.withConnectRetry( + this.getBdcStatusImpl, + promptConnect, + localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", this._url)); } - public async mountHdfs(mountPath: string, remoteUri: string, credentials: {}): Promise { + private async getBdcStatusImpl(self: ClusterController): Promise { + let auth = await self.authPromise; + const bdcApi = new BdcApiWrapper(self.username, self.password, self._url, auth); + + const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true); + return { + response: bdcStatus.response, + bdcStatus: bdcStatus.body + }; + } + + public async mountHdfs(mountPath: string, remoteUri: string, credentials: {}, promptConnection: boolean = false): Promise { + return await this.withConnectRetry( + this.mountHdfsImpl, + promptConnection, + localize('bdc.error.mountHdfs', "Error creating mount"), + mountPath, + remoteUri, + credentials); + } + + private async mountHdfsImpl(self: ClusterController, mountPath: string, remoteUri: string, credentials: {}): Promise { + let auth = await self.authPromise; + const api = new DefaultApiWrapper(self.username, self.password, self._url, auth); + + const mountStatus = await api.createMount('', '', remoteUri, mountPath, credentials); + return { + response: mountStatus.response, + status: mountStatus.body + }; + } + + public async getMountStatus(mountPath?: string, promptConnect: boolean = false): Promise { + return await this.withConnectRetry( + this.getMountStatusImpl, + promptConnect, + localize('bdc.error.mountHdfs', "Error creating mount"), + mountPath); + } + + private async getMountStatusImpl(self: ClusterController, mountPath?: string): Promise { + const auth = await self.authPromise; + const api = new DefaultApiWrapper(self.username, self.password, self._url, auth); + + const mountStatus = await api.listMounts('', '', mountPath); + return { + response: mountStatus.response, + mount: mountStatus.body ? JSON.parse(mountStatus.body) : undefined + }; + } + + public async refreshMount(mountPath: string, promptConnect: boolean = false): Promise { + return await this.withConnectRetry( + this.refreshMountImpl, + promptConnect, + localize('bdc.error.refreshHdfs', "Error refreshing mount"), + mountPath); + } + + private async refreshMountImpl(self: ClusterController, mountPath: string): Promise { + const auth = await self.authPromise; + const api = new DefaultApiWrapper(self.username, self.password, self._url, auth); + + const mountStatus = await api.refreshMount('', '', mountPath); + return { + response: mountStatus.response, + status: mountStatus.body + }; + } + + public async deleteMount(mountPath: string, promptConnect: boolean = false): Promise { + return await this.withConnectRetry( + this.deleteMountImpl, + promptConnect, + localize('bdc.error.deleteHdfs', "Error deleting mount"), + mountPath); + } + + private async deleteMountImpl(mountPath: string): Promise { let auth = await this.authPromise; const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); - try { - const mountStatus = await api.createMount('', '', remoteUri, mountPath, credentials); - return { - response: mountStatus.response, - status: mountStatus.body - }; - } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.mountHdfs', "Error creating mount")); - } + const mountStatus = await api.deleteMount('', '', mountPath); + return { + response: mountStatus.response, + status: mountStatus.body + }; } - public async getMountStatus(mountPath?: string): Promise { - let auth = await this.authPromise; - const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); - + /** + * Helper function that wraps a function call in a try/catch and if promptConnect is true + * will prompt the user to re-enter connection information and if that succeeds updates + * this with the new information. + * @param f The API function we're wrapping + * @param promptConnect Whether to actually prompt for connection on failure + * @param errorMessage The message to include in the wrapped error thrown + * @param args The args to pass to the function + */ + private async withConnectRetry(f: (...args: any[]) => Promise, promptConnect: boolean, errorMessage: string, ...args: any[]): Promise { try { - const mountStatus = await api.listMounts('', '', mountPath); - return { - response: mountStatus.response, - mount: mountStatus.body ? JSON.parse(mountStatus.body) : undefined - }; + try { + return await f(this, args); + } catch (error) { + if (promptConnect) { + // We don't want to open multiple dialogs here if multiple calls come in the same time so check + // and see if we have are actively waiting on an open dialog to return and if so then just wait + // on that promise. + if (!this.connectionPromise) { + this.connectionPromise = this.dialog.showDialog(); + } + const controller = await this.connectionPromise; + this.connectionPromise = undefined; + if (controller) { + this.username = controller.username; + this.password = controller.password; + this._url = controller._url; + this.authType = controller.authType; + this.authPromise = controller.authPromise; + } + return await f(this, args); + } + throw error; + } } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.mountHdfs', "Error creating mount")); - } - } - - public async refreshMount(mountPath: string): Promise { - let auth = await this.authPromise; - const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); - - try { - const mountStatus = await api.refreshMount('', '', mountPath); - return { - response: mountStatus.response, - status: mountStatus.body - }; - } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.refreshHdfs', "Error refreshing mount")); - } - } - - public async deleteMount(mountPath: string): Promise { - let auth = await this.authPromise; - const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); - - try { - const mountStatus = await api.deleteMount('', '', mountPath); - return { - response: mountStatus.response, - status: mountStatus.body - }; - } catch (error) { - // TODO handle 401 by reauthenticating - throw new ControllerError(error, localize('bdc.error.deleteHdfs', "Error deleting mount")); + throw new ControllerError(error, errorMessage); } } } + /** * Fixes missing protocol and wrong character for port entered by user */ diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts index 09ef025746..bdb1c0672c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts @@ -47,12 +47,12 @@ export class BdcDashboardModel { public async refresh(): Promise { await Promise.all([ - this._clusterController.getBdcStatus().then(response => { + this._clusterController.getBdcStatus(true).then(response => { this._bdcStatus = response.bdcStatus; this._bdcStatusLastUpdated = new Date(); this._onDidUpdateBdcStatus.fire(this.bdcStatus); }), - this._clusterController.getEndPoints().then(response => { + this._clusterController.getEndPoints(true).then(response => { this._endpoints = response.endPoints || []; fixEndpoints(this._endpoints); this._endpointsLastUpdated = new Date(); diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/connectControllerDialog.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/connectControllerDialog.ts new file mode 100644 index 0000000000..1251baaa79 --- /dev/null +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/connectControllerDialog.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase'; +import { ClusterController } from '../controller/clusterControllerApi'; + +const localize = nls.loadMessageBundle(); + +export class ConnectControllerDialog extends HdfsDialogBase { + constructor(model: ConnectControllerModel) { + super(localize('connectController.dialog.title', "Connect to Controller"), model); + } + + protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] { + return []; + } + + protected async validate(): Promise<{ validated: boolean, value?: ClusterController }> { + try { + const controller = await this.model.onComplete({ + url: this.urlInputBox && this.urlInputBox.value, + auth: this.authValue, + username: this.usernameInputBox && this.usernameInputBox.value, + password: this.passwordInputBox && this.passwordInputBox.value + }); + return { validated: true, value: controller }; + } catch (error) { + await this.reportError(error); + return { validated: false, value: undefined }; + } + } +} + +export class ConnectControllerModel extends HdfsDialogModelBase { + + constructor(props: HdfsDialogProperties) { + super(props); + } + + protected async handleCompleted(): Promise { + this.throwIfMissingUsernamePassword(); + + // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) + return await this.createAndVerifyControllerConnection(); + } +} diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts new file mode 100644 index 0000000000..b3943ea1b5 --- /dev/null +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import { ClusterController, ControllerError, IEndPointsResponse } from '../controller/clusterControllerApi'; +import { AuthType } from '../constants'; +import { Deferred } from '../../common/promise'; + +const localize = nls.loadMessageBundle(); + +const basicAuthDisplay = localize('basicAuthName', "Basic"); +const integratedAuthDisplay = localize('integratedAuthName', "Windows Authentication"); + +function getAuthCategory(name: AuthType): azdata.CategoryValue { + if (name === 'basic') { + return { name: name, displayName: basicAuthDisplay }; + } + return { name: name, displayName: integratedAuthDisplay }; +} + +export interface HdfsDialogProperties { + url?: string; + auth?: AuthType; + username?: string; + password?: string; +} + +export abstract class HdfsDialogModelBase { + protected _canceled = false; + private _authTypes: azdata.CategoryValue[]; + constructor( + public props: T + ) { + if (!props.auth) { + this.props.auth = 'basic'; + } + } + + public get authCategories(): azdata.CategoryValue[] { + if (!this._authTypes) { + this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')]; + } + return this._authTypes; + } + + public get authCategory(): azdata.CategoryValue { + return getAuthCategory(this.props.auth); + } + + public async onComplete(props: T): Promise { + try { + this.props = props; + return await this.handleCompleted(); + } catch (error) { + // Ignore the error if we cancelled the request since we can't stop the actual request from completing + if (!this._canceled) { + throw error; + } + return undefined; + } + } + + protected abstract handleCompleted(): Promise; + + public async onError(error: ControllerError): Promise { + // implement + } + + public async onCancel(): Promise { + this._canceled = true; + } + + protected createController(): ClusterController { + return new ClusterController(this.props.url, this.props.auth, this.props.username, this.props.password, true); + } + + protected async createAndVerifyControllerConnection(): Promise { + // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) + let controller = this.createController(); + let response: IEndPointsResponse; + try { + response = await controller.getEndPoints(); + if (!response || !response.endPoints) { + throw new Error(localize('mount.hdfs.loginerror1', "Login to controller failed")); + } + } catch (err) { + throw new Error(localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", err.message)); + } + return controller; + } + + protected throwIfMissingUsernamePassword(): void { + if (this.props.auth === 'basic') { + // Verify username and password as we can't make them required in the UI + if (!this.props.username) { + throw new Error(localize('err.controller.username.required', "Username is required")); + } else if (!this.props.password) { + throw new Error(localize('err.controller.password.required', "Password is required")); + } + } + } +} + +export abstract class HdfsDialogBase { + + protected dialog: azdata.window.Dialog; + protected uiModelBuilder!: azdata.ModelBuilder; + + protected urlInputBox!: azdata.InputBoxComponent; + protected authDropdown!: azdata.DropDownComponent; + protected usernameInputBox!: azdata.InputBoxComponent; + protected passwordInputBox!: azdata.InputBoxComponent; + + private returnPromise: Deferred; + + constructor(private title: string, protected model: HdfsDialogModelBase) { + } + + public async showDialog(): Promise { + this.returnPromise = new Deferred(); + this.createDialog(); + azdata.window.openDialog(this.dialog); + return this.returnPromise.promise; + } + + private createDialog(): void { + this.dialog = azdata.window.createModelViewDialog(this.title); + this.dialog.registerContent(async view => { + this.uiModelBuilder = view.modelBuilder; + + this.urlInputBox = this.uiModelBuilder.inputBox() + .withProperties({ + placeHolder: localize('textUrlLower', "url"), + value: this.model.props.url, + enabled: false + }).component(); + + this.authDropdown = this.uiModelBuilder.dropDown().withProperties({ + values: this.model.authCategories, + value: this.model.authCategory, + editable: false, + }).component(); + this.authDropdown.onValueChanged(e => this.onAuthChanged()); + this.usernameInputBox = this.uiModelBuilder.inputBox() + .withProperties({ + placeHolder: localize('textUsernameLower', "username"), + value: this.model.props.username + }).component(); + this.passwordInputBox = this.uiModelBuilder.inputBox() + .withProperties({ + placeHolder: localize('textPasswordLower', "password"), + inputType: 'password', + value: this.model.props.password + }) + .component(); + + let connectionSection: azdata.FormComponentGroup = { + components: [ + { + component: this.urlInputBox, + title: localize('textUrlCapital', "URL"), + required: true + }, { + component: this.authDropdown, + title: localize('textAuthCapital', "Authentication type"), + required: true + }, { + component: this.usernameInputBox, + title: localize('textUsernameCapital', "Username"), + required: false + }, { + component: this.passwordInputBox, + title: localize('textPasswordCapital', "Password"), + required: false + } + ], + title: localize('hdsf.dialog.connection.section', "Cluster Connection") + }; + let formModel = this.uiModelBuilder.formContainer() + .withFormItems( + this.getMainSectionComponents().concat( + connectionSection) + ).withLayout({ width: '100%' }).component(); + + await view.initializeModel(formModel); + this.onAuthChanged(); + }); + + this.dialog.registerCloseValidator(async () => { + const result = await this.validate(); + if (result.validated) { + this.returnPromise.resolve(result.value); + this.returnPromise = undefined; + } + return result.validated; + }); + this.dialog.cancelButton.onClick(async () => await this.cancel()); + this.dialog.okButton.label = localize('hdfs.dialog.ok', "OK"); + this.dialog.cancelButton.label = localize('hdfs.dialog.cancel', "Cancel"); + } + + protected abstract getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[]; + + protected get authValue(): AuthType { + return (this.authDropdown.value).name as AuthType; + } + + private onAuthChanged(): void { + let isBasic = this.authValue === 'basic'; + this.usernameInputBox.enabled = isBasic; + this.passwordInputBox.enabled = isBasic; + if (!isBasic) { + this.usernameInputBox.value = ''; + this.passwordInputBox.value = ''; + } + } + + protected abstract validate(): Promise<{ validated: boolean, value?: R }>; + + private async cancel(): Promise { + if (this.model && this.model.onCancel) { + await this.model.onCancel(); + } + this.returnPromise.reject(new Error('Dialog cancelled')); + } + + protected async reportError(error: any): Promise { + this.dialog.message = { + text: (typeof error === 'string') ? error : error.message, + level: azdata.window.MessageLevel.Error + }; + if (this.model && this.model.onError) { + await this.model.onError(error as ControllerError); + } + } +} diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts index 7660981b96..0960c71ad6 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts @@ -6,23 +6,14 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import { ClusterController, ControllerError, MountInfo, MountState, IEndPointsResponse } from '../controller/clusterControllerApi'; -import { AuthType } from '../constants'; +import { ClusterController, MountInfo, MountState } from '../controller/clusterControllerApi'; +import { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase'; const localize = nls.loadMessageBundle(); -const basicAuthDisplay = localize('basicAuthName', "Basic"); -const integratedAuthDisplay = localize('integratedAuthName', "Windows Authentication"); const mountConfigutationTitle = localize('mount.main.section', "Mount Configuration"); const hdfsPathTitle = localize('mount.hdfsPath', "HDFS Path"); -function getAuthCategory(name: AuthType): azdata.CategoryValue { - if (name === 'basic') { - return { name: name, displayName: basicAuthDisplay }; - } - return { name: name, displayName: integratedAuthDisplay }; -} - /** * Converts a comma-delimited set of key value pair credentials to a JSON object. * This code is taken from the azdata implementation written in Python @@ -65,96 +56,13 @@ function convertCredsToJson(creds: string): { credentials: {} } { return credObj; } -export interface DialogProperties { - url?: string; - auth?: AuthType; - username?: string; - password?: string; -} - -export interface MountHdfsProperties extends DialogProperties { +export interface MountHdfsProperties extends HdfsDialogProperties { hdfsPath?: string; remoteUri?: string; credentials?: string; } -abstract class HdfsDialogModelBase { - protected _canceled = false; - private _authTypes: azdata.CategoryValue[]; - constructor( - public props: T - ) { - if (!props.auth) { - this.props.auth = 'basic'; - } - } - - public get authCategories(): azdata.CategoryValue[] { - if (!this._authTypes) { - this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')]; - } - return this._authTypes; - } - - public get authCategory(): azdata.CategoryValue { - return getAuthCategory(this.props.auth); - } - - public async onComplete(props: T): Promise { - try { - this.props = props; - await this.handleCompleted(); - - } catch (error) { - // Ignore the error if we cancelled the request since we can't stop the actual request from completing - if (!this._canceled) { - throw error; - } - } - } - - protected abstract handleCompleted(): Promise; - - public async onError(error: ControllerError): Promise { - // implement - } - - public async onCancel(): Promise { - this._canceled = true; - } - - protected createController(): ClusterController { - return new ClusterController(this.props.url, this.props.auth, this.props.username, this.props.password, true); - } - - protected async createAndVerifyControllerConnection(): Promise { - // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) - let controller = this.createController(); - let response: IEndPointsResponse; - try { - response = await controller.getEndPoints(); - if (!response || !response.endPoints) { - throw new Error(localize('mount.hdfs.loginerror1', "Login to controller failed")); - } - } catch (err) { - throw new Error(localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", err.message)); - } - return controller; - } - - protected throwIfMissingUsernamePassword(): void { - if (this.props.auth === 'basic') { - // Verify username and password as we can't make them required in the UI - if (!this.props.username) { - throw new Error(localize('err.controller.username.required', "Username is required")); - } else if (!this.props.password) { - throw new Error(localize('err.controller.password.required', "Password is required")); - } - } - } -} - -export class MountHdfsDialogModel extends HdfsDialogModelBase { +export class MountHdfsDialogModel extends HdfsDialogModelBase { private credentials: {}; constructor(props: MountHdfsProperties) { @@ -235,128 +143,7 @@ export class MountHdfsDialogModel extends HdfsDialogModelBase { - - protected dialog: azdata.window.Dialog; - protected uiModelBuilder!: azdata.ModelBuilder; - - protected urlInputBox!: azdata.InputBoxComponent; - protected authDropdown!: azdata.DropDownComponent; - protected usernameInputBox!: azdata.InputBoxComponent; - protected passwordInputBox!: azdata.InputBoxComponent; - - constructor(private title: string, protected model: HdfsDialogModelBase) { - } - - public showDialog(): void { - this.createDialog(); - azdata.window.openDialog(this.dialog); - } - - private createDialog(): void { - this.dialog = azdata.window.createModelViewDialog(this.title); - this.dialog.registerContent(async view => { - this.uiModelBuilder = view.modelBuilder; - - this.urlInputBox = this.uiModelBuilder.inputBox() - .withProperties({ - placeHolder: localize('textUrlLower', "url"), - value: this.model.props.url, - }).component(); - this.urlInputBox.enabled = false; - - this.authDropdown = this.uiModelBuilder.dropDown().withProperties({ - values: this.model.authCategories, - value: this.model.authCategory, - editable: false, - }).component(); - this.authDropdown.onValueChanged(e => this.onAuthChanged()); - this.usernameInputBox = this.uiModelBuilder.inputBox() - .withProperties({ - placeHolder: localize('textUsernameLower', "username"), - value: this.model.props.username - }).component(); - this.passwordInputBox = this.uiModelBuilder.inputBox() - .withProperties({ - placeHolder: localize('textPasswordLower', "password"), - inputType: 'password', - value: this.model.props.password - }) - .component(); - - let connectionSection: azdata.FormComponentGroup = { - components: [ - { - component: this.urlInputBox, - title: localize('textUrlCapital', "URL"), - required: true - }, { - component: this.authDropdown, - title: localize('textAuthCapital', "Authentication type"), - required: true - }, { - component: this.usernameInputBox, - title: localize('textUsernameCapital', "Username"), - required: false - }, { - component: this.passwordInputBox, - title: localize('textPasswordCapital', "Password"), - required: false - } - ], - title: localize('hdsf.dialog.connection.section', "Cluster Connection") - }; - let formModel = this.uiModelBuilder.formContainer() - .withFormItems([ - this.getMainSection(), - connectionSection - ]).withLayout({ width: '100%' }).component(); - - await view.initializeModel(formModel); - this.onAuthChanged(); - }); - - this.dialog.registerCloseValidator(async () => await this.validate()); - this.dialog.cancelButton.onClick(async () => await this.cancel()); - this.dialog.okButton.label = localize('hdfs.dialog.ok', "OK"); - this.dialog.cancelButton.label = localize('hdfs.dialog.cancel', "Cancel"); - } - - protected abstract getMainSection(): azdata.FormComponentGroup; - - protected get authValue(): AuthType { - return (this.authDropdown.value).name as AuthType; - } - - private onAuthChanged(): void { - let isBasic = this.authValue === 'basic'; - this.usernameInputBox.enabled = isBasic; - this.passwordInputBox.enabled = isBasic; - if (!isBasic) { - this.usernameInputBox.value = ''; - this.passwordInputBox.value = ''; - } - } - - protected abstract validate(): Promise; - - private async cancel(): Promise { - if (this.model && this.model.onCancel) { - await this.model.onCancel(); - } - } - - protected async reportError(error: any): Promise { - this.dialog.message = { - text: (typeof error === 'string') ? error : error.message, - level: azdata.window.MessageLevel.Error - }; - if (this.model && this.model.onError) { - await this.model.onError(error as ControllerError); - } - } -} -export class MountHdfsDialog extends HdfsDialogBase { +export class MountHdfsDialog extends HdfsDialogBase { private pathInputBox: azdata.InputBoxComponent; private remoteUriInputBox: azdata.InputBoxComponent; private credentialsInputBox: azdata.InputBoxComponent; @@ -365,7 +152,7 @@ export class MountHdfsDialog extends HdfsDialogBase { super(localize('mount.dialog.title', "Mount HDFS Folder"), model); } - protected getMainSection(): azdata.FormComponentGroup { + protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] { const newMountName = '/mymount'; let pathVal = this.model.props.hdfsPath; pathVal = (!pathVal || pathVal === '/') ? newMountName : (pathVal + newMountName); @@ -385,27 +172,28 @@ export class MountHdfsDialog extends HdfsDialogBase { }) .component(); - return { - components: [ - { - component: this.pathInputBox, - title: hdfsPathTitle, - required: true - }, { - component: this.remoteUriInputBox, - title: localize('mount.remoteUri', "Remote URI"), - required: true - }, { - component: this.credentialsInputBox, - title: localize('mount.credentials', "Credentials"), - required: false - } - ], - title: mountConfigutationTitle - }; + return [ + { + components: [ + { + component: this.pathInputBox, + title: hdfsPathTitle, + required: true + }, { + component: this.remoteUriInputBox, + title: localize('mount.remoteUri', "Remote URI"), + required: true + }, { + component: this.credentialsInputBox, + title: localize('mount.credentials', "Credentials"), + required: false + } + ], + title: mountConfigutationTitle + }]; } - protected async validate(): Promise { + protected async validate(): Promise<{ validated: boolean }> { try { await this.model.onComplete({ url: this.urlInputBox && this.urlInputBox.value, @@ -416,39 +204,40 @@ export class MountHdfsDialog extends HdfsDialogBase { remoteUri: this.remoteUriInputBox && this.remoteUriInputBox.value, credentials: this.credentialsInputBox && this.credentialsInputBox.value }); - return true; + return { validated: true }; } catch (error) { await this.reportError(error); - return false; + return { validated: false }; } } } -export class RefreshMountDialog extends HdfsDialogBase { +export class RefreshMountDialog extends HdfsDialogBase { private pathInputBox: azdata.InputBoxComponent; constructor(model: RefreshMountModel) { super(localize('refreshmount.dialog.title', "Refresh Mount"), model); } - protected getMainSection(): azdata.FormComponentGroup { + protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] { this.pathInputBox = this.uiModelBuilder.inputBox() .withProperties({ value: this.model.props.hdfsPath }).component(); - return { - components: [ - { - component: this.pathInputBox, - title: hdfsPathTitle, - required: true - } - ], - title: mountConfigutationTitle - }; + return [ + { + components: [ + { + component: this.pathInputBox, + title: hdfsPathTitle, + required: true + } + ], + title: mountConfigutationTitle + }]; } - protected async validate(): Promise { + protected async validate(): Promise<{ validated: boolean }> { try { await this.model.onComplete({ url: this.urlInputBox && this.urlInputBox.value, @@ -457,15 +246,15 @@ export class RefreshMountDialog extends HdfsDialogBase { password: this.passwordInputBox && this.passwordInputBox.value, hdfsPath: this.pathInputBox && this.pathInputBox.value }); - return true; + return { validated: true }; } catch (error) { await this.reportError(error); - return false; + return { validated: false }; } } } -export class RefreshMountModel extends HdfsDialogModelBase { +export class RefreshMountModel extends HdfsDialogModelBase { constructor(props: MountHdfsProperties) { super(props); @@ -504,31 +293,32 @@ export class RefreshMountModel extends HdfsDialogModelBase } } -export class DeleteMountDialog extends HdfsDialogBase { +export class DeleteMountDialog extends HdfsDialogBase { private pathInputBox: azdata.InputBoxComponent; constructor(model: DeleteMountModel) { super(localize('deleteMount.dialog.title', "Delete Mount"), model); } - protected getMainSection(): azdata.FormComponentGroup { + protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] { this.pathInputBox = this.uiModelBuilder.inputBox() .withProperties({ value: this.model.props.hdfsPath }).component(); - return { - components: [ - { - component: this.pathInputBox, - title: hdfsPathTitle, - required: true - } - ], - title: mountConfigutationTitle - }; + return [ + { + components: [ + { + component: this.pathInputBox, + title: hdfsPathTitle, + required: true + } + ], + title: mountConfigutationTitle + }]; } - protected async validate(): Promise { + protected async validate(): Promise<{ validated: boolean }> { try { await this.model.onComplete({ url: this.urlInputBox && this.urlInputBox.value, @@ -537,15 +327,15 @@ export class DeleteMountDialog extends HdfsDialogBase { password: this.passwordInputBox && this.passwordInputBox.value, hdfsPath: this.pathInputBox && this.pathInputBox.value }); - return true; + return { validated: true }; } catch (error) { await this.reportError(error); - return false; + return { validated: false }; } } } -export class DeleteMountModel extends HdfsDialogModelBase { +export class DeleteMountModel extends HdfsDialogModelBase { constructor(props: MountHdfsProperties) { super(props); diff --git a/extensions/big-data-cluster/src/common/promise.ts b/extensions/big-data-cluster/src/common/promise.ts new file mode 100644 index 0000000000..68f39d4dcd --- /dev/null +++ b/extensions/big-data-cluster/src/common/promise.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Deferred promise + */ +export class Deferred { + promise: Promise; + resolve: (value?: T | PromiseLike) => void; + reject: (reason?: any) => void; + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable { + return this.promise.then(onfulfilled, onrejected); + } +}