mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
loading indicator for table designer (#17407)
* loading indicator for table designer * fix layering error * bug fix
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface DesignerState {
|
||||
valid: boolean;
|
||||
dirty: boolean;
|
||||
saving: boolean;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
export const NameProperty = 'name';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
85
src/sql/base/browser/ui/loadingSpinner/loadingSpinner.ts
Normal file
85
src/sql/base/browser/ui/loadingSpinner/loadingSpinner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class SaveTableChangesAction extends Action {
|
||||
}
|
||||
|
||||
private updateState(): void {
|
||||
this.enabled = this._input.dirty && this._input.valid && !this._input.saving;
|
||||
this.enabled = this._input.dirty && this._input.valid && !this._input.processing;
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
|
||||
@@ -18,6 +18,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
private _valid: boolean = true;
|
||||
private _dirty: boolean = false;
|
||||
private _saving: boolean = false;
|
||||
private _processing: boolean = false;
|
||||
private _onStateChange = new Emitter<DesignerState>();
|
||||
|
||||
public readonly onStateChange: Event<DesignerState> = this._onStateChange.event;
|
||||
@@ -39,6 +40,10 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
return this._saving;
|
||||
}
|
||||
|
||||
get processing(): boolean {
|
||||
return this._processing;
|
||||
}
|
||||
|
||||
get objectTypeDisplayName(): string {
|
||||
return localize('tableDesigner.tableObjectType', "Table");
|
||||
}
|
||||
@@ -58,11 +63,12 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
}
|
||||
|
||||
async processEdit(edit: DesignerEdit): Promise<DesignerEditResult> {
|
||||
this.updateState(this.valid, this.dirty, this.saving, true);
|
||||
const result = await this._provider.processTableEdit(this._tableInfo, this._viewModel!, edit);
|
||||
if (result.isValid) {
|
||||
this._viewModel = result.viewModel;
|
||||
}
|
||||
this.updateState(result.isValid, true, this.saving);
|
||||
this.updateState(result.isValid, true, this.saving, false);
|
||||
return {
|
||||
isValid: result.isValid,
|
||||
errors: result.errors
|
||||
@@ -75,37 +81,40 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
message: localize('tableDesigner.savingChanges', "Saving table designer changes...")
|
||||
});
|
||||
try {
|
||||
this.updateState(this.valid, this.dirty, true);
|
||||
this.updateState(this.valid, this.dirty, true, true);
|
||||
await this._provider.saveTable(this._tableInfo, this._viewModel);
|
||||
this.updateState(true, false, false);
|
||||
this.updateState(true, false, false, false);
|
||||
notificationHandle.updateMessage(localize('tableDesigner.savedChangeSuccess', "The changes have been successfully saved."));
|
||||
} catch (error) {
|
||||
notificationHandle.updateSeverity(Severity.Error);
|
||||
notificationHandle.updateMessage(localize('tableDesigner.saveChangeError', "An error occured while saving changes: {0}", error?.message ?? error));
|
||||
this.updateState(this.valid, this.dirty, false);
|
||||
this.updateState(this.valid, this.dirty, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
async revert(): Promise<void> {
|
||||
this.updateState(true, false, false);
|
||||
this.updateState(true, false, false, false);
|
||||
}
|
||||
|
||||
private updateState(valid: boolean, dirty: boolean, saving: boolean): void {
|
||||
if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving) {
|
||||
private updateState(valid: boolean, dirty: boolean, saving: boolean, processing: boolean): void {
|
||||
if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving || this._processing !== processing) {
|
||||
this._dirty = dirty;
|
||||
this._valid = valid;
|
||||
this._saving = saving;
|
||||
this._processing = processing;
|
||||
this._onStateChange.fire({
|
||||
valid: this._valid,
|
||||
dirty: this._dirty,
|
||||
saving: this._saving
|
||||
saving: this._saving,
|
||||
processing: this._processing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
this.updateState(this.valid, this.dirty, this.saving, true);
|
||||
const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo);
|
||||
|
||||
this.updateState(this.valid, this.dirty, this.saving, false);
|
||||
this._viewModel = designerInfo.viewModel;
|
||||
this.setDefaultData();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user