diff --git a/extensions/machine-learning/src/test/views/utils.ts b/extensions/machine-learning/src/test/views/utils.ts index 8e07391ab3..9888e94c32 100644 --- a/extensions/machine-learning/src/test/views/utils.ts +++ b/extensions/machine-learning/src/test/views/utils.ts @@ -306,7 +306,10 @@ export function createViewContext(): ViewTestContext { onClosed: new vscode.EventEmitter().event, registerContent: () => { }, modelView: undefined!, - valid: true + valid: true, + loading: false, + loadingText: '', + loadingCompletedText: '' }; let wizard: azdata.window.Wizard = { title: '', @@ -327,7 +330,10 @@ export function createViewContext(): ViewTestContext { close: () => { return Promise.resolve(); }, registerNavigationValidator: () => { }, message: dialogMessage, - registerOperation: () => { } + registerOperation: () => { }, + loading: false, + loadingText: '', + loadingCompletedText: '' }; let wizardPage: azdata.window.WizardPage = { title: '', diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index d14acc8d4f..eb6ed26cc6 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -134,6 +134,24 @@ declare module 'azdata' { } } + export interface LoadingComponentBase { + /** + * When true, the component will display a loading spinner. + */ + loading?: boolean; + + /** + * This sets the alert text which gets announced when the loading spinner is shown. + */ + loadingText?: string; + + /** + * The text to display while loading is set to false. Will also be announced through screen readers + * once loading is completed. + */ + loadingCompletedText?: string; + } + /** * The column information of a data set. */ @@ -1702,4 +1720,12 @@ declare module 'azdata' { */ objectType?: string; } + + export namespace window { + export interface Wizard extends LoadingComponentBase { + } + + export interface Dialog extends LoadingComponentBase { + } + } } diff --git a/src/sql/media/icons/loading.svg b/src/sql/media/icons/loading.svg index ecea83cc5e..440a6cbd64 100644 --- a/src/sql/media/icons/loading.svg +++ b/src/sql/media/icons/loading.svg @@ -4,7 +4,6 @@ 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; } @@ -12,7 +11,6 @@ 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 { 0% { opacity: 1; } 100% { opacity: 0.3; } diff --git a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts index af89d5ea1e..edda5c55e3 100644 --- a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts @@ -144,6 +144,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); } dialog.message = details.message; + dialog.loading = details.loading; + dialog.loadingText = dialog.loadingText; + dialog.loadingCompletedText = dialog.loadingCompletedText; return Promise.resolve(); } @@ -223,7 +226,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM wizard.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); } wizard.message = details.message; - + wizard.loading = details.loading; + wizard.loadingText = details.loadingText; + wizard.loadingCompletedText = details.loadingCompletedText; return Promise.resolve(); } diff --git a/src/sql/workbench/api/common/extHostModelViewDialog.ts b/src/sql/workbench/api/common/extHostModelViewDialog.ts index 71243ecef7..ae239d0f1e 100644 --- a/src/sql/workbench/api/common/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/common/extHostModelViewDialog.ts @@ -133,6 +133,9 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog { private _renderHeader: boolean; private _renderFooter: boolean; private _dialogProperties: IDialogProperties; + private _loading: boolean; + private _loadingText: string; + private _loadingCompletedText: string; private _onClosed = new Emitter(); public onClosed = this._onClosed.event; @@ -216,6 +219,33 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog { this._extHostModelViewDialog.updateDialogContent(this); } + public get loading(): boolean { + return this._loading; + } + + public set loading(value: boolean) { + this._loading = value; + this._extHostModelViewDialog.updateDialogContent(this); + } + + public get loadingText(): string { + return this._loadingText; + } + + public set loadingText(value: string) { + this._loadingText = value; + this._extHostModelViewDialog.updateDialogContent(this); + } + + public get loadingCompletedText(): string { + return this._loadingCompletedText; + } + + public set loadingCompletedText(value: string) { + this._loadingCompletedText = value; + this._extHostModelViewDialog.updateDialogContent(this); + } + public get dialogName(): string { return this._dialogName; } @@ -443,6 +473,9 @@ class WizardImpl implements azdata.window.Wizard { public readonly onPageChanged = this._pageChangedEmitter.event; private _navigationValidator: (info: azdata.window.WizardPageChangeInfo) => boolean | Thenable; private _message: azdata.window.DialogMessage; + private _loading: boolean; + private _loadingText: string; + private _loadingCompletedText: string; private _displayPageTitles: boolean = true; private _operationHandler: BackgroundOperationHandler; private _width: DialogWidth; @@ -487,6 +520,33 @@ class WizardImpl implements azdata.window.Wizard { this._extHostModelViewDialog.updateWizard(this); } + public get loading(): boolean { + return this._loading; + } + + public set loading(value: boolean) { + this._loading = value; + this._extHostModelViewDialog.updateWizard(this); + } + + public get loadingText(): string { + return this._loadingText + } + + public set loadingText(value: string) { + this._loadingText = value; + this._extHostModelViewDialog.updateWizard(this); + } + + public get loadingCompletedText(): string { + return this._loadingCompletedText; + } + + public set loadingCompletedText(value: string) { + this._loadingCompletedText = value; + this._extHostModelViewDialog.updateWizard(this); + } + public get displayPageTitles(): boolean { return this._displayPageTitles; } @@ -797,7 +857,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { cancelButton: this.getHandle(dialog.cancelButton), content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string, customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getHandle(button)) : undefined, - message: dialog.message + message: dialog.message, + loading: dialog.loading, + loadingText: dialog.loadingText, + loadingCompletedText: dialog.loadingCompletedText }); } @@ -944,7 +1007,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { nextButton: this.getHandle(wizard.nextButton), customButtons: wizard.customButtons ? wizard.customButtons.map(button => this.getHandle(button)) : undefined, message: wizard.message, - displayPageTitles: wizard.displayPageTitles + displayPageTitles: wizard.displayPageTitles, + loading: wizard.loading, + loadingText: wizard.loadingText, + loadingCompletedText: wizard.loadingCompletedText, }); } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index c7753c99b9..ee63d60e7e 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -268,6 +268,9 @@ export interface IModelViewDialogDetails { renderHeader: boolean; renderFooter: boolean; dialogProperties: IDialogProperties; + loading: boolean; + loadingText: string; + loadingCompletedText: string; } export interface IModelViewTabDetails { @@ -307,6 +310,9 @@ export interface IModelViewWizardDetails { message: DialogMessage; displayPageTitles: boolean; width: DialogWidth; + loading: boolean; + loadingText: string; + loadingCompletedText: string; } export type DialogWidth = 'narrow' | 'medium' | 'wide' | number | string; diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index bc804fade4..5d0d7863d1 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -81,6 +81,7 @@ export interface IModalOptions { hasErrors?: boolean; hasSpinner?: boolean; spinnerTitle?: string; + onSpinnerHideText?: string; renderHeader?: boolean; renderFooter?: boolean; dialogProperties?: IDialogProperties; @@ -94,7 +95,7 @@ const defaultOptions: IModalOptions = { hasBackButton: false, hasTitleIcon: false, hasErrors: false, - hasSpinner: false, + hasSpinner: true, renderHeader: true, renderFooter: true, dialogProperties: undefined @@ -638,6 +639,9 @@ export abstract class Modal extends Disposable implements IThemable { } } else { DOM.hide(this._spinnerElement!); + if (this._modalOptions.onSpinnerHideText) { + alert(this._modalOptions.onSpinnerHideText); + } } } } diff --git a/src/sql/workbench/services/dialog/browser/customDialogService.ts b/src/sql/workbench/services/dialog/browser/customDialogService.ts index 79b8e292ac..057a6db1f1 100644 --- a/src/sql/workbench/services/dialog/browser/customDialogService.ts +++ b/src/sql/workbench/services/dialog/browser/customDialogService.ts @@ -9,8 +9,8 @@ import { Dialog, Wizard } from 'sql/workbench/services/dialog/common/dialogTypes import { IModalOptions } from 'sql/workbench/browser/modal/modal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true }; -export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true }; +export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true, hasSpinner: true }; +export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true, hasSpinner: true }; export class CustomDialogService { private _dialogModals = new Map(); diff --git a/src/sql/workbench/services/dialog/browser/dialogModal.ts b/src/sql/workbench/services/dialog/browser/dialogModal.ts index 12214d0e72..74db4d562b 100644 --- a/src/sql/workbench/services/dialog/browser/dialogModal.ts +++ b/src/sql/workbench/services/dialog/browser/dialogModal.ts @@ -93,7 +93,17 @@ export class DialogModal extends Modal { }; messageChangeHandler(this._dialog.message); - this._dialog.onMessageChange(message => messageChangeHandler(message)); + this._register(this._dialog.onMessageChange(message => messageChangeHandler(message))); + this._register(this._dialog.onLoadingChange((loadingState) => { + this.spinner = loadingState; + })); + this._register(this._dialog.onLoadingTextChange((loadingText) => { + this._modalOptions.spinnerTitle = loadingText; + + })); + this._register(this._dialog.onLoadingCompletedTextChange((loadingCompletedText) => { + this._modalOptions.onSpinnerHideText = loadingCompletedText; + })); } private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requireDialogValid: boolean = false): Button { diff --git a/src/sql/workbench/services/dialog/browser/wizardModal.ts b/src/sql/workbench/services/dialog/browser/wizardModal.ts index 5e4fb26ae8..f3b0d251a8 100644 --- a/src/sql/workbench/services/dialog/browser/wizardModal.ts +++ b/src/sql/workbench/services/dialog/browser/wizardModal.ts @@ -96,7 +96,21 @@ export class WizardModal extends Modal { }; messageChangeHandler(this._wizard.message); - this._wizard.onMessageChange(message => messageChangeHandler(message)); + this._register(this._wizard.onMessageChange(message => messageChangeHandler(message))); + + this._register(this._wizard.onLoadingChange((loadingState) => { + this.spinner = loadingState; + })); + this._register(this._wizard.onLoadingChange((loadingState) => { + this.spinner = loadingState; + })); + this._register(this._wizard.onLoadingTextChange((loadingText) => { + this._modalOptions.spinnerTitle = loadingText; + + })); + this._register(this._wizard.onLoadingCompletedTextChange((loadingCompletedText) => { + this._modalOptions.onSpinnerHideText = loadingCompletedText; + })); } private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false, index?: number): Button { diff --git a/src/sql/workbench/services/dialog/common/dialogTypes.ts b/src/sql/workbench/services/dialog/common/dialogTypes.ts index a2bb22938b..78f9ac189b 100644 --- a/src/sql/workbench/services/dialog/common/dialogTypes.ts +++ b/src/sql/workbench/services/dialog/common/dialogTypes.ts @@ -53,6 +53,16 @@ export class Dialog extends ModelViewPane { public customButtons: DialogButton[] = []; private _onMessageChange = new Emitter(); public readonly onMessageChange = this._onMessageChange.event; + private _loading: boolean = false; + private _loadingText: string; + private _loadingCompletedText: string; + private _onLoadingChange = new Emitter(); + private _onLoadingTextChange = new Emitter(); + private _onLoadingCompletedTextChange = new Emitter(); + public readonly onLoadingChange = this._onLoadingChange.event; + public readonly onLoadingTextChange = this._onLoadingTextChange.event; + public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event; + private _message: DialogMessage | undefined; private _closeValidator: CloseValidator | undefined; @@ -87,6 +97,39 @@ export class Dialog extends ModelViewPane { this._onMessageChange.fire(this._message); } + public get loading(): boolean { + return this._loading; + } + + public set loading(value: boolean) { + if (this.loading !== value) { + this._loading = value; + this._onLoadingChange.fire(this._loading); + } + } + + public get loadingText(): string | undefined { + return this._loadingText; + } + + public set loadingText(value: string | undefined) { + if (this.loadingText !== value) { + this._loadingText = value; + this._onLoadingTextChange.fire(this._loadingText); + } + } + + public get loadingCompletedText(): string | undefined { + return this._loadingCompletedText; + } + + public set loadingCompletedText(value: string | undefined) { + if (this._loadingCompletedText !== value) { + this._loadingCompletedText = value; + this._onLoadingCompletedTextChange.fire(this._loadingCompletedText); + } + } + public registerCloseValidator(validator: CloseValidator): void { this._closeValidator = validator; } @@ -247,6 +290,15 @@ export class Wizard { private _message: DialogMessage | undefined; public displayPageTitles: boolean = false; public width: DialogWidth | undefined; + private _loading: boolean = false; + private _loadingText: string; + private _loadingCompletedText: string; + private _onLoadingChange = new Emitter(); + private _onLoadingTextChange = new Emitter(); + private _onLoadingCompletedTextChange = new Emitter(); + public readonly onLoadingChange = this._onLoadingChange.event; + public readonly onLoadingTextChange = this._onLoadingTextChange.event; + public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event; constructor(public title: string, public readonly name: string, @@ -329,4 +381,39 @@ export class Wizard { this._message = value; this._onMessageChange.fire(this._message); } + + public get loading(): boolean { + return this._loading; + } + + public set loading(value: boolean) { + if (this.loading !== value) { + this._loading = value; + this._onLoadingChange.fire(this._loading); + } + } + + public get loadingText(): string | undefined { + return this._loadingText; + } + + public set loadingText(value: string | undefined) { + if (this.loadingText !== value) { + this._loadingText = value; + this._onLoadingTextChange.fire(this._loadingText); + } + } + + public get loadingCompletedText(): string | undefined { + return this._loadingCompletedText; + } + + public set loadingCompletedText(value: string | undefined) { + if (this._loadingCompletedText !== value) { + this._loadingCompletedText = value; + this._onLoadingCompletedTextChange.fire(this._loadingCompletedText); + } + } + + } diff --git a/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts b/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts index 75b438ed84..7b00d7ea01 100644 --- a/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts +++ b/src/sql/workbench/test/electron-browser/api/mainThreadModelViewDialog.test.ts @@ -136,7 +136,10 @@ suite('MainThreadModelViewDialog Tests', () => { okButton: okButtonHandle, cancelButton: cancelButtonHandle, customButtons: [button1Handle, button2Handle], - message: undefined + message: undefined, + loading: false, + loadingText: undefined, + loadingCompletedText: undefined, }; // Set up the wizard details @@ -183,7 +186,10 @@ suite('MainThreadModelViewDialog Tests', () => { pages: [page1Handle, page2Handle], message: undefined, displayPageTitles: false, - width: 'wide' + width: 'wide', + loading: false, + loadingText: undefined, + loadingCompletedText: undefined }; // Register the buttons, tabs, and dialog