diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index d2f661ea45..dbbe38c633 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -74,9 +74,9 @@ export default class MainController implements vscode.Disposable { dialog.cancelButton.onClick(() => console.log('cancel clicked!')); dialog.okButton.label = 'ok'; dialog.cancelButton.label = 'no'; - let customButton1 = sqlops.window.modelviewdialog.createButton('Test button 1'); + let customButton1 = sqlops.window.modelviewdialog.createButton('Load name'); customButton1.onClick(() => console.log('button 1 clicked!')); - let customButton2 = sqlops.window.modelviewdialog.createButton('Test button 2'); + let customButton2 = sqlops.window.modelviewdialog.createButton('Load all'); customButton2.onClick(() => console.log('button 2 clicked!')); dialog.customButtons = [customButton1, customButton2]; tab1.registerContent(async (view) => { @@ -84,7 +84,14 @@ export default class MainController implements vscode.Disposable { .withProperties({ //width: 300 }).component(); + let inputBoxWrapper = view.modelBuilder.loadingComponent().withItem(inputBox).component(); + inputBoxWrapper.loading = false; + customButton1.onClick(() => { + inputBoxWrapper.loading = true; + setTimeout(() => inputBoxWrapper.loading = false, 5000); + }); let inputBox2 = view.modelBuilder.inputBox().component(); + let backupFilesInputBox = view.modelBuilder.inputBox().component(); let checkbox = view.modelBuilder.checkBox() .withProperties({ @@ -107,7 +114,7 @@ export default class MainController implements vscode.Disposable { let button2 = view.modelBuilder.button() .component(); button.onDidClick(e => { - inputBox2.value = 'Button clicked'; + backupFilesInputBox.value = 'Button clicked'; }); let dropdown = view.modelBuilder.dropDown() .withProperties({ @@ -175,7 +182,7 @@ export default class MainController implements vscode.Disposable { , { flex: '1 1 50%' }).component(); let formModel = view.modelBuilder.formContainer() .withFormItems([{ - component: inputBox, + component: inputBoxWrapper, title: 'Backup name' }, { component: inputBox2, @@ -187,7 +194,7 @@ export default class MainController implements vscode.Disposable { component: checkbox, title: '' }, { - component: inputBox2, + component: backupFilesInputBox, title: 'Backup files', actions: [button, button3] }, { @@ -197,7 +204,13 @@ export default class MainController implements vscode.Disposable { horizontal: false, componentWidth: 400 }).component(); - await view.initializeModel(formModel); + let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component(); + formWrapper.loading = false; + customButton2.onClick(() => { + formWrapper.loading = true; + setTimeout(() => formWrapper.loading = false, 5000); + }); + await view.initializeModel(formWrapper); }); sqlops.window.modelviewdialog.openDialog(dialog); diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index 1de60e57e3..e694cdf54a 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -16,6 +16,7 @@ import RadioButtonComponent from './radioButton.component'; import WebViewComponent from './webview.component'; import TableComponent from './table.component'; import TextComponent from './text.component'; +import LoadingComponent from './loadingComponent.component'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -58,3 +59,6 @@ registerComponentType(TEXT_COMPONENT, ModelComponentTypes.Text, TextComponent); export const TABLE_COMPONENT = 'table-component'; registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent); + +export const LOADING_COMPONENT = 'loading-component'; +registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent); diff --git a/src/sql/parts/modelComponents/loading.svg b/src/sql/parts/modelComponents/loading.svg new file mode 100644 index 0000000000..e762f06d5e --- /dev/null +++ b/src/sql/parts/modelComponents/loading.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/sql/parts/modelComponents/loadingComponent.component.ts b/src/sql/parts/modelComponents/loadingComponent.component.ts new file mode 100644 index 0000000000..3cc15443a1 --- /dev/null +++ b/src/sql/parts/modelComponents/loadingComponent.component.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./loadingComponent'; +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ViewChild, ElementRef +} from '@angular/core'; + +import * as sqlops from 'sqlops'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces'; +import * as nls from 'vs/nls'; + +@Component({ + selector: 'modelview-loadingComponent', + template: ` +
+
+
+ + + ` +}) +export default class LoadingComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { + private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading'); + private _component: IComponentDescriptor; + + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + + @ViewChild('spinnerElement', { read: ElementRef }) private _spinnerElement: ElementRef; + @ViewChild('childElement', { read: ElementRef }) private _childElement: ElementRef; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) { + super(changeRef); + this._validations.push(() => { + if (!this._component) { + return true; + } + return this.modelStore.getComponent(this._component.id).validate(); + }); + } + + ngOnInit(): void { + this.baseInit(); + + } + + ngAfterViewInit(): void { + this.setLayout(); + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + /// IComponent implementation + + public layout(): void { + this._changeRef.detectChanges(); + } + + public setLayout(): void { + this.layout(); + } + + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + } + + public get loading(): boolean { + return this.getPropertyOrDefault((props) => props.loading, false); + } + + public set loading(newValue: boolean) { + this.setPropertyFromUI((properties, value) => { properties.loading = value; }, newValue); + this.layout(); + } + + public addToContainer(componentDescriptor: IComponentDescriptor): void { + this._component = componentDescriptor; + this.layout(); + } +} diff --git a/src/sql/parts/modelComponents/loadingComponent.css b/src/sql/parts/modelComponents/loadingComponent.css new file mode 100644 index 0000000000..a3e44083f9 --- /dev/null +++ b/src/sql/parts/modelComponents/loadingComponent.css @@ -0,0 +1,24 @@ +.modelview-loadingComponent-container { + display: flex; + flex-direction: row; + justify-content: center; +} + +.vs .modelview-loadingComponent-spinner { + content: url("loading.svg"); +} + +.vs-dark .modelview-loadingComponent-spinner, +.hc-black .modelview-loadingComponent-spinner { + content: url("loading_inverse.svg"); +} + +.modelview-loadingComponent-spinner { + height: 20px; + padding-top: 5px; + padding-bottom: 5px; +} + +.modelview-loadingComponent-content-loading { + display: none; +} \ No newline at end of file diff --git a/src/sql/parts/modelComponents/loading_inverse.svg b/src/sql/parts/modelComponents/loading_inverse.svg new file mode 100644 index 0000000000..c3633c0dda --- /dev/null +++ b/src/sql/parts/modelComponents/loading_inverse.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index a4ccb8917d..2bf1d16cf9 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -32,6 +32,7 @@ declare module 'sqlops' { formContainer(): FormBuilder; groupContainer(): GroupBuilder; toolbarContainer(): ToolbarBuilder; + loadingComponent(): LoadingComponentBuilder; } export interface ComponentBuilder { @@ -69,6 +70,14 @@ declare module 'sqlops' { addToolbarItem(toolbarComponent: ToolbarComponent): void; } + export interface LoadingComponentBuilder extends ComponentBuilder { + /** + * Set the component wrapped by the LoadingComponent + * @param component The component to wrap + */ + withItem(component: Component): LoadingComponentBuilder; + } + export interface FormBuilder extends ContainerBuilder { withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder; @@ -335,6 +344,10 @@ declare module 'sqlops' { iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; } + export interface LoadingComponentProperties { + loading?: boolean; + } + export interface CardComponent extends Component { label: string; value: string; @@ -390,6 +403,22 @@ declare module 'sqlops' { webviewId: string; } + /** + * Component used to wrap another component that needs to be loaded, and show a loading spinner + * while the contained component is loading + */ + export interface LoadingComponent extends Component { + /** + * Whether to show the loading spinner instead of the contained component. True by default + */ + loading: boolean; + + /** + * The component displayed when the loading property is false + */ + component: Component; + } + /** * A view backed by a model provided by an extension. * This model contains enough information to lay out the view diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index adb3248e67..e6dfd9b746 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -79,7 +79,8 @@ export enum ModelComponentTypes { DashboardWebview, Form, Group, - Toolbar + Toolbar, + LoadingComponent } export interface IComponentShape { diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index e29e788906..db805e8130 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -137,6 +137,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + loadingComponent(): sqlops.LoadingComponentBuilder { + let id = this.getNextComponentId(); + let builder = new LoadingComponentBuilder(new LoadingComponentWrapper(this._proxy, this._handle, 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); @@ -299,6 +306,13 @@ class ToolbarContainerBuilder extends ContainerBuilderImpl implements sqlops.LoadingComponentBuilder { + withItem(component: sqlops.Component) { + this.component().component = component; + return this; + } +} + class InternalItemConfig { constructor(private _component: ComponentWrapper, public config: any) { } @@ -761,6 +775,30 @@ class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent { } } +class LoadingComponentWrapper extends ComponentWrapper implements sqlops.LoadingComponent { + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.LoadingComponent, id); + this.properties = {}; + this.loading = true; + } + + public get loading(): boolean { + return this.properties['loading']; + } + + public set loading(value: boolean) { + this.setProperty('loading', value); + } + + public get component(): sqlops.Component { + return this.items[0]; + } + + public set component(value: sqlops.Component) { + this.addItem(value); + } +} + class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter();