diff --git a/extensions/big-data-cluster/resources/dark/notebook_inverse.svg b/extensions/big-data-cluster/resources/dark/notebook_inverse.svg new file mode 100644 index 0000000000..841199cf11 --- /dev/null +++ b/extensions/big-data-cluster/resources/dark/notebook_inverse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/big-data-cluster/resources/dark/status_ok_dark.svg b/extensions/big-data-cluster/resources/dark/status_ok_dark.svg new file mode 100644 index 0000000000..776e1fd909 --- /dev/null +++ b/extensions/big-data-cluster/resources/dark/status_ok_dark.svg @@ -0,0 +1 @@ +success_16x16 \ No newline at end of file diff --git a/extensions/big-data-cluster/resources/dark/status_warning_dark.svg b/extensions/big-data-cluster/resources/dark/status_warning_dark.svg new file mode 100644 index 0000000000..a267963e58 --- /dev/null +++ b/extensions/big-data-cluster/resources/dark/status_warning_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/big-data-cluster/resources/light/notebook.svg b/extensions/big-data-cluster/resources/light/notebook.svg new file mode 100644 index 0000000000..2711d10b2a --- /dev/null +++ b/extensions/big-data-cluster/resources/light/notebook.svg @@ -0,0 +1,7 @@ + + Artboard 20 + + + + + diff --git a/extensions/big-data-cluster/resources/light/status_ok_light.svg b/extensions/big-data-cluster/resources/light/status_ok_light.svg new file mode 100644 index 0000000000..776e1fd909 --- /dev/null +++ b/extensions/big-data-cluster/resources/light/status_ok_light.svg @@ -0,0 +1 @@ +success_16x16 \ No newline at end of file diff --git a/extensions/big-data-cluster/resources/light/status_warning_light.svg b/extensions/big-data-cluster/resources/light/status_warning_light.svg new file mode 100644 index 0000000000..f2e2aa741e --- /dev/null +++ b/extensions/big-data-cluster/resources/light/status_warning_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/big-data-cluster/src/bigDataCluster/constants.ts b/extensions/big-data-cluster/src/bigDataCluster/constants.ts index 3f3e39c09e..7cab730ced 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/constants.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/constants.ts @@ -17,36 +17,56 @@ export enum BdcItemType { loadingController = 'bigDataClusters.itemType.loadingControllerNode' } -export class IconPath { +export interface IconPath { + dark: string; + light: string; +} + +export class IconPathHelper { private static extensionContext: vscode.ExtensionContext; - public static controllerNode: { dark: string, light: string }; - public static folderNode: { dark: string, light: string }; - public static sqlMasterNode: { dark: string, light: string }; - public static copy: { dark: string, light: string }; - public static refresh: { dark: string, light: string }; + public static controllerNode: IconPath; + public static folderNode: IconPath; + public static sqlMasterNode: IconPath; + public static copy: IconPath; + public static refresh: IconPath; + public static status_ok: IconPath; + public static status_warning: IconPath; + public static notebook: IconPath; public static setExtensionContext(extensionContext: vscode.ExtensionContext) { - IconPath.extensionContext = extensionContext; - IconPath.controllerNode = { - dark: IconPath.extensionContext.asAbsolutePath('resources/dark/bigDataCluster_controller.svg'), - light: IconPath.extensionContext.asAbsolutePath('resources/light/bigDataCluster_controller.svg') + IconPathHelper.extensionContext = extensionContext; + IconPathHelper.controllerNode = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/bigDataCluster_controller.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/bigDataCluster_controller.svg') }; - IconPath.folderNode = { - dark: IconPath.extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), - light: IconPath.extensionContext.asAbsolutePath('resources/light/folder.svg') + IconPathHelper.folderNode = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/folder.svg') }; - IconPath.sqlMasterNode = { - dark: IconPath.extensionContext.asAbsolutePath('resources/dark/sql_bigdata_cluster_inverse.svg'), - light: IconPath.extensionContext.asAbsolutePath('resources/light/sql_bigdata_cluster.svg') + IconPathHelper.sqlMasterNode = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/sql_bigdata_cluster_inverse.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/sql_bigdata_cluster.svg') }; - IconPath.copy = { - light: IconPath.extensionContext.asAbsolutePath('resources/light/copy.svg'), - dark: IconPath.extensionContext.asAbsolutePath('resources/dark/copy_inverse.svg') + IconPathHelper.copy = { + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/copy.svg'), + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/copy_inverse.svg') }; - IconPath.refresh = { - light: IconPath.extensionContext.asAbsolutePath('resources/light/refresh.svg'), - dark: IconPath.extensionContext.asAbsolutePath('resources/dark/refresh_inverse.svg') + IconPathHelper.refresh = { + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/refresh.svg'), + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/refresh_inverse.svg') + }; + IconPathHelper.status_ok = { + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_ok_light.svg'), + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_ok_dark.svg') + }; + IconPathHelper.status_warning = { + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_warning_light.svg'), + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_warning_dark.svg') + }; + IconPathHelper.notebook = { + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/notebook.svg'), + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg') }; } } diff --git a/extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts b/extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts index 9e6714d594..15b8321152 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts @@ -10,7 +10,6 @@ import localVarRequest = require('request'); import http = require('http'); -import Promise = require('bluebird'); let defaultBasePath = 'https://localhost'; diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts index 4825f33508..cb6ec5e297 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -6,12 +6,14 @@ 'use strict'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { BdcDashboardModel } from './bdcDashboardModel'; -import { IconPath } from '../constants'; +import { IconPathHelper } from '../constants'; import { BdcServiceStatusPage } from './bdcServiceStatusPage'; import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage'; import { EndpointModel, BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; +import { getHealthStatusDot, getServiceNameDisplayText } from '../utils'; const localize = nls.loadMessageBundle(); @@ -59,13 +61,30 @@ export class BdcDashboard { const refreshButton = modelView.modelBuilder.button() .withProperties({ label: localize('bdc.dashboard.refreshButton', "Refresh"), - iconPath: IconPath.refresh, - height: '50' + iconPath: IconPathHelper.refresh, + height: '50px' }).component(); refreshButton.onDidClick(() => this.model.refresh()); - const toolbarContainer = modelView.modelBuilder.toolbarContainer().withToolbarItems([{ component: refreshButton }]).component(); + const openTroubleshootNotebookButton = modelView.modelBuilder.button() + .withProperties({ + label: localize('bdc.dashboard.troubleshootButton', "Troubleshoot"), + iconPath: IconPathHelper.notebook, + height: '50px' + }).component(); + + openTroubleshootNotebookButton.onDidClick(() => { + vscode.commands.executeCommand('mssqlCluster.task.openNotebook'); + }); + + const toolbarContainer = modelView.modelBuilder.toolbarContainer() + .withToolbarItems( + [ + { component: refreshButton }, + { component: openTroubleshootNotebookButton } + ] + ).component(); rootContainer.addItem(toolbarContainer, { flex: '0 0 auto' }); @@ -143,7 +162,7 @@ export class BdcDashboard { if (this.initialized && !this.serviceTabsCreated && services) { // Add a nav item for each service services.forEach(s => { - const navItem = createServiceNavTab(this.modelView.modelBuilder, getFriendlyServiceName(s.serviceName)); + const navItem = createServiceNavTab(this.modelView.modelBuilder, s); const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView).container; navItem.onDidClick(() => { this.mainAreaContainer.removeItem(this.currentPage); @@ -157,28 +176,11 @@ export class BdcDashboard { } } -function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceName: string): azdata.DivContainer { - const navItem = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); - navItem.addItem(modelBuilder.text().withProperties({ value: serviceName }).component(), { CSSStyles: { 'user-select': 'text' } }); - return navItem; -} - -function getFriendlyServiceName(serviceName: string): string { - serviceName = serviceName || ''; - switch (serviceName.toLowerCase()) { - case 'sql': - return localize('bdc.dashboard.sql', "SQL Server"); - case 'hdfs': - return localize('bdc.dashboard.hdfs', "HDFS"); - case 'spark': - return localize('bdc.dashboard.spark', "Spark"); - case 'control': - return localize('bdc.dashboard.control', "Control"); - case 'gateway': - return localize('bdc.dashboard.gateway', "Gateway"); - case 'app': - return localize('bdc.dashboard.app', "App"); - default: - return serviceName; - } +function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceStatus: ServiceStatusModel): azdata.DivContainer { + const div = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); + const innerContainer = modelBuilder.flexContainer().withLayout({ width: navWidth, height: '30px', flexFlow: 'row' }).component(); + innerContainer.addItem(modelBuilder.text().withProperties({ value: getHealthStatusDot(serviceStatus.healthStatus), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'none', 'color': 'red', 'font-size': '40px', 'width': '20px' } }).component(), { flex: '0 0 auto' }); + innerContainer.addItem(modelBuilder.text().withProperties({ value: getServiceNameDisplayText(serviceStatus.serviceName), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(), { flex: '0 0 auto' }); + div.addItem(innerContainer); + return div; } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts index b83f02663a..058941517c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts @@ -14,6 +14,8 @@ export class BdcDashboardModel { private _bdcStatus: BdcStatusModel; private _endpoints: EndpointModel[] = []; + private _bdcStatusLastUpdated: Date; + private _endpointsLastUpdated: Date; private readonly _onDidUpdateEndpoints = new vscode.EventEmitter(); private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter(); public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event; @@ -31,40 +33,26 @@ export class BdcDashboardModel { return this._endpoints || []; } + public get bdcStatusLastUpdated(): Date { + return this._bdcStatusLastUpdated; + } + + public get endpointsLastUpdated(): Date { + return this._endpointsLastUpdated; + } + public async refresh(): Promise { await Promise.all([ getBdcStatus(this.url, this.username, this.password, true).then(response => { this._bdcStatus = response.bdcStatus; + this._bdcStatusLastUpdated = new Date(); this._onDidUpdateBdcStatus.fire(this.bdcStatus); }), getEndPoints(this.url, this.username, this.password, true).then(response => { this._endpoints = response.endPoints || []; + this._endpointsLastUpdated = new Date(); this._onDidUpdateEndpoints.fire(this.serviceEndpoints); }) ]).catch(error => showErrorMessage(error)); } } - -export enum Endpoint { - gateway = 'gateway', - sparkHistory = 'spark-history', - yarnUi = 'yarn-ui', - appProxy = 'app-proxy', - mgmtproxy = 'mgmtproxy', - managementProxy = 'management-proxy', - logsui = 'logsui', - metricsui = 'metricsui', - controller = 'controller', - sqlServerMaster = 'sql-server-master', - webhdfs = 'webhdfs', - livy = 'livy' -} - -export enum Service { - sql = 'sql', - hdfs = 'hdfs', - spark = 'spark', - control = 'control', - gateway = 'gateway', - app = 'app' -} diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts index 2bd6217e3c..6619e61539 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts @@ -7,51 +7,35 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import { BdcDashboardModel, Endpoint, Service } from './bdcDashboardModel'; -import { IconPath } from '../constants'; +import { BdcDashboardModel } from './bdcDashboardModel'; +import { IconPathHelper } from '../constants'; +import { getStateDisplayText, getHealthStatusDisplayText, getHealthStatusIcon, getEndpointDisplayText, getServiceNameDisplayText, Endpoint } from '../utils'; import { EndpointModel, ServiceStatusModel, BdcStatusModel } from '../controller/apiGenerated'; const localize = nls.loadMessageBundle(); -interface IServiceStatusRow { - stateLoadingComponent: azdata.LoadingComponent; - healthStatusLoadingComponent: azdata.LoadingComponent; -} - -interface IServiceEndpointRow { - endpointLoadingComponent: azdata.LoadingComponent; - isHyperlink: boolean; -} - -const navWidth = '175px'; +const overviewIconColumnWidth = '50px'; const overviewServiceNameCellWidth = '100px'; const overviewStateCellWidth = '75px'; -const overviewHealthStatusCellWidth = '75px'; +const overviewHealthStatusCellWidth = '100px'; const serviceEndpointRowServiceNameCellWidth = '125px'; const serviceEndpointRowEndpointCellWidth = '350px'; +const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi]; + export class BdcDashboardOverviewPage { private initialized: boolean = false; + private modelBuilder: azdata.ModelBuilder; + private lastUpdatedLabel: azdata.TextComponent; private clusterStateLoadingComponent: azdata.LoadingComponent; private clusterHealthStatusLoadingComponent: azdata.LoadingComponent; - private sqlServerStatusRow: IServiceStatusRow; - private hdfsStatusRow: IServiceStatusRow; - private sparkStatusRow: IServiceStatusRow; - private controlStatusRow: IServiceStatusRow; - private gatewayStatusRow: IServiceStatusRow; - private appStatusRow: IServiceStatusRow; + private serviceStatusRowContainer: azdata.FlexContainer; - private sqlServerEndpointRow: IServiceEndpointRow; - private controllerEndpointRow: IServiceEndpointRow; - private hdfsSparkGatewayEndpointRow: IServiceEndpointRow; - private sparkHistoryEndpointRow: IServiceEndpointRow; - private yarnHistoryEndpointRow: IServiceEndpointRow; - private grafanaDashboardEndpointRow: IServiceEndpointRow; - private kibanaDashboardEndpointRow: IServiceEndpointRow; + private endpointsRowContainer: azdata.FlexContainer; constructor(private model: BdcDashboardModel) { this.model.onDidUpdateEndpoints(endpoints => this.handleEndpointsUpdate(endpoints)); @@ -59,6 +43,7 @@ export class BdcDashboardOverviewPage { } public create(view: azdata.ModelView): azdata.FlexContainer { + this.modelBuilder = view.modelBuilder; const rootContainer = view.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', @@ -109,30 +94,51 @@ export class BdcDashboardOverviewPage { // # OVERVIEW # // ############ + const overviewHeaderContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component(); + rootContainer.addItem(overviewHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } }); + const overviewLabel = view.modelBuilder.text() - .withProperties({ value: localize('bdc.dashboard.overviewHeader', "Cluster Overview"), CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } }) + .withProperties({ + value: localize('bdc.dashboard.overviewHeader', "Cluster Overview"), + CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } + }) .component(); - rootContainer.addItem(overviewLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } }); + + overviewHeaderContainer.addItem(overviewLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold' } }); + + this.lastUpdatedLabel = view.modelBuilder.text() + .withProperties({ + value: localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", '-'), + CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'color': 'lightgray' } + }).component(); + + overviewHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } }); const overviewContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%', alignItems: 'left' }).component(); // Service Status header row const serviceStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + const serviceStatusIconHeader = view.modelBuilder.text().component(); + serviceStatusHeaderRow.addItem(serviceStatusIconHeader, { CSSStyles: { 'width': overviewIconColumnWidth, 'min-width': overviewIconColumnWidth } }); const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.serviceNameHeader', "Service Name") }).component(); serviceStatusHeaderRow.addItem(nameCell, { CSSStyles: { 'width': overviewServiceNameCellWidth, 'min-width': overviewServiceNameCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); - const stateCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State") }).component(); - serviceStatusHeaderRow.addItem(stateCell, { CSSStyles: { 'width': overviewStateCellWidth, 'min-width': overviewStateCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); - const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component(); - serviceStatusHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': overviewHealthStatusCellWidth, 'min-width': overviewHealthStatusCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const stateCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State"), CSSStyles: { 'text-align': 'center', 'font-weight': 'bold' } }).component(); + serviceStatusHeaderRow.addItem(stateCell, { CSSStyles: { 'width': overviewStateCellWidth, 'min-width': overviewStateCellWidth, 'user-select': 'text' } }); + const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status"), CSSStyles: { 'text-align': 'center', 'font-weight': 'bold' } }).component(); + serviceStatusHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': overviewHealthStatusCellWidth, 'min-width': overviewHealthStatusCellWidth, 'user-select': 'text' } }); overviewContainer.addItem(serviceStatusHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); - this.sqlServerStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.sqlServerLabel', "SQL Server")); - this.hdfsStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.hdfsLabel', "HDFS")); - this.sparkStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.sparkLabel', "Spark")); - this.controlStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.controlLabel', "Control")); - this.gatewayStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.gatewayLabel', "Gateway")); - this.appStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.appLabel', "App")); + // 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 + // messes up the layout for the row container that we display after loading is finished. Instead we just remove the loading component + // and replace it with the rows directly + const serviceStatusRowContainerLoadingComponent = view.modelBuilder.loadingComponent() + .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) + .component(); + this.serviceStatusRowContainer.addItem(serviceStatusRowContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); + overviewContainer.addItem(this.serviceStatusRowContainer); rootContainer.addItem(overviewContainer, { flex: '0 0 auto' }); // ##################### @@ -154,13 +160,16 @@ export class BdcDashboardOverviewPage { endpointsHeaderRow.addItem(endpointsEndpointHeaderCell, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); endpointsContainer.addItem(endpointsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); - this.sqlServerEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('sql-server'), false); - this.controllerEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('controller'), false); - this.hdfsSparkGatewayEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('gateway'), false); - this.sparkHistoryEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('spark-history'), true); - this.yarnHistoryEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('yarn-history'), true); - this.grafanaDashboardEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('grafana'), true); - this.kibanaDashboardEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('kibana'), true); + 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 + // and replace it with the rows directly + const endpointRowContainerLoadingComponent = view.modelBuilder.loadingComponent() + .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) + .component(); + this.endpointsRowContainer.addItem(endpointRowContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); + + endpointsContainer.addItem(this.endpointsRowContainer); rootContainer.addItem(endpointsContainer, { flex: '0 0 auto' }); @@ -177,31 +186,22 @@ export class BdcDashboardOverviewPage { if (!this.initialized || !bdcStatus) { return; } + this.lastUpdatedLabel.value = + localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", + this.model.bdcStatusLastUpdated ? + `${this.model.bdcStatusLastUpdated.toLocaleDateString()} ${this.model.bdcStatusLastUpdated.toLocaleTimeString()}` + : '-'); this.clusterStateLoadingComponent.loading = false; this.clusterHealthStatusLoadingComponent.loading = false; - this.clusterStateLoadingComponent.component.updateProperty('value', bdcStatus.state); - this.clusterHealthStatusLoadingComponent.component.updateProperty('value', bdcStatus.healthStatus); + (this.clusterStateLoadingComponent.component).value = getStateDisplayText(bdcStatus.state); + (this.clusterHealthStatusLoadingComponent.component).value = getHealthStatusDisplayText(bdcStatus.healthStatus); if (bdcStatus.services) { - // Service Status - const sqlServerServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.sql); - updateServiceStatusRow(this.sqlServerStatusRow, sqlServerServiceStatus); - - const hdfsServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.hdfs); - updateServiceStatusRow(this.hdfsStatusRow, hdfsServiceStatus); - - const sparkServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.spark); - updateServiceStatusRow(this.sparkStatusRow, sparkServiceStatus); - - const controlServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.control); - updateServiceStatusRow(this.controlStatusRow, controlServiceStatus); - - const gatewayServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.gateway); - updateServiceStatusRow(this.gatewayStatusRow, gatewayServiceStatus); - - const appServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.app); - updateServiceStatusRow(this.appStatusRow, appServiceStatus); + this.serviceStatusRowContainer.clearItems(); + bdcStatus.services.forEach((s, i) => { + createServiceStatusRow(this.modelBuilder, this.serviceStatusRowContainer, s, i === bdcStatus.services.length - 1); + }); } } @@ -210,105 +210,45 @@ export class BdcDashboardOverviewPage { return; } - // Service Endpoints - const sqlServerEndpoint = endpoints.find(e => e.name === Endpoint.sqlServerMaster); - updateServiceEndpointRow(this.sqlServerEndpointRow, sqlServerEndpoint); - - const controllerEndpoint = endpoints.find(e => e.name === Endpoint.controller); - updateServiceEndpointRow(this.controllerEndpointRow, controllerEndpoint); - - const gatewayEndpoint = endpoints.find(e => e.name === Endpoint.gateway); - updateServiceEndpointRow(this.hdfsSparkGatewayEndpointRow, gatewayEndpoint); - - const yarnHistoryEndpoint = endpoints.find(e => e.name === Endpoint.yarnUi); - updateServiceEndpointRow(this.yarnHistoryEndpointRow, yarnHistoryEndpoint); - - const sparkHistoryEndpoint = endpoints.find(e => e.name === Endpoint.sparkHistory); - updateServiceEndpointRow(this.sparkHistoryEndpointRow, sparkHistoryEndpoint); - - const grafanaDashboardEndpoint = endpoints.find(e => e.name === Endpoint.metricsui); - updateServiceEndpointRow(this.grafanaDashboardEndpointRow, grafanaDashboardEndpoint); - - const kibanaDashboardEndpoint = endpoints.find(e => e.name === Endpoint.logsui); - updateServiceEndpointRow(this.kibanaDashboardEndpointRow, kibanaDashboardEndpoint); + this.endpointsRowContainer.clearItems(); + endpoints.forEach((e, i) => { + createServiceEndpointRow(this.modelBuilder, this.endpointsRowContainer, e, hyperlinkedEndpoints.some(he => he === e.name), i === endpoints.length - 1); + }); } } -function updateServiceStatusRow(serviceStatusRow: IServiceStatusRow, serviceStatus: ServiceStatusModel) { - if (serviceStatus) { - serviceStatusRow.stateLoadingComponent.loading = false; - serviceStatusRow.healthStatusLoadingComponent.loading = false; - serviceStatusRow.stateLoadingComponent.component.updateProperty('value', serviceStatus.state); - serviceStatusRow.healthStatusLoadingComponent.component.updateProperty('value', serviceStatus.healthStatus); - } - else { - serviceStatusRow.stateLoadingComponent.loading = true; - serviceStatusRow.healthStatusLoadingComponent.loading = true; - } -} - -function updateServiceEndpointRow(serviceEndpointRow: IServiceEndpointRow, endpoint: EndpointModel) { - if (endpoint) { - serviceEndpointRow.endpointLoadingComponent.loading = false; - if (serviceEndpointRow.isHyperlink) { - serviceEndpointRow.endpointLoadingComponent.component.updateProperties({ label: endpoint.endpoint, url: endpoint.endpoint }); - } - else { - serviceEndpointRow.endpointLoadingComponent.component.updateProperty('value', endpoint.endpoint); - } - } - else { - serviceEndpointRow.endpointLoadingComponent.loading = true; - } -} - -function createServiceStatusRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, name: string): IServiceStatusRow { +function createServiceStatusRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, serviceStatus: ServiceStatusModel, isLastRow: boolean): void { const serviceStatusRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); - const nameCell = modelBuilder.text().withProperties({ value: name, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); - serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); - const stateCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); - const stateLoadingComponent = modelBuilder.loadingComponent() - .withItem(stateCell) - .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) - .component(); - serviceStatusRow.addItem(stateLoadingComponent, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); - const healthStatusCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); - const healthStatusLoadingComponent = modelBuilder.loadingComponent() - .withItem(healthStatusCell) - .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) - .component(); - serviceStatusRow.addItem(healthStatusLoadingComponent, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); + const statusIconCell = modelBuilder.text().withProperties({ value: getHealthStatusIcon(serviceStatus.healthStatus), CSSStyles: { 'user-select': 'none' } }).component(); + serviceStatusRow.addItem(statusIconCell, { CSSStyles: { 'width': overviewIconColumnWidth, 'min-width': overviewIconColumnWidth } }); + const nameCell = modelBuilder.text().withProperties({ value: getServiceNameDisplayText(serviceStatus.serviceName), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); + serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': overviewServiceNameCellWidth, 'min-width': overviewServiceNameCellWidth, 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); + const stateCell = modelBuilder.text().withProperties({ value: getStateDisplayText(serviceStatus.state), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text', 'text-align': 'center' } }).component(); + serviceStatusRow.addItem(stateCell, { CSSStyles: { 'width': overviewStateCellWidth, 'min-width': overviewStateCellWidth } }); + const healthStatusCell = modelBuilder.text().withProperties({ value: getHealthStatusDisplayText(serviceStatus.healthStatus), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text', 'text-align': 'center' } }).component(); + serviceStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': overviewHealthStatusCellWidth, 'min-width': overviewHealthStatusCellWidth } }); - container.addItem(serviceStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); - - return { stateLoadingComponent: stateLoadingComponent, healthStatusLoadingComponent: healthStatusLoadingComponent }; + container.addItem(serviceStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'border-bottom': isLastRow ? 'solid 1px #ccc' : '', 'box-sizing': 'border-box', 'user-select': 'text' } }); } -function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, name: string, isHyperlink: boolean): IServiceEndpointRow { - const endPointRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); - const nameCell = modelBuilder.text().withProperties({ value: name, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); - endPointRow.addItem(nameCell, { CSSStyles: { 'width': serviceEndpointRowServiceNameCellWidth, 'min-width': serviceEndpointRowServiceNameCellWidth, 'user-select': 'text' } }); - let retRow: IServiceEndpointRow; +function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, endpoint: EndpointModel, isHyperlink: boolean, isLastRow: boolean): void { + const endPointRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '40px' }).component(); + const nameCell = modelBuilder.text().withProperties({ value: getEndpointDisplayText(endpoint.name, endpoint.description), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); + endPointRow.addItem(nameCell, { CSSStyles: { 'width': serviceEndpointRowServiceNameCellWidth, 'min-width': serviceEndpointRowServiceNameCellWidth, 'user-select': 'text', 'text-align': 'center' } }); if (isHyperlink) { - const endpointCell = modelBuilder.hyperlink().withProperties({ CSSStyles: { 'height': '15px' } }).component(); - const endpointLoadingComponent = modelBuilder.loadingComponent() - .withItem(endpointCell) - .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) + const endpointCell = modelBuilder.hyperlink() + .withProperties({ label: endpoint.endpoint, url: endpoint.endpoint, CSSStyles: { 'height': '15px' } }) .component(); - retRow = { endpointLoadingComponent: endpointLoadingComponent, isHyperlink: true }; - endPointRow.addItem(endpointLoadingComponent, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'color': '#0078d4', 'text-decoration': 'underline', 'overflow': 'hidden' } }); + endPointRow.addItem(endpointCell, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'color': '#0078d4', 'text-decoration': 'underline', 'overflow': 'hidden' } }); } else { - const endpointCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); - const endpointLoadingComponent = modelBuilder.loadingComponent() - .withItem(endpointCell) - .withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } }) + const endpointCell = modelBuilder.text() + .withProperties({ value: endpoint.endpoint, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }) .component(); - retRow = { endpointLoadingComponent: endpointLoadingComponent, isHyperlink: false }; - endPointRow.addItem(endpointLoadingComponent, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'overflow': 'hidden' } }); + endPointRow.addItem(endpointCell, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'overflow': 'hidden' } }); } const copyValueCell = modelBuilder.button().component(); - copyValueCell.iconPath = IconPath.copy; + copyValueCell.iconPath = IconPathHelper.copy; copyValueCell.onDidClick(() => { // vscode.env.clipboard.writeText(hyperlink); }); @@ -317,34 +257,5 @@ function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: copyValueCell.iconWidth = '14px'; endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '50px', 'min-width': '50px', 'padding-left': '10px', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); - container.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); - - return retRow; -} - -function getFriendlyEndpointNames(name: string): string { - switch (name) { - case 'app-proxy': - return localize('bdc.dashboard.appproxy', "Application Proxy"); - case 'controller': - return localize('bdc.dashboard.controller', "Controller"); - case 'gateway': - return localize('bdc.dashboard.gateway', "HDFS/Spark Gateway"); - case 'management-proxy': - return localize('bdc.dashboard.managementproxy', "Management Proxy"); - case 'mgmtproxy': - return localize('bdc.dashboard.mgmtproxy', "Management Proxy"); - case 'sql-server': - return localize('bdc.dashboard.sqlServerEndpoint', "SQL Server Master Instance"); - case 'grafana': - return localize('bdc.dashboard.grafana', "Metrics Dashboard"); - case 'kibana': - return localize('bdc.dashboard.kibana', "Log Search Dashboard"); - case 'yarn-history': - localize('bdc.dashboard.yarnHistory', "Spark Resource Management"); - case 'spark-history': - localize('sparkHistory', "Spark Job Monitoring"); - default: - return name; - } + container.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'border-bottom': isLastRow ? 'solid 1px #ccc' : '', 'box-sizing': 'border-box', 'user-select': 'text' } }); } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts index 75da85a71b..55f19307e5 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts @@ -8,6 +8,7 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import { BdcDashboardModel } from './bdcDashboardModel'; import { BdcStatusModel, InstanceStatusModel } from '../controller/apiGenerated'; +import { getHealthStatusDisplayText, getHealthStatusIcon, getStateDisplayText } from '../utils'; const localize = nls.loadMessageBundle(); @@ -22,9 +23,14 @@ export interface IInstanceStatus { healthStatus: string; } -const metricsAndLogsInstanceNameCellWidth = '100px'; -const metricsAndLogsMetricsCellWidth = '75px'; -const metricsAndLogsLogsCellWidth = '75px'; +const healthAndStatusIconColumnWidth = '50px'; +const healthAndStatusInstanceNameColumnWidth = '100px'; +const healthAndStatusStateColumnWidth = '75px'; +const healthAndStatusHealthColumnWidth = '75px'; + +const metricsAndLogsInstanceNameColumnWidth = '100px'; +const metricsAndLogsMetricsColumnWidth = '75px'; +const metricsAndLogsLogsColumnWidth = '75px'; export class BdcDashboardResourceStatusPage { @@ -32,7 +38,7 @@ export class BdcDashboardResourceStatusPage { private rootContainer: azdata.FlexContainer; private instanceHealthStatusRowsContainer: azdata.FlexContainer; private metricsAndLogsRowsContainer: azdata.FlexContainer; - + private lastUpdatedLabel: azdata.TextComponent; private initialized: boolean = false; constructor(private model: BdcDashboardModel, private modelView: azdata.ModelView, private serviceName: string, private resourceName: string) { @@ -57,20 +63,41 @@ export class BdcDashboardResourceStatusPage { // # INSTANCE HEALTH AND STATUS # // ############################## - // Instance Health Label label - const propertiesLabel = view.modelBuilder.text() - .withProperties({ value: localize('bdc.dashboard.healthStatusDetailsHeader', "Health Status Details"), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } }) + const healthStatusHeaderContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component(); + rootContainer.addItem(healthStatusHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } }); + + // Header label + const healthStatusHeaderLabel = view.modelBuilder.text() + .withProperties({ + value: localize('bdc.dashboard.healthStatusDetailsHeader', "Health Status Details"), + CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } + }) .component(); - rootContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } }); + + healthStatusHeaderContainer.addItem(healthStatusHeaderLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold' } }); + + // Last updated label + this.lastUpdatedLabel = view.modelBuilder.text() + .withProperties({ + value: localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", '-'), + CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'color': 'lightgray' } + }).component(); + + healthStatusHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } }); + + + healthStatusHeaderContainer.addItem(healthStatusHeaderLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold' } }); // Header row const instanceHealthStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const instanceHealthAndStatusNameHeaderRow = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.metricsAndLogsHeader', "Metrics and Logs") }).component(); - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusNameHeaderRow, { CSSStyles: { 'width': metricsAndLogsInstanceNameCellWidth, 'min-width': metricsAndLogsInstanceNameCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); - const instanceHealthAndStatusStateRow = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State") }).component(); - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusStateRow, { CSSStyles: { 'width': metricsAndLogsMetricsCellWidth, 'min-width': metricsAndLogsMetricsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); - const instanceHealthAndStatusHealthStatusRow = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component(); - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusHealthStatusRow, { CSSStyles: { 'width': metricsAndLogsLogsCellWidth, 'min-width': metricsAndLogsLogsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const instanceHealthAndStatusIconHeader = view.modelBuilder.text().component(); + instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusIconHeader, { CSSStyles: { 'width': healthAndStatusIconColumnWidth, 'min-width': healthAndStatusIconColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const instanceHealthAndStatusNameHeader = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.instanceHeader', "Instance") }).component(); + instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusNameHeader, { CSSStyles: { 'width': healthAndStatusInstanceNameColumnWidth, 'min-width': healthAndStatusInstanceNameColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const instanceHealthAndStatusState = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State") }).component(); + instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusState, { CSSStyles: { 'width': healthAndStatusStateColumnWidth, 'min-width': healthAndStatusStateColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const instanceHealthAndStatusHealthStatus = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component(); + instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusHealthStatus, { CSSStyles: { 'width': healthAndStatusHealthColumnWidth, 'min-width': healthAndStatusHealthColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); rootContainer.addItem(instanceHealthStatusHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); this.instanceHealthStatusRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); @@ -88,12 +115,12 @@ export class BdcDashboardResourceStatusPage { // Header row const metricsAndLogsHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.metricsAndLogsHeader', "Metrics and Logs") }).component(); - metricsAndLogsHeaderRow.addItem(nameCell, { CSSStyles: { 'width': metricsAndLogsInstanceNameCellWidth, 'min-width': metricsAndLogsInstanceNameCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.instanceHeader', "Instance") }).component(); + metricsAndLogsHeaderRow.addItem(nameCell, { CSSStyles: { 'width': metricsAndLogsInstanceNameColumnWidth, 'min-width': metricsAndLogsInstanceNameColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); const metricsCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.metricsHeader', "Metrics") }).component(); - metricsAndLogsHeaderRow.addItem(metricsCell, { CSSStyles: { 'width': metricsAndLogsMetricsCellWidth, 'min-width': metricsAndLogsMetricsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + metricsAndLogsHeaderRow.addItem(metricsCell, { CSSStyles: { 'width': metricsAndLogsMetricsColumnWidth, 'min-width': metricsAndLogsMetricsColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.logsHeader', "Logs") }).component(); - metricsAndLogsHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': metricsAndLogsLogsCellWidth, 'min-width': metricsAndLogsLogsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } }); + metricsAndLogsHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': metricsAndLogsLogsColumnWidth, 'min-width': metricsAndLogsLogsColumnWidth, 'font-weight': 'bold', 'user-select': 'text' } }); rootContainer.addItem(metricsAndLogsHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); this.metricsAndLogsRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); @@ -113,6 +140,12 @@ export class BdcDashboardResourceStatusPage { return; } + this.lastUpdatedLabel.value = + localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", + this.model.bdcStatusLastUpdated ? + `${this.model.bdcStatusLastUpdated.toLocaleDateString()} ${this.model.bdcStatusLastUpdated.toLocaleTimeString()}` + : '-'); + this.instanceHealthStatusRowsContainer.clearItems(); this.metricsAndLogsRowsContainer.clearItems(); @@ -134,12 +167,18 @@ export class BdcDashboardResourceStatusPage { */ function createInstanceHealthStatusRow(modelBuilder: azdata.ModelBuilder, instanceStatus: InstanceStatusModel): azdata.FlexContainer { const instanceHealthStatusRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); + const statusIconCell = modelBuilder.text() + .withProperties({ + value: getHealthStatusIcon(instanceStatus.healthStatus), + CSSStyles: { 'user-select': 'none' } + }).component(); + instanceHealthStatusRow.addItem(statusIconCell, { CSSStyles: { 'width': healthAndStatusIconColumnWidth, 'min-width': healthAndStatusIconColumnWidth } }); const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); - instanceHealthStatusRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); - const stateCell = modelBuilder.text().withProperties({ value: instanceStatus.state, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); - instanceHealthStatusRow.addItem(stateCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); - const healthStatusCell = modelBuilder.text().withProperties({ value: instanceStatus.healthStatus, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); - instanceHealthStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); + instanceHealthStatusRow.addItem(nameCell, { CSSStyles: { 'width': healthAndStatusInstanceNameColumnWidth, 'min-width': healthAndStatusInstanceNameColumnWidth, 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); + const stateCell = modelBuilder.text().withProperties({ value: getStateDisplayText(instanceStatus.state), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); + instanceHealthStatusRow.addItem(stateCell, { CSSStyles: { 'width': healthAndStatusStateColumnWidth, 'min-width': healthAndStatusStateColumnWidth } }); + const healthStatusCell = modelBuilder.text().withProperties({ value: getHealthStatusDisplayText(instanceStatus.healthStatus), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); + instanceHealthStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': healthAndStatusHealthColumnWidth, 'min-width': healthAndStatusHealthColumnWidth } }); return instanceHealthStatusRow; } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts index 34da9b5157..5494431283 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts @@ -8,6 +8,7 @@ import * as azdata from 'azdata'; import { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated'; import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage'; import { BdcDashboardModel } from './bdcDashboardModel'; +import { getHealthStatusDot } from '../utils'; export class BdcServiceStatusPage { @@ -75,7 +76,7 @@ export class BdcServiceStatusPage { private createResourceNavTabs(resources: ResourceStatusModel[]) { if (this.initialized && !this.resourceTabsCreated) { resources.forEach(resource => { - const resourceHeaderTab = createResourceHeaderTab(this.modelView, resource.resourceName); + const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource); const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container; resourceHeaderTab.onDidClick(() => { this.changeSelectedTabPage(resourceStatusPage); @@ -92,12 +93,15 @@ export class BdcServiceStatusPage { /** * Creates a single resource header tab - * @param view TheModelView used to construct the object + * @param modelBuilder The ModelBuilder used to construct the object * @param title The text to display in the tab */ -function createResourceHeaderTab(view: azdata.ModelView, title: string): azdata.DivContainer { - const resourceHeaderTab = view.modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).withProperties({ CSSStyles: { 'text-align': 'center' } }).component(); - const resourceHeaderLabel = view.modelBuilder.text().withProperties({ value: title, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); - resourceHeaderTab.addItem(resourceHeaderLabel); +function createResourceHeaderTab(modelBuilder: azdata.ModelBuilder, resourceStatus: ResourceStatusModel): azdata.DivContainer { + const resourceHeaderTab = modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).component(); + const innerContainer = modelBuilder.flexContainer().withLayout({ width: '100px', height: '25px', flexFlow: 'row' }).component(); + innerContainer.addItem(modelBuilder.text().withProperties({ value: getHealthStatusDot(resourceStatus.healthStatus), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'none', 'color': 'red', 'font-size': '40px', 'width': '20px', 'text-align': 'right' } }).component(), { flex: '0 0 auto' }); + const resourceHeaderLabel = modelBuilder.text().withProperties({ value: resourceStatus.resourceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'text-align': 'left' } }).component(); + innerContainer.addItem(resourceHeaderLabel); + resourceHeaderTab.addItem(innerContainer); return resourceHeaderTab; } diff --git a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts index 1f617324b6..f00515d380 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeNode.ts @@ -10,7 +10,7 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler'; import { TreeNode } from './treeNode'; -import { IconPath, BdcItemType } from '../constants'; +import { IconPathHelper, BdcItemType, IconPath } from '../constants'; import { getEndPoints } from '../controller/clusterControllerApi'; import { showErrorMessage } from '../utils'; import { EndpointModel } from '../controller/apiGenerated'; @@ -25,7 +25,7 @@ export abstract class ControllerTreeNode extends TreeNode { private _treeChangeHandler: IControllerTreeChangeHandler, private _description?: string, private _nodeType?: string, - private _iconPath?: { dark: string, light: string } + private _iconPath?: IconPath ) { super(label, parent); this._description = this._description || this.label; @@ -84,11 +84,11 @@ export abstract class ControllerTreeNode extends TreeNode { this._nodeType = nodeType; } - public set iconPath(iconPath: { dark: string, light: string }) { + public set iconPath(iconPath: IconPath) { this._iconPath = iconPath; } - public get iconPath(): { dark: string, light: string } { + public get iconPath(): IconPath { return this._iconPath; } @@ -175,7 +175,7 @@ export class ControllerNode extends ControllerTreeNode { treeChangeHandler: IControllerTreeChangeHandler, description?: string, ) { - super(label, parent, treeChangeHandler, description, BdcItemType.controller, IconPath.controllerNode); + super(label, parent, treeChangeHandler, description, BdcItemType.controller, IconPathHelper.controllerNode); this.label = label; this.description = description; @@ -297,7 +297,7 @@ export class FolderNode extends ControllerTreeNode { parent: ControllerTreeNode, treeChangeHandler: IControllerTreeChangeHandler ) { - super(label, parent, treeChangeHandler, label, BdcItemType.folder, IconPath.folderNode); + super(label, parent, treeChangeHandler, label, BdcItemType.folder, IconPathHelper.folderNode); } } @@ -313,7 +313,7 @@ export class SqlMasterNode extends ControllerTreeNode { treeChangeHandler: IControllerTreeChangeHandler, description?: string, ) { - super(label, parent, treeChangeHandler, description, BdcItemType.sqlMaster, IconPath.sqlMasterNode); + super(label, parent, treeChangeHandler, description, BdcItemType.sqlMaster, IconPathHelper.sqlMasterNode); this._username = 'sa'; this.label = label; this.description = description; diff --git a/extensions/big-data-cluster/src/bigDataCluster/utils.ts b/extensions/big-data-cluster/src/bigDataCluster/utils.ts index d9c68b8102..07f2aaa6da 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/utils.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/utils.ts @@ -6,6 +6,33 @@ 'use strict'; import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export enum Endpoint { + gateway = 'gateway', + sparkHistory = 'spark-history', + yarnUi = 'yarn-ui', + appProxy = 'app-proxy', + mgmtproxy = 'mgmtproxy', + managementProxy = 'management-proxy', + logsui = 'logsui', + metricsui = 'metricsui', + controller = 'controller', + sqlServerMaster = 'sql-server-master', + webhdfs = 'webhdfs', + livy = 'livy' +} + +export enum Service { + sql = 'sql', + hdfs = 'hdfs', + spark = 'spark', + control = 'control', + gateway = 'gateway', + app = 'app' +} export function generateGuid(): string { let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; @@ -41,3 +68,144 @@ export function showErrorMessage(error: any, prefixText?: string): void { vscode.window.showErrorMessage(text); } } + +/** + * Gets the localized text to display for a corresponding state + * @param state The state to get the display text for + */ +export function getStateDisplayText(state?: string): string { + state = state || ''; + switch (state.toLowerCase()) { + case 'creating': + return localize('state.creating', "Creating"); + case 'waiting': + return localize('state.waiting', "Waiting"); + case 'ready': + return localize('state.ready', "Ready"); + case 'deleting': + return localize('state.deleting', "Deleting"); + case 'waitingfordeletion': + return localize('state.waitingForDeletion', "Waiting For Deletion"); + case 'deleted': + return localize('state.deleted', "Deleted"); + case 'upgrading': + return localize('state.upgrading', "Upgrading"); + case 'waitingforupgrade': + return localize('state.waitingForUpgrade', "Waiting For Upgrade"); + case 'error': + return localize('state.error', "Error"); + case 'running': + return localize('state.running', "Running"); + default: + return state; + } +} + +/** + * Gets the localized text to display for a corresponding endpoint + * @param serviceName The endpoint name to get the display text for + * @param description The backup description to use if we don't have our own + */ +export function getEndpointDisplayText(endpointName?: string, description?: string): string { + endpointName = endpointName || ''; + switch (endpointName.toLowerCase()) { + case Endpoint.appProxy: + return localize('endpoint.appproxy', "Application Proxy"); + case Endpoint.controller: + return localize('endpoint.controller', "Controller"); + case Endpoint.gateway: + return localize('endpoint.gateway', "HDFS/Spark Gateway"); + case Endpoint.managementProxy: + return localize('endpoint.managementproxy', "Management Proxy"); + case Endpoint.mgmtproxy: + return localize('endpoint.mgmtproxy', "Management Proxy"); + case Endpoint.sqlServerMaster: + return localize('endpoint.sqlServerEndpoint', "SQL Server Master Instance"); + case Endpoint.metricsui: + return localize('endpoint.grafana', "Metrics Dashboard"); + case Endpoint.logsui: + return localize('endpoint.kibana', "Log Search Dashboard"); + case Endpoint.yarnUi: + return localize('endpoint.yarnHistory', "Spark Resource Management"); + case Endpoint.sparkHistory: + return localize('endpoint.sparkHistory', "Spark Job Monitoring"); + case Endpoint.webhdfs: + return localize('endpoint.webhdfs', "HDFS File System Proxy"); + case Endpoint.livy: + return localize('endpoint.livy', "Spark Proxy"); + default: + // Default is to use the description if one was given, otherwise worst case just fall back to using the + // original service name + return description && description.length > 0 ? description : endpointName; + } +} + +/** + * Gets the localized text to display for a corresponding service + * @param serviceName The service name to get the display text for + */ +export function getServiceNameDisplayText(serviceName?: string): string { + serviceName = serviceName || ''; + switch (serviceName.toLowerCase()) { + case Service.sql: + return localize('service.sql', "SQL Server"); + case Service.hdfs: + return localize('service.hdfs', "HDFS"); + case Service.spark: + return localize('service.spark', "Spark"); + case Service.control: + return localize('service.control', "Control"); + case Service.gateway: + return localize('service.gateway', "Gateway"); + case Service.app: + return localize('service.app', "App"); + default: + return serviceName; + } +} + +/** + * Gets the localized text to display for a corresponding health status + * @param healthStatus The health status to get the display text for + */ +export function getHealthStatusDisplayText(healthStatus?: string) { + healthStatus = healthStatus || ''; + switch (healthStatus.toLowerCase()) { + case 'healthy': + return localize('bdc.healthy', "Healthy"); + case 'unhealthy': + return localize('bdc.unhealthy', "Unhealthy"); + default: + return healthStatus; + } +} + +/** + * Returns the status icon for the corresponding health status + * @param healthStatus The status to check + */ +export function getHealthStatusIcon(healthStatus?: string): string { + healthStatus = healthStatus || ''; + switch (healthStatus.toLowerCase()) { + case 'healthy': + return '✔️'; + default: + // Consider all non-healthy status' as errors + return '⚠️'; + } +} + +/** + * Returns the status dot string which will be a • for all non-healthy states + * @param healthStatus The status to check + */ +export function getHealthStatusDot(healthStatus?: string): string { + healthStatus = healthStatus || ''; + switch (healthStatus.toLowerCase()) { + case 'healthy': + return ''; + default: + // Display status dot for all non-healthy status' + return '•'; + } +} diff --git a/extensions/big-data-cluster/src/extension.ts b/extensions/big-data-cluster/src/extension.ts index a02e9d2cb4..7fbaf3e491 100644 --- a/extensions/big-data-cluster/src/extension.ts +++ b/extensions/big-data-cluster/src/extension.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ControllerTreeDataProvider } from './bigDataCluster/tree/controllerTreeDataProvider'; -import { IconPath } from './bigDataCluster/constants'; +import { IconPathHelper } from './bigDataCluster/constants'; import { TreeNode } from './bigDataCluster/tree/treeNode'; import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog'; import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode'; @@ -25,7 +25,7 @@ const ManageControllerCommand = 'bigDataClusters.command.manageController'; let throttleTimers: { [key: string]: any } = {}; export function activate(extensionContext: vscode.ExtensionContext) { - IconPath.setExtensionContext(extensionContext); + IconPathHelper.setExtensionContext(extensionContext); let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState); registerTreeDataProvider(treeDataProvider); registerCommands(extensionContext, treeDataProvider);