From fbcd3c620c88d551c768a614d95a7e4d18cc18c7 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Thu, 31 Oct 2019 13:36:14 -0700 Subject: [PATCH] Merge BDC error fixes into release (#8145) * Fix BDC remember password and reprompting connection (#7957) * Fix remember password and reprompting connection * comment * Fix to remember password for session * Fix floating promises * Add error messages to BDC dashboard page (#8103) * Add error messages to BDC dashboard page * Remove testing code * PR fixes --- .../src/bigDataCluster/constants.ts | 1 + .../controller/clusterControllerApi.ts | 87 +++++++++++------- .../dialog/addControllerDialog.ts | 6 +- .../src/bigDataCluster/dialog/bdcDashboard.ts | 30 ++++-- .../dialog/bdcDashboardModel.ts | 76 +++++++++++---- .../dialog/bdcDashboardOverviewPage.ts | 92 +++++++++++++++++-- .../bigDataCluster/dialog/hdfsDialogBase.ts | 10 +- .../tree/controllerTreeDataProvider.ts | 28 ++++-- .../bigDataCluster/tree/controllerTreeNode.ts | 18 +++- .../src/bigDataCluster/utils.ts | 4 + extensions/big-data-cluster/src/extension.ts | 46 +++++----- .../modelComponents/divContainer.component.ts | 2 +- 12 files changed, 286 insertions(+), 114 deletions(-) diff --git a/extensions/big-data-cluster/src/bigDataCluster/constants.ts b/extensions/big-data-cluster/src/bigDataCluster/constants.ts index 0237a64a89..b54abfb93c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/constants.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/constants.ts @@ -71,6 +71,7 @@ export namespace cssStyles { export const selectedTabDiv = { 'border-bottom': '2px solid #000' }; export const unselectedTabDiv = { 'border-bottom': '1px solid #ccc' }; export const lastUpdatedText = { ...text, 'color': '#595959' }; + export const errorText = { ...text, 'color': 'red' }; } export type AuthType = 'integrated' | 'basic'; diff --git a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts index c238513f29..ecb4c7d50d 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts @@ -88,35 +88,51 @@ class DefaultApiWrapper extends DefaultApi { export class ClusterController { - private authPromise: Promise; + private _authPromise: Promise; private _url: string; - private readonly dialog: ConnectControllerDialog; - private connectionPromise: Promise; + private readonly _dialog: ConnectControllerDialog; + private _connectionPromise: Promise; constructor(url: string, - private authType: AuthType, - private username?: string, - private password?: string, + private _authType: AuthType, + private _username?: string, + private _password?: string, ignoreSslVerification?: boolean ) { - if (!url || (authType === 'basic' && (!username || !password))) { + if (!url || (_authType === 'basic' && (!_username || !_password))) { throw new Error('Missing required inputs for Cluster controller API (URL, username, password)'); } this._url = adjustUrl(url); - if (this.authType === 'basic') { - this.authPromise = Promise.resolve(new BasicAuth(username, password, !!ignoreSslVerification)); + if (this._authType === 'basic') { + this._authPromise = Promise.resolve(new BasicAuth(_username, _password, !!ignoreSslVerification)); } else { - this.authPromise = this.requestTokenUsingKerberos(ignoreSslVerification); + this._authPromise = this.requestTokenUsingKerberos(ignoreSslVerification); } - this.dialog = new ConnectControllerDialog(new ConnectControllerModel( + this._dialog = new ConnectControllerDialog(new ConnectControllerModel( { url: this._url, - auth: this.authType, - username: this.username, - password: this.password + auth: this._authType, + username: this._username, + password: this._password })); } + public get url(): string { + return this._url; + } + + public get authType(): AuthType { + return this._authType; + } + + public get username(): string | undefined { + return this._username; + } + + public get password(): string | undefined { + return this._password; + } + private async requestTokenUsingKerberos(ignoreSslVerification?: boolean): Promise { let supportsKerberos = await this.verifyKerberosSupported(ignoreSslVerification); if (!supportsKerberos) { @@ -166,8 +182,8 @@ export class ClusterController { } private async getEndpointsImpl(self: ClusterController): Promise { - let auth = await self.authPromise; - let endPointApi = new BdcApiWrapper(self.username, self.password, self._url, auth); + let auth = await self._authPromise; + let endPointApi = new BdcApiWrapper(self._username, self._password, self._url, auth); let options: any = {}; let result = await endPointApi.endpointsGet(options); @@ -185,8 +201,8 @@ export class ClusterController { } private async getBdcStatusImpl(self: ClusterController): Promise { - let auth = await self.authPromise; - const bdcApi = new BdcApiWrapper(self.username, self.password, self._url, auth); + let auth = await self._authPromise; + const bdcApi = new BdcApiWrapper(self._username, self._password, self._url, auth); const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true); return { @@ -206,8 +222,8 @@ export class ClusterController { } 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); + 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 { @@ -225,8 +241,8 @@ export class ClusterController { } 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 auth = await self._authPromise; + const api = new DefaultApiWrapper(self._username, self._password, self._url, auth); const mountStatus = await api.listMounts('', '', mountPath); return { @@ -244,8 +260,8 @@ export class ClusterController { } 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 auth = await self._authPromise; + const api = new DefaultApiWrapper(self._username, self._password, self._url, auth); const mountStatus = await api.refreshMount('', '', mountPath); return { @@ -263,8 +279,8 @@ export class ClusterController { } private async deleteMountImpl(mountPath: string): Promise { - let auth = await this.authPromise; - const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); + let auth = await this._authPromise; + const api = new DefaultApiWrapper(this._username, this._password, this._url, auth); const mountStatus = await api.deleteMount('', '', mountPath); return { @@ -291,17 +307,17 @@ export class ClusterController { // 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(); + if (!this._connectionPromise) { + this._connectionPromise = this._dialog.showDialog(); } - const controller = await this.connectionPromise; - this.connectionPromise = undefined; + const controller = await this._connectionPromise; + this._connectionPromise = undefined; if (controller) { - this.username = controller.username; - this.password = controller.password; + this._username = controller._username; + this._password = controller._password; this._url = controller._url; - this.authType = controller.authType; - this.authPromise = controller.authPromise; + this._authType = controller._authType; + this._authPromise = controller._authPromise; } return await f(this, args); } @@ -378,7 +394,7 @@ export class ControllerError extends Error { public code?: number; public reason?: string; public address?: string; - + public statusMessage?: string; /** * * @param error The original error to wrap @@ -391,6 +407,7 @@ export class ControllerError extends Error { this.code = error.response.statusCode; this.message += `${error.response.statusMessage ? ` - ${error.response.statusMessage}` : ''}` || ''; this.address = error.response.url || ''; + this.statusMessage = error.response.statusMessage; } else if (error.message) { this.message += ` - ${error.message}`; diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts index ab6dc54c21..7e2ecda5aa 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts @@ -6,11 +6,14 @@ 'use strict'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ClusterController, ControllerError } from '../controller/clusterControllerApi'; import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider'; import { TreeNode } from '../tree/treeNode'; import { AuthType } from '../constants'; +import { ManageControllerCommand } from '../../extension'; +import { BdcDashboardOptions } from './bdcDashboardModel'; const localize = nls.loadMessageBundle(); @@ -73,7 +76,8 @@ export class AddControllerDialogModel { if (this._canceled) { return; } - this.treeDataProvider.addController(url, auth, username, password, rememberPassword); + this.treeDataProvider.addOrUpdateController(url, auth, username, password, rememberPassword); + vscode.commands.executeCommand(ManageControllerCommand, { url: url, auth: auth, username: username, password: password }); await this.treeDataProvider.saveControllers(); } } catch (error) { diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts index 2999b2c1e0..62c6a518e2 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -8,12 +8,13 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { BdcDashboardModel, getTroubleshootNotebookUrl } from './bdcDashboardModel'; +import { BdcDashboardModel, getTroubleshootNotebookUrl, BdcErrorEvent } from './bdcDashboardModel'; import { IconPathHelper, cssStyles } from '../constants'; import { BdcServiceStatusPage } from './bdcServiceStatusPage'; import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage'; import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; -import { getHealthStatusDot, getServiceNameDisplayText } from '../utils'; +import { getHealthStatusDot, getServiceNameDisplayText, showErrorMessage } from '../utils'; +import { HdfsDialogCancelledError } from './hdfsDialogBase'; const localize = nls.loadMessageBundle(); @@ -33,6 +34,7 @@ export class BdcDashboard { private modelView: azdata.ModelView; private mainAreaContainer: azdata.FlexContainer; private navContainer: azdata.FlexContainer; + private overviewPage: BdcDashboardOverviewPage; private currentTab: NavTab; private currentPage: azdata.FlexContainer; @@ -43,6 +45,7 @@ export class BdcDashboard { constructor(private title: string, private model: BdcDashboardModel) { this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); + this.model.onBdcError(errorEvent => this.handleError(errorEvent)); } public showDashboard(): void { @@ -73,6 +76,7 @@ export class BdcDashboard { }).component(); this.refreshButton.onDidClick(async () => { + this.overviewPage.onRefreshStarted(); await this.doRefresh(); }); @@ -128,18 +132,19 @@ export class BdcDashboard { const overviewNavItemText = modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewNavTitle', 'Big data cluster overview') }).component(); overviewNavItemText.updateCssStyles(selectedTabCss); overviewNavItemDiv.addItem(overviewNavItemText, { CSSStyles: { 'user-select': 'text' } }); - const overviewPage = new BdcDashboardOverviewPage(this, this.model).create(modelView); - this.currentPage = overviewPage; + this.overviewPage = new BdcDashboardOverviewPage(this, this.model); + const overviewContainer: azdata.FlexContainer = this.overviewPage.create(modelView); + this.currentPage = overviewContainer; this.currentTab = { serviceName: undefined, div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; - this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); + this.mainAreaContainer.addItem(overviewContainer, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); overviewNavItemDiv.onDidClick(() => { if (this.currentTab) { this.currentTab.text.updateCssStyles(unselectedTabCss); } this.mainAreaContainer.removeItem(this.currentPage); - this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); - this.currentPage = overviewPage; + this.mainAreaContainer.addItem(overviewContainer, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); + this.currentPage = overviewContainer; this.currentTab = { serviceName: undefined, div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; this.currentTab.text.updateCssStyles(selectedTabCss); }); @@ -165,6 +170,17 @@ export class BdcDashboard { this.updateServiceNavTabs(bdcStatus.services); } + private handleError(errorEvent: BdcErrorEvent): void { + if (errorEvent.errorType !== 'general') { + return; + } + // We don't want to show an error for the connection dialog being + // canceled since that's a normal case. + if (!(errorEvent.error instanceof HdfsDialogCancelledError)) { + showErrorMessage(errorEvent.error.message); + } + } + private async doRefresh(): Promise { try { this.refreshButton.enabled = false; diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts index 76c419d714..f4996e9be9 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts @@ -7,11 +7,16 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { ClusterController } from '../controller/clusterControllerApi'; import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated'; -import { showErrorMessage, Endpoint, Service } from '../utils'; +import { Endpoint, Service } from '../utils'; import { AuthType } from '../constants'; +import { ConnectControllerDialog, ConnectControllerModel } from './connectControllerDialog'; +import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider'; export type BdcDashboardOptions = { url: string, auth: AuthType, username: string, password: string }; +export type BdcErrorType = 'bdcStatus' | 'bdcEndpoints' | 'general'; +export type BdcErrorEvent = { error: Error, errorType: BdcErrorType }; + export class BdcDashboardModel { private _clusterController: ClusterController; @@ -21,12 +26,23 @@ export class BdcDashboardModel { private _endpointsLastUpdated: Date; private readonly _onDidUpdateEndpoints = new vscode.EventEmitter(); private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter(); + private readonly _onBdcError = new vscode.EventEmitter(); public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event; public onDidUpdateBdcStatus = this._onDidUpdateBdcStatus.event; + public onBdcError = this._onBdcError.event; - constructor(private options: BdcDashboardOptions, ignoreSslVerification = true) { - this._clusterController = new ClusterController(options.url, options.auth, options.username, options.password, ignoreSslVerification); - this.refresh(); + constructor(private _options: BdcDashboardOptions, private _treeDataProvider: ControllerTreeDataProvider, ignoreSslVerification = true) { + try { + this._clusterController = new ClusterController(_options.url, _options.auth, _options.username, _options.password, ignoreSslVerification); + // tslint:disable-next-line:no-floating-promises + this.refresh(); + } catch { + this.promptReconnect().then(async () => { + await this.refresh(); + }).catch(error => { + this._onBdcError.fire({ error: error, errorType: 'general' }); + }); + } } public get bdcStatus(): BdcStatusModel | undefined { @@ -46,21 +62,28 @@ export class BdcDashboardModel { } public async refresh(): Promise { - await Promise.all([ - this._clusterController.getBdcStatus(true).then(response => { - this._bdcStatus = response.bdcStatus; - this._bdcStatusLastUpdated = new Date(); - this._onDidUpdateBdcStatus.fire(this.bdcStatus); - }), - this._clusterController.getEndPoints(true).then(response => { - this._endpoints = response.endPoints || []; - fixEndpoints(this._endpoints); - this._endpointsLastUpdated = new Date(); - this._onDidUpdateEndpoints.fire(this.serviceEndpoints); - }) - ]).catch(error => { - showErrorMessage(error); - }); + try { + if (!this._clusterController) { + // If this succeeds without error we know we have a clusterController at this point + await this.promptReconnect(); + } + + await Promise.all([ + this._clusterController.getBdcStatus(true).then(response => { + this._bdcStatus = response.bdcStatus; + this._bdcStatusLastUpdated = new Date(); + this._onDidUpdateBdcStatus.fire(this.bdcStatus); + }).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcStatus' })), + this._clusterController.getEndPoints(true).then(response => { + this._endpoints = response.endPoints || []; + fixEndpoints(this._endpoints); + this._endpointsLastUpdated = new Date(); + this._onDidUpdateEndpoints.fire(this.serviceEndpoints); + }).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcEndpoints' })) + ]); + } catch (error) { + this._onBdcError.fire({ error: error, errorType: 'general' }); + } } /** @@ -81,7 +104,7 @@ export class BdcDashboardModel { serverName: sqlServerMasterEndpoint.endpoint, databaseName: undefined, userName: 'sa', - password: this.options.password, + password: this._options.password, authenticationType: '', savePassword: true, groupFullName: undefined, @@ -92,6 +115,19 @@ export class BdcDashboardModel { options: {} }; } + + /** + * Opens up a dialog prompting the user to re-enter credentials for the controller + */ + private async promptReconnect(): Promise { + this._clusterController = await new ConnectControllerDialog(new ConnectControllerModel(this._options)).showDialog(); + this._treeDataProvider.addOrUpdateController( + this._clusterController.url, + this._clusterController.authType, + this._clusterController.username, + this._clusterController.password, + /* Remember password */false); + } } /** diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts index 8519e974d0..1b887cf13c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts @@ -3,17 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { BdcDashboardModel } from './bdcDashboardModel'; +import { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel'; import { IconPathHelper, cssStyles } from '../constants'; -import { getStateDisplayText, getHealthStatusDisplayText, getEndpointDisplayText, getHealthStatusIcon, getServiceNameDisplayText, Endpoint } from '../utils'; +import { getStateDisplayText, getHealthStatusDisplayText, getEndpointDisplayText, getHealthStatusIcon, getServiceNameDisplayText, Endpoint, getBdcStatusErrorMessage } from '../utils'; import { EndpointModel, ServiceStatusModel, BdcStatusModel } from '../controller/apiGenerated'; import { BdcDashboard } from './bdcDashboard'; import { createViewDetailsButton } from './commonControls'; +import { HdfsDialogCancelledError } from './hdfsDialogBase'; const localize = nls.loadMessageBundle(); @@ -31,24 +30,29 @@ const serviceEndpointRowEndpointCellWidth = 350; const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi]; -type ActionItem = (vscode.MessageItem & { execute: () => void; }); - export class BdcDashboardOverviewPage { private initialized: boolean = false; private modelBuilder: azdata.ModelBuilder; private lastUpdatedLabel: azdata.TextComponent; + private propertiesContainer: azdata.DivContainer; private clusterStateLoadingComponent: azdata.LoadingComponent; private clusterHealthStatusLoadingComponent: azdata.LoadingComponent; private serviceStatusRowContainer: azdata.FlexContainer; private endpointsRowContainer: azdata.FlexContainer; + private endpointsDisplayContainer: azdata.DivContainer; + private serviceStatusDisplayContainer: azdata.DivContainer; + private propertiesErrorMessage: azdata.TextComponent; + private endpointsErrorMessage: azdata.TextComponent; + private serviceStatusErrorMessage: azdata.TextComponent; constructor(private dashboard: BdcDashboard, private model: BdcDashboardModel) { this.model.onDidUpdateEndpoints(endpoints => this.handleEndpointsUpdate(endpoints)); this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); + this.model.onBdcError(error => this.handleBdcError(error)); } public create(view: azdata.ModelView): azdata.FlexContainer { @@ -69,6 +73,11 @@ export class BdcDashboardOverviewPage { .component(); rootContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.title } }); + this.propertiesErrorMessage = view.modelBuilder.text().withProperties({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component(); + rootContainer.addItem(this.propertiesErrorMessage, { flex: '0 0 auto' }); + + this.propertiesContainer = view.modelBuilder.divContainer().component(); + // Row 1 const row1 = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '30px', alignItems: 'center' }).component(); @@ -86,7 +95,9 @@ export class BdcDashboardOverviewPage { row1.addItem(healthStatusLabel, { CSSStyles: { 'width': `${healthStatusColumnWidth}px`, 'min-width': `${healthStatusColumnWidth}px`, 'user-select': 'none', 'font-weight': 'bold' } }); row1.addItem(this.clusterHealthStatusLoadingComponent, { CSSStyles: { 'width': `${healthStatusColumnWidth}px`, 'min-width': `${healthStatusColumnWidth}px` } }); - rootContainer.addItem(row1, { CSSStyles: { 'padding-left': '10px', 'border-bottom': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + this.propertiesContainer.addItem(row1, { CSSStyles: { 'padding-left': '10px', 'border-bottom': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + + rootContainer.addItem(this.propertiesContainer, { flex: '0 0 auto' }); // ############ // # OVERVIEW # @@ -125,6 +136,8 @@ export class BdcDashboardOverviewPage { serviceStatusHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': `${overviewHealthStatusCellWidthPx}px`, 'min-width': `${overviewHealthStatusCellWidthPx}px` } }); overviewContainer.addItem(serviceStatusHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); + this.serviceStatusDisplayContainer = view.modelBuilder.divContainer().component(); + // Service Status row container this.serviceStatusRowContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); // Note we don't give the rows container as a child of the loading component since in order to align the loading component correctly @@ -135,7 +148,12 @@ export class BdcDashboardOverviewPage { .component(); this.serviceStatusRowContainer.addItem(serviceStatusRowContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); - overviewContainer.addItem(this.serviceStatusRowContainer); + this.serviceStatusErrorMessage = view.modelBuilder.text().withProperties({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component(); + overviewContainer.addItem(this.serviceStatusErrorMessage); + + this.serviceStatusDisplayContainer.addItem(this.serviceStatusRowContainer); + overviewContainer.addItem(this.serviceStatusDisplayContainer); + rootContainer.addItem(overviewContainer, { flex: '0 0 auto' }); // ##################### @@ -147,6 +165,8 @@ export class BdcDashboardOverviewPage { .component(); rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } }); + this.endpointsErrorMessage = view.modelBuilder.text().withProperties({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component(); + const endpointsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component(); // Service endpoints header row @@ -157,6 +177,7 @@ export class BdcDashboardOverviewPage { endpointsHeaderRow.addItem(endpointsEndpointHeaderCell, { CSSStyles: { 'width': `${serviceEndpointRowEndpointCellWidth}px`, 'min-width': `${serviceEndpointRowEndpointCellWidth}px`, ...cssStyles.tableHeader } }); endpointsContainer.addItem(endpointsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); + this.endpointsDisplayContainer = view.modelBuilder.divContainer().component(); this.endpointsRowContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); // Note we don't give the rows container as a child of the loading component since in order to align the loading component correctly // messes up the layout for the row container that we display after loading is finished. Instead we just remove the loading component @@ -166,8 +187,9 @@ export class BdcDashboardOverviewPage { .component(); this.endpointsRowContainer.addItem(endpointRowContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); - endpointsContainer.addItem(this.endpointsRowContainer); - + this.endpointsDisplayContainer.addItem(this.endpointsRowContainer); + endpointsContainer.addItem(this.endpointsErrorMessage); + endpointsContainer.addItem(this.endpointsDisplayContainer); rootContainer.addItem(endpointsContainer, { flex: '0 0 auto' }); this.initialized = true; @@ -179,10 +201,22 @@ export class BdcDashboardOverviewPage { return rootContainer; } + public onRefreshStarted(): void { + this.propertiesErrorMessage.display = 'none'; + this.serviceStatusErrorMessage.display = 'none'; + this.endpointsErrorMessage.display = 'none'; + + this.serviceStatusDisplayContainer.display = undefined; + this.propertiesContainer.display = undefined; + this.endpointsDisplayContainer.display = undefined; + + + } private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void { if (!this.initialized || !bdcStatus) { return; } + this.lastUpdatedLabel.value = localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", this.model.bdcStatusLastUpdated ? @@ -225,6 +259,44 @@ export class BdcDashboardOverviewPage { }); } + private handleBdcError(errorEvent: BdcErrorEvent): void { + if (errorEvent.errorType === 'bdcEndpoints') { + const errorMessage = localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", errorEvent.error.message); + this.showEndpointsError(errorMessage); + } else if (errorEvent.errorType === 'bdcStatus') { + this.showBdcStatusError(getBdcStatusErrorMessage(errorEvent.error)); + } else { + this.handleGeneralError(errorEvent.error); + } + } + + private showBdcStatusError(errorMessage: string): void { + this.serviceStatusDisplayContainer.display = 'none'; + this.propertiesContainer.display = 'none'; + this.serviceStatusErrorMessage.value = errorMessage; + this.serviceStatusErrorMessage.display = undefined; + this.propertiesErrorMessage.value = errorMessage; + this.propertiesErrorMessage.display = undefined; + } + + private showEndpointsError(errorMessage: string): void { + this.endpointsDisplayContainer.display = 'none'; + this.endpointsErrorMessage.display = undefined; + this.endpointsErrorMessage.value = errorMessage; + } + + private handleGeneralError(error: Error): void { + if (error instanceof HdfsDialogCancelledError) { + const errorMessage = localize('bdc.dashboard.noConnection', "The dashboard requires a connection. Please click retry to enter your credentials."); + this.showBdcStatusError(errorMessage); + this.showEndpointsError(errorMessage); + } else { + const errorMessage = localize('bdc.dashboard.unexpectedError', "Unexpected error occurred: {0}", error.message); + this.showBdcStatusError(errorMessage); + this.showEndpointsError(errorMessage); + } + } + private createServiceStatusRow(container: azdata.FlexContainer, serviceStatus: ServiceStatusModel, isLastRow: boolean): void { const serviceStatusRow = this.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); const statusIconCell = this.modelBuilder.text().withProperties({ value: getHealthStatusIcon(serviceStatus.healthStatus), CSSStyles: { 'user-select': 'none' } }).component(); diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts index b3943ea1b5..bcc1cb7387 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/hdfsDialogBase.ts @@ -28,6 +28,12 @@ export interface HdfsDialogProperties { password?: string; } +export class HdfsDialogCancelledError extends Error { + constructor(message: string = 'Dialog cancelled') { + super(message); + } +} + export abstract class HdfsDialogModelBase { protected _canceled = false; private _authTypes: azdata.CategoryValue[]; @@ -87,7 +93,7 @@ export abstract class HdfsDialogModelBase { 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)); + throw new Error(localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", err.statusMessage || err.message)); } return controller; } @@ -224,7 +230,7 @@ export abstract class HdfsDialogBase { if (this.model && this.model.onCancel) { await this.model.onCancel(); } - this.returnPromise.reject(new Error('Dialog cancelled')); + this.returnPromise.reject(new HdfsDialogCancelledError()); } protected async reportError(error: any): Promise { diff --git a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts index ef85ff08fe..c28c8028fa 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts @@ -45,7 +45,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider { - let controllers = this.root.children.map((e): IControllerInfoSlim => { - let controller = e as ControllerNode; + const controllers = this.root.children.map((e): IControllerInfoSlim => { + const controller = e as ControllerNode; return { url: controller.url, auth: controller.auth, username: controller.username, password: controller.password, - rememberPassword: !!controller.rememberPassword + rememberPassword: controller.rememberPassword }; }); - let controllersWithoutPassword = controllers.map((e): IControllerInfoSlim => { + const controllersWithoutPassword = controllers.map((e): IControllerInfoSlim => { return { url: e.url, auth: e.auth, @@ -164,7 +172,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider { + private async getPassword(url: string, username: string): Promise { let provider = await this.getCredentialProvider(); let id = this.createId(url, username); let credential = await provider.readCredential(id); diff --git a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts index 5e7eda8b92..c9aaf553bc 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts @@ -103,7 +103,15 @@ export class ControllerRootNode extends ControllerTreeNode { return this.children as ControllerNode[]; } - public addControllerNode( + /** + * Creates or updates a node in the tree with the specified connection information + * @param url The URL for the BDC management endpoint + * @param auth The type of auth to use + * @param username The username (if basic auth) + * @param password The password (if basic auth) + * @param rememberPassword Whether to store the password in the password store when saving + */ + public addOrUpdateControllerNode( url: string, auth: AuthType, username: string, @@ -201,6 +209,10 @@ export class ControllerNode extends ControllerTreeNode { this._password = pw; } + public set label(label: string) { + super.label = label || this.generateLabel(); + } + public get rememberPassword() { return this._rememberPassword; } @@ -209,10 +221,6 @@ export class ControllerNode extends ControllerTreeNode { this._rememberPassword = rememberPassword; } - public set label(label: string) { - super.label = label || this.generateLabel(); - } - private generateLabel(): string { let label = `controller: ${ControllerNode.toIpAndPort(this._url)}`; if (this._auth === 'basic') { diff --git a/extensions/big-data-cluster/src/bigDataCluster/utils.ts b/extensions/big-data-cluster/src/bigDataCluster/utils.ts index 282107c341..237295104b 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/utils.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/utils.ts @@ -269,3 +269,7 @@ export function getControllerEndpoint(serverInfo: azdata.ServerInfo): string | u } return undefined; } + +export function getBdcStatusErrorMessage(error: Error): string { + return localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", error.message); +} diff --git a/extensions/big-data-cluster/src/extension.ts b/extensions/big-data-cluster/src/extension.ts index 9ecf965234..a771e015a6 100644 --- a/extensions/big-data-cluster/src/extension.ts +++ b/extensions/big-data-cluster/src/extension.ts @@ -20,13 +20,13 @@ import { getControllerEndpoint } from './bigDataCluster/utils'; const localize = nls.loadMessageBundle(); -const AddControllerCommand = 'bigDataClusters.command.addController'; -const DeleteControllerCommand = 'bigDataClusters.command.deleteController'; -const RefreshControllerCommand = 'bigDataClusters.command.refreshController'; -const ManageControllerCommand = 'bigDataClusters.command.manageController'; -const MountHdfsCommand = 'bigDataClusters.command.mount'; -const RefreshMountCommand = 'bigDataClusters.command.refreshmount'; -const DeleteMountCommand = 'bigDataClusters.command.deletemount'; +export const AddControllerCommand = 'bigDataClusters.command.addController'; +export const DeleteControllerCommand = 'bigDataClusters.command.deleteController'; +export const RefreshControllerCommand = 'bigDataClusters.command.refreshController'; +export const ManageControllerCommand = 'bigDataClusters.command.manageController'; +export const MountHdfsCommand = 'bigDataClusters.command.mount'; +export const RefreshMountCommand = 'bigDataClusters.command.refreshmount'; +export const DeleteMountCommand = 'bigDataClusters.command.deletemount'; const endpointNotFoundError = localize('mount.error.endpointNotFound', "Controller endpoint information was not found"); @@ -51,8 +51,8 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node)); }); - vscode.commands.registerCommand(DeleteControllerCommand, (node: TreeNode) => { - deleteBdcController(treeDataProvider, node); + vscode.commands.registerCommand(DeleteControllerCommand, async (node: TreeNode) => { + await deleteBdcController(treeDataProvider, node); }); vscode.commands.registerCommand(RefreshControllerCommand, (node: TreeNode) => { @@ -64,7 +64,7 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co vscode.commands.registerCommand(ManageControllerCommand, async (info: ControllerNode | BdcDashboardOptions) => { const title: string = `${localize('bdc.dashboard.title', "Big Data Cluster Dashboard -")} ${ControllerNode.toIpAndPort(info.url)}`; - const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(info)); + const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(info, treeDataProvider)); dashboard.showDashboard(); }); @@ -80,26 +80,26 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co } async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promise { - let mountProps = await getMountProps(explorerContext); + const mountProps = await getMountProps(explorerContext); if (mountProps) { - let dialog = new MountHdfsDialog(new MountHdfsModel(mountProps)); - dialog.showDialog(); + const dialog = new MountHdfsDialog(new MountHdfsModel(mountProps)); + await dialog.showDialog(); } } async function refreshMount(explorerContext?: azdata.ObjectExplorerContext): Promise { - let mountProps = await getMountProps(explorerContext); + const mountProps = await getMountProps(explorerContext); if (mountProps) { - let dialog = new RefreshMountDialog(new RefreshMountModel(mountProps)); - dialog.showDialog(); + const dialog = new RefreshMountDialog(new RefreshMountModel(mountProps)); + await dialog.showDialog(); } } async function deleteMount(explorerContext?: azdata.ObjectExplorerContext): Promise { - let mountProps = await getMountProps(explorerContext); + const mountProps = await getMountProps(explorerContext); if (mountProps) { - let dialog = new DeleteMountDialog(new DeleteMountModel(mountProps)); - dialog.showDialog(); + const dialog = new DeleteMountDialog(new DeleteMountModel(mountProps)); + await dialog.showDialog(); } } @@ -169,15 +169,15 @@ async function deleteBdcController(treeDataProvider: ControllerTreeDataProvider, let result = await vscode.window.showQuickPick(Object.keys(choices), options); let remove: boolean = !!(result && choices[result]); if (remove) { - deleteControllerInternal(treeDataProvider, controllerNode); + await deleteControllerInternal(treeDataProvider, controllerNode); } return remove; } -function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider, controllerNode: ControllerNode): void { - let deleted = treeDataProvider.deleteController(controllerNode.url, controllerNode.auth, controllerNode.username); +async function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider, controllerNode: ControllerNode): Promise { + const deleted = treeDataProvider.deleteController(controllerNode.url, controllerNode.auth, controllerNode.username); if (deleted) { - treeDataProvider.saveControllers(); + await treeDataProvider.saveControllers(); } } diff --git a/src/sql/workbench/browser/modelComponents/divContainer.component.ts b/src/sql/workbench/browser/modelComponents/divContainer.component.ts index ab3458ef6f..a542d7867b 100644 --- a/src/sql/workbench/browser/modelComponents/divContainer.component.ts +++ b/src/sql/workbench/browser/modelComponents/divContainer.component.ts @@ -23,7 +23,7 @@ class DivItem { @Component({ template: ` -
+