diff --git a/extensions/dacpac/src/test/testContext.ts b/extensions/dacpac/src/test/testContext.ts index c9216c80de..823b300a7e 100644 --- a/extensions/dacpac/src/test/testContext.ts +++ b/extensions/dacpac/src/test/testContext.ts @@ -312,7 +312,8 @@ export function createViewContext(): ViewTestContext { hyperlink: () => undefined!, tabbedPanel: undefined!, separator: undefined!, - propertiesContainer: undefined! + propertiesContainer: undefined!, + infoBox: undefined! } }; return { diff --git a/extensions/machine-learning/src/test/views/utils.ts b/extensions/machine-learning/src/test/views/utils.ts index b804cda025..3620660470 100644 --- a/extensions/machine-learning/src/test/views/utils.ts +++ b/extensions/machine-learning/src/test/views/utils.ts @@ -261,7 +261,8 @@ export function createViewContext(): ViewTestContext { hyperlink: () => hyperLinkBuilder, tabbedPanel: undefined!, separator: undefined!, - propertiesContainer: undefined! + propertiesContainer: undefined!, + infoBox: undefined! } }; let tab: azdata.window.DialogTab = { diff --git a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts index f05ec43fa5..20acf40931 100644 --- a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts +++ b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts @@ -40,7 +40,7 @@ describe('Manage Package Dialog', () => { it('getLocationComponent should create text component for undefined location', async function (): Promise { let testContext = createViewContext(); - let locations: IPackageLocation[] | undefined = undefined; + let locations: IPackageLocation[] | undefined = undefined; testContext.model.setup(x => x.getLocations()).returns(() => Promise.resolve(locations)); let actual = await InstalledPackagesTab.getLocationComponent(testContext.view, testContext.dialog.object); @@ -300,7 +300,8 @@ describe('Manage Package Dialog', () => { hyperlink: undefined!, tabbedPanel: undefined!, separator: undefined!, - propertiesContainer: undefined! + propertiesContainer: undefined!, + infoBox: undefined! } }; diff --git a/extensions/schema-compare/src/test/testContext.ts b/extensions/schema-compare/src/test/testContext.ts index 12e1090e93..eff6eeebd4 100644 --- a/extensions/schema-compare/src/test/testContext.ts +++ b/extensions/schema-compare/src/test/testContext.ts @@ -352,7 +352,8 @@ export function createViewContext(): ViewTestContext { hyperlink: () => undefined!, tabbedPanel: undefined!, separator: undefined!, - propertiesContainer: undefined! + propertiesContainer: undefined!, + infoBox: undefined! } }; return { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index af698f05d2..1ebc6dc3e2 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -330,6 +330,7 @@ declare module 'azdata' { tabbedPanel(): TabbedPanelComponentBuilder; separator(): ComponentBuilder; propertiesContainer(): ComponentBuilder; + infoBox(): ComponentBuilder; } export interface ComponentBuilder { @@ -604,6 +605,32 @@ declare module 'azdata' { propertyItems?: PropertiesContainerItem[]; } + /** + * Component to display text with an icon representing the severity + */ + export interface InfoBoxComponent extends Component, InfoBoxComponentProperties { + } + + export type InfoBoxStyle = 'information' | 'warning' | 'error' | 'success'; + + /** + * Properties for configuring a InfoBoxComponent + */ + export interface InfoBoxComponentProperties extends ComponentProperties { + /** + * The style of the InfoBox + */ + style: InfoBoxStyle; + /** + * The display text of the InfoBox + */ + text: string; + /** + * Controls whether the text should be announced by the screen reader. Default value is false. + */ + announceText?: boolean; + } + export namespace nb { /** * An event that is emitted when the active Notebook editor is changed. diff --git a/src/sql/base/browser/ui/infoBox/infoBox.ts b/src/sql/base/browser/ui/infoBox/infoBox.ts new file mode 100644 index 0000000000..72f94ca9f0 --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/infoBox.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/infoBox'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { alert, status } from 'vs/base/browser/ui/aria/aria'; +import { IThemable } from 'vs/base/common/styler'; +import { Color } from 'vs/base/common/color'; + +export interface IInfoBoxStyles { + informationBackground?: Color; + warningBackground?: Color; + errorBackground?: Color; + successBackground?: Color; +} + +export type InfoBoxStyle = 'information' | 'warning' | 'error' | 'success'; + +export interface InfoBoxOptions { + text: string; + style: InfoBoxStyle; + announceText?: boolean; +} + +export class InfoBox extends Disposable implements IThemable { + private _imageElement: HTMLDivElement; + private _textElement: HTMLDivElement; + private _infoBoxElement: HTMLDivElement; + private _text = ''; + private _infoBoxStyle: InfoBoxStyle = 'information'; + private _styles: IInfoBoxStyles; + private _announceText: boolean = false; + + constructor(container: HTMLElement, options?: InfoBoxOptions) { + super(); + this._infoBoxElement = document.createElement('div'); + this._imageElement = document.createElement('div'); + this._imageElement.setAttribute('role', 'image'); + this._textElement = document.createElement('div'); + this._textElement.classList.add('infobox-text'); + container.appendChild(this._infoBoxElement); + this._infoBoxElement.appendChild(this._imageElement); + this._infoBoxElement.appendChild(this._textElement); + if (options) { + this.infoBoxStyle = options.style; + this.text = options.text; + this._announceText = (options.announceText === true); + } + } + + public style(styles: IInfoBoxStyles): void { + this._styles = styles; + this.updateStyle(); + } + + public get announceText(): boolean { + return this._announceText; + } + + public set announceText(v: boolean) { + this._announceText = v; + } + + public get infoBoxStyle(): InfoBoxStyle { + return this._infoBoxStyle; + } + + public set infoBoxStyle(style: InfoBoxStyle) { + this._infoBoxStyle = style; + this._infoBoxElement.classList.remove(...this._infoBoxElement.classList); + this._imageElement.classList.remove(...this._imageElement.classList); + this._imageElement.setAttribute('aria-label', style); + this._infoBoxElement.classList.add('infobox-container', style); + this._imageElement.classList.add('infobox-image', style); + this.updateStyle(); + } + + public get text(): string { + return this._text; + } + + public set text(text: string) { + if (this._text !== text) { + this._text = text; + this._textElement.innerText = text; + if (this.announceText) { + if (this.infoBoxStyle === 'warning' || this.infoBoxStyle === 'error') { + alert(text); + } + else { + status(text); + } + } + } + } + + private updateStyle(): void { + if (this._styles) { + let backgroundColor: Color; + switch (this.infoBoxStyle) { + case 'error': + backgroundColor = this._styles.errorBackground; + break; + case 'warning': + backgroundColor = this._styles.warningBackground; + break; + case 'success': + backgroundColor = this._styles.successBackground; + break; + default: + backgroundColor = this._styles.informationBackground; + break; + } + this._infoBoxElement.style.backgroundColor = backgroundColor.toString(); + } + } +} diff --git a/src/sql/base/browser/ui/infoBox/media/infoBox.css b/src/sql/base/browser/ui/infoBox/media/infoBox.css new file mode 100644 index 0000000000..75dd1d127d --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/media/infoBox.css @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.infobox-container { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; + overflow: scroll; +} + +.infobox-image { + padding: 10px; + width: 16px; + height: 16px; + background-size: 16px; + background-position: 10px 10px; + background-repeat: no-repeat; + flex: 0 0 auto; +} + +.infobox-image.success { + background-image: url('status_success.svg'); +} + +.infobox-image.information { + background-image: url('status_info.svg'); +} + +.infobox-image.warning { + background-image: url('status_warning.svg'); +} + +.infobox-image.error { + background-image: url('status_error.svg'); +} + +.infobox-text { + flex: 1 1 auto; + padding: 10px 0; + user-select: text; +} diff --git a/src/sql/base/browser/ui/infoBox/media/status_error.svg b/src/sql/base/browser/ui/infoBox/media/status_error.svg new file mode 100644 index 0000000000..abe3c65921 --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/media/status_error.svg @@ -0,0 +1 @@ +error_16x16 \ No newline at end of file diff --git a/src/sql/base/browser/ui/infoBox/media/status_info.svg b/src/sql/base/browser/ui/infoBox/media/status_info.svg new file mode 100644 index 0000000000..43106fb6b0 --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/media/status_info.svg @@ -0,0 +1 @@ + diff --git a/src/sql/base/browser/ui/infoBox/media/status_success.svg b/src/sql/base/browser/ui/infoBox/media/status_success.svg new file mode 100644 index 0000000000..3123332093 --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/media/status_success.svg @@ -0,0 +1 @@ +success_complete \ No newline at end of file diff --git a/src/sql/base/browser/ui/infoBox/media/status_warning.svg b/src/sql/base/browser/ui/infoBox/media/status_warning.svg new file mode 100644 index 0000000000..0c37571576 --- /dev/null +++ b/src/sql/base/browser/ui/infoBox/media/status_warning.svg @@ -0,0 +1 @@ +warning_16x16 diff --git a/src/sql/platform/dashboard/browser/interfaces.ts b/src/sql/platform/dashboard/browser/interfaces.ts index 0674275272..f5a577a8aa 100644 --- a/src/sql/platform/dashboard/browser/interfaces.ts +++ b/src/sql/platform/dashboard/browser/interfaces.ts @@ -139,5 +139,6 @@ export enum ModelComponentTypes { ListView, TabbedPanel, Separator, - PropertiesContainer + PropertiesContainer, + InfoBox } diff --git a/src/sql/platform/theme/common/colorRegistry.ts b/src/sql/platform/theme/common/colorRegistry.ts index 1e1c3b4e43..8bfef31f75 100644 --- a/src/sql/platform/theme/common/colorRegistry.ts +++ b/src/sql/platform/theme/common/colorRegistry.ts @@ -68,3 +68,10 @@ export const codeEditorToolbarBackground = registerColor('notebook.codeEditorToo export const codeEditorToolbarBorder = registerColor('notebook.codeEditorToolbarBorder', { light: '#C8C6C4', dark: '#333333', hc: '#000000' }, nls.localize('notebook.codeEditorToolbarBorder', "Notebook: Code editor toolbar right border")); export const notebookCellTagBackground = registerColor('notebook.notebookCellTagBackground', { light: '#0078D4', dark: '#0078D4', hc: '#0078D4' }, nls.localize('notebook.notebookCellTagBackground', "Tag background color.")); export const notebookCellTagForeground = registerColor('notebook.notebookCellTagForeground', { light: '#FFFFFF', dark: '#FFFFFF', hc: '#FFFFFF' }, nls.localize('notebook.notebookCellTagForeground', "Tag foreground color.")); + +// Info Box +export const InfoBoxInformationBackground = registerColor('infoBox.infomationBackground', { light: '#F0F6FF', dark: '#001433', hc: '#000000' }, nls.localize('infoBox.infomationBackground', "InfoBox: The background color when the notification type is information.")); +export const InfoBoxWarningBackground = registerColor('infoBox.warningBackground', { light: '#FFF8F0', dark: '#331B00', hc: '#000000' }, nls.localize('infoBox.warningBackground', "InfoBox: The background color when the notification type is warning.")); +export const InfoBoxErrorBackground = registerColor('infoBox.errorBackground', { light: '#FEF0F1', dark: '#300306', hc: '#000000' }, nls.localize('infoBox.errorBackground', "InfoBox: The background color when the notification type is error.")); +export const InfoBoxSuccessBackground = registerColor('infoBox.successBackground', { light: '#F8FFF0', dark: '#1B3300', hc: '#000000' }, nls.localize('infoBox.successBackground', "InfoBox: The background color when the notification type is success.")); + diff --git a/src/sql/platform/theme/common/styler.ts b/src/sql/platform/theme/common/styler.ts index 97f9ecf911..7247b01c9a 100644 --- a/src/sql/platform/theme/common/styler.ts +++ b/src/sql/platform/theme/common/styler.ts @@ -7,6 +7,7 @@ import * as colors from './colors'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import * as cr from 'vs/platform/theme/common/colorRegistry'; +import * as sqlcr from 'sql/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemable } from 'vs/base/common/styler'; @@ -306,3 +307,21 @@ export function attachCheckboxStyler(widget: IThemable, themeService: IThemeServ disabledCheckboxForeground: (style && style.disabledCheckboxForeground) || colors.disabledCheckboxForeground }, widget); } + +export interface IInfoBoxStyleOverrides { + informationBackground: cr.ColorIdentifier, + warningBackground: cr.ColorIdentifier, + errorBackground: cr.ColorIdentifier, + successBackground: cr.ColorIdentifier +} + +export const defaultInfoBoxStyles: IInfoBoxStyleOverrides = { + informationBackground: sqlcr.InfoBoxInformationBackground, + warningBackground: sqlcr.InfoBoxWarningBackground, + errorBackground: sqlcr.InfoBoxErrorBackground, + successBackground: sqlcr.InfoBoxSuccessBackground +}; + +export function attachInfoBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInfoBoxStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultInfoBoxStyles, ...style }, widget); +} diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index bed6869198..5d40ef43b8 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -271,6 +271,14 @@ class ModelBuilderImpl implements azdata.ModelBuilder { return builder; } + infoBox(): azdata.ComponentBuilder { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new InfoBoxComponentWrapper(this._proxy, this._handle, id), id); + + this._componentBuilders.set(id, builder); + return builder; + } + getComponentBuilder(component: ComponentWrapper, id: string): ComponentBuilderImpl { let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); this._componentBuilders.set(id, componentBuilder); @@ -1984,6 +1992,37 @@ class PropertiesContainerComponentWrapper extends ComponentWrapper implements az } } +class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBoxComponent { + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.InfoBox, id); + this.properties = {}; + } + + public get style(): azdata.InfoBoxStyle { + return this.properties['style']; + } + + public set style(v: azdata.InfoBoxStyle) { + this.setProperty('style', v); + } + + public get text(): string { + return this.properties['text']; + } + + public set text(v: string) { + this.setProperty('text', v); + } + + public get announceText(): boolean { + return this.properties['announceText']; + } + + public set announceText(v: boolean) { + this.setProperty('announceText', v); + } +} + class GroupContainerComponentWrapper extends ComponentWrapper implements azdata.GroupContainer { constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { super(proxy, handle, type, id); diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 68072447c0..18ece7dc7b 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -177,7 +177,8 @@ export enum ModelComponentTypes { ListView, TabbedPanel, Separator, - PropertiesContainer + PropertiesContainer, + InfoBox } export enum ModelViewAction { diff --git a/src/sql/workbench/browser/modelComponents/infoBox.component.ts b/src/sql/workbench/browser/modelComponents/infoBox.component.ts new file mode 100644 index 0000000000..2830c81e4a --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/infoBox.component.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, + OnDestroy, AfterViewInit, ElementRef, ViewChild +} from '@angular/core'; + +import * as azdata from 'azdata'; +import { IComponent, IComponentDescriptor, IModelStore } from 'sql/platform/dashboard/browser/interfaces'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase'; +import { InfoBox, InfoBoxStyle } from 'sql/base/browser/ui/infoBox/infoBox'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { attachInfoBoxStyler } from 'sql/platform/theme/common/styler'; + +@Component({ + selector: 'modelview-infobox', + template: ` +
+
` +}) +export default class InfoBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + @ViewChild('container', { read: ElementRef }) private _container: ElementRef; + + private _infoBox: InfoBox; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) el: ElementRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(ILogService) logService: ILogService) { + super(changeRef, el, logService); + } + + ngAfterViewInit(): void { + this.baseInit(); + if (this._container) { + this._infoBox = new InfoBox(this._container.nativeElement); + this._register(attachInfoBoxStyler(this._infoBox, this.themeService)); + this.updateInfoBox(); + } + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + public setLayout(layout: any): void { + this.layout(); + } + + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + this.updateInfoBox(); + } + + private updateInfoBox(): void { + if (this._infoBox) { + this._container.nativeElement.style.width = this.getWidth(); + this._container.nativeElement.style.height = this.getHeight(); + this._infoBox.announceText = this.announceText; + this._infoBox.infoBoxStyle = this.style; + this._infoBox.text = this.text; + } + } + + public get style(): InfoBoxStyle { + return this.getPropertyOrDefault((props) => props.style, 'information'); + } + + public get text(): string { + return this.getPropertyOrDefault((props) => props.text, ''); + } + + public get announceText(): boolean { + return this.getPropertyOrDefault((props) => props.announceText, false); + } +} diff --git a/src/sql/workbench/contrib/modelView/browser/components.contribution.ts b/src/sql/workbench/contrib/modelView/browser/components.contribution.ts index b1fe467418..fb24ac12ea 100644 --- a/src/sql/workbench/contrib/modelView/browser/components.contribution.ts +++ b/src/sql/workbench/contrib/modelView/browser/components.contribution.ts @@ -35,6 +35,7 @@ import SeparatorComponent from 'sql/workbench/browser/modelComponents/separator. import { ModelComponentTypes } from 'sql/platform/dashboard/browser/interfaces'; import PropertiesContainerComponent from 'sql/workbench/browser/modelComponents/propertiesContainer.component'; import ListViewComponent from 'sql/workbench/browser/modelComponents/listView.component'; +import InfoBoxComponent from 'sql/workbench/browser/modelComponents/infoBox.component'; export const DIV_CONTAINER = 'div-container'; registerComponentType(DIV_CONTAINER, ModelComponentTypes.DivContainer, DivContainer); @@ -126,3 +127,6 @@ registerComponentType(SEPARATOR_COMPONENT, ModelComponentTypes.Separator, Separa export const PROPERTIESCONTAINER_COMPONENT = 'propertiescontainer-component'; registerComponentType(PROPERTIESCONTAINER_COMPONENT, ModelComponentTypes.PropertiesContainer, PropertiesContainerComponent); + +export const INFOBOX_COMPONENT = 'infobox-component'; +registerComponentType(INFOBOX_COMPONENT, ModelComponentTypes.InfoBox, InfoBoxComponent);