mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -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 { ButtonColumn } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
|
||||||
import { Codicon } from 'vs/base/common/codicons';
|
import { Codicon } from 'vs/base/common/codicons';
|
||||||
import { Color } from 'vs/base/common/color';
|
import { Color } from 'vs/base/common/color';
|
||||||
|
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
|
||||||
|
|
||||||
export interface IDesignerStyle {
|
export interface IDesignerStyle {
|
||||||
tabbedPanelStyles?: ITabbedPanelStyles;
|
tabbedPanelStyles?: ITabbedPanelStyles;
|
||||||
@@ -44,6 +45,7 @@ export type CreateComponentFunc = (container: HTMLElement, component: DesignerDa
|
|||||||
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
|
export type SetComponentValueFunc = (definition: DesignerDataPropertyInfo, component: DesignerUIComponent, data: DesignerViewModel) => void;
|
||||||
|
|
||||||
export class Designer extends Disposable implements IThemable {
|
export class Designer extends Disposable implements IThemable {
|
||||||
|
private _loadingSpinner: LoadingSpinner;
|
||||||
private _horizontalSplitViewContainer: HTMLElement;
|
private _horizontalSplitViewContainer: HTMLElement;
|
||||||
private _verticalSplitViewContainer: HTMLElement;
|
private _verticalSplitViewContainer: HTMLElement;
|
||||||
private _tabbedPanelContainer: HTMLElement;
|
private _tabbedPanelContainer: HTMLElement;
|
||||||
@@ -89,6 +91,7 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
}
|
}
|
||||||
}, this._contextViewProvider
|
}, this._contextViewProvider
|
||||||
);
|
);
|
||||||
|
this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true });
|
||||||
this._verticalSplitViewContainer = DOM.$('.designer-component');
|
this._verticalSplitViewContainer = DOM.$('.designer-component');
|
||||||
this._horizontalSplitViewContainer = DOM.$('.container');
|
this._horizontalSplitViewContainer = DOM.$('.container');
|
||||||
this._contentContainer = DOM.$('.content-container');
|
this._contentContainer = DOM.$('.content-container');
|
||||||
@@ -201,7 +204,10 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
private async initializeDesignerView(): Promise<void> {
|
private async initializeDesignerView(): Promise<void> {
|
||||||
this._propertiesPane.clear();
|
this._propertiesPane.clear();
|
||||||
DOM.clearNode(this._topContentContainer);
|
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();
|
const view = await this._input.getView();
|
||||||
|
this.stopLoading(handle, localize('designer.loadingDesignerCompleted', "Loading designer completed"));
|
||||||
if (view.components) {
|
if (view.components) {
|
||||||
view.components.forEach(component => {
|
view.components.forEach(component => {
|
||||||
this.createComponent(this._topContentContainer, component, component.propertyName, true, true);
|
this.createComponent(this._topContentContainer, component, component.propertyName, true, true);
|
||||||
@@ -268,7 +274,9 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.applyEdit(edit);
|
await this.applyEdit(edit);
|
||||||
|
const handle = this.startLoading(localize('designer.processingChanges', "Processing changes..."));
|
||||||
const result = await this._input.processEdit(edit);
|
const result = await this._input.processEdit(edit);
|
||||||
|
this.stopLoading(handle, localize('designer.processingChangesCompleted', "Processing changes completed"));
|
||||||
if (result.isValid) {
|
if (result.isValid) {
|
||||||
this._supressEditProcessing = true;
|
this._supressEditProcessing = true;
|
||||||
await this.updateComponentValues();
|
await this.updateComponentValues();
|
||||||
@@ -415,8 +423,10 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
ariaLabel: inputProperties.title,
|
ariaLabel: inputProperties.title,
|
||||||
type: inputProperties.inputType,
|
type: inputProperties.inputType,
|
||||||
});
|
});
|
||||||
input.onDidChange(async (newValue) => {
|
input.onLoseFocus(async (args) => {
|
||||||
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
|
if (args.hasChanged) {
|
||||||
|
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (setWidth && inputProperties.width !== undefined) {
|
if (setWidth && inputProperties.width !== undefined) {
|
||||||
input.width = inputProperties.width as number;
|
input.width = inputProperties.width as number;
|
||||||
@@ -567,4 +577,22 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
this.styleComponent(component);
|
this.styleComponent(component);
|
||||||
return 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;
|
valid: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
|
processing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NameProperty = 'name';
|
export const NameProperty = 'name';
|
||||||
|
|||||||
@@ -176,4 +176,13 @@ export class InputBox extends vsInputBox {
|
|||||||
super.width = width;
|
super.width = width;
|
||||||
this.element.style.width = 'fit-content';
|
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 {
|
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() {
|
override dispose() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
private _valid: boolean = true;
|
private _valid: boolean = true;
|
||||||
private _dirty: boolean = false;
|
private _dirty: boolean = false;
|
||||||
private _saving: boolean = false;
|
private _saving: boolean = false;
|
||||||
|
private _processing: boolean = false;
|
||||||
private _onStateChange = new Emitter<DesignerState>();
|
private _onStateChange = new Emitter<DesignerState>();
|
||||||
|
|
||||||
public readonly onStateChange: Event<DesignerState> = this._onStateChange.event;
|
public readonly onStateChange: Event<DesignerState> = this._onStateChange.event;
|
||||||
@@ -39,6 +40,10 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
return this._saving;
|
return this._saving;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get processing(): boolean {
|
||||||
|
return this._processing;
|
||||||
|
}
|
||||||
|
|
||||||
get objectTypeDisplayName(): string {
|
get objectTypeDisplayName(): string {
|
||||||
return localize('tableDesigner.tableObjectType', "Table");
|
return localize('tableDesigner.tableObjectType', "Table");
|
||||||
}
|
}
|
||||||
@@ -58,11 +63,12 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processEdit(edit: DesignerEdit): Promise<DesignerEditResult> {
|
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);
|
const result = await this._provider.processTableEdit(this._tableInfo, this._viewModel!, edit);
|
||||||
if (result.isValid) {
|
if (result.isValid) {
|
||||||
this._viewModel = result.viewModel;
|
this._viewModel = result.viewModel;
|
||||||
}
|
}
|
||||||
this.updateState(result.isValid, true, this.saving);
|
this.updateState(result.isValid, true, this.saving, false);
|
||||||
return {
|
return {
|
||||||
isValid: result.isValid,
|
isValid: result.isValid,
|
||||||
errors: result.errors
|
errors: result.errors
|
||||||
@@ -75,37 +81,40 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
message: localize('tableDesigner.savingChanges', "Saving table designer changes...")
|
message: localize('tableDesigner.savingChanges', "Saving table designer changes...")
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
this.updateState(this.valid, this.dirty, true);
|
this.updateState(this.valid, this.dirty, true, true);
|
||||||
await this._provider.saveTable(this._tableInfo, this._viewModel);
|
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."));
|
notificationHandle.updateMessage(localize('tableDesigner.savedChangeSuccess', "The changes have been successfully saved."));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notificationHandle.updateSeverity(Severity.Error);
|
notificationHandle.updateSeverity(Severity.Error);
|
||||||
notificationHandle.updateMessage(localize('tableDesigner.saveChangeError', "An error occured while saving changes: {0}", error?.message ?? 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> {
|
async revert(): Promise<void> {
|
||||||
this.updateState(true, false, false);
|
this.updateState(true, false, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateState(valid: boolean, dirty: boolean, saving: boolean): void {
|
private updateState(valid: boolean, dirty: boolean, saving: boolean, processing: boolean): void {
|
||||||
if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving) {
|
if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving || this._processing !== processing) {
|
||||||
this._dirty = dirty;
|
this._dirty = dirty;
|
||||||
this._valid = valid;
|
this._valid = valid;
|
||||||
this._saving = saving;
|
this._saving = saving;
|
||||||
|
this._processing = processing;
|
||||||
this._onStateChange.fire({
|
this._onStateChange.fire({
|
||||||
valid: this._valid,
|
valid: this._valid,
|
||||||
dirty: this._dirty,
|
dirty: this._dirty,
|
||||||
saving: this._saving
|
saving: this._saving,
|
||||||
|
processing: this._processing
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initialize(): Promise<void> {
|
private async initialize(): Promise<void> {
|
||||||
|
this.updateState(this.valid, this.dirty, this.saving, true);
|
||||||
const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo);
|
const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo);
|
||||||
|
this.updateState(this.valid, this.dirty, this.saving, false);
|
||||||
this._viewModel = designerInfo.viewModel;
|
this._viewModel = designerInfo.viewModel;
|
||||||
this.setDefaultData();
|
this.setDefaultData();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user