diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index 65e212fd1d..9d2dec1232 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -69,10 +69,10 @@ export default class MainController implements vscode.Disposable { private async getTabContent(view: sqlops.ModelView, customButton1: sqlops.window.modelviewdialog.Button, customButton2: sqlops.window.modelviewdialog.Button, componentWidth: number | string): Promise { let inputBox = view.modelBuilder.inputBox() - .withProperties({ - multiline: true, - height: 100 - }).component(); + .withProperties({ + multiline: true, + height: 100 + }).component(); let inputBoxWrapper = view.modelBuilder.loadingComponent().withItem(inputBox).component(); inputBoxWrapper.loading = false; customButton1.onClick(() => { @@ -145,8 +145,8 @@ export default class MainController implements vscode.Disposable { component: inputBox4, title: 'inputBox4' }], { - horizontal: true - }).component(); + horizontal: true + }).component(); let groupModel1 = view.modelBuilder.groupContainer() .withLayout({ }).withItems([ @@ -178,8 +178,8 @@ export default class MainController implements vscode.Disposable { }).component(); let declarativeTable = view.modelBuilder.declarativeTable() - .withProperties({ - columns: [{ + .withProperties({ + columns: [{ displayName: 'Column 1', valueType: sqlops.DeclarativeDataType.string, width: '20px', @@ -204,12 +204,12 @@ export default class MainController implements vscode.Disposable { { name: 'options2', displayName: 'option 2' } ] } - ], - data: [ - ['Data00', 'Data01', false, 'options2'], - ['Data10', 'Data11', true, 'options1'] - ] - }).component(); + ], + data: [ + ['Data00', 'Data01', false, 'options2'], + ['Data10', 'Data11', true, 'options1'] + ] + }).component(); declarativeTable.onDataChanged(e => { inputBox2.value = e.row.toString() + ' ' + e.column.toString() + ' ' + e.value.toString(); @@ -223,7 +223,7 @@ export default class MainController implements vscode.Disposable { height: 150 }).withItems([ radioButton, groupModel1, radioButton2] - , { flex: '1 1 50%' }).component(); + , { flex: '1 1 50%' }).component(); let formModel = view.modelBuilder.formContainer() .withFormItems([{ component: inputBoxWrapper, @@ -254,9 +254,9 @@ export default class MainController implements vscode.Disposable { component: listBox, title: 'List Box' }], { - horizontal: false, - componentWidth: componentWidth - }).component(); + horizontal: false, + componentWidth: componentWidth + }).component(); let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component(); formWrapper.loading = false; customButton2.onClick(() => { @@ -301,6 +301,17 @@ export default class MainController implements vscode.Disposable { page1.registerContent(async (view) => { await this.getTabContent(view, customButton1, customButton2, 800); }); + wizard.registerOperation({ + displayName: 'test task', + description: 'task description', + isCancelable: true + }, op => { + op.updateStatus(sqlops.TaskStatus.InProgress); + op.updateStatus(sqlops.TaskStatus.InProgress, 'Task is running'); + setTimeout(() => { + op.updateStatus(sqlops.TaskStatus.Succeeded); + }, 5000); + }); wizard.pages = [page1, page2]; wizard.open(); } @@ -378,13 +389,14 @@ export default class MainController implements vscode.Disposable { let monitorLightPath = vscode.Uri.file(path.join(__dirname, '..', 'media', 'monitor.svg')); let monitorIcon = { light: monitorLightPath, - dark: path.join(__dirname, '..', 'media', 'monitor_inverse.svg') }; + dark: path.join(__dirname, '..', 'media', 'monitor_inverse.svg') + }; let monitorButton = view.modelBuilder.button() - .withProperties({ - label: 'Monitor', - iconPath: monitorIcon - }).component(); + .withProperties({ + label: 'Monitor', + iconPath: monitorIcon + }).component(); let toolbarModel = view.modelBuilder.toolbarContainer() .withToolbarItems([{ component: inputBox, diff --git a/src/sql/parts/connection/common/connectionManagement.ts b/src/sql/parts/connection/common/connectionManagement.ts index 53e774a2f8..c4bd45aa02 100644 --- a/src/sql/parts/connection/common/connectionManagement.ts +++ b/src/sql/parts/connection/common/connectionManagement.ts @@ -320,12 +320,13 @@ export enum MetadataType { } export enum TaskStatus { - notStarted = 0, - inProgress = 1, - succeeded = 2, - succeededWithWarning = 3, - failed = 4, - canceled = 5 + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 } export interface IConnectionParams { diff --git a/src/sql/parts/disasterRecovery/restore/common/restoreServiceImpl.ts b/src/sql/parts/disasterRecovery/restore/common/restoreServiceImpl.ts index 98c92ccc60..67a83f0365 100644 --- a/src/sql/parts/disasterRecovery/restore/common/restoreServiceImpl.ts +++ b/src/sql/parts/disasterRecovery/restore/common/restoreServiceImpl.ts @@ -179,8 +179,8 @@ export class RestoreDialogController implements IRestoreDialogController { private isSuccessfulRestore(response: TaskNode): boolean { return (response.taskName === this._restoreTaskName && response.message === this._restoreCompleted && - (response.status === TaskStatus.succeeded || - response.status === TaskStatus.succeededWithWarning) && + (response.status === TaskStatus.Succeeded || + response.status === TaskStatus.SucceededWithWarning) && (response.taskExecutionMode === TaskExecutionMode.execute || response.taskExecutionMode === TaskExecutionMode.executeAndScript)); } diff --git a/src/sql/parts/taskHistory/common/taskNode.ts b/src/sql/parts/taskHistory/common/taskNode.ts index b14c7d851f..b8a0fe1fea 100644 --- a/src/sql/parts/taskHistory/common/taskNode.ts +++ b/src/sql/parts/taskHistory/common/taskNode.ts @@ -7,13 +7,13 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { generateUuid } from 'vs/base/common/uuid'; export enum TaskStatus { - notStarted = 0, - inProgress = 1, - succeeded = 2, - succeededWithWarning = 3, - failed = 4, - canceled = 5, - canceling = 6 + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 } export enum TaskExecutionMode { @@ -107,7 +107,7 @@ export class TaskNode { this.databaseName = databaseName; this.timer = StopWatch.create(); this.startTime = new Date().toLocaleTimeString(); - this.status = TaskStatus.inProgress; + this.status = TaskStatus.InProgress; this.hasChildren = false; this.taskExecutionMode = taskExecutionMode; this.isCancelable = isCancelable; diff --git a/src/sql/parts/taskHistory/common/taskService.ts b/src/sql/parts/taskHistory/common/taskService.ts index 01f9460cd6..5fe9b9f5b4 100644 --- a/src/sql/parts/taskHistory/common/taskService.ts +++ b/src/sql/parts/taskHistory/common/taskService.ts @@ -14,6 +14,7 @@ import { localize } from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; export const SERVICE_ID = 'taskHistoryService'; export const ITaskService = createDecorator(SERVICE_ID); @@ -27,6 +28,8 @@ export interface ITaskService { getAllTasks(): TaskNode; getNumberOfInProgressTasks(): number; onNewTaskCreated(handle: number, taskInfo: sqlops.TaskInfo); + createNewTask(taskInfo: sqlops.TaskInfo); + updateTask(taskProgressInfo: sqlops.TaskProgressInfo); onTaskStatusChanged(handle: number, taskProgressInfo: sqlops.TaskProgressInfo); cancelTask(providerId: string, taskId: string): Thenable; /** @@ -52,7 +55,8 @@ export class TaskService implements ITaskService { constructor( @ILifecycleService lifecycleService: ILifecycleService, @IDialogService private dialogService: IDialogService, - @IQueryEditorService private queryEditorService: IQueryEditorService + @IQueryEditorService private queryEditorService: IQueryEditorService, + @IConnectionManagementService private connectionManagementService: IConnectionManagementService ) { this._taskQueue = new TaskNode('Root', undefined, undefined); this._onTaskComplete = new Emitter(); @@ -70,12 +74,27 @@ export class TaskService implements ITaskService { } public onNewTaskCreated(handle: number, taskInfo: sqlops.TaskInfo) { - let node: TaskNode = new TaskNode(taskInfo.name, taskInfo.serverName, taskInfo.databaseName, taskInfo.taskId, taskInfo.taskExecutionMode, taskInfo.isCancelable); + this.createNewTask(taskInfo); + } + + public createNewTask(taskInfo: sqlops.TaskInfo) { + let databaseName: string = taskInfo.databaseName; + let serverName: string = taskInfo.serverName; + if (taskInfo && taskInfo.connection) { + let connectionProfile = this.connectionManagementService.getConnectionProfile(taskInfo.connection.connectionId); + if (connectionProfile && !!databaseName) { + databaseName = connectionProfile.databaseName; + } + if (connectionProfile && !!serverName) { + serverName = connectionProfile.serverName; + } + } + let node: TaskNode = new TaskNode(taskInfo.name, serverName, databaseName, taskInfo.taskId, taskInfo.taskExecutionMode, taskInfo.isCancelable); node.providerName = taskInfo.providerName; this.handleNewTask(node); } - public onTaskStatusChanged(handle: number, taskProgressInfo: sqlops.TaskProgressInfo) { + public updateTask(taskProgressInfo: sqlops.TaskProgressInfo) { this.handleTaskComplete({ taskId: taskProgressInfo.taskId, status: taskProgressInfo.status, @@ -84,15 +103,23 @@ export class TaskService implements ITaskService { }); } + public onTaskStatusChanged(handle: number, taskProgressInfo: sqlops.TaskProgressInfo) { + this.updateTask(taskProgressInfo); + } + public cancelTask(providerId: string, taskId: string): Thenable { let task = this.getTaskInQueue(taskId); - task.status = TaskStatus.canceling; + task.status = TaskStatus.Canceling; this._onTaskComplete.fire(task); - let provider = this._providers[providerId]; - if (provider) { - return provider.cancelTask({ - taskId: taskId - }); + if (providerId) { + let provider = this._providers[providerId]; + if (provider && provider.cancelTask) { + return provider.cancelTask({ + taskId: taskId + }); + } + } else { + return Promise.resolve(true); } return Promise.resolve(undefined); } @@ -100,7 +127,7 @@ export class TaskService implements ITaskService { private cancelAllTasks(): Thenable { return new TPromise((resolve, reject) => { let promises = this._taskQueue.children.map(task => { - if (task.status === TaskStatus.inProgress || task.status === TaskStatus.notStarted) { + if (task.status === TaskStatus.InProgress || task.status === TaskStatus.NotStarted) { return this.cancelTask(task.providerName, task.id); } return Promise.resolve(true); @@ -173,10 +200,10 @@ export class TaskService implements ITaskService { task.message = eventArgs.message; } switch (task.status) { - case TaskStatus.canceled: - case TaskStatus.succeeded: - case TaskStatus.succeededWithWarning: - case TaskStatus.failed: + case TaskStatus.Canceled: + case TaskStatus.Succeeded: + case TaskStatus.SucceededWithWarning: + case TaskStatus.Failed: task.endTime = new Date().toLocaleTimeString(); task.timer.stop(); this._onTaskComplete.fire(task); @@ -185,7 +212,7 @@ export class TaskService implements ITaskService { break; } - if ((task.status === TaskStatus.succeeded || task.status === TaskStatus.succeededWithWarning) + if ((task.status === TaskStatus.Succeeded || task.status === TaskStatus.SucceededWithWarning) && eventArgs.script && eventArgs.script !== '') { if (task.taskExecutionMode === TaskExecutionMode.script) { this.queryEditorService.newSqlEditor(eventArgs.script); @@ -214,7 +241,7 @@ export class TaskService implements ITaskService { public getNumberOfInProgressTasks(): number { if (this._taskQueue.hasChildren) { - var inProgressTasks = this._taskQueue.children.filter(x => x.status === TaskStatus.inProgress); + var inProgressTasks = this._taskQueue.children.filter(x => x.status === TaskStatus.InProgress); return inProgressTasks ? inProgressTasks.length : 0; } return 0; diff --git a/src/sql/parts/taskHistory/viewlet/taskHistoryActionProvider.ts b/src/sql/parts/taskHistory/viewlet/taskHistoryActionProvider.ts index cf25409142..e74ff066e6 100644 --- a/src/sql/parts/taskHistory/viewlet/taskHistoryActionProvider.ts +++ b/src/sql/parts/taskHistory/viewlet/taskHistoryActionProvider.ts @@ -52,12 +52,12 @@ export class TaskHistoryActionProvider extends ContributableActionProvider { var actions = []; // get actions for tasks in progress - if (element.status === TaskStatus.inProgress && element.isCancelable) { + if (element.status === TaskStatus.InProgress && element.isCancelable) { actions.push(this._instantiationService.createInstance(CancelAction, CancelAction.ID, CancelAction.LABEL)); } // get actions for tasks succeeded - if (element.status === TaskStatus.succeeded || element.status === TaskStatus.succeededWithWarning) { + if (element.status === TaskStatus.Succeeded || element.status === TaskStatus.SucceededWithWarning) { if (element.taskExecutionMode === TaskExecutionMode.executeAndScript) { actions.push(this._instantiationService.createInstance(ScriptAction, ScriptAction.ID, ScriptAction.LABEL)); } diff --git a/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts b/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts index 01a7f08bef..ecd20d52e9 100644 --- a/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts +++ b/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts @@ -67,27 +67,27 @@ export class TaskHistoryRenderer implements IRenderer { if (taskNode) { templateData.icon.className = TaskHistoryRenderer.ICON_CLASS; switch (taskNode.status) { - case TaskStatus.succeeded: + case TaskStatus.Succeeded: templateData.icon.classList.add(TaskHistoryRenderer.SUCCESS_CLASS); taskStatus = localize('succeeded', "succeeded"); break; - case TaskStatus.failed: + case TaskStatus.Failed: templateData.icon.classList.add(TaskHistoryRenderer.FAIL_CLASS); taskStatus = localize('failed', "failed"); break; - case TaskStatus.inProgress: + case TaskStatus.InProgress: templateData.icon.classList.add(TaskHistoryRenderer.INPROGRESS_CLASS); taskStatus = localize('inProgress', "in progress"); break; - case TaskStatus.notStarted: + case TaskStatus.NotStarted: templateData.icon.classList.add(TaskHistoryRenderer.NOTSTARTED_CLASS); taskStatus = localize('notStarted', "not started"); break; - case TaskStatus.canceled: + case TaskStatus.Canceled: templateData.icon.classList.add(TaskHistoryRenderer.CANCELED_CLASS); taskStatus = localize('canceled', "canceled"); break; - case TaskStatus.canceling: + case TaskStatus.Canceling: templateData.icon.classList.add(TaskHistoryRenderer.INPROGRESS_CLASS); taskStatus = localize('canceling', "canceling"); break; @@ -117,7 +117,7 @@ export class TaskHistoryRenderer implements IRenderer { public timer(taskNode: TaskNode, templateData: ITaskHistoryTemplateData) { let timeLabel = ''; - if (taskNode.status === TaskStatus.failed) { + if (taskNode.status === TaskStatus.Failed) { timeLabel += taskNode.startTime + ' Error: ' + taskNode.message; } else { if (taskNode.startTime) { diff --git a/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts b/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts index 1c2fc7f5d2..176007b0af 100644 --- a/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts +++ b/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts @@ -141,9 +141,9 @@ export class TaskHistoryView { let isMouseOrigin = event.payload && (event.payload.origin === 'mouse'); let isDoubleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 2; if (isDoubleClick) { - if (task.status === TaskStatus.failed) { + if (task.status === TaskStatus.Failed) { var err = task.taskName + ': ' + task.message; - this._errorMessageService.showDialog(Severity.Error, nls.localize('taskError','Task error'), err); + this._errorMessageService.showDialog(Severity.Error, nls.localize('taskError', 'Task error'), err); } } } diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 83b019c959..b9def8914d 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1531,12 +1531,13 @@ declare module 'sqlops' { // Task service interfaces ---------------------------------------------------------------------------- export enum TaskStatus { - notStarted = 0, - inProgress = 1, - succeeded = 2, - succeededWithWarning = 3, - failed = 4, - canceled = 5 + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 } export enum TaskExecutionMode { @@ -1550,6 +1551,7 @@ declare module 'sqlops' { } export interface TaskInfo { + connection?: connection.Connection; taskId: string; status: TaskStatus; taskExecutionMode: TaskExecutionMode; @@ -1573,8 +1575,7 @@ declare module 'sqlops' { taskId: string; status: TaskStatus; message: string; - script: string; - duration: number; + script?: string; } export interface TaskServicesProvider extends DataProvider { diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 727992b838..651725887c 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -712,6 +712,12 @@ declare module 'sqlops' { * done. Return true to allow the dialog to close or false to block it from closing */ registerCloseValidator(validator: () => boolean | Thenable): void; + + /** + * Register an operation to run in the background when the dialog is done + * @param operationInfo Operation Information + */ + registerOperation(operationInfo: BackgroundOperationInfo): void; } export interface DialogTab extends ModelViewPanel { @@ -895,6 +901,12 @@ declare module 'sqlops' { * undefined or the text is empty or undefined. The default level is error. */ message: DialogMessage + + /** + * Register an operation to run in the background when the wizard is done + * @param operationInfo Operation Information + */ + registerOperation(operationInfo: BackgroundOperationInfo): void; } } } @@ -1001,4 +1013,69 @@ declare module 'sqlops' { nodeInfo: NodeInfo; } + /** + * Background Operation + */ + export interface BackgroundOperation { + /** + * Updates the operation status or adds progress message + * @param status Operation Status + * @param message Progress message + */ + updateStatus(status: TaskStatus, message?: string): void; + + /** + * Operation Id + */ + id: string; + + /** + * Event raised when operation is canceled in UI + */ + onCanceled: vscode.Event; + } + + /** + * Operation Information + */ + export interface BackgroundOperationInfo { + + /** + * The operation id. A unique id will be assigned to it If not specified a + */ + operationId?: string; + /** + * Connection information + */ + connection: connection.Connection; + + /** + * Operation Display Name + */ + displayName: string; + + /** + * Operation Description + */ + description: string; + + /** + * True if the operation is cancelable + */ + isCancelable: boolean; + + /** + * The actual operation to execute + */ + operation: (operation: BackgroundOperation) => void + } + + namespace tasks { + /** + * Starts an operation to run in the background + * @param operationInfo Operation Information + */ + export function startBackgroundOperation(operationInfo: BackgroundOperationInfo): void; + + } } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 0ccec8257b..dafc7ba2f6 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -39,12 +39,13 @@ export enum EditRowState { } export enum TaskStatus { - notStarted = 0, - inProgress = 1, - succeeded = 2, - succeededWithWarning = 3, - failed = 4, - canceled = 5 + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 } export enum TaskExecutionMode { @@ -99,7 +100,7 @@ export enum AlertType { } export enum FrequencyTypes { - Unknown , + Unknown, OneTime = 1 << 1, Daily = 1 << 2, Weekly = 1 << 3, @@ -275,7 +276,7 @@ export enum DeclarativeDataType { } export enum CardType { - VerticalButton = 'VerticalButton', + VerticalButton = 'VerticalButton', Details = 'Details' } export class SqlThemeIcon { diff --git a/src/sql/workbench/api/node/extHostBackgroundTaskManagement.ts b/src/sql/workbench/api/node/extHostBackgroundTaskManagement.ts new file mode 100644 index 0000000000..66256a748e --- /dev/null +++ b/src/sql/workbench/api/node/extHostBackgroundTaskManagement.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostBackgroundTaskManagementShape, SqlMainContext, MainThreadBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import { Emitter } from 'vs/base/common/event'; +import { generateUuid } from 'vs/base/common/uuid'; + +export enum TaskStatus { + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 +} + +export class ExtBackgroundOperation implements sqlops.BackgroundOperation { + private readonly _proxy: MainThreadBackgroundTaskManagementShape; + private _onCanceled = new Emitter(); + + constructor( + private _id: string, + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(SqlMainContext.MainThreadBackgroundTaskManagement); + } + + public updateStatus(status: TaskStatus, message?: string): void { + this._proxy.$updateTask({ + message: message, + status: status, + taskId: this.id + }); + } + + public get onCanceled(): vscode.Event { + return this._onCanceled.event; + } + + public cancel(): void { + this._onCanceled.fire(); + } + + public get id(): string { + return this._id; + } +} + +export class ExtHostBackgroundTaskManagement implements ExtHostBackgroundTaskManagementShape { + private readonly _proxy: MainThreadBackgroundTaskManagementShape; + private readonly _handlers = new Map(); + private readonly _operations = new Map(); + private readonly _mainContext: IMainContext; + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(SqlMainContext.MainThreadBackgroundTaskManagement); + this._mainContext = mainContext; + } + + $onTaskRegistered(operationId: string): void { + let extOperationInfo = new ExtBackgroundOperation(operationId, this._mainContext); + this._operations.set(operationId, extOperationInfo); + let operationInfo = this._handlers.get(operationId); + if (operationInfo) { + operationInfo.operation(extOperationInfo); + } + } + + $onTaskCanceled(operationId: string): void { + let operation = this._operations.get(operationId); + if (operation) { + operation.cancel(); + } + } + + $registerTask(operationInfo: sqlops.BackgroundOperationInfo): void { + let operationId = operationInfo.operationId || `OperationId${generateUuid()}`; + if (this._handlers.has(operationId)) { + throw new Error(`operation '${operationId}' already exists`); + } + + this._handlers.set(operationId, operationInfo); + let taskInfo: sqlops.TaskInfo = { + databaseName: undefined, + serverName: undefined, + description: operationInfo.description, + isCancelable: operationInfo.isCancelable, + name: operationInfo.displayName, + providerName: undefined, //setting provider name will cause the task to be processed by the provider. But this task is created in the extension and needs to be handled + //by the extension + taskExecutionMode: 0, + taskId: operationId, + status: TaskStatus.NotStarted, + connection: operationInfo.connection + }; + this._proxy.$registerTask(taskInfo); + } + + $removeTask(operationId: string) { + if (this._handlers.has(operationId)) { + this._handlers.delete(operationId); + } + } +} \ No newline at end of file diff --git a/src/sql/workbench/api/node/extHostModelViewDialog.ts b/src/sql/workbench/api/node/extHostModelViewDialog.ts index 6ce7b86315..428e0a7883 100644 --- a/src/sql/workbench/api/node/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/node/extHostModelViewDialog.ts @@ -8,11 +8,12 @@ import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import { Event, Emitter } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; +import { generateUuid } from 'vs/base/common/uuid'; import * as vscode from 'vscode'; import * as sqlops from 'sqlops'; -import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape, ExtHostBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; const DONE_LABEL = nls.localize('dialogDoneLabel', 'Done'); @@ -95,12 +96,22 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi public customButtons: sqlops.window.modelviewdialog.Button[]; private _message: sqlops.window.modelviewdialog.DialogMessage; private _closeValidator: () => boolean | Thenable; + private _operationHandler: BackgroundOperationHandler; constructor(extHostModelViewDialog: ExtHostModelViewDialog, - extHostModelView: ExtHostModelViewShape) { + extHostModelView: ExtHostModelViewShape, + extHostTaskManagement: ExtHostBackgroundTaskManagementShape) { super('modelViewDialog', extHostModelViewDialog, extHostModelView); this.okButton = this._extHostModelViewDialog.createButton(DONE_LABEL); this.cancelButton = this._extHostModelViewDialog.createButton(CANCEL_LABEL); + this._operationHandler = new BackgroundOperationHandler('dialog', extHostTaskManagement); + this.okButton.onClick(() => { + this._operationHandler.createOperation(); + }); + } + + public registerOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + this._operationHandler.registerOperation(operationInfo); } public setModelViewId(value: string) { @@ -192,13 +203,40 @@ class ButtonImpl implements sqlops.window.modelviewdialog.Button { } } +class BackgroundOperationHandler { + + private _operationInfo: sqlops.BackgroundOperationInfo; + + constructor( + private _name: string, + private _extHostTaskManagement: ExtHostBackgroundTaskManagementShape) { + } + + public createOperation(): void { + if (!this._operationInfo.operationId) { + let uniqueId = generateUuid(); + this._operationInfo.operationId = 'OperationId' + uniqueId + this._name; + } + + if (this._operationInfo && this._operationInfo.operation) { + this._extHostTaskManagement.$registerTask(this._operationInfo); + } + } + + public registerOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + this._operationInfo = operationInfo; + } +} + class WizardPageImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdialog.WizardPage { public customButtons: sqlops.window.modelviewdialog.Button[]; private _enabled: boolean = true; private _description: string; - constructor(public title: string, _extHostModelViewDialog: ExtHostModelViewDialog, _extHostModelView: ExtHostModelViewShape) { - super('modelViewWizardPage', _extHostModelViewDialog, _extHostModelView); + constructor(public title: string, + extHostModelViewDialog: ExtHostModelViewDialog, + extHostModelView: ExtHostModelViewShape) { + super('modelViewWizardPage', extHostModelViewDialog, extHostModelView); } public get enabled(): boolean { @@ -253,8 +291,9 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard { private _navigationValidator: (info: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable; private _message: sqlops.window.modelviewdialog.DialogMessage; private _displayPageTitles: boolean = true; + private _operationHandler: BackgroundOperationHandler; - constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog) { + constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog, extHostTaskManagement: ExtHostBackgroundTaskManagementShape) { this.doneButton = this._extHostModelViewDialog.createButton(DONE_LABEL); this.cancelButton = this._extHostModelViewDialog.createButton(CANCEL_LABEL); this.generateScriptButton = this._extHostModelViewDialog.createButton(GENERATE_SCRIPT_LABEL); @@ -263,6 +302,14 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard { this._extHostModelViewDialog.registerWizardPageInfoChangedCallback(this, info => this.handlePageInfoChanged(info)); this._currentPage = 0; this.onPageChanged(info => this._currentPage = info.newPage); + this._operationHandler = new BackgroundOperationHandler('wizard' + this.title, extHostTaskManagement); + this.doneButton.onClick(() => { + this._operationHandler.createOperation(); + }); + } + + public registerOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + this._operationHandler.registerOperation(operationInfo); } public get currentPage(): number { @@ -344,7 +391,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { constructor( mainContext: IMainContext, - private _extHostModelView: ExtHostModelViewShape + private _extHostModelView: ExtHostModelViewShape, + private _extHostTaskManagement: ExtHostBackgroundTaskManagementShape ) { this._proxy = mainContext.getProxy(SqlMainContext.MainThreadModelViewDialog); } @@ -473,7 +521,7 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { - let dialog = new DialogImpl(this, this._extHostModelView); + let dialog = new DialogImpl(this, this._extHostModelView, this._extHostTaskManagement); dialog.title = title; dialog.handle = this.getHandle(dialog); return dialog; @@ -516,7 +564,7 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } public createWizard(title: string): sqlops.window.modelviewdialog.Wizard { - let wizard = new WizardImpl(title, this); + let wizard = new WizardImpl(title, this, this._extHostTaskManagement); this.getHandle(wizard); return wizard; } diff --git a/src/sql/workbench/api/node/mainThreadBackgroundTaskManagement.ts b/src/sql/workbench/api/node/mainThreadBackgroundTaskManagement.ts new file mode 100644 index 0000000000..8838940ed0 --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadBackgroundTaskManagement.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { ITaskService } from 'sql/parts/taskHistory/common/taskService'; +import { MainThreadBackgroundTaskManagementShape, SqlMainContext, ExtHostBackgroundTaskManagementShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; + +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { Disposable } from 'vs/base/common/lifecycle'; + + +import * as sqlops from 'sqlops'; + +export enum TaskStatus { + NotStarted = 0, + InProgress = 1, + Succeeded = 2, + SucceededWithWarning = 3, + Failed = 4, + Canceled = 5, + Canceling = 6 +} + +@extHostNamedCustomer(SqlMainContext.MainThreadBackgroundTaskManagement) +export class MainThreadBackgroundTaskManagement extends Disposable implements MainThreadBackgroundTaskManagementShape { + private readonly _proxy: ExtHostBackgroundTaskManagementShape; + + constructor( + context: IExtHostContext, + @ITaskService private _taskService: ITaskService + ) { + super(); + this._proxy = context.getProxy(SqlExtHostContext.ExtHostBackgroundTaskManagement); + this._register(this._taskService.onTaskComplete(task => { + if (task.status === TaskStatus.Canceling) { + this._proxy.$onTaskCanceled(task.id); + } + })); + } + + $registerTask(taskInfo: sqlops.TaskInfo): void { + this._taskService.createNewTask(taskInfo); + this._proxy.$onTaskRegistered(taskInfo.taskId); + } + + $updateTask(taskProgressInfo: sqlops.TaskProgressInfo): void { + this._taskService.updateTask(taskProgressInfo); + } +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index d0aadd165f..1ba29ed9db 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -34,6 +34,7 @@ import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplo import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; +import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -63,10 +64,11 @@ export function createApiFactory( const extHostResourceProvider = rpcProtocol.set(SqlExtHostContext.ExtHostResourceProvider, new ExtHostResourceProvider(rpcProtocol)); const extHostModalDialogs = rpcProtocol.set(SqlExtHostContext.ExtHostModalDialogs, new ExtHostModalDialogs(rpcProtocol)); const extHostTasks = rpcProtocol.set(SqlExtHostContext.ExtHostTasks, new ExtHostTasks(rpcProtocol, logService)); + const extHostBackgroundTaskManagement = rpcProtocol.set(SqlExtHostContext.ExtHostBackgroundTaskManagement, new ExtHostBackgroundTaskManagement(rpcProtocol)); const extHostWebviewWidgets = rpcProtocol.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(rpcProtocol)); const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol)); const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol)); - const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView)); + const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); @@ -334,6 +336,9 @@ export function createApiFactory( const tasks: typeof sqlops.tasks = { registerTask(id: string, task: (...args: any[]) => any, thisArgs?: any): vscode.Disposable { return extHostTasks.registerTask(id, task, thisArgs); + }, + startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + extHostBackgroundTaskManagement.$registerTask(operationInfo); } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index 8b3b656887..c5bacce74a 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -14,6 +14,7 @@ import 'sql/workbench/api/node/mainThreadConnectionManagement'; import 'sql/workbench/api/node/mainThreadCredentialManagement'; import 'sql/workbench/api/node/mainThreadDataProtocol'; import 'sql/workbench/api/node/mainThreadObjectExplorer'; +import 'sql/workbench/api/node/mainThreadBackgroundTaskManagement'; import 'sql/workbench/api/node/mainThreadSerializationProvider'; import 'sql/workbench/api/node/mainThreadResourceProvider'; import 'sql/workbench/api/electron-browser/mainThreadTasks'; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index e357da8e37..cca1cf9fab 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -496,6 +496,7 @@ export const SqlMainContext = { MainThreadCredentialManagement: createMainId('MainThreadCredentialManagement'), MainThreadDataProtocol: createMainId('MainThreadDataProtocol'), MainThreadObjectExplorer: createMainId('MainThreadObjectExplorer'), + MainThreadBackgroundTaskManagement: createMainId('MainThreadBackgroundTaskManagement'), MainThreadSerializationProvider: createMainId('MainThreadSerializationProvider'), MainThreadResourceProvider: createMainId('MainThreadResourceProvider'), MainThreadModalDialog: createMainId('MainThreadModalDialog'), @@ -517,6 +518,7 @@ export const SqlExtHostContext = { ExtHostResourceProvider: createExtId('ExtHostResourceProvider'), ExtHostModalDialogs: createExtId('ExtHostModalDialogs'), ExtHostTasks: createExtId('ExtHostTasks'), + ExtHostBackgroundTaskManagement: createExtId('ExtHostBackgroundTaskManagement'), ExtHostDashboardWebviews: createExtId('ExtHostDashboardWebviews'), ExtHostModelView: createExtId('ExtHostModelView'), ExtHostDashboard: createExtId('ExtHostDashboard'), @@ -578,6 +580,18 @@ export interface ExtHostModelViewShape { $runCustomValidations(handle: number, id: string): Thenable; } +export interface ExtHostBackgroundTaskManagementShape { + $onTaskRegistered(operationId: string): void; + $onTaskCanceled(operationId: string): void; + $registerTask(operationInfo: sqlops.BackgroundOperationInfo): void; + $removeTask(operationId: string): void; +} + +export interface MainThreadBackgroundTaskManagementShape extends IDisposable { + $registerTask(taskInfo: sqlops.TaskInfo): void; + $updateTask(taskProgressInfo: sqlops.TaskProgressInfo): void; +} + export interface MainThreadModelViewShape extends IDisposable { $registerProvider(id: string): void; $initializeModel(handle: number, rootComponent: IComponentShape): Thenable; diff --git a/src/sqltest/workbench/api/extHostBackgroundTaskManagement.test.ts b/src/sqltest/workbench/api/extHostBackgroundTaskManagement.test.ts new file mode 100644 index 0000000000..88a918a60c --- /dev/null +++ b/src/sqltest/workbench/api/extHostBackgroundTaskManagement.test.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sqlops from 'sqlops'; +import * as assert from 'assert'; +import { Mock, It, Times } from 'typemoq'; +import { ExtHostBackgroundTaskManagement, TaskStatus } from 'sql/workbench/api/node/extHostBackgroundTaskManagement'; +import { MainThreadBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; + +'use strict'; + +suite('ExtHostBackgroundTaskManagement Tests', () => { + let extHostBackgroundTaskManagement: ExtHostBackgroundTaskManagement; + let mockProxy: Mock; + let nothing: void; + let operationId = 'operation is'; + setup(() => { + mockProxy = Mock.ofInstance({ + + $registerTask: (taskInfo: sqlops.TaskInfo) => nothing, + $updateTask: (taskProgressInfo: sqlops.TaskProgressInfo) => nothing + }); + let mainContext = { + getProxy: proxyType => mockProxy.object + }; + + mockProxy.setup(x => x.$registerTask(It.isAny())).callback(() => { + extHostBackgroundTaskManagement.$onTaskRegistered(operationId); + }); + extHostBackgroundTaskManagement = new ExtHostBackgroundTaskManagement(mainContext); + }); + + test('RegisterTask should successfully create background task and update status', () => { + let operationInfo: sqlops.BackgroundOperationInfo = { + connection: undefined, + description: 'description', + displayName: 'displayName', + isCancelable: true, + operation: (op: sqlops.BackgroundOperation) => { op.updateStatus(TaskStatus.Succeeded); }, + operationId: operationId + }; + extHostBackgroundTaskManagement.$registerTask(operationInfo); + mockProxy.verify(x => x.$registerTask(It.is( + t => t.name === operationInfo.displayName && + t.description === operationInfo.description && + t.taskId === operationId && + t.isCancelable === operationInfo.isCancelable && + t.providerName === undefined + )), Times.once()); + mockProxy.verify(x => x.$updateTask(It.is(t => t.status === TaskStatus.Succeeded)), Times.once()); + extHostBackgroundTaskManagement.$removeTask(operationId); + }); + + test('Canceling the task should notify the extension', () => { + let operationInfo: sqlops.BackgroundOperationInfo = { + connection: undefined, + description: 'description', + displayName: 'displayName', + isCancelable: true, + operation: (op: sqlops.BackgroundOperation) => { + op.onCanceled(() => { + op.updateStatus(TaskStatus.Canceled); + }) + }, + operationId: operationId + }; + extHostBackgroundTaskManagement.$registerTask(operationInfo); + extHostBackgroundTaskManagement.$onTaskCanceled(operationId); + + mockProxy.verify(x => x.$updateTask(It.is(t => t.status === TaskStatus.Canceled)), Times.once()); + extHostBackgroundTaskManagement.$removeTask(operationId); + }); + + test('RegisterTask should assign unique id to the operation is not assigned', () => { + let operationInfo: sqlops.BackgroundOperationInfo = { + connection: undefined, + description: 'description', + displayName: 'displayName', + isCancelable: true, + operation: (op: sqlops.BackgroundOperation) => { op.updateStatus(TaskStatus.Succeeded); }, + operationId: undefined + }; + extHostBackgroundTaskManagement.$registerTask(operationInfo); + mockProxy.verify(x => x.$registerTask(It.is(t => t.taskId !== undefined)), Times.once()); + + extHostBackgroundTaskManagement.$removeTask(operationId); + }); + + test('RegisterTask should fail given id of an existing operation', () => { + let operationInfo: sqlops.BackgroundOperationInfo = { + connection: undefined, + description: 'description', + displayName: 'displayName', + isCancelable: true, + operation: (op: sqlops.BackgroundOperation) => { op.updateStatus(TaskStatus.Succeeded); }, + operationId: operationId + }; + extHostBackgroundTaskManagement.$registerTask(operationInfo); + mockProxy.verify(x => x.$registerTask(It.is(t => t.taskId === operationId)), Times.once()); + assert.throws(() => extHostBackgroundTaskManagement.$registerTask(operationInfo)); + + extHostBackgroundTaskManagement.$removeTask(operationId); + }); +}); \ No newline at end of file diff --git a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts index a37b04b156..410d741bcc 100644 --- a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts @@ -37,7 +37,7 @@ suite('ExtHostModelViewDialog Tests', () => { extHostModelView = Mock.ofInstance({ $registerProvider: (widget, handler) => undefined }); - extHostModelViewDialog = new ExtHostModelViewDialog(mainContext, extHostModelView.object); + extHostModelViewDialog = new ExtHostModelViewDialog(mainContext, extHostModelView.object, undefined); }); test('Creating a dialog returns a dialog with initialized ok and cancel buttons and the given title', () => { diff --git a/src/sqltest/workbench/api/mainThreadBackgroundTaskManagement.test.ts b/src/sqltest/workbench/api/mainThreadBackgroundTaskManagement.test.ts new file mode 100644 index 0000000000..8a693005f4 --- /dev/null +++ b/src/sqltest/workbench/api/mainThreadBackgroundTaskManagement.test.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sqlops from 'sqlops'; +import * as assert from 'assert'; +import { Mock, It, Times } from 'typemoq'; +import { MainThreadBackgroundTaskManagement, TaskStatus } from 'sql/workbench/api/node/mainThreadBackgroundTaskManagement'; +import { ExtHostBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { ITaskService } from 'sql/parts/taskHistory/common/taskService'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { TaskNode } from 'sql/parts/taskHistory/common/taskNode'; +import { Event, Emitter } from 'vs/base/common/event'; + +'use strict'; + +suite('MainThreadBackgroundTaskManagement Tests', () => { + let mainThreadBackgroundTaskManagement: MainThreadBackgroundTaskManagement; + let mockProxy: Mock; + let taskService: Mock; + let nothing: void; + let operationId = 'operation is'; + let onTaskComplete = new Emitter(); + setup(() => { + mockProxy = Mock.ofInstance({ + $onTaskRegistered: (operationId: string) => nothing, + $onTaskCanceled: (operationId: string) => nothing, + $registerTask: (operationInfo: sqlops.BackgroundOperationInfo) => nothing, + $removeTask: (operationId: string) => nothing, + }); + taskService = Mock.ofInstance({ + _serviceBrand: undefined, + onTaskComplete: undefined, + onAddNewTask: undefined, + handleNewTask: undefined, + handleTaskComplete: undefined, + getAllTasks: undefined, + getNumberOfInProgressTasks: undefined, + onNewTaskCreated: undefined, + createNewTask: (taskInfo: sqlops.TaskInfo) => nothing, + updateTask: (taskProgressInfo: sqlops.TaskProgressInfo) => nothing, + onTaskStatusChanged: undefined, + cancelTask: undefined, + registerProvider: undefined + }); + let mainContext = { + getProxy: proxyType => mockProxy.object + }; + + taskService.setup(x => x.onTaskComplete).returns(() => onTaskComplete.event); + + mainThreadBackgroundTaskManagement = new MainThreadBackgroundTaskManagement(mainContext, taskService.object); + }); + + test('RegisterTask should successfully create background task', () => { + let taskInfo: sqlops.TaskInfo = { + taskId: operationId, + databaseName: undefined, + description: undefined, + isCancelable: true, + name: 'task name', + providerName: undefined, + serverName: undefined, + status: TaskStatus.NotStarted, + taskExecutionMode: 0 + }; + mainThreadBackgroundTaskManagement.$registerTask(taskInfo); + taskService.verify(x => x.createNewTask(It.is(t => t.status === TaskStatus.NotStarted)), Times.once()); + mockProxy.verify(x => x.$onTaskRegistered(operationId), Times.once()); + }); + + test('UpdateTask should successfully update the background task status', () => { + let taskInfo: sqlops.TaskProgressInfo = { + taskId: operationId, + status: TaskStatus.InProgress, + message: undefined, + }; + mainThreadBackgroundTaskManagement.$updateTask(taskInfo); + taskService.verify(x => x.updateTask(It.is(t => t.status === TaskStatus.InProgress)), Times.once()); + }); + + test('Canceling the task should notify the proxy', () => { + let taskInfo: sqlops.TaskProgressInfo = { + taskId: operationId, + status: TaskStatus.InProgress, + message: undefined, + }; + let taskNode = new TaskNode('', '', '', operationId, undefined); + taskNode.status = TaskStatus.Canceling; + + onTaskComplete.fire(taskNode); + mainThreadBackgroundTaskManagement.$updateTask(taskInfo); + mockProxy.verify(x => x.$onTaskCanceled(It.is(t => t === operationId)), Times.once()); + }); + +}); \ No newline at end of file