Add styling to selected tabs in BDC Dashboard (#6978)

* Change cursor over tabs to be hand (pointer)

* Change hyperlink color

* Hook up CSSStyles to be updated from extension side and make BDC dashboard tabs change style when selected

* Remove unused file

* Add back in call to updateStyles

* Fix typos
This commit is contained in:
Charles Gagnon
2019-08-27 14:14:46 -07:00
committed by GitHub
parent 1e217094a7
commit a27b759b10
7 changed files with 84 additions and 33 deletions

View File

@@ -12,12 +12,17 @@ import { BdcDashboardModel } from './bdcDashboardModel';
import { IconPathHelper } from '../constants'; import { IconPathHelper } from '../constants';
import { BdcServiceStatusPage } from './bdcServiceStatusPage'; import { BdcServiceStatusPage } from './bdcServiceStatusPage';
import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage'; import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage';
import { EndpointModel, BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated';
import { getHealthStatusDot, getServiceNameDisplayText } from '../utils'; import { getHealthStatusDot, getServiceNameDisplayText } from '../utils';
const localize = nls.loadMessageBundle(); 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 { export class BdcDashboard {
@@ -29,6 +34,8 @@ export class BdcDashboard {
private modelView: azdata.ModelView; private modelView: azdata.ModelView;
private mainAreaContainer: azdata.FlexContainer; private mainAreaContainer: azdata.FlexContainer;
private navContainer: azdata.FlexContainer; private navContainer: azdata.FlexContainer;
private currentTab: NavTab;
private currentPage: azdata.FlexContainer; private currentPage: azdata.FlexContainer;
constructor(private title: string, private model: BdcDashboardModel) { 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' } }); 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 // Overview nav item - this will be the initial page
const overviewNavItem = modelView.modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); const overviewNavItemDiv = modelView.modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).withProperties({ CSSStyles: { 'cursor': 'pointer' } }).component();
overviewNavItem.addItem(modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewNavTitle', 'Big data cluster overview') }).component(), { CSSStyles: { 'user-select': 'text' } }); 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); const overviewPage = new BdcDashboardOverviewPage(this.model).create(modelView);
this.currentPage = overviewPage; this.currentPage = overviewPage;
this.currentTab = { div: overviewNavItemDiv, dot: undefined, text: overviewNavItemText };
this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' }); 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.removeItem(this.currentPage);
this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' }); this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' });
this.currentPage = overviewPage; 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(); 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' } }); 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 => { services.forEach(s => {
const navItem = createServiceNavTab(this.modelView.modelBuilder, s); 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).container;
navItem.onDidClick(() => { navItem.div.onDidClick(() => {
if (this.currentTab) {
this.currentTab.text.updateCssStyles(unselectedTabCss);
}
this.mainAreaContainer.removeItem(this.currentPage); this.mainAreaContainer.removeItem(this.currentPage);
this.mainAreaContainer.addItem(serviceStatusPage); this.mainAreaContainer.addItem(serviceStatusPage);
this.currentPage = 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; this.serviceTabsCreated = true;
} }
} }
} }
function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceStatus: ServiceStatusModel): azdata.DivContainer { function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceStatus: ServiceStatusModel): NavTab {
const div = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component(); 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(); 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' }); 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(modelBuilder.text().withProperties({ value: getServiceNameDisplayText(serviceStatus.serviceName), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component(), { flex: '0 0 auto' }); 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); div.addItem(innerContainer);
return div; return { div: div, dot: dot, text: text };
} }

View File

@@ -200,9 +200,9 @@ function createMetricsAndLogsRow(modelBuilder: azdata.ModelBuilder, instanceStat
const metricsAndLogsRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component(); 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(); 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' } }); 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' } }); 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' } }); metricsAndLogsRow.addItem(logsCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
return metricsAndLogsRow; return metricsAndLogsRow;

View File

@@ -10,11 +10,15 @@ import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage
import { BdcDashboardModel } from './bdcDashboardModel'; import { BdcDashboardModel } from './bdcDashboardModel';
import { getHealthStatusDot } from '../utils'; import { getHealthStatusDot } from '../utils';
const selectedTabCss = { 'font-weight': 'bold' };
const unselectedTabCss = { 'font-weight': '' };
export class BdcServiceStatusPage { export class BdcServiceStatusPage {
private initialized: boolean = false; private initialized: boolean = false;
private resourceTabsCreated: boolean = false; private resourceTabsCreated: boolean = false;
private currentTabText: azdata.TextComponent;
private currentTabPage: azdata.FlexContainer; private currentTabPage: azdata.FlexContainer;
private rootContainer: azdata.FlexContainer; private rootContainer: azdata.FlexContainer;
private resourceHeader: azdata.FlexContainer; private resourceHeader: azdata.FlexContainer;
@@ -78,13 +82,20 @@ export class BdcServiceStatusPage {
resources.forEach(resource => { resources.forEach(resource => {
const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource); const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource);
const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container; 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.changeSelectedTabPage(resourceStatusPage);
this.currentTabText = resourceHeaderTab.text;
this.currentTabText.updateCssStyles(selectedTabCss);
}); });
if (!this.currentTabPage) { if (!this.currentTabPage) {
this.changeSelectedTabPage(resourceStatusPage); 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; this.resourceTabsCreated = true;
} }
@@ -96,12 +107,12 @@ export class BdcServiceStatusPage {
* @param modelBuilder The ModelBuilder used to construct the object * @param modelBuilder The ModelBuilder used to construct the object
* @param title The text to display in the tab * @param title The text to display in the tab
*/ */
function createResourceHeaderTab(modelBuilder: azdata.ModelBuilder, resourceStatus: ResourceStatusModel): azdata.DivContainer { function createResourceHeaderTab(modelBuilder: azdata.ModelBuilder, resourceStatus: ResourceStatusModel): { div: azdata.DivContainer, text: azdata.TextComponent } {
const resourceHeaderTab = modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).component(); 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(); 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' }); 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(); const resourceHeaderLabel = modelBuilder.text().withProperties({ value: resourceStatus.resourceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'text-align': 'left' } }).component();
innerContainer.addItem(resourceHeaderLabel); innerContainer.addItem(resourceHeaderLabel);
resourceHeaderTab.addItem(innerContainer); resourceHeaderTab.addItem(innerContainer);
return resourceHeaderTab; return { div: resourceHeaderTab, text: resourceHeaderLabel };
} }

7
src/sql/azdata.d.ts vendored
View File

@@ -2543,6 +2543,13 @@ declare module 'azdata' {
*/ */
updateProperty(key: string, value: any): Thenable<void>; updateProperty(key: string, value: any): Thenable<void>;
/**
* 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<void>;
enabled: boolean; enabled: boolean;
/** /**
* Event fired to notify that the component's validity has changed * Event fired to notify that the component's validity has changed

View File

@@ -161,6 +161,13 @@ declare module 'sqlops' {
*/ */
updateProperty(key: string, value: any): Thenable<void>; updateProperty(key: string, value: any): Thenable<void>;
/**
* 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<void>;
enabled: boolean; enabled: boolean;
/** /**
* Event fired to notify that the component's validity has changed * Event fired to notify that the component's validity has changed

View File

@@ -532,6 +532,14 @@ class ComponentWrapper implements azdata.Component {
this.setProperty('required', v); 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 { public toComponentShape(): IComponentShape {
return <IComponentShape>{ return <IComponentShape>{
id: this.id, id: this.id,
@@ -604,6 +612,11 @@ class ComponentWrapper implements azdata.Component {
return this.setProperty(key, value); return this.setProperty(key, value);
} }
public updateCssStyles(cssStyles: { [key: string]: string }): Thenable<void> {
this.properties.CSSStyles = Object.assign(this.properties.CSSStyles || {}, cssStyles);
return this.notifyPropertyChanged();
}
protected notifyPropertyChanged(): Thenable<void> { protected notifyPropertyChanged(): Thenable<void> {
return this._proxy.$setProperties(this._handle, this._id, this.properties); return this._proxy.$setProperties(this._handle, this._id, this.properties);
} }
@@ -626,7 +639,6 @@ class ComponentWrapper implements azdata.Component {
} }
} }
protected setDataProvider(): Thenable<void> { protected setDataProvider(): Thenable<void> {
return this._proxy.$setDataProvider(this._handle, this._id); return this._proxy.$setDataProvider(this._handle, this._id);
} }

View File

@@ -30,7 +30,6 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
private _valid: boolean = true; private _valid: boolean = true;
protected _validations: (() => boolean | Thenable<boolean>)[] = []; protected _validations: (() => boolean | Thenable<boolean>)[] = [];
private _eventQueue: IComponentEventArgs[] = []; private _eventQueue: IComponentEventArgs[] = [];
private _CSSStyles: { [key: string]: string } = {};
constructor( constructor(
protected _changeRef: ChangeDetectorRef, protected _changeRef: ChangeDetectorRef,
@@ -82,7 +81,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
public refreshDataProvider(item: any): void { public refreshDataProvider(item: any): void {
} }
public updateStyles() { public updateStyles(): void {
const element = (<HTMLElement>this._el.nativeElement); const element = (<HTMLElement>this._el.nativeElement);
for (const style in this.CSSStyles) { for (const style in this.CSSStyles) {
element.style[style] = this.CSSStyles[style]; 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 { public setProperties(properties: { [key: string]: any; }): void {
if (!properties) { properties = properties || {};
this.properties = {};
}
this.properties = properties; this.properties = properties;
if (this.CSSStyles !== this._CSSStyles) { this.updateStyles();
this.updateStyles();
}
this.layout(); this.layout();
this.validate(); this.validate();
} }
@@ -105,10 +100,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
public updateProperty(key: string, value: any): void { public updateProperty(key: string, value: any): void {
if (key) { if (key) {
this.properties[key] = value; this.properties[key] = value;
this.updateStyles();
if (this.CSSStyles !== this._CSSStyles) {
this.updateStyles();
}
this.layout(); this.layout();
this.validate(); this.validate();
} }