diff --git a/extensions/big-data-cluster/src/bigDataCluster/constants.ts b/extensions/big-data-cluster/src/bigDataCluster/constants.ts index e7c9ec1fc1..139e682d7c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/constants.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/constants.ts @@ -58,7 +58,7 @@ export class IconPathHelper { export namespace cssStyles { export const title = { 'font-size': '14px', 'font-weight': '600' }; - export const tableHeader = { 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' }; + export const tableHeader = { 'text-align': 'left', 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' }; export const hyperlink = { 'user-select': 'text', 'color': '#0078d4', 'text-decoration': 'underline', 'cursor': 'pointer' }; export const text = { 'margin-block-start': '0px', 'margin-block-end': '0px' }; export const overflowEllipsisText = { ...text, 'overflow': 'hidden', 'text-overflow': 'ellipsis' }; diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts index ff46db0509..5899b0b910 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -34,11 +34,11 @@ export class BdcDashboard extends BdcDashboardPage { private overviewPage: BdcDashboardOverviewPage; private currentTab: NavTab; - private currentPage: azdata.FlexContainer; + private currentPageContainer: azdata.FlexContainer; private refreshButton: azdata.ButtonComponent; - private serviceTabPageMapping = new Map(); + private serviceTabPageMapping = new Map(); constructor(private title: string, private model: BdcDashboardModel) { super(); @@ -141,7 +141,7 @@ export class BdcDashboard extends BdcDashboardPage { overviewNavItemDiv.addItem(overviewNavItemText, { CSSStyles: { 'user-select': 'text' } }); this.overviewPage = new BdcDashboardOverviewPage(this, this.model); const overviewContainer: azdata.FlexContainer = this.overviewPage.create(modelView); - this.currentPage = overviewContainer; + this.currentPageContainer = overviewContainer; this.currentTab = { serviceName: undefined, div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; this.mainAreaContainer.addItem(overviewContainer, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); @@ -150,9 +150,9 @@ export class BdcDashboard extends BdcDashboardPage { this.currentTab.text.updateCssStyles(unselectedTabCss); this.currentTab.div.ariaSelected = false; } - this.mainAreaContainer.removeItem(this.currentPage); + this.mainAreaContainer.removeItem(this.currentPageContainer); this.mainAreaContainer.addItem(overviewContainer, { flex: '0 0 100%', CSSStyles: { 'margin': '0 20px 0 20px' } }); - this.currentPage = overviewContainer; + this.currentPageContainer = overviewContainer; this.currentTab = { serviceName: undefined, div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; this.currentTab.text.updateCssStyles(selectedTabCss); this.currentTab.div.ariaSelected = true; @@ -211,9 +211,9 @@ export class BdcDashboard extends BdcDashboardPage { this.currentTab.text.updateCssStyles(unselectedTabCss); this.currentTab.div.ariaSelected = false; } - this.mainAreaContainer.removeItem(this.currentPage); - this.mainAreaContainer.addItem(tabPageMapping.servicePage, { CSSStyles: { 'margin': '0 20px 0 20px' } }); - this.currentPage = tabPageMapping.servicePage; + this.mainAreaContainer.removeItem(this.currentPageContainer); + this.mainAreaContainer.addItem(tabPageMapping.servicePage.container, { CSSStyles: { 'margin': '0 20px 0 20px' } }); + this.currentPageContainer = tabPageMapping.servicePage.container; this.currentTab = tabPageMapping.navTab; this.currentTab.text.updateCssStyles(selectedTabCss); this.currentTab.div.ariaSelected = true; @@ -233,7 +233,7 @@ export class BdcDashboard extends BdcDashboardPage { } else { // New service - create the page and tab const navItem = createServiceNavTab(this.modelView.modelBuilder, s); - const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView).container; + const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView); this.serviceTabPageMapping.set(s.serviceName, { navTab: navItem, servicePage: serviceStatusPage }); navItem.div.onDidClick(() => { this.switchToServiceTab(s.serviceName); diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts index de8417851b..224c72204c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardOverviewPage.ts @@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'; import { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel'; import { IconPathHelper, cssStyles } from '../constants'; import { getStateDisplayText, getHealthStatusDisplayText, getEndpointDisplayText, getHealthStatusIcon, getServiceNameDisplayText, Endpoint, getBdcStatusErrorMessage } from '../utils'; -import { EndpointModel, ServiceStatusModel, BdcStatusModel } from '../controller/apiGenerated'; +import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated'; import { BdcDashboard } from './bdcDashboard'; import { createViewDetailsButton } from './commonControls'; import { HdfsDialogCancelledError } from './hdfsDialogBase'; @@ -21,14 +21,6 @@ const clusterStateLabelColumnWidth = 100; const clusterStateValueColumnWidth = 225; const healthStatusColumnWidth = 125; -const overviewIconColumnWidthPx = 25; -const overviewServiceNameCellWidthPx = 175; -const overviewStateCellWidthPx = 150; -const overviewHealthStatusCellWidthPx = 100; - -const serviceEndpointRowServiceNameCellWidth = 200; -const serviceEndpointRowEndpointCellWidth = 350; - const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi]; export class BdcDashboardOverviewPage extends BdcDashboardPage { @@ -40,11 +32,12 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { 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 serviceStatusTable: azdata.DeclarativeTableComponent; + private endpointsTable: azdata.DeclarativeTableComponent; + private endpointsLoadingComponent: azdata.LoadingComponent; + private endpointsDisplayContainer: azdata.FlexContainer; + private serviceStatusLoadingComponent: azdata.LoadingComponent; + private serviceStatusDisplayContainer: azdata.FlexContainer; private propertiesErrorMessage: azdata.TextComponent; private endpointsErrorMessage: azdata.TextComponent; private serviceStatusErrorMessage: azdata.TextComponent; @@ -126,33 +119,114 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { const overviewContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component(); - // Service Status header row - const serviceStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.serviceNameHeader', "Service Name") }).component(); - // Service name cell covers both icon + service name so width stretches both cells - serviceStatusHeaderRow.addItem(nameCell, { CSSStyles: { 'width': `${overviewServiceNameCellWidthPx + overviewIconColumnWidthPx}px`, 'min-width': `${overviewServiceNameCellWidthPx + overviewIconColumnWidthPx}px`, ...cssStyles.tableHeader } }); - const stateCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State"), CSSStyles: { ...cssStyles.tableHeader } }).component(); - serviceStatusHeaderRow.addItem(stateCell, { CSSStyles: { 'width': `${overviewStateCellWidthPx}px`, 'min-width': `${overviewStateCellWidthPx}px` } }); - const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status"), CSSStyles: { ...cssStyles.tableHeader } }).component(); - 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.serviceStatusTable = view.modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // status icon + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 25, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF' + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // service + displayName: localize('bdc.dashboard.serviceNameHeader', "Service Name"), + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 175, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // state + displayName: localize('bdc.dashboard.stateHeader', "State"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 150, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // health status + displayName: localize('bdc.dashboard.healthStatusHeader', "Health Status"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + 'text-align': 'left', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + } + }, + { // view details button + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 150, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF' + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + ], + data: [] + }) + .component(); - this.serviceStatusDisplayContainer = view.modelBuilder.divContainer().component(); + this.serviceStatusDisplayContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this.serviceStatusDisplayContainer.addItem(this.serviceStatusTable); - // 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() + // Note we don't make the table a child of the loading component since making the loading component align correctly + // messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading + // component once it's finished loading the content + this.serviceStatusLoadingComponent = 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' } }); + + this.serviceStatusDisplayContainer.addItem(this.serviceStatusLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); 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' }); @@ -170,25 +244,77 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { const endpointsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component(); - // Service endpoints header row - const endpointsHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const endpointsServiceNameHeaderCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.serviceHeader', "Service") }).component(); - endpointsHeaderRow.addItem(endpointsServiceNameHeaderCell, { CSSStyles: { 'width': `${serviceEndpointRowServiceNameCellWidth}px`, 'min-width': `${serviceEndpointRowServiceNameCellWidth}px`, ...cssStyles.tableHeader } }); - const endpointsEndpointHeaderCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.endpointHeader', "Endpoint") }).component(); - 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.endpointsTable = view.modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // service + displayName: localize('bdc.dashboard.serviceHeader', "Service"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 200, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // endpoint + displayName: localize('bdc.dashboard.endpointHeader', "Endpoint"), + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 350, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none', + 'overflow': 'hidden', + 'text-overflow': 'ellipsis' + }, + }, + { // copy + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 50, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF' + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + } + } + ], + data: [] + }).component(); - 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 - // and replace it with the rows directly - const endpointRowContainerLoadingComponent = view.modelBuilder.loadingComponent() + this.endpointsDisplayContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this.endpointsDisplayContainer.addItem(this.endpointsTable); + + // Note we don't make the table a child of the loading component since making the loading component align correctly + // messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading + // component once it's finished loading the content + this.endpointsLoadingComponent = 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' } }); + this.endpointsDisplayContainer.addItem(this.endpointsLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } }); - this.endpointsDisplayContainer.addItem(this.endpointsRowContainer); endpointsContainer.addItem(this.endpointsErrorMessage); endpointsContainer.addItem(this.endpointsDisplayContainer); rootContainer.addItem(endpointsContainer, { flex: '0 0 auto' }); @@ -228,16 +354,37 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { (this.clusterHealthStatusLoadingComponent.component).value = getHealthStatusDisplayText(bdcStatus.healthStatus); if (bdcStatus.services) { - this.serviceStatusRowContainer.clearItems(); - bdcStatus.services.forEach((s, i) => { - this.createServiceStatusRow(this.serviceStatusRowContainer, s, i === bdcStatus.services.length - 1); + this.serviceStatusTable.data = bdcStatus.services.map(serviceStatus => { + const statusIconCell = this.modelBuilder.text() + .withProperties({ + value: getHealthStatusIcon(serviceStatus.healthStatus), + ariaRole: 'img', + title: getHealthStatusDisplayText(serviceStatus.healthStatus), + CSSStyles: { 'user-select': 'none', ...cssStyles.text } + }).component(); + const nameCell = this.modelBuilder.hyperlink() + .withProperties({ + label: getServiceNameDisplayText(serviceStatus.serviceName), + url: '', + CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } + }).component(); + nameCell.onDidClick(() => { + this.dashboard.switchToServiceTab(serviceStatus.serviceName); + }); + + const viewDetailsButton = serviceStatus.healthStatus !== 'healthy' && serviceStatus.details && serviceStatus.details.length > 0 ? createViewDetailsButton(this.modelBuilder, serviceStatus.details) : undefined; + return [ + statusIconCell, + nameCell, + getStateDisplayText(serviceStatus.state), + getHealthStatusDisplayText(serviceStatus.healthStatus), + viewDetailsButton]; }); + this.serviceStatusDisplayContainer.removeItem(this.serviceStatusLoadingComponent); } } private handleEndpointsUpdate(endpoints: EndpointModel[]): void { - this.endpointsRowContainer.clearItems(); - // Sort the endpoints. The sort method is that SQL Server Master is first - followed by all // others in alphabetical order by endpoint const sqlServerMasterEndpoints = endpoints.filter(e => e.name === Endpoint.sqlServerMaster); @@ -249,9 +396,21 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { }); endpoints.unshift(...sqlServerMasterEndpoints); - endpoints.forEach((e, i) => { - createServiceEndpointRow(this.modelBuilder, this.endpointsRowContainer, e, this.model, hyperlinkedEndpoints.some(he => he === e.name), i === endpoints.length - 1); + this.endpointsTable.data = endpoints.map(e => { + const copyValueCell = this.modelBuilder.button().withProperties({ title: localize('bdc.dashboard.copyTitle', "Copy") }).component(); + copyValueCell.iconPath = IconPathHelper.copy; + copyValueCell.onDidClick(() => { + vscode.env.clipboard.writeText(e.endpoint); + vscode.window.showInformationMessage(localize('copiedEndpoint', "Endpoint '{0}' copied to clipboard", getEndpointDisplayText(e.name, e.description))); + }); + copyValueCell.iconHeight = '14px'; + copyValueCell.iconWidth = '14px'; + return [getEndpointDisplayText(e.name, e.description), + createEndpointComponent(this.modelBuilder, e, this.model, hyperlinkedEndpoints.some(he => he === e.name)), //e.endpoint, + copyValueCell]; }); + + this.endpointsDisplayContainer.removeItem(this.endpointsLoadingComponent); } private handleBdcError(errorEvent: BdcErrorEvent): void { @@ -291,54 +450,17 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage { 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), - ariaRole: 'img', - title: getHealthStatusDisplayText(serviceStatus.healthStatus), - CSSStyles: { 'user-select': 'none' } - }).component(); - serviceStatusRow.addItem(statusIconCell, { CSSStyles: { 'width': `${overviewIconColumnWidthPx}px`, 'min-width': `${overviewIconColumnWidthPx}px` } }); - const nameCell = this.modelBuilder.hyperlink().withProperties({ - label: getServiceNameDisplayText(serviceStatus.serviceName), - url: '', - CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } - }).component(); - nameCell.onDidClick(() => { - this.dashboard.switchToServiceTab(serviceStatus.serviceName); - }); - serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': `${overviewServiceNameCellWidthPx}px`, 'min-width': `${overviewServiceNameCellWidthPx}px`, ...cssStyles.text } }); - const stateText = getStateDisplayText(serviceStatus.state); - const stateCell = this.modelBuilder.text().withProperties({ value: stateText, title: stateText, CSSStyles: { ...cssStyles.overflowEllipsisText } }).component(); - serviceStatusRow.addItem(stateCell, { CSSStyles: { 'width': `${overviewStateCellWidthPx}px`, 'min-width': `${overviewStateCellWidthPx}px` } }); - const healthStatusText = getHealthStatusDisplayText(serviceStatus.healthStatus); - const healthStatusCell = this.modelBuilder.text().withProperties({ value: healthStatusText, title: healthStatusText, CSSStyles: { ...cssStyles.overflowEllipsisText } }).component(); - serviceStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': `${overviewHealthStatusCellWidthPx}px`, 'min-width': `${overviewHealthStatusCellWidthPx}px` } }); - - if (serviceStatus.healthStatus !== 'healthy' && serviceStatus.details && serviceStatus.details.length > 0) { - serviceStatusRow.addItem(createViewDetailsButton(this.modelBuilder, serviceStatus.details), { flex: '0 0 auto' }); - } - - 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, endpoint: EndpointModel, bdcModel: BdcDashboardModel, 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: { ...cssStyles.text } }).component(); - endPointRow.addItem(nameCell, { CSSStyles: { 'width': `${serviceEndpointRowServiceNameCellWidth}px`, 'min-width': `${serviceEndpointRowServiceNameCellWidth}px`, 'text-align': 'center' } }); +function createEndpointComponent(modelBuilder: azdata.ModelBuilder, endpoint: EndpointModel, bdcModel: BdcDashboardModel, isHyperlink: boolean): azdata.HyperlinkComponent | azdata.TextComponent { if (isHyperlink) { - const endpointCell = modelBuilder.hyperlink() + return modelBuilder.hyperlink() .withProperties({ label: endpoint.endpoint, title: endpoint.endpoint, - url: endpoint.endpoint, CSSStyles: { 'height': '15px' } + url: endpoint.endpoint, CSSStyles: { ...cssStyles.hyperlink } }) .component(); - endPointRow.addItem(endpointCell, { CSSStyles: { 'width': `${serviceEndpointRowEndpointCellWidth}px`, 'min-width': `${serviceEndpointRowEndpointCellWidth}px`, 'overflow': 'hidden', 'text-overflow': 'ellipsis', ...cssStyles.hyperlink } }); } else if (endpoint.name === Endpoint.sqlServerMaster) { const endpointCell = modelBuilder.hyperlink() @@ -346,7 +468,7 @@ function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: title: endpoint.endpoint, label: endpoint.endpoint, url: '', - CSSStyles: { 'overflow': 'hidden', 'text-overflow': 'ellipsis', ...cssStyles.text, ...cssStyles.hyperlink } + CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } }).component(); endpointCell.onDidClick(async () => { const connProfile = bdcModel.getSqlServerMasterConnectionProfile(); @@ -361,27 +483,15 @@ function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: azdata.connection.openConnectionDialog(undefined, connProfile); } }); - endPointRow.addItem(endpointCell, { CSSStyles: { 'width': `${serviceEndpointRowEndpointCellWidth}px`, 'min-width': `${serviceEndpointRowEndpointCellWidth}px` } }); + return endpointCell; } else { - const endpointCell = modelBuilder.text() + return modelBuilder.text() .withProperties({ value: endpoint.endpoint, title: endpoint.endpoint, - CSSStyles: { 'overflow': 'hidden', 'text-overflow': 'ellipsis', ...cssStyles.text } + CSSStyles: { ...cssStyles.text } }) .component(); - endPointRow.addItem(endpointCell, { CSSStyles: { 'width': `${serviceEndpointRowEndpointCellWidth}px`, 'min-width': `${serviceEndpointRowEndpointCellWidth}px` } }); } - const copyValueCell = modelBuilder.button().withProperties({ title: localize('bdc.dashboard.copyTitle', "Copy") }).component(); - copyValueCell.iconPath = IconPathHelper.copy; - copyValueCell.onDidClick(() => { - vscode.env.clipboard.writeText(endpoint.endpoint); - vscode.window.showInformationMessage(localize('copiedEndpoint', "Endpoint '{0}' copied to clipboard", getEndpointDisplayText(endpoint.name, endpoint.description))); - }); - copyValueCell.iconHeight = '14px'; - copyValueCell.iconWidth = '14px'; - endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '14px', 'min-width': '14px', 'padding-left': '10px', ...cssStyles.text } }); - - 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 5d39d6fb8b..cfcdfbc1f6 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import { BdcDashboardModel } from './bdcDashboardModel'; -import { BdcStatusModel, InstanceStatusModel } from '../controller/apiGenerated'; +import { BdcStatusModel, InstanceStatusModel, ResourceStatusModel } from '../controller/apiGenerated'; import { getHealthStatusDisplayText, getHealthStatusIcon, getStateDisplayText, Service } from '../utils'; import { cssStyles } from '../constants'; import { isNullOrUndefined } from 'util'; @@ -15,38 +15,35 @@ import { BdcDashboardPage } from './bdcDashboardPage'; const localize = nls.loadMessageBundle(); -const healthAndStatusIconColumnWidth = 25; -const healthAndStatusInstanceNameColumnWidth = 100; -const healthAndStatusStateColumnWidth = 150; -const healthAndStatusHealthColumnWidth = 100; - -const metricsAndLogsInstanceNameColumnWidth = 125; -const metricsAndLogsNodeMetricsColumnWidth = 80; -const metricsAndLogsSqlMetricsColumnWidth = 80; -const metricsAndLogsLogsColumnWidth = 75; - const viewText = localize('bdc.dashboard.viewHyperlink', "View"); const notAvailableText = localize('bdc.dashboard.notAvailable', "N/A"); export class BdcDashboardResourceStatusPage extends BdcDashboardPage { + private resourceStatusModel: ResourceStatusModel; private rootContainer: azdata.FlexContainer; - private instanceHealthStatusRowsContainer: azdata.FlexContainer; - private metricsAndLogsRowsContainer: azdata.FlexContainer; + private instanceHealthStatusTable: azdata.DeclarativeTableComponent; + private metricsAndLogsRowsTable: azdata.DeclarativeTableComponent; private lastUpdatedLabel: azdata.TextComponent; constructor(private model: BdcDashboardModel, private modelView: azdata.ModelView, private serviceName: string, private resourceName: string) { super(); this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus))); - this.rootContainer = this.createContainer(modelView); } public get container(): azdata.FlexContainer { + // Lazily create the container only when needed + if (!this.rootContainer) { + // We do this here so that we can have the resource model to use for populating the data + // in the tables. This is to get around a timing issue with ModelView tables + this.updateResourceStatusModel(this.model.bdcStatus); + this.createContainer(); + } return this.rootContainer; } - private createContainer(view: azdata.ModelView): azdata.FlexContainer { - const rootContainer = view.modelBuilder.flexContainer().withLayout( + private createContainer(): void { + this.rootContainer = this.modelView.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', width: '100%', @@ -57,11 +54,11 @@ export class BdcDashboardResourceStatusPage extends BdcDashboardPage { // # INSTANCE HEALTH AND STATUS # // ############################## - const healthStatusHeaderContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component(); - rootContainer.addItem(healthStatusHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } }); + const healthStatusHeaderContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component(); + this.rootContainer.addItem(healthStatusHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } }); // Header label - const healthStatusHeaderLabel = view.modelBuilder.text() + const healthStatusHeaderLabel = this.modelView.modelBuilder.text() .withProperties({ value: localize('bdc.dashboard.healthStatusDetailsHeader', "Health Status Details"), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } @@ -71,180 +68,306 @@ export class BdcDashboardResourceStatusPage extends BdcDashboardPage { healthStatusHeaderContainer.addItem(healthStatusHeaderLabel, { CSSStyles: { ...cssStyles.title } }); // Last updated label - this.lastUpdatedLabel = view.modelBuilder.text() + this.lastUpdatedLabel = this.modelView.modelBuilder.text() .withProperties({ - value: localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", '-'), + value: this.getLastUpdatedText(), CSSStyles: { ...cssStyles.lastUpdatedText } }).component(); healthStatusHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } }); - // Header row - const instanceHealthStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const instanceHealthAndStatusNameHeader = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.instanceHeader', "Instance") }).component(); - // Instance name cell covers both icon + service name so width stretches both cells - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusNameHeader, { CSSStyles: { 'width': `${healthAndStatusIconColumnWidth + healthAndStatusInstanceNameColumnWidth}px`, 'min-width': `${healthAndStatusIconColumnWidth + healthAndStatusInstanceNameColumnWidth}px`, ...cssStyles.tableHeader } }); - const instanceHealthAndStatusState = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State") }).component(); - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusState, { CSSStyles: { 'width': `${healthAndStatusStateColumnWidth}px`, 'min-width': `${healthAndStatusStateColumnWidth}px`, ...cssStyles.tableHeader } }); - const instanceHealthAndStatusHealthStatus = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component(); - instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusHealthStatus, { CSSStyles: { 'width': `${healthAndStatusHealthColumnWidth}px`, 'min-width': `${healthAndStatusHealthColumnWidth}px`, ...cssStyles.tableHeader } }); - rootContainer.addItem(instanceHealthStatusHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box' } }); - - this.instanceHealthStatusRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - rootContainer.addItem(this.instanceHealthStatusRowsContainer, { flex: '0 0 auto' }); + this.instanceHealthStatusTable = this.modelView.modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // status icon + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 25, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF' + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // instance + displayName: localize('bdc.dashboard.instanceHeader', "Instance"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // state + displayName: localize('bdc.dashboard.stateHeader', "State"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 150, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // health status + displayName: localize('bdc.dashboard.healthStatusHeader', "Health Status"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + 'text-align': 'left', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + } + }, + { // view details button + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 150, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF' + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + ], + data: this.createHealthStatusRows() + }).component(); + this.rootContainer.addItem(this.instanceHealthStatusTable, { flex: '0 0 auto' }); // #################### // # METRICS AND LOGS # // #################### // Title label - const endpointsLabel = view.modelBuilder.text() + const endpointsLabel = this.modelView.modelBuilder.text() .withProperties({ value: localize('bdc.dashboard.metricsAndLogsLabel', "Metrics and Logs"), CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } }) .component(); - rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } }); + this.rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } }); + + let metricsAndLogsColumns: azdata.DeclarativeTableColumn[] = + [ + { // instance + displayName: localize('bdc.dashboard.instanceHeader', "Instance"), + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 125, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }, + { // node metrics + displayName: localize('bdc.dashboard.nodeMetricsHeader', "Node Metrics"), + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + } + ]; - // Header row - const metricsAndLogsHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.instanceHeader', "Instance") }).component(); - metricsAndLogsHeaderRow.addItem(nameCell, { CSSStyles: { 'width': `${metricsAndLogsInstanceNameColumnWidth}px`, 'min-width': `${metricsAndLogsInstanceNameColumnWidth}px`, ...cssStyles.tableHeader } }); - const nodeMetricsCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.nodeMetricsHeader', "Node Metrics") }).component(); - metricsAndLogsHeaderRow.addItem(nodeMetricsCell, { CSSStyles: { 'width': `${metricsAndLogsNodeMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsNodeMetricsColumnWidth}px`, ...cssStyles.tableHeader } }); // Only show SQL metrics column for SQL resource instances if (this.serviceName.toLowerCase() === Service.sql) { - const sqlMetricsCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.sqlMetricsHeader', "SQL Metrics") }).component(); - metricsAndLogsHeaderRow.addItem(sqlMetricsCell, { CSSStyles: { 'width': `${metricsAndLogsSqlMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsSqlMetricsColumnWidth}px`, ...cssStyles.tableHeader } }); + metricsAndLogsColumns.push( + { // sql metrics + displayName: localize('bdc.dashboard.sqlMetricsHeader', "SQL Metrics"), + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + 'text-align': 'left', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + } + }); } - const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.logsHeader', "Logs") }).component(); - metricsAndLogsHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': `${metricsAndLogsLogsColumnWidth}px`, 'min-width': `${metricsAndLogsLogsColumnWidth}px`, ...cssStyles.tableHeader } }); - rootContainer.addItem(metricsAndLogsHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box' } }); - this.metricsAndLogsRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - rootContainer.addItem(this.metricsAndLogsRowsContainer, { flex: '0 0 auto' }); + metricsAndLogsColumns.push( + { // logs + displayName: localize('bdc.dashboard.logsHeader', "Logs"), + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 100, + headerCssStyles: { + 'border': 'none', + 'background-color': '#FFFFFF', + 'text-align': 'left', + ...cssStyles.tableHeader + }, + rowCssStyles: { + 'border-top': 'solid 1px #ccc', + 'border-bottom': 'solid 1px #ccc', + 'border-left': 'none', + 'border-right': 'none' + }, + }); + + this.metricsAndLogsRowsTable = this.modelView.modelBuilder.declarativeTable() + .withProperties( + { + columns: metricsAndLogsColumns, + data: this.createMetricsAndLogsRows() + }).component(); + this.rootContainer.addItem(this.metricsAndLogsRowsTable, { flex: '0 0 auto' }); this.initialized = true; - this.handleBdcStatusUpdate(this.model.bdcStatus); - - return rootContainer; } - private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void { + private updateResourceStatusModel(bdcStatus?: BdcStatusModel): void { + // If we can't find the resource model for this resource then just + // default to keeping what we had originally if (!bdcStatus) { return; } const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined; - const resource = service ? service.resources.find(r => r.resourceName === this.resourceName) : undefined; + this.resourceStatusModel = service ? service.resources.find(r => r.resourceName === this.resourceName) : this.resourceStatusModel; + } - if (!resource || isNullOrUndefined(resource.instances)) { + private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void { + this.updateResourceStatusModel(bdcStatus); + + if (!this.resourceStatusModel || isNullOrUndefined(this.resourceStatusModel.instances)) { return; } - this.lastUpdatedLabel.value = - localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", - this.model.bdcStatusLastUpdated ? - `${this.model.bdcStatusLastUpdated.toLocaleDateString()} ${this.model.bdcStatusLastUpdated.toLocaleTimeString()}` - : '-'); + this.lastUpdatedLabel.value = this.getLastUpdatedText(); - this.instanceHealthStatusRowsContainer.clearItems(); - this.metricsAndLogsRowsContainer.clearItems(); + this.instanceHealthStatusTable.data = this.createHealthStatusRows(); - resource.instances.forEach(i => { - const instanceHealthStatusRow = createInstanceHealthStatusRow(this.modelView.modelBuilder, i); - this.instanceHealthStatusRowsContainer.addItem(instanceHealthStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); - - let metricsAndLogsRow = createMetricsAndLogsRow(this.modelView.modelBuilder, i, this.serviceName); - this.metricsAndLogsRowsContainer.addItem(metricsAndLogsRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); - }); - } -} - -/** - * Creates a row with the name, state and health status for a particular instance on this resource - * - * @param modelBuilder The builder used to create the component - * @param instanceStatus The status object for the instance this row is for - */ -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), - ariaRole: 'img', - title: getHealthStatusDisplayText(instanceStatus.healthStatus), - CSSStyles: { 'user-select': 'none' } - }).component(); - instanceHealthStatusRow.addItem(statusIconCell, { CSSStyles: { 'width': `${healthAndStatusIconColumnWidth}px`, 'min-width': `${healthAndStatusIconColumnWidth}px` } }); - const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { ...cssStyles.text } }).component(); - instanceHealthStatusRow.addItem(nameCell, { CSSStyles: { 'width': `${healthAndStatusInstanceNameColumnWidth}px`, 'min-width': `${healthAndStatusInstanceNameColumnWidth}px`, ...cssStyles.text } }); - const stateText = getStateDisplayText(instanceStatus.state); - const stateCell = modelBuilder.text().withProperties({ value: stateText, title: stateText, CSSStyles: { ...cssStyles.overflowEllipsisText } }).component(); - instanceHealthStatusRow.addItem(stateCell, { CSSStyles: { 'width': `${healthAndStatusStateColumnWidth}px`, 'min-width': `${healthAndStatusStateColumnWidth}px` } }); - const healthStatusText = getHealthStatusDisplayText(instanceStatus.healthStatus); - const healthStatusCell = modelBuilder.text().withProperties({ value: healthStatusText, title: healthStatusText, CSSStyles: { ...cssStyles.overflowEllipsisText } }).component(); - instanceHealthStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': `${healthAndStatusHealthColumnWidth}px`, 'min-width': `${healthAndStatusHealthColumnWidth}px` } }); - - if (instanceStatus.healthStatus !== 'healthy' && instanceStatus.details && instanceStatus.details.length > 0) { - instanceHealthStatusRow.addItem(createViewDetailsButton(modelBuilder, instanceStatus.details), { flex: '0 0 auto' }); - } - return instanceHealthStatusRow; -} - -/** - * Creates a row with the name, link to the metrics and a link to the logs for a particular instance on this resource - * @param modelBuilder The builder used to create the component - * @param instanceStatus The status object for the instance this row is for - * @param serviceName The name of the service this resource instance belongs to - */ -function createMetricsAndLogsRow(modelBuilder: azdata.ModelBuilder, instanceStatus: InstanceStatusModel, serviceName: string): azdata.FlexContainer { - const metricsAndLogsRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); - const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { ...cssStyles.text } }).component(); - metricsAndLogsRow.addItem(nameCell, { CSSStyles: { 'width': `${metricsAndLogsInstanceNameColumnWidth}px`, 'min-width': `${metricsAndLogsInstanceNameColumnWidth}px`, ...cssStyles.text } }); - - // Not all instances have all logs available - in that case just display N/A instead of a link - if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.nodeMetricsUrl)) { - const metricsCell = modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component(); - metricsAndLogsRow.addItem(metricsCell, { CSSStyles: { 'width': `${metricsAndLogsNodeMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsNodeMetricsColumnWidth}px`, ...cssStyles.text } }); - } else { - const nodeMetricsCell = modelBuilder.hyperlink().withProperties({ - label: viewText, - url: instanceStatus.dashboards.nodeMetricsUrl, - title: instanceStatus.dashboards.nodeMetricsUrl, - CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } - }) - .component(); - metricsAndLogsRow.addItem(nodeMetricsCell, { CSSStyles: { 'width': `${metricsAndLogsNodeMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsNodeMetricsColumnWidth}px` } }); + this.metricsAndLogsRowsTable.data = this.createMetricsAndLogsRows(); } - // Only show SQL metrics column for SQL resource instances - if (serviceName === Service.sql) { + private createMetricsAndLogsRows(): any[][] { + return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createMetricsAndLogsRow(instanceStatus)) : []; + } + + private createHealthStatusRows(): any[][] { + return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createHealthStatusRow(instanceStatus)) : []; + } + + private createMetricsAndLogsRow(instanceStatus: InstanceStatusModel): any[] { + const row: any[] = [instanceStatus.instanceName]; + // Not all instances have all logs available - in that case just display N/A instead of a link - if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.sqlMetricsUrl)) { - const metricsCell = modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component(); - metricsAndLogsRow.addItem(metricsCell, { CSSStyles: { 'width': `${metricsAndLogsSqlMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsSqlMetricsColumnWidth}px`, ...cssStyles.text } }); + if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.nodeMetricsUrl)) { + row.push(this.modelView.modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component()); } else { - const sqlMetricsCell = modelBuilder.hyperlink().withProperties({ + row.push(this.modelView.modelBuilder.hyperlink().withProperties({ label: viewText, - url: instanceStatus.dashboards.sqlMetricsUrl, - title: instanceStatus.dashboards.sqlMetricsUrl, + url: instanceStatus.dashboards.nodeMetricsUrl, + title: instanceStatus.dashboards.nodeMetricsUrl, CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } - }) - .component(); - metricsAndLogsRow.addItem(sqlMetricsCell, { CSSStyles: { 'width': `${metricsAndLogsSqlMetricsColumnWidth}px`, 'min-width': `${metricsAndLogsSqlMetricsColumnWidth}px` } }); + }).component()); } + + // Only show SQL metrics column for SQL resource instances + if (this.serviceName === Service.sql) { + // Not all instances have all logs available - in that case just display N/A instead of a link + if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.sqlMetricsUrl)) { + row.push(this.modelView.modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component()); + } else { + row.push(this.modelView.modelBuilder.hyperlink().withProperties({ + label: viewText, + url: instanceStatus.dashboards.sqlMetricsUrl, + title: instanceStatus.dashboards.sqlMetricsUrl, + CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } + }).component()); + } + } + + if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.logsUrl)) { + row.push(this.modelView.modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component()); + } else { + row.push(this.modelView.modelBuilder.hyperlink().withProperties({ + label: viewText, + url: instanceStatus.dashboards.logsUrl, + title: instanceStatus.dashboards.logsUrl, + CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } + }).component()); + } + return row; } - if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.logsUrl)) { - const logsCell = modelBuilder.text().withProperties({ value: notAvailableText, CSSStyles: { ...cssStyles.text } }).component(); - metricsAndLogsRow.addItem(logsCell, { CSSStyles: { 'width': `${metricsAndLogsLogsColumnWidth}px`, 'min-width': `${metricsAndLogsLogsColumnWidth}px`, ...cssStyles.text } }); - } else { - const logsCell = modelBuilder.hyperlink().withProperties({ - label: viewText, - url: instanceStatus.dashboards.logsUrl, - title: instanceStatus.dashboards.logsUrl, - CSSStyles: { ...cssStyles.text, ...cssStyles.hyperlink } - }) - .component(); - metricsAndLogsRow.addItem(logsCell, { CSSStyles: { 'width': `${metricsAndLogsLogsColumnWidth}px`, 'min-width': `${metricsAndLogsLogsColumnWidth}px` } }); + private createHealthStatusRow(instanceStatus: InstanceStatusModel): any[] { + const statusIconCell = this.modelView.modelBuilder.text() + .withProperties({ + value: getHealthStatusIcon(instanceStatus.healthStatus), + ariaRole: 'img', + title: getHealthStatusDisplayText(instanceStatus.healthStatus), + CSSStyles: { 'user-select': 'none', ...cssStyles.text } + }).component(); + + const viewDetailsButton = instanceStatus.healthStatus !== 'healthy' && instanceStatus.details && instanceStatus.details.length > 0 ? createViewDetailsButton(this.modelView.modelBuilder, instanceStatus.details) : undefined; + return [ + statusIconCell, + instanceStatus.instanceName, + getStateDisplayText(instanceStatus.state), + getHealthStatusDisplayText(instanceStatus.healthStatus), + viewDetailsButton]; } - return metricsAndLogsRow; + private getLastUpdatedText(): string { + return localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", + this.model.bdcStatusLastUpdated ? + `${this.model.bdcStatusLastUpdated.toLocaleDateString()} ${this.model.bdcStatusLastUpdated.toLocaleTimeString()}` + : '-'); + } } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts index 9d2cbf0611..86f1effee1 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts @@ -16,7 +16,7 @@ type ServiceTab = { div: azdata.DivContainer, dot: azdata.TextComponent, text: a export class BdcServiceStatusPage extends BdcDashboardPage { private currentTab: { tab: ServiceTab, index: number }; - private currentTabPage: azdata.FlexContainer; + private currentTabPage: BdcDashboardResourceStatusPage; private rootContainer: azdata.FlexContainer; private resourceHeader: azdata.FlexContainer; @@ -25,10 +25,13 @@ export class BdcServiceStatusPage extends BdcDashboardPage { constructor(private serviceName: string, private model: BdcDashboardModel, private modelView: azdata.ModelView) { super(); this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus))); - this.createPage(); } public get container(): azdata.FlexContainer { + // Lazily create the container only when needed + if (!this.rootContainer) { + this.createPage(); + } return this.rootContainer; } @@ -67,11 +70,11 @@ export class BdcServiceStatusPage extends BdcDashboardPage { } } - private changeSelectedTabPage(newPage: azdata.FlexContainer): void { + private changeSelectedTabPage(newPage: BdcDashboardResourceStatusPage): void { if (this.currentTabPage) { - this.rootContainer.removeItem(this.currentTabPage); + this.rootContainer.removeItem(this.currentTabPage.container); } - this.rootContainer.addItem(newPage); + this.rootContainer.addItem(newPage.container); this.currentTabPage = newPage; } @@ -90,7 +93,7 @@ export class BdcServiceStatusPage extends BdcDashboardPage { const currentIndex = tabIndex++; const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource); this.createdTabs.set(resource.resourceName, resourceHeaderTab); - const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container; + const resourceStatusPage = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName); resourceHeaderTab.div.onDidClick(() => { // Don't need to do anything if this is already the currently selected tab if (this.currentTab.index === currentIndex) { diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 6a1943a684..70e2118490 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -3171,10 +3171,10 @@ declare module 'azdata' { export interface DeclarativeTableColumn { displayName: string; - categoryValues: CategoryValue[]; valueType: DeclarativeDataType; isReadOnly: boolean; width: number | string; + categoryValues?: CategoryValue[]; } export interface DeclarativeTableProperties { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 55a2a25a2e..e252a47bbc 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -87,6 +87,15 @@ declare module 'azdata' { onDidClick: vscode.Event; } + export interface DeclarativeTableColumn { + headerCssStyles?: { [key: string]: string }; + rowCssStyles?: { [key: string]: string }; + } + + export enum DeclarativeDataType { + component = 'component' + } + /* * Add optional azureAccount for connectionWidget. */ diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 0ed5d04494..eb7ae05fc5 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -587,7 +587,8 @@ declare module 'sqlops' { string = 'string', category = 'category', boolean = 'boolean', - editableCategory = 'editableCategory' + editableCategory = 'editableCategory', + component = 'component' } export interface RadioButtonProperties { @@ -625,10 +626,10 @@ declare module 'sqlops' { export interface DeclarativeTableColumn { displayName: string; - categoryValues: CategoryValue[]; valueType: DeclarativeDataType; isReadOnly: boolean; width: number | string; + categoryValues?: CategoryValue[]; } export interface DeclarativeTableProperties { diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index b5b94fa7e3..636b2ce6a3 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -1368,6 +1368,46 @@ class DeclarativeTableWrapper extends ComponentWrapper implements azdata.Declara let emitter = this._emitterMap.get(ComponentEventType.onDidChange); return emitter && emitter.event; } + + protected notifyPropertyChanged(): Thenable { + return this._proxy.$setProperties(this._handle, this._id, this.getPropertiesForMainThread()); + } + + public toComponentShape(): IComponentShape { + // Overridden to ensure we send the correct properties mapping. + return { + id: this.id, + type: this.type, + layout: this.layout, + properties: this.getPropertiesForMainThread(), + itemConfigs: this.itemConfigs ? this.itemConfigs.map(item => item.toIItemConfig()) : undefined + }; + } + + /** + * Gets the properties map to send to the main thread. + */ + private getPropertiesForMainThread(): { [key: string]: string } { + // This is necessary because we can't send the actual ComponentWrapper objects + // and so map them into their IDs instead. We don't want to update the actual + // data property though since the caller would still expect that to contain + // the Component objects they created + const properties = assign({}, this.properties); + if (properties.data) { + properties.data = properties.data.map((row: any[]) => row.map(cell => { + if (cell instanceof ComponentWrapper) { + // First ensure that we register the component using addItem + // such that it gets added to the ModelStore. We don't want to + // make the table component an actual container since that exposes + // a lot of functionality we don't need. + this.addItem(cell); + return cell.id; + } + return cell; + })); + } + return properties; + } } class ListBoxWrapper extends ComponentWrapper implements azdata.ListBoxComponent { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 6d1839acc5..ebf77e4065 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -349,7 +349,8 @@ export enum DeclarativeDataType { string = 'string', category = 'category', boolean = 'boolean', - editableCategory = 'editableCategory' + editableCategory = 'editableCategory', + component = 'component' } export enum CardType { diff --git a/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts b/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts index 5bdf33e4c3..5c988dd467 100644 --- a/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts +++ b/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts @@ -21,7 +21,8 @@ export enum DeclarativeDataType { string = 'string', category = 'category', boolean = 'boolean', - editableCategory = 'editableCategory' + editableCategory = 'editableCategory', + component = 'component' } @Component({ @@ -29,20 +30,21 @@ export enum DeclarativeDataType { template: ` - - + + - @@ -79,57 +81,57 @@ export default class DeclarativeTableComponent extends ComponentBase implements this.baseDestroy(); } - public isCheckBox(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public isCheckBox(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.valueType === DeclarativeDataType.boolean; } - public isControlEnabled(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public isControlEnabled(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return !column.isReadOnly; } - private isLabel(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + private isLabel(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.isReadOnly && column.valueType === DeclarativeDataType.string; } - public isChecked(row: number, cell: number): boolean { - let cellData = this.data[row][cell]; + public isChecked(rowIdx: number, colIdx: number): boolean { + let cellData = this.data[rowIdx][colIdx]; return cellData; } - public onInputBoxChanged(e: string, row: number, cell: number): void { - this.onCellDataChanged(e, row, cell); + public onInputBoxChanged(e: string, rowIdx: number, colIdx: number): void { + this.onCellDataChanged(e, rowIdx, colIdx); } - public onCheckBoxChanged(e: boolean, row: number, cell: number): void { - this.onCellDataChanged(e, row, cell); + public onCheckBoxChanged(e: boolean, rowIdx: number, colIdx: number): void { + this.onCellDataChanged(e, rowIdx, colIdx); } - public onSelectBoxChanged(e: ISelectData | string, row: number, cell: number): void { + public onSelectBoxChanged(e: ISelectData | string, rowIdx: number, colIdx: number): void { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; if (column.categoryValues) { if (typeof e === 'string') { let category = find(column.categoryValues, c => c.displayName === e); if (category) { - this.onCellDataChanged(category.name, row, cell); + this.onCellDataChanged(category.name, rowIdx, colIdx); } else { - this.onCellDataChanged(e, row, cell); + this.onCellDataChanged(e, rowIdx, colIdx); } } else { - this.onCellDataChanged(column.categoryValues[e.index].name, row, cell); + this.onCellDataChanged(column.categoryValues[e.index].name, rowIdx, colIdx); } } } - private onCellDataChanged(newValue: any, row: number, cell: number): void { - this.data[row][cell] = newValue; + private onCellDataChanged(newValue: any, rowIdx: number, colIdx: number): void { + this.data[rowIdx][colIdx] = newValue; this.data = this.data; let newCellData: azdata.TableCell = { - row: row, - column: cell, + row: rowIdx, + column: colIdx, value: newValue }; this.fireEvent({ @@ -138,39 +140,43 @@ export default class DeclarativeTableComponent extends ComponentBase implements }); } - public isSelectBox(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public isSelectBox(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.valueType === DeclarativeDataType.category; } - private isEditableSelectBox(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + private isEditableSelectBox(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.valueType === DeclarativeDataType.editableCategory; } - public isInputBox(cell: number): boolean { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public isInputBox(colIdx: number): boolean { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.valueType === DeclarativeDataType.string && !column.isReadOnly; } - public getColumnWidth(cell: number): string { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public isComponent(colIdx: number): boolean { + return this.columns[colIdx].valueType === DeclarativeDataType.component; + } + + public getColumnWidth(colIdx: number): string { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return this.convertSize(column.width, '30px'); } - public getOptions(cell: number): string[] { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; + public getOptions(colIdx: number): string[] { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; return column.categoryValues ? column.categoryValues.map(x => x.displayName) : []; } - public getSelectedOptionDisplayName(row: number, cell: number): string { - let column: azdata.DeclarativeTableColumn = this.columns[cell]; - let cellData = this.data[row][cell]; + public getSelectedOptionDisplayName(rowIdx: number, colIdx: number): string { + let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; + let cellData = this.data[rowIdx][colIdx]; if (cellData && column.categoryValues) { let category = find(column.categoryValues, v => v.name === cellData); if (category) { return category.displayName; - } else if (this.isEditableSelectBox(cell)) { + } else if (this.isEditableSelectBox(colIdx)) { return cellData; } else { return undefined; @@ -180,9 +186,13 @@ export default class DeclarativeTableComponent extends ComponentBase implements } } - public getAriaLabel(row: number, column: number): string { - const cellData = this.data[row][column]; - return this.isLabel(column) ? (cellData && cellData !== '' ? cellData : localize('blankValue', "blank")) : ''; + public getAriaLabel(rowIdx: number, colIdx: number): string { + const cellData = this.data[rowIdx][colIdx]; + return this.isLabel(colIdx) ? (cellData && cellData !== '' ? cellData : localize('blankValue', "blank")) : ''; + } + + public getItemDescriptor(componentId: string): IComponentDescriptor { + return this.modelStore.getComponentDescriptor(componentId); } /// IComponent implementation @@ -211,4 +221,18 @@ export default class DeclarativeTableComponent extends ComponentBase implements public set columns(newValue: azdata.DeclarativeTableColumn[]) { this.setPropertyFromUI((props, value) => props.columns = value, newValue); } + + // IComponent container-related implementation + // This is needed for the component column type - in order to have the components in the cells registered we call addItem + // on the extension side to create and register the component with the ModelStore. That requires that these methods be implemented + // though which isn't done by default for non-Container components and so we just stub out the implementation here (we already have + // the component IDs in the data property so there's no need to store them here as well) + public addToContainer(componentDescriptor: IComponentDescriptor, config: any, index?: number): void { + this._changeRef.detectChanges(); + } + + public clearContainer(): void { + this._changeRef.detectChanges(); + } + } diff --git a/src/sql/workbench/browser/modelComponents/media/declarativeTable.css b/src/sql/workbench/browser/modelComponents/media/declarativeTable.css index 7c1069e80d..35d0a1e89c 100644 --- a/src/sql/workbench/browser/modelComponents/media/declarativeTable.css +++ b/src/sql/workbench/browser/modelComponents/media/declarativeTable.css @@ -11,26 +11,30 @@ .declarative-table-header { padding: 5px; - border: 1px solid gray; + border-left: 1px solid gray; + border-top: 1px solid gray; + border-right: 1px solid gray; + border-bottom: 1px solid gray; background-color: #F5F5F5; vertical-align: top; } -.vs-dark .declarative-table-header { - padding: 5px; - border: 1px solid gray; - background-color: #333334; -} - +.vs-dark .declarative-table-header .hc-black .declarative-table-header { padding: 5px; - border: 1px solid gray; + border-left: 1px solid gray; + border-top: 1px solid gray; + border-right: 1px solid gray; + border-bottom: 1px solid gray; background-color: #333334; } .declarative-table-cell { padding: 5px; - border: 1px solid gray; + border-left: 1px solid gray; + border-top: 1px solid gray; + border-right: 1px solid gray; + border-bottom: 1px solid gray; } .declarative-table [role="gridcell"]:focus,
{{column.displayName}}{{column.displayName}}
+ {{cellData}} +