diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts index 778e0e6843..9744d5351d 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -12,12 +12,17 @@ import { BdcDashboardModel } from './bdcDashboardModel'; import { IconPathHelper } from '../constants'; import { BdcServiceStatusPage } from './bdcServiceStatusPage'; import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage'; -import { EndpointModel, BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; +import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; import { getHealthStatusDot, getServiceNameDisplayText } from '../utils'; const localize = nls.loadMessageBundle(); -const navWidth = '175px'; +const navWidth = '200px'; + +const selectedTabCss = { 'font-weight': 'bold' }; +const unselectedTabCss = { 'font-weight': '' }; + +type NavTab = { div: azdata.DivContainer, dot: azdata.TextComponent, text: azdata.TextComponent }; export class BdcDashboard { @@ -29,6 +34,8 @@ export class BdcDashboard { private modelView: azdata.ModelView; private mainAreaContainer: azdata.FlexContainer; private navContainer: azdata.FlexContainer; + + private currentTab: NavTab; private currentPage: azdata.FlexContainer; constructor(private title: string, private model: BdcDashboardModel) { @@ -117,18 +124,26 @@ export class BdcDashboard { this.mainAreaContainer.addItem(this.navContainer, { flex: `0 0 ${navWidth}`, CSSStyles: { 'padding-left': '10px', 'border-right': 'solid 1px #ccc' } }); // Overview nav item - this will be the initial page - const overviewNavItem = modelView.modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); - overviewNavItem.addItem(modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewNavTitle', 'Big data cluster overview') }).component(), { CSSStyles: { 'user-select': 'text' } }); + const overviewNavItemDiv = modelView.modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).withProperties({ CSSStyles: { 'cursor': 'pointer' } }).component(); + const overviewNavItemText = modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewNavTitle', 'Big data cluster overview') }).component(); + overviewNavItemText.updateCssStyles(selectedTabCss); + overviewNavItemDiv.addItem(overviewNavItemText, { CSSStyles: { 'user-select': 'text' } }); const overviewPage = new BdcDashboardOverviewPage(this.model).create(modelView); this.currentPage = overviewPage; + this.currentTab = { div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' }); - overviewNavItem.onDidClick(() => { + overviewNavItemDiv.onDidClick(() => { + if (this.currentTab) { + this.currentTab.text.updateCssStyles(unselectedTabCss); + } this.mainAreaContainer.removeItem(this.currentPage); this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' }); this.currentPage = overviewPage; + this.currentTab = { div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText }; + this.currentTab.text.updateCssStyles(selectedTabCss); }); - this.navContainer.addItem(overviewNavItem, { flex: '0 0 auto' }); + this.navContainer.addItem(overviewNavItemDiv, { flex: '0 0 auto' }); const clusterDetailsHeader = modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.clusterDetails', 'Cluster Details'), CSSStyles: { 'margin-block-end': '0px' } }).component(); this.navContainer.addItem(clusterDetailsHeader, { CSSStyles: { 'user-select': 'none', 'font-weight': 'bold', 'border-bottom': 'solid 1px #ccc', 'margin-bottom': '10px' } }); @@ -159,23 +174,30 @@ export class BdcDashboard { services.forEach(s => { const navItem = createServiceNavTab(this.modelView.modelBuilder, s); const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView).container; - navItem.onDidClick(() => { + navItem.div.onDidClick(() => { + if (this.currentTab) { + this.currentTab.text.updateCssStyles(unselectedTabCss); + } this.mainAreaContainer.removeItem(this.currentPage); this.mainAreaContainer.addItem(serviceStatusPage); this.currentPage = serviceStatusPage; + this.currentTab = navItem; + this.currentTab.text.updateCssStyles(selectedTabCss); }); - this.navContainer.addItem(navItem, { flex: '0 0 auto' }); + this.navContainer.addItem(navItem.div, { flex: '0 0 auto' }); }); this.serviceTabsCreated = true; } } } -function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceStatus: ServiceStatusModel): azdata.DivContainer { - const div = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); +function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceStatus: ServiceStatusModel): NavTab { + const div = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).withProperties({ CSSStyles: { 'cursor': 'pointer' } }).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' }); + const dot = 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(); + innerContainer.addItem(dot, { flex: '0 0 auto' }); + const text = modelBuilder.text().withProperties({ value: getServiceNameDisplayText(serviceStatus.serviceName), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'none' } }).component(); + innerContainer.addItem(text, { flex: '0 0 auto' }); div.addItem(innerContainer); - return div; + return { div: div, dot: dot, text: text }; } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts index a889da1a34..dd6a7a9e6c 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardResourceStatusPage.ts @@ -200,9 +200,9 @@ function createMetricsAndLogsRow(modelBuilder: azdata.ModelBuilder, instanceStat const metricsAndLogsRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); metricsAndLogsRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } }); - const metricsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.nodeMetricsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); + const metricsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.nodeMetricsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text', 'color': '#0078d4', 'text-decoration': 'underline' } }).component(); metricsAndLogsRow.addItem(metricsCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); - const logsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.logsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(); + const logsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.logsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text', 'color': '#0078d4', 'text-decoration': 'underline' } }).component(); metricsAndLogsRow.addItem(logsCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } }); return metricsAndLogsRow; diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts index 5494431283..139cfb7a37 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcServiceStatusPage.ts @@ -10,11 +10,15 @@ import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage import { BdcDashboardModel } from './bdcDashboardModel'; import { getHealthStatusDot } from '../utils'; +const selectedTabCss = { 'font-weight': 'bold' }; +const unselectedTabCss = { 'font-weight': '' }; + export class BdcServiceStatusPage { private initialized: boolean = false; private resourceTabsCreated: boolean = false; + private currentTabText: azdata.TextComponent; private currentTabPage: azdata.FlexContainer; private rootContainer: azdata.FlexContainer; private resourceHeader: azdata.FlexContainer; @@ -78,13 +82,20 @@ export class BdcServiceStatusPage { resources.forEach(resource => { const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource); const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container; - resourceHeaderTab.onDidClick(() => { + resourceHeaderTab.div.onDidClick(() => { + if (this.currentTabText) { + this.currentTabText.updateCssStyles(unselectedTabCss); + } this.changeSelectedTabPage(resourceStatusPage); + this.currentTabText = resourceHeaderTab.text; + this.currentTabText.updateCssStyles(selectedTabCss); }); if (!this.currentTabPage) { this.changeSelectedTabPage(resourceStatusPage); + this.currentTabText = resourceHeaderTab.text; + this.currentTabText.updateCssStyles(selectedTabCss); } - this.resourceHeader.addItem(resourceHeaderTab, { flex: '0 0 auto', CSSStyles: { 'border-bottom': 'solid #ccc' } }); + this.resourceHeader.addItem(resourceHeaderTab.div, { flex: '0 0 auto', CSSStyles: { 'border-bottom': 'solid #ccc' } }); }); this.resourceTabsCreated = true; } @@ -96,12 +107,12 @@ export class BdcServiceStatusPage { * @param modelBuilder The ModelBuilder used to construct the object * @param title The text to display in the tab */ -function createResourceHeaderTab(modelBuilder: azdata.ModelBuilder, resourceStatus: ResourceStatusModel): azdata.DivContainer { - const resourceHeaderTab = modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).component(); +function createResourceHeaderTab(modelBuilder: azdata.ModelBuilder, resourceStatus: ResourceStatusModel): { div: azdata.DivContainer, text: azdata.TextComponent } { + const resourceHeaderTab = modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).withProperties({ CSSStyles: { 'cursor': 'pointer' } }).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; + return { div: resourceHeaderTab, text: resourceHeaderLabel }; } diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 3f9887d4f2..ce6b0d5fdf 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2543,6 +2543,13 @@ declare module 'azdata' { */ updateProperty(key: string, value: any): Thenable; + /** + * Updates the specified CSS Styles and notifies the UI + * @param cssStyles The styles to update + * @returns Thenable that completes once the update has been applied to the UI + */ + updateCssStyles(cssStyles: { [key: string]: string }): Thenable; + enabled: boolean; /** * Event fired to notify that the component's validity has changed diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 83f411a6ae..f4c0d1d2d0 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -161,6 +161,13 @@ declare module 'sqlops' { */ updateProperty(key: string, value: any): Thenable; + /** + * Updates the specified CSS Styles and notifies the UI + * @param cssStyles The styles to update + * @returns Thenable that completes once the update has been applied to the UI + */ + updateCssStyles(cssStyles: { [key: string]: string }): Thenable; + enabled: boolean; /** * Event fired to notify that the component's validity has changed diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index d8fc0e2cd9..be0cbe6f67 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -532,6 +532,14 @@ class ComponentWrapper implements azdata.Component { this.setProperty('required', v); } + public get CSSStyles(): { [key: string]: string } { + return this.properties['CSSStyles']; + } + + public set CSSStyles(cssStyles: { [key: string]: string }) { + this.setProperty('CSSStyles', cssStyles); + } + public toComponentShape(): IComponentShape { return { id: this.id, @@ -604,6 +612,11 @@ class ComponentWrapper implements azdata.Component { return this.setProperty(key, value); } + public updateCssStyles(cssStyles: { [key: string]: string }): Thenable { + this.properties.CSSStyles = Object.assign(this.properties.CSSStyles || {}, cssStyles); + return this.notifyPropertyChanged(); + } + protected notifyPropertyChanged(): Thenable { return this._proxy.$setProperties(this._handle, this._id, this.properties); } @@ -626,7 +639,6 @@ class ComponentWrapper implements azdata.Component { } } - protected setDataProvider(): Thenable { return this._proxy.$setDataProvider(this._handle, this._id); } diff --git a/src/sql/workbench/browser/modelComponents/componentBase.ts b/src/sql/workbench/browser/modelComponents/componentBase.ts index b44048dffa..2dc476a134 100644 --- a/src/sql/workbench/browser/modelComponents/componentBase.ts +++ b/src/sql/workbench/browser/modelComponents/componentBase.ts @@ -30,7 +30,6 @@ export abstract class ComponentBase extends Disposable implements IComponent, On private _valid: boolean = true; protected _validations: (() => boolean | Thenable)[] = []; private _eventQueue: IComponentEventArgs[] = []; - private _CSSStyles: { [key: string]: string } = {}; constructor( protected _changeRef: ChangeDetectorRef, @@ -82,7 +81,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On public refreshDataProvider(item: any): void { } - public updateStyles() { + public updateStyles(): void { const element = (this._el.nativeElement); for (const style in this.CSSStyles) { element.style[style] = this.CSSStyles[style]; @@ -90,13 +89,9 @@ export abstract class ComponentBase extends Disposable implements IComponent, On } public setProperties(properties: { [key: string]: any; }): void { - if (!properties) { - this.properties = {}; - } + properties = properties || {}; this.properties = properties; - if (this.CSSStyles !== this._CSSStyles) { - this.updateStyles(); - } + this.updateStyles(); this.layout(); this.validate(); } @@ -105,10 +100,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On public updateProperty(key: string, value: any): void { if (key) { this.properties[key] = value; - - if (this.CSSStyles !== this._CSSStyles) { - this.updateStyles(); - } + this.updateStyles(); this.layout(); this.validate(); }