Add loading spinner component (#1580)

This commit is contained in:
Matt Irvine
2018-06-07 17:54:48 -07:00
committed by GitHub
parent 44de602e52
commit a5b4eeb932
9 changed files with 266 additions and 7 deletions

View File

@@ -74,9 +74,9 @@ export default class MainController implements vscode.Disposable {
dialog.cancelButton.onClick(() => console.log('cancel clicked!')); dialog.cancelButton.onClick(() => console.log('cancel clicked!'));
dialog.okButton.label = 'ok'; dialog.okButton.label = 'ok';
dialog.cancelButton.label = 'no'; 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!')); 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!')); customButton2.onClick(() => console.log('button 2 clicked!'));
dialog.customButtons = [customButton1, customButton2]; dialog.customButtons = [customButton1, customButton2];
tab1.registerContent(async (view) => { tab1.registerContent(async (view) => {
@@ -84,7 +84,14 @@ export default class MainController implements vscode.Disposable {
.withProperties({ .withProperties({
//width: 300 //width: 300
}).component(); }).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 inputBox2 = view.modelBuilder.inputBox().component();
let backupFilesInputBox = view.modelBuilder.inputBox().component();
let checkbox = view.modelBuilder.checkBox() let checkbox = view.modelBuilder.checkBox()
.withProperties({ .withProperties({
@@ -107,7 +114,7 @@ export default class MainController implements vscode.Disposable {
let button2 = view.modelBuilder.button() let button2 = view.modelBuilder.button()
.component(); .component();
button.onDidClick(e => { button.onDidClick(e => {
inputBox2.value = 'Button clicked'; backupFilesInputBox.value = 'Button clicked';
}); });
let dropdown = view.modelBuilder.dropDown() let dropdown = view.modelBuilder.dropDown()
.withProperties({ .withProperties({
@@ -175,7 +182,7 @@ export default class MainController implements vscode.Disposable {
, { flex: '1 1 50%' }).component(); , { flex: '1 1 50%' }).component();
let formModel = view.modelBuilder.formContainer() let formModel = view.modelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
component: inputBox, component: inputBoxWrapper,
title: 'Backup name' title: 'Backup name'
}, { }, {
component: inputBox2, component: inputBox2,
@@ -187,7 +194,7 @@ export default class MainController implements vscode.Disposable {
component: checkbox, component: checkbox,
title: '' title: ''
}, { }, {
component: inputBox2, component: backupFilesInputBox,
title: 'Backup files', title: 'Backup files',
actions: [button, button3] actions: [button, button3]
}, { }, {
@@ -197,7 +204,13 @@ export default class MainController implements vscode.Disposable {
horizontal: false, horizontal: false,
componentWidth: 400 componentWidth: 400
}).component(); }).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); sqlops.window.modelviewdialog.openDialog(dialog);

View File

@@ -16,6 +16,7 @@ import RadioButtonComponent from './radioButton.component';
import WebViewComponent from './webview.component'; import WebViewComponent from './webview.component';
import TableComponent from './table.component'; import TableComponent from './table.component';
import TextComponent from './text.component'; import TextComponent from './text.component';
import LoadingComponent from './loadingComponent.component';
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
@@ -58,3 +59,6 @@ registerComponentType(TEXT_COMPONENT, ModelComponentTypes.Text, TextComponent);
export const TABLE_COMPONENT = 'table-component'; export const TABLE_COMPONENT = 'table-component';
registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent); registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent);
export const LOADING_COMPONENT = 'loading-component';
registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent);

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g>
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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: `
<div class="modelview-loadingComponent-container" *ngIf="loading">
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
</div>
<model-component-wrapper #childElement [descriptor]="_component" [modelStore]="modelStore" *ngIf="_component" [ngClass]="{'modelview-loadingComponent-content-loading': loading}">
</model-component-wrapper>
`
})
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<sqlops.LoadingComponentProperties, boolean>((props) => props.loading, false);
}
public set loading(newValue: boolean) {
this.setPropertyFromUI<sqlops.LoadingComponentProperties, boolean>((properties, value) => { properties.loading = value; }, newValue);
this.layout();
}
public addToContainer(componentDescriptor: IComponentDescriptor): void {
this._component = componentDescriptor;
this.layout();
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:white;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -32,6 +32,7 @@ declare module 'sqlops' {
formContainer(): FormBuilder; formContainer(): FormBuilder;
groupContainer(): GroupBuilder; groupContainer(): GroupBuilder;
toolbarContainer(): ToolbarBuilder; toolbarContainer(): ToolbarBuilder;
loadingComponent(): LoadingComponentBuilder;
} }
export interface ComponentBuilder<T extends Component> { export interface ComponentBuilder<T extends Component> {
@@ -69,6 +70,14 @@ declare module 'sqlops' {
addToolbarItem(toolbarComponent: ToolbarComponent): void; addToolbarItem(toolbarComponent: ToolbarComponent): void;
} }
export interface LoadingComponentBuilder extends ComponentBuilder<LoadingComponent> {
/**
* Set the component wrapped by the LoadingComponent
* @param component The component to wrap
*/
withItem(component: Component): LoadingComponentBuilder;
}
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> { export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>; withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
@@ -335,6 +344,10 @@ declare module 'sqlops' {
iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
} }
export interface LoadingComponentProperties {
loading?: boolean;
}
export interface CardComponent extends Component { export interface CardComponent extends Component {
label: string; label: string;
value: string; value: string;
@@ -390,6 +403,22 @@ declare module 'sqlops' {
webviewId: string; 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. * A view backed by a model provided by an extension.
* This model contains enough information to lay out the view * This model contains enough information to lay out the view

View File

@@ -79,7 +79,8 @@ export enum ModelComponentTypes {
DashboardWebview, DashboardWebview,
Form, Form,
Group, Group,
Toolbar Toolbar,
LoadingComponent
} }
export interface IComponentShape { export interface IComponentShape {

View File

@@ -137,6 +137,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
return builder; 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<T extends sqlops.Component>(component: ComponentWrapper, id: string): ComponentBuilderImpl<T> { getComponentBuilder<T extends sqlops.Component>(component: ComponentWrapper, id: string): ComponentBuilderImpl<T> {
let componentBuilder: ComponentBuilderImpl<T> = new ComponentBuilderImpl<T>(component); let componentBuilder: ComponentBuilderImpl<T> = new ComponentBuilderImpl<T>(component);
this._componentBuilders.set(id, componentBuilder); this._componentBuilders.set(id, componentBuilder);
@@ -299,6 +306,13 @@ class ToolbarContainerBuilder extends ContainerBuilderImpl<sqlops.ToolbarContain
} }
} }
class LoadingComponentBuilder extends ComponentBuilderImpl<sqlops.LoadingComponent> implements sqlops.LoadingComponentBuilder {
withItem(component: sqlops.Component) {
this.component().component = component;
return this;
}
}
class InternalItemConfig { class InternalItemConfig {
constructor(private _component: ComponentWrapper, public config: any) { } 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 { class ModelViewImpl implements sqlops.ModelView {
public onClosedEmitter = new Emitter<any>(); public onClosedEmitter = new Emitter<any>();