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/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts index 34fa54db40..9e696f66b3 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -8,7 +8,7 @@ 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'; @@ -34,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; @@ -44,7 +45,7 @@ export class BdcDashboard { constructor(private title: string, private model: BdcDashboardModel) { this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); - this.model.onError(error => this.handleError(error)); + this.model.onBdcError(errorEvent => this.handleError(errorEvent)); } public showDashboard(): void { @@ -75,6 +76,7 @@ export class BdcDashboard { }).component(); this.refreshButton.onDidClick(async () => { + this.overviewPage.onRefreshStarted(); await this.doRefresh(); }); @@ -130,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); }); @@ -167,11 +170,14 @@ export class BdcDashboard { this.updateServiceNavTabs(bdcStatus.services); } - private handleError(error: Error): void { + 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 (!(error instanceof HdfsDialogCancelledError)) { - showErrorMessage(error.message); + if (!(errorEvent.error instanceof HdfsDialogCancelledError)) { + showErrorMessage(errorEvent.error.message); } } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts index ab4c99b734..f4996e9be9 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts @@ -14,6 +14,9 @@ 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; @@ -23,10 +26,10 @@ export class BdcDashboardModel { private _endpointsLastUpdated: Date; private readonly _onDidUpdateEndpoints = new vscode.EventEmitter(); private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter(); - private readonly _onError = new vscode.EventEmitter(); + private readonly _onBdcError = new vscode.EventEmitter(); public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event; public onDidUpdateBdcStatus = this._onDidUpdateBdcStatus.event; - public onError = this._onError.event; + public onBdcError = this._onBdcError.event; constructor(private _options: BdcDashboardOptions, private _treeDataProvider: ControllerTreeDataProvider, ignoreSslVerification = true) { try { @@ -37,7 +40,7 @@ export class BdcDashboardModel { this.promptReconnect().then(async () => { await this.refresh(); }).catch(error => { - this._onError.fire(error); + this._onBdcError.fire({ error: error, errorType: 'general' }); }); } } @@ -70,16 +73,16 @@ export class BdcDashboardModel { 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._onError.fire(error); + this._onBdcError.fire({ error: error, errorType: 'general' }); } } 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/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/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: ` -
+