loading indicator for table designer (#17407)

* loading indicator for table designer

* fix layering error

* bug fix
This commit is contained in:
Alan Ren
2021-10-20 12:54:23 -07:00
committed by GitHub
parent 328ed83cb9
commit c89aa26c0a
7 changed files with 165 additions and 12 deletions

View File

@@ -27,6 +27,7 @@ import { Button, IButtonStyles } from 'sql/base/browser/ui/button/button';
import { ButtonColumn } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
import { Codicon } from 'vs/base/common/codicons';
import { Color } from 'vs/base/common/color';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
export interface IDesignerStyle {
tabbedPanelStyles?: ITabbedPanelStyles;
@@ -44,6 +45,7 @@ export type CreateComponentFunc = (container: HTMLElement, component: DesignerDa
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
export class Designer extends Disposable implements IThemable {
private _loadingSpinner: LoadingSpinner;
private _horizontalSplitViewContainer: HTMLElement;
private _verticalSplitViewContainer: HTMLElement;
private _tabbedPanelContainer: HTMLElement;
@@ -89,6 +91,7 @@ export class Designer extends Disposable implements IThemable {
}
}, this._contextViewProvider
);
this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true });
this._verticalSplitViewContainer = DOM.$('.designer-component');
this._horizontalSplitViewContainer = DOM.$('.container');
this._contentContainer = DOM.$('.content-container');
@@ -201,7 +204,10 @@ export class Designer extends Disposable implements IThemable {
private async initializeDesignerView(): Promise<void> {
this._propertiesPane.clear();
DOM.clearNode(this._topContentContainer);
// For initialization, we would want to show the loading indicator immediately.
const handle = this.startLoading(localize('designer.loadingDesigner', "Loading designer..."), 0);
const view = await this._input.getView();
this.stopLoading(handle, localize('designer.loadingDesignerCompleted', "Loading designer completed"));
if (view.components) {
view.components.forEach(component => {
this.createComponent(this._topContentContainer, component, component.propertyName, true, true);
@@ -268,7 +274,9 @@ export class Designer extends Disposable implements IThemable {
return;
}
await this.applyEdit(edit);
const handle = this.startLoading(localize('designer.processingChanges', "Processing changes..."));
const result = await this._input.processEdit(edit);
this.stopLoading(handle, localize('designer.processingChangesCompleted', "Processing changes completed"));
if (result.isValid) {
this._supressEditProcessing = true;
await this.updateComponentValues();
@@ -415,8 +423,10 @@ export class Designer extends Disposable implements IThemable {
ariaLabel: inputProperties.title,
type: inputProperties.inputType,
});
input.onDidChange(async (newValue) => {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
input.onLoseFocus(async (args) => {
if (args.hasChanged) {
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value });
}
});
if (setWidth && inputProperties.width !== undefined) {
input.width = inputProperties.width as number;
@@ -567,4 +577,22 @@ export class Designer extends Disposable implements IThemable {
this.styleComponent(component);
return component;
}
private startLoading(message: string, timeout: number = 500): any {
// To make the experience smoother, only show the loading indicator if the request is not returning in 500ms(default value).
return setTimeout(() => {
this._loadingSpinner.loadingMessage = message;
this._loadingSpinner.loading = true;
this._container.removeChild(this._verticalSplitViewContainer);
}, timeout);
}
private stopLoading(handle: any, message: string): void {
clearTimeout(handle);
if (this._loadingSpinner.loading) {
this._loadingSpinner.loadingCompletedMessage = message;
this._loadingSpinner.loading = false;
this._container.appendChild(this._verticalSplitViewContainer);
}
}
}

View File

@@ -52,6 +52,7 @@ export interface DesignerState {
valid: boolean;
dirty: boolean;
saving: boolean;
processing: boolean;
}
export const NameProperty = 'name';

View File

@@ -176,4 +176,13 @@ export class InputBox extends vsInputBox {
super.width = width;
this.element.style.width = 'fit-content';
}
public override get value() {
return super.value;
}
public override set value(newValue: string) {
this._lastLoseFocusValue = newValue;
super.value = newValue;
}
}

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* 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/loadingSpinner';
import * as nls from 'vs/nls';
import { status } from 'vs/base/browser/ui/aria/aria';
import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { mixin } from 'vs/base/common/objects';
const DefaultLoadingMessage = nls.localize('loadingMessage', "Loading");
const DefaultLoadingCompletedMessage = nls.localize('loadingCompletedMessage', "Loading completed");
export interface LoadingSpinnerOptions {
/**
* Whether to show the messages. The default value is false.
*/
showText?: boolean;
/**
* Whether the loading spinner should take up all the avaliable spaces. The default value is false.
*/
fullSize?: boolean;
}
const defaultLoadingSpinnerOptions: LoadingSpinnerOptions = {
showText: false,
fullSize: false
};
export class LoadingSpinner extends Disposable {
private _loading: boolean = false;
private _loadingMessage?: string;
private _loadingCompletedMessage?: string;
private _loadingSpinner: HTMLElement;
private _loadingSpinnerText: HTMLElement;
private _options: LoadingSpinnerOptions;
constructor(private _container: HTMLElement, options?: LoadingSpinnerOptions) {
super();
this._options = mixin(options || {}, defaultLoadingSpinnerOptions, false);
this._loadingSpinner = DOM.$(`.loading-spinner-component-container${this._options.fullSize ? '.full-size' : ''}`);
this._loadingSpinner.appendChild(DOM.$('.loading-spinner.codicon.in-progress'));
if (this._options.showText) {
this._loadingSpinnerText = this._loadingSpinner.appendChild(DOM.$(''));
}
}
get loadingMessage(): string {
return this._loadingMessage ?? DefaultLoadingMessage;
}
set loadingMessage(v: string) {
this._loadingMessage = v;
}
get loadingCompletedMessage(): string {
return this._loadingCompletedMessage ?? DefaultLoadingCompletedMessage;
}
set loadingCompletedMessage(v: string) {
this._loadingCompletedMessage = v;
}
get loading(): boolean {
return this._loading;
}
set loading(v: boolean) {
if (v !== this._loading) {
this._loading = v;
const message = this._loading ? this.loadingMessage : this.loadingCompletedMessage;
status(message);
if (this._loading) {
this._container.appendChild(this._loadingSpinner);
} else {
this._container.removeChild(this._loadingSpinner);
}
if (this._options.showText) {
this._loadingSpinnerText.innerText = message;
}
}
}
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.loading-spinner-component-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.loading-spinner-component-container .loading-spinner {
height: 20px;
padding: 5px;
}
.loading-spinner-component-container.full-size {
width: 100%;
height: 100%;
}