diff --git a/extensions/agent/package.json b/extensions/agent/package.json index 0bf70c4887..9725b565c3 100644 --- a/extensions/agent/package.json +++ b/extensions/agent/package.json @@ -1,62 +1,93 @@ { - "name": "agent", - "displayName": "SQL Server Agent", - "description": "Manage and troubleshoot SQL Server Agent jobs", - "version": "0.42.0", - "publisher": "Microsoft", - "preview": true, - "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt", - "icon": "images/sqlserver.png", - "aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412", - "engines": { - "vscode": "^1.25.0" - }, - "activationEvents": [ - "*" - ], - "main": "./out/main", - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/azuredatastudio.git" - }, - "extensionDependencies": [ - "Microsoft.mssql" - ], - "contributes": { - "outputChannels": [ - "sqlagent" - ], - "dashboard.tabs": [ - { - "id": "data-management-agent", - "description": "Manage and troubleshoot SQL Agent jobs", - "provider": "MSSQL", - "title": "SQL Agent", - "when": "connectionProvider == 'MSSQL' && !mssql:iscloud", - "container": { - "controlhost-container": { - "type": "agent" - } - } - } - ] - }, - "dependencies": { - "vscode-nls": "^3.2.1" - }, - "devDependencies": { - "mocha-junit-reporter": "^1.17.0", - "mocha-multi-reporters": "^1.1.7", - "@types/mocha": "^5.2.5", - "@types/node": "^8.10.25", - "mocha": "^5.2.0", - "should": "^13.2.1", - "typemoq": "^2.1.0", - "vscode": "1.1.5" - }, - "__metadata": { - "id": "10", - "publisherDisplayName": "Microsoft", - "publisherId": "Microsoft" - } + "name": "agent", + "displayName": "SQL Server Agent", + "description": "Manage and troubleshoot SQL Server Agent jobs", + "version": "0.41.0", + "publisher": "Microsoft", + "preview": true, + "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt", + "icon": "images/sqlserver.png", + "aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412", + "engines": { + "vscode": "^1.25.0" + }, + "activationEvents": [ + "*" + ], + "main": "./out/main", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "extensionDependencies": [ + "Microsoft.mssql" + ], + "contributes": { + "outputChannels": [ + "sqlagent" + ], + "dashboard.tabs": [ + { + "id": "data-management-agent", + "description": "Manage and troubleshoot SQL Agent jobs", + "provider": "MSSQL", + "title": "SQL Agent", + "when": "connectionProvider == 'MSSQL' && !mssql:iscloud", + "container": { + "controlhost-container": { + "type": "agent" + } + } + } + ], + "commands": [ + { + "command": "agent.openNotebookDialog", + "title": "Schedule Notebook", + "icon": { + "dark": "resources/dark/open_notebook_inverse.svg", + "light": "resources/light/open_notebook.svg" + } + }, + { + "command": "agent.reuploadTemplate", + "title": "Reupload Template", + "icon": { + "dark": "resources/dark/open_notebook_inverse.svg", + "light": "resources/light/open_notebook.svg" + } + } + ], + "menus": { + "notebook/toolbar": [ + { + "command": "agent.openNotebookDialog", + "when": "providerId == sql" + }, + { + "command": "agent.reuploadTemplate", + "when": "agent:trackedTemplate" + } + ], + "explorer/context": [ + { + "command": "agent.openNotebookDialog", + "when": "resourceExtname == .ipynb" + } + ] + } + }, + "dependencies": { + "vscode-nls": "^3.2.1" + }, + "devDependencies": { + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^8.10.25", + "mocha": "^5.2.0", + "should": "^13.2.1", + "typemoq": "^2.1.0", + "vscode": "1.1.5" + } } diff --git a/extensions/agent/resources/dark/notebook_inverse.svg b/extensions/agent/resources/dark/notebook_inverse.svg new file mode 100644 index 0000000000..fb495dda69 --- /dev/null +++ b/extensions/agent/resources/dark/notebook_inverse.svg @@ -0,0 +1 @@ +notebook_inverse \ No newline at end of file diff --git a/extensions/agent/resources/dark/open_notebook_inverse.svg b/extensions/agent/resources/dark/open_notebook_inverse.svg new file mode 100644 index 0000000000..a95750c49f --- /dev/null +++ b/extensions/agent/resources/dark/open_notebook_inverse.svg @@ -0,0 +1 @@ +open_notebook_inverse \ No newline at end of file diff --git a/extensions/agent/resources/light/notebook.svg b/extensions/agent/resources/light/notebook.svg new file mode 100644 index 0000000000..dae58b840e --- /dev/null +++ b/extensions/agent/resources/light/notebook.svg @@ -0,0 +1 @@ +notebook \ No newline at end of file diff --git a/extensions/agent/resources/light/open_notebook.svg b/extensions/agent/resources/light/open_notebook.svg new file mode 100644 index 0000000000..0041ae9b21 --- /dev/null +++ b/extensions/agent/resources/light/open_notebook.svg @@ -0,0 +1 @@ +open_notebook \ No newline at end of file diff --git a/extensions/agent/src/agentUtils.ts b/extensions/agent/src/agentUtils.ts index 2fa8e27556..4801e3ba66 100644 --- a/extensions/agent/src/agentUtils.ts +++ b/extensions/agent/src/agentUtils.ts @@ -6,6 +6,9 @@ 'use strict'; import * as azdata from 'azdata'; +import * as fs from 'fs'; +import { promisify } from 'util'; + export class AgentUtils { @@ -13,6 +16,12 @@ export class AgentUtils { private static _connectionService: azdata.ConnectionProvider; private static _queryProvider: azdata.QueryProvider; + public static async setupProvidersFromConnection(connection?: azdata.connection.Connection) { + this._agentService = azdata.dataprotocol.getProvider(connection.providerName, azdata.DataProviderType.AgentServicesProvider); + this._connectionService = azdata.dataprotocol.getProvider(connection.providerName, azdata.DataProviderType.ConnectionProvider); + this._queryProvider = azdata.dataprotocol.getProvider(connection.providerName, azdata.DataProviderType.QueryProvider); + } + public static async getAgentService(): Promise { if (!AgentUtils._agentService) { let currentConnection = await azdata.connection.getCurrentConnection(); @@ -41,4 +50,20 @@ export class AgentUtils { return this._queryProvider; } -} \ No newline at end of file +} + +export function exists(path: string): Promise { + return promisify(fs.exists)(path); +} + +export function mkdir(path: string): Promise { + return promisify(fs.mkdir)(path); +} + +export function unlink(path: string): Promise { + return promisify(fs.unlink)(path); +} + +export function writeFile(path: string, data: string): Promise { + return promisify(fs.writeFile)(path, data); +} diff --git a/extensions/agent/src/data/notebookData.ts b/extensions/agent/src/data/notebookData.ts new file mode 100644 index 0000000000..2b75561552 --- /dev/null +++ b/extensions/agent/src/data/notebookData.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { AgentUtils } from '../agentUtils'; +import { IAgentDialogData, AgentDialogMode } from '../interfaces'; +import { NotebookDialogOptions } from '../dialogs/notebookDialog'; +import { createConnection } from 'net'; + +const localize = nls.loadMessageBundle(); +const NotebookCompletionActionCondition_Always: string = localize('notebookData.whenJobCompletes', 'When the notebook completes'); +const NotebookCompletionActionCondition_OnFailure: string = localize('notebookData.whenJobFails', 'When the notebook fails'); +const NotebookCompletionActionCondition_OnSuccess: string = localize('notebookData.whenJobSucceeds', 'When the notebook succeeds'); + +// Error Messages +const CreateNotebookErrorMessage_NameIsEmpty = localize('notebookData.jobNameRequired', 'Notebook name must be provided'); +const TemplatePathEmptyErrorMessage = localize('notebookData.templatePathRequired', 'Template path must be provided'); +const InvalidNotebookPathErrorMessage = localize('notebookData.invalidNotebookPath', 'Invalid notebook path'); +const SelectStorageDatabaseErrorMessage = localize('notebookData.selectStorageDatabase', 'Select storage database'); +const SelectExecutionDatabaseErrorMessage = localize('notebookData.selectExecutionDatabase', 'Select execution database'); +const JobWithSameNameExistsErrorMessage = localize('notebookData.jobExists', 'Job with similar name already exists'); + +export class NotebookData implements IAgentDialogData { + + private _ownerUri: string; + private _jobCategories: string[]; + private _operators: string[]; + private _defaultOwner: string; + private _jobCompletionActionConditions: azdata.CategoryValue[]; + private _jobCategoryIdsMap: azdata.AgentJobCategory[]; + + public dialogMode: AgentDialogMode = AgentDialogMode.CREATE; + public name: string; + public originalName: string; + public enabled: boolean = true; + public description: string; + public category: string; + public categoryId: number; + public owner: string; + public emailLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure; + public pageLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure; + public eventLogLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure; + public deleteLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnSuccess; + public operatorToEmail: string; + public operatorToPage: string; + public jobSteps: azdata.AgentJobStepInfo[]; + public jobSchedules: azdata.AgentJobScheduleInfo[]; + public alerts: azdata.AgentAlertInfo[]; + public jobId: string; + public startStepId: number; + public categoryType: number; + public targetDatabase: string; + public executeDatabase: string; + public templateId: number; + public templatePath: string; + public static jobLists: azdata.AgentJobInfo[]; + public connection: azdata.connection.Connection; + + constructor( + ownerUri: string, + options: NotebookDialogOptions = undefined, + private _agentService: azdata.AgentServicesProvider = undefined) { + this._ownerUri = ownerUri; + this.enabled = true; + if (options.notebookInfo) { + let notebookInfo = options.notebookInfo; + this.dialogMode = AgentDialogMode.EDIT; + this.name = notebookInfo.name; + this.originalName = notebookInfo.name; + this.owner = notebookInfo.owner; + this.category = notebookInfo.category; + this.description = notebookInfo.description; + this.enabled = notebookInfo.enabled; + this.jobSteps = notebookInfo.jobSteps; + this.jobSchedules = notebookInfo.jobSchedules; + this.alerts = notebookInfo.alerts; + this.jobId = notebookInfo.jobId; + this.startStepId = notebookInfo.startStepId; + this.categoryId = notebookInfo.categoryId; + this.categoryType = notebookInfo.categoryType; + this.targetDatabase = notebookInfo.targetDatabase; + this.executeDatabase = notebookInfo.executeDatabase; + } + if (options.filePath) { + this.name = path.basename(options.filePath).split('.').slice(0, -1).join('.'); + this.templatePath = options.filePath; + } + if (options.connection) { + this.connection = options.connection; + } + } + + public get jobCategories(): string[] { + return this._jobCategories; + } + + public get jobCategoryIdsMap(): azdata.AgentJobCategory[] { + return this._jobCategoryIdsMap; + } + + public get operators(): string[] { + return this._operators; + } + + public get ownerUri(): string { + return this._ownerUri; + } + + public get defaultOwner(): string { + return this._defaultOwner; + } + + public get JobCompletionActionConditions(): azdata.CategoryValue[] { + return this._jobCompletionActionConditions; + } + + public async initialize() { + if (this.connection) { + await AgentUtils.setupProvidersFromConnection(this.connection); + } + this._agentService = await AgentUtils.getAgentService(); + let jobDefaults = await this._agentService.getJobDefaults(this.ownerUri); + if (jobDefaults && jobDefaults.success) { + this._jobCategories = jobDefaults.categories.map((cat) => { + return cat.name; + }); + this._jobCategoryIdsMap = jobDefaults.categories; + this._defaultOwner = jobDefaults.owner; + + this._operators = ['', this._defaultOwner]; + this.owner = this.owner ? this.owner : this._defaultOwner; + } + + this._jobCompletionActionConditions = [{ + displayName: NotebookCompletionActionCondition_OnSuccess, + name: azdata.JobCompletionActionCondition.OnSuccess.toString() + }, { + displayName: NotebookCompletionActionCondition_OnFailure, + name: azdata.JobCompletionActionCondition.OnFailure.toString() + }, { + displayName: NotebookCompletionActionCondition_Always, + name: azdata.JobCompletionActionCondition.Always.toString() + }]; + + this._agentService.getJobs(this.ownerUri).then((value) => { + NotebookData.jobLists = value.jobs; + }); + } + + public async save() { + let notebookInfo: azdata.AgentNotebookInfo = this.toAgentJobInfo(); + let result = this.dialogMode === AgentDialogMode.CREATE + ? await this._agentService.createNotebook(this.ownerUri, notebookInfo, this.templatePath) + : await this._agentService.updateNotebook(this.ownerUri, this.originalName, notebookInfo, this.templatePath); + if (!result || !result.success) { + if (this.dialogMode === AgentDialogMode.EDIT) { + vscode.window.showErrorMessage( + localize('notebookData.saveErrorMessage', "Notebook update failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown')); + } else { + vscode.window.showErrorMessage( + localize('notebookData.newJobErrorMessage', "Notebook creation failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown')); + } + } else { + if (this.dialogMode === AgentDialogMode.EDIT) { + vscode.window.showInformationMessage( + localize('notebookData.saveSucessMessage', "Notebook '{0}' updated successfully", notebookInfo.name)); + } else { + vscode.window.showInformationMessage( + localize('notebookData.newJobSuccessMessage', "Notebook '{0}' created successfully", notebookInfo.name)); + } + + } + } + + public validate(): { valid: boolean, errorMessages: string[] } { + let validationErrors: string[] = []; + if (this.dialogMode !== AgentDialogMode.EDIT) { + if (!(this.name && this.name.trim())) { + validationErrors.push(CreateNotebookErrorMessage_NameIsEmpty); + } + if (!(this.templatePath && this.name.trim())) { + validationErrors.push(TemplatePathEmptyErrorMessage); + } + if (!fs.existsSync(this.templatePath)) { + validationErrors.push(InvalidNotebookPathErrorMessage); + } + if (NotebookData.jobLists) { + for (let i = 0; i < NotebookData.jobLists.length; i++) { + if (this.name === NotebookData.jobLists[i].name) { + validationErrors.push(JobWithSameNameExistsErrorMessage); + break; + } + } + } + } + else { + if (this.templatePath && this.templatePath !== '' && !fs.existsSync(this.templatePath)) { + validationErrors.push(InvalidNotebookPathErrorMessage); + } + } + if (this.targetDatabase === 'Select Database') { + validationErrors.push(SelectStorageDatabaseErrorMessage); + } + if (this.executeDatabase === 'Select Database') { + validationErrors.push(SelectExecutionDatabaseErrorMessage); + } + + return { + valid: validationErrors.length === 0, + errorMessages: validationErrors + }; + } + + public toAgentJobInfo(): azdata.AgentNotebookInfo { + return { + name: this.name, + owner: this.owner ? this.owner : this.defaultOwner, + description: this.description, + emailLevel: this.emailLevel, + pageLevel: this.pageLevel, + eventLogLevel: this.eventLogLevel, + deleteLevel: this.deleteLevel, + operatorToEmail: this.operatorToEmail, + operatorToPage: this.operatorToPage, + enabled: this.enabled, + category: this.category, + alerts: this.alerts, + jobSchedules: this.jobSchedules, + jobSteps: this.jobSteps, + targetDatabase: this.targetDatabase, + executeDatabase: this.executeDatabase, + // The properties below are not collected from UI + // We could consider using a seperate class for create job request + // + templateId: this.templateId, + currentExecutionStatus: 0, + lastRunOutcome: 0, + currentExecutionStep: '', + hasTarget: true, + hasSchedule: false, + hasStep: false, + runnable: true, + categoryId: this.categoryId, + categoryType: this.categoryType, + lastRun: '', + nextRun: '', + jobId: this.jobId, + startStepId: this.startStepId, + lastRunNotebookError: '', + + }; + } +} diff --git a/extensions/agent/src/dialogs/jobStepDialog.ts b/extensions/agent/src/dialogs/jobStepDialog.ts index f5b1e0ed08..fbe27860bf 100644 --- a/extensions/agent/src/dialogs/jobStepDialog.ts +++ b/extensions/agent/src/dialogs/jobStepDialog.ts @@ -169,7 +169,7 @@ export class JobStepDialog extends AgentDialog { isFile: false }).component(); this.openButton.onDidClick(e => { - let queryContent = e; + let queryContent = e.fileContent; this.commandTextBox.value = queryContent; }); this.parseButton.onDidClick(e => { diff --git a/extensions/agent/src/dialogs/notebookDialog.ts b/extensions/agent/src/dialogs/notebookDialog.ts new file mode 100644 index 0000000000..839b9b9a60 --- /dev/null +++ b/extensions/agent/src/dialogs/notebookDialog.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import * as path from 'path'; +import * as azdata from 'azdata'; +import { PickScheduleDialog } from './pickScheduleDialog'; +import { AgentDialog } from './agentDialog'; +import { AgentUtils } from '../agentUtils'; +import { NotebookData } from '../data/notebookData'; + +const localize = nls.loadMessageBundle(); +// TODO: localize +// Top level +const CreateDialogTitle: string = localize('notebookDialog.newJob', "New Notebook Job"); +const EditDialogTitle: string = localize('notebookDialog.editJob', "Edit Notebook Job"); +const GeneralTabText: string = localize('notebookDialog.general', "General"); +const BlankJobNameErrorText: string = localize('notebookDialog.blankJobNameError', "The name of the job cannot be blank."); + +// Notebook details strings +const NotebookDetailsSeparatorTitle: string = localize('notebookDialog.notebookSection', "Notebook Details"); +const TemplateNotebookTextBoxLabel: string = localize('notebookDialog.templateNotebook', "Notebook Path"); +const TargetDatabaseDropdownLabel: string = localize('notebookDialog.targetDatabase', "Storage Database"); +const ExecuteDatabaseDropdownLabel: string = localize('notebookDialog.executeDatabase', "Execution Database"); +const DefaultDropdownString: string = localize('notebookDialog.defaultDropdownString', "Select Database"); + +// Job details string +const JobDetailsSeparatorTitle: string = localize('notebookDialog.jobSection', "Job Details"); +const NameTextBoxLabel: string = localize('notebookDialog.name', "Name"); +const OwnerTextBoxLabel: string = localize('notebookDialog.owner', "Owner"); +const SchedulesTopLabelString: string = localize('notebookDialog.schedulesaLabel', "Schedules list"); +const PickScheduleButtonString: string = localize('notebookDialog.pickSchedule', "Pick Schedule"); +const RemoveScheduleButtonString: string = localize('notebookDialog.removeSchedule', "Remove Schedule"); +const ScheduleNameLabelString: string = localize('notebookDialog.scheduleNameLabel', "Schedule Name"); +const DescriptionTextBoxLabel: string = localize('notebookDialog.description', "Description"); + +// Event Name strings +const NewJobDialogEvent: string = 'NewNotebookJobDialogOpened'; +const EditJobDialogEvent: string = 'EditNotebookJobDialogOpened'; + +export class NotebookDialogOptions { + notebookInfo?: azdata.AgentNotebookInfo; + filePath?: string; + connection?: azdata.connection.Connection; +} + +export class NotebookDialog extends AgentDialog { + + // UI Components + private generalTab: azdata.window.DialogTab; + + // Notebook Details controls + private templateFilePathBox: azdata.InputBoxComponent; + private openTemplateFileButton: azdata.ButtonComponent; + private targetDatabaseDropDown: azdata.DropDownComponent; + private executeDatabaseDropDown: azdata.DropDownComponent; + + // Job Details controls + + private nameTextBox: azdata.InputBoxComponent; + private ownerTextBox: azdata.InputBoxComponent; + private schedulesTable: azdata.TableComponent; + private pickScheduleButton: azdata.ButtonComponent; + private removeScheduleButton: azdata.ButtonComponent; + private descriptionTextBox: azdata.InputBoxComponent; + + + + private isEdit: boolean = false; + + // Job objects + private steps: azdata.AgentJobStepInfo[]; + private schedules: azdata.AgentJobScheduleInfo[]; + + constructor(ownerUri: string, options: NotebookDialogOptions = undefined) { + super( + ownerUri, + new NotebookData(ownerUri, options), + options.notebookInfo ? EditDialogTitle : CreateDialogTitle); + this.steps = this.model.jobSteps ? this.model.jobSteps : []; + this.schedules = this.model.jobSchedules ? this.model.jobSchedules : []; + this.isEdit = options.notebookInfo ? true : false; + this.dialogName = this.isEdit ? EditJobDialogEvent : NewJobDialogEvent; + } + + protected async initializeDialog() { + this.generalTab = azdata.window.createTab(GeneralTabText); + this.initializeGeneralTab(); + this.dialog.content = [this.generalTab]; + this.dialog.registerCloseValidator(() => { + this.updateModel(); + let validationResult = this.model.validate(); + if (!validationResult.valid) { + // TODO: Show Error Messages + this.dialog.message = { text: validationResult.errorMessages[0] }; + console.error(validationResult.errorMessages.join(',')); + } + + return validationResult.valid; + }); + } + + private initializeGeneralTab() { + this.generalTab.registerContent(async view => { + this.templateFilePathBox = view.modelBuilder.inputBox() + .withProperties({ + width: 400, + inputType: 'text' + }).component(); + this.openTemplateFileButton = view.modelBuilder.button() + .withProperties({ + label: '...', + title: '...', + width: '20px', + isFile: true, + fileType: '.ipynb' + }).component(); + this.openTemplateFileButton.onDidClick(e => { + if (e) { + this.templateFilePathBox.value = e.filePath; + if (!this.isEdit) { + let fileName = path.basename(e.filePath).split('.').slice(0, -1).join('.'); + this.nameTextBox.value = fileName; + } + } + }); + let outputButtonContainer = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + textAlign: 'right', + width: 20 + }).withItems([this.openTemplateFileButton], { flex: '1 1 80%' }).component(); + let notebookPathFlexBox = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + width: '100%', + }).withItems([this.templateFilePathBox, outputButtonContainer], { + flex: '1 1 50%' + }).component(); + this.targetDatabaseDropDown = view.modelBuilder.dropDown().component(); + this.executeDatabaseDropDown = view.modelBuilder.dropDown().component(); + let databases = await AgentUtils.getDatabases(this.ownerUri); + databases.unshift(DefaultDropdownString); + this.targetDatabaseDropDown = view.modelBuilder.dropDown() + .withProperties({ + value: databases[0], + values: databases + }).component(); + this.descriptionTextBox = view.modelBuilder.inputBox().withProperties({ + multiline: true, + height: 50 + }).component(); + this.executeDatabaseDropDown = view.modelBuilder.dropDown() + .withProperties({ + value: databases[0], + values: databases + }).component(); + this.targetDatabaseDropDown.required = true; + this.executeDatabaseDropDown.required = true; + this.descriptionTextBox = view.modelBuilder.inputBox().withProperties({ + multiline: true, + height: 50 + }).component(); + this.nameTextBox = view.modelBuilder.inputBox().component(); + this.nameTextBox.required = true; + this.nameTextBox.onTextChanged(() => { + if (this.nameTextBox.value && this.nameTextBox.value.length > 0) { + this.dialog.message = null; + // Change the job name immediately since steps + // depends on the job name + this.model.name = this.nameTextBox.value; + } + }); + + this.ownerTextBox = view.modelBuilder.inputBox().component(); + this.schedulesTable = view.modelBuilder.table() + .withProperties({ + columns: [ + PickScheduleDialog.SchedulesIDText, + PickScheduleDialog.ScheduleNameLabelText, + PickScheduleDialog.ScheduleDescription + ], + data: [], + height: 50, + width: 420 + }).component(); + + this.pickScheduleButton = view.modelBuilder.button().withProperties({ + label: PickScheduleButtonString, + width: 100 + }).component(); + this.removeScheduleButton = view.modelBuilder.button().withProperties({ + label: RemoveScheduleButtonString, + width: 100 + }).component(); + this.pickScheduleButton.onDidClick(() => { + let pickScheduleDialog = new PickScheduleDialog(this.model.ownerUri, this.model.name); + pickScheduleDialog.onSuccess((dialogModel) => { + let selectedSchedule = dialogModel.selectedSchedule; + if (selectedSchedule) { + let existingSchedule = this.schedules.find(item => item.name === selectedSchedule.name); + if (!existingSchedule) { + selectedSchedule.jobName = this.model.name ? this.model.name : this.nameTextBox.value; + this.schedules.push(selectedSchedule); + } + this.populateScheduleTable(); + } + }); + pickScheduleDialog.showDialog(); + }); + this.removeScheduleButton.onDidClick(() => { + if (this.schedulesTable.selectedRows.length === 1) { + let selectedRow = this.schedulesTable.selectedRows[0]; + let selectedScheduleName = this.schedulesTable.data[selectedRow][1]; + for (let i = 0; i < this.schedules.length; i++) { + if (this.schedules[i].name === selectedScheduleName) { + this.schedules.splice(i, 1); + } + } + this.populateScheduleTable(); + } + }); + + let formModel = view.modelBuilder.formContainer() + .withFormItems([ + { + components: [{ + component: notebookPathFlexBox, + title: TemplateNotebookTextBoxLabel, + layout: { + info: localize('notebookDialog.templatePath', 'Select a notebook to schedule from PC') + } + }, + { + component: this.targetDatabaseDropDown, + title: TargetDatabaseDropdownLabel, + layout: { + info: localize('notebookDialog.targetDatabaseInfo', 'Select a database to store all notebook job metadata and results') + } + }, { + component: this.executeDatabaseDropDown, + title: ExecuteDatabaseDropdownLabel, + layout: { + info: localize('notebookDialog.executionDatabaseInfo', 'Select a database against which notebook queries will run') + } + }], + title: NotebookDetailsSeparatorTitle + }, { + components: [{ + component: this.nameTextBox, + title: NameTextBoxLabel + }, { + component: this.ownerTextBox, + title: OwnerTextBoxLabel + }, { + component: this.schedulesTable, + title: SchedulesTopLabelString, + actions: [this.pickScheduleButton, this.removeScheduleButton] + }, { + component: this.descriptionTextBox, + title: DescriptionTextBoxLabel + }], + title: JobDetailsSeparatorTitle + }]).withLayout({ width: '100%' }).component(); + + await view.initializeModel(formModel); + + + + this.nameTextBox.value = this.model.name; + this.ownerTextBox.value = this.model.owner; + this.templateFilePathBox.value = this.model.templatePath; + if (this.isEdit) { + this.templateFilePathBox.placeHolder = this.model.targetDatabase + '\\' + this.model.name; + this.targetDatabaseDropDown.value = this.model.targetDatabase; + this.executeDatabaseDropDown.value = this.model.executeDatabase; + this.targetDatabaseDropDown.enabled = false; + this.schedules = this.model.jobSchedules; + } + else { + this.templateFilePathBox.required = true; + } + let idx: number = undefined; + if (this.model.category && this.model.category !== '') { + idx = this.model.jobCategories.indexOf(this.model.category); + } + this.descriptionTextBox.value = this.model.description; + this.openTemplateFileButton.onDidClick(e => { + }); + this.populateScheduleTable(); + }); + } + + private populateScheduleTable() { + let data = this.convertSchedulesToData(this.schedules); + this.schedulesTable.data = data; + this.schedulesTable.height = 100; + + } + + private createRowContainer(view: azdata.ModelView): azdata.FlexBuilder { + return view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + alignItems: 'left', + justifyContent: 'space-between' + }); + } + private convertSchedulesToData(jobSchedules: azdata.AgentJobScheduleInfo[]): any[][] { + let result = []; + jobSchedules.forEach(schedule => { + let cols = []; + cols.push(schedule.id); + cols.push(schedule.name); + cols.push(schedule.description); + result.push(cols); + }); + return result; + } + + protected updateModel() { + this.model.name = this.nameTextBox.value; + this.model.owner = this.ownerTextBox.value; + this.model.description = this.descriptionTextBox.value; + this.model.templatePath = this.templateFilePathBox.value; + this.model.targetDatabase = this.targetDatabaseDropDown.value as string; + this.model.executeDatabase = this.executeDatabaseDropDown.value as string; + if (!this.model.jobSchedules) { + this.model.jobSchedules = []; + } + this.model.alerts = []; + this.model.jobSteps = []; + this.model.jobSchedules = this.schedules; + this.model.category = '[Uncategorized (Local)]'; + this.model.categoryId = 0; + this.model.eventLogLevel = 0; + + } +} diff --git a/extensions/agent/src/dialogs/pickScheduleDialog.ts b/extensions/agent/src/dialogs/pickScheduleDialog.ts index 52cf6f1813..76986d3204 100644 --- a/extensions/agent/src/dialogs/pickScheduleDialog.ts +++ b/extensions/agent/src/dialogs/pickScheduleDialog.ts @@ -82,9 +82,22 @@ export class PickScheduleDialog { }]).withLayout({ width: '100%' }).component(); this.loadingComponent = view.modelBuilder.loadingComponent().withItem(formModel).component(); + this.loadingComponent.loading = true; + this.model.initialize().then((result) => { + this.loadingComponent.loading = false; + if (this.model.schedules) { + let data: any[][] = []; + for (let i = 0; i < this.model.schedules.length; ++i) { + let schedule = this.model.schedules[i]; + data[i] = [schedule.id, schedule.name, schedule.description]; + } + this.schedulesTable.data = data; + } + }); this.loadingComponent.loading = !this.model.isInitialized(); await view.initializeModel(this.loadingComponent); }); + } private async execute() { diff --git a/extensions/agent/src/mainController.ts b/extensions/agent/src/mainController.ts index 6ee033adf3..e23f2650db 100644 --- a/extensions/agent/src/mainController.ts +++ b/extensions/agent/src/mainController.ts @@ -7,6 +7,9 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { AlertDialog } from './dialogs/alertDialog'; import { JobDialog } from './dialogs/jobDialog'; import { OperatorDialog } from './dialogs/operatorDialog'; @@ -14,13 +17,22 @@ import { ProxyDialog } from './dialogs/proxyDialog'; import { JobStepDialog } from './dialogs/jobStepDialog'; import { PickScheduleDialog } from './dialogs/pickScheduleDialog'; import { JobData } from './data/jobData'; -import { AgentUtils } from './agentUtils'; +import { AgentUtils, exists, mkdir, unlink, writeFile } from './agentUtils'; +import { NotebookDialog, NotebookDialogOptions } from './dialogs/notebookDialog'; +import { promisify } from 'util'; + const localize = nls.loadMessageBundle(); /** * The main controller class that initializes the extension */ +export class TemplateMapObject { + notebookInfo: azdata.AgentNotebookInfo; + fileUri: vscode.Uri; + tempPath: string; + ownerUri: string; +} export class MainController { protected _context: vscode.ExtensionContext; @@ -29,7 +41,8 @@ export class MainController { private alertDialog: AlertDialog; private operatorDialog: OperatorDialog; private proxyDialog: ProxyDialog; - + private notebookDialog: NotebookDialog; + private notebookTemplateMap = new Map(); // PUBLIC METHODS ////////////////////////////////////////////////////// public constructor(context: vscode.ExtensionContext) { this._context = context; @@ -82,6 +95,26 @@ export class MainController { this.operatorDialog.dialogName ? await this.operatorDialog.openDialog(this.operatorDialog.dialogName) : await this.operatorDialog.openDialog(); } }); + + vscode.commands.registerCommand('agent.reuploadTemplate', async (ownerUri: string, operatorInfo: azdata.AgentOperatorInfo) => { + let nbEditor = azdata.nb.activeNotebookEditor; + // await nbEditor.document.save(); + let templateMap = this.notebookTemplateMap.get(nbEditor.document.uri.toString()); + let vsEditor = await vscode.workspace.openTextDocument(templateMap.fileUri); + let content = vsEditor.getText(); + promisify(fs.writeFile)(templateMap.tempPath, content); + AgentUtils.getAgentService().then(async (agentService) => { + let result = await agentService.updateNotebook(templateMap.ownerUri, templateMap.notebookInfo.name, templateMap.notebookInfo, templateMap.tempPath); + if (result.success) { + vscode.window.showInformationMessage(localize('agent.templateUploadSuccessful', 'Template updated successfully')); + } + else { + vscode.window.showInformationMessage(localize('agent.templateUploadError', 'Template update failure')); + } + }); + + }); + vscode.commands.registerCommand('agent.openProxyDialog', async (ownerUri: string, proxyInfo: azdata.AgentProxyInfo, credentials: azdata.CredentialInfo[]) => { if (!this.proxyDialog || (this.proxyDialog && !this.proxyDialog.isOpen)) { this.proxyDialog = new ProxyDialog(ownerUri, proxyInfo, credentials); @@ -91,6 +124,117 @@ export class MainController { } this.proxyDialog.dialogName ? await this.proxyDialog.openDialog(this.proxyDialog.dialogName) : await this.proxyDialog.openDialog(); }); + + vscode.commands.registerCommand('agent.openNotebookEditorFromJsonString', async (filename: string, jsonNotebook: string, notebookInfo?: azdata.AgentNotebookInfo, ownerUri?: string) => { + const tempfilePath = path.join(os.tmpdir(), 'mssql_notebooks', filename + '.ipynb'); + if (!await exists(path.join(os.tmpdir(), 'mssql_notebooks'))) { + await mkdir(path.join(os.tmpdir(), 'mssql_notebooks')); + } + let editors = azdata.nb.visibleNotebookEditors; + if (await exists(tempfilePath)) { + await unlink(tempfilePath); + } + try { + await writeFile(tempfilePath, jsonNotebook); + let uri = vscode.Uri.parse(`untitled:${path.basename(tempfilePath)}`); + if (notebookInfo) { + this.notebookTemplateMap.set(uri.toString(), { notebookInfo: notebookInfo, fileUri: uri, ownerUri: ownerUri, tempPath: tempfilePath }); + vscode.commands.executeCommand('setContext', 'agent:trackedTemplate', true); + } + await azdata.nb.showNotebookDocument(uri, { + initialContent: jsonNotebook, + initialDirtyState: false + }); + vscode.commands.executeCommand('setContext', 'agent:trackedTemplate', false); + } + catch (e) { + vscode.window.showErrorMessage(e); + } + }); + + vscode.commands.registerCommand('agent.openNotebookDialog', async (ownerUri: any, notebookInfo: azdata.AgentNotebookInfo) => { + + /* + There are four entry points to this commands: + 1. Explorer context menu: + The first arg becomes a vscode URI + the second argument is undefined + 2. Notebook toolbar: + both the args are undefined + 3. Agent New Notebook Action + the first arg is database OwnerUri + the second arg is undefined + 4. Agent Edit Notebook Action + the first arg is database OwnerUri + the second arg is notebookInfo from database + */ + if (!ownerUri || ownerUri instanceof vscode.Uri) { + let path: string; + if (!ownerUri) { + if (azdata.nb.activeNotebookEditor.document.isDirty) { + vscode.window.showErrorMessage(localize('agent.unsavedFileSchedulingError', 'Save file before scheduling'), { modal: true }); + return; + } + path = azdata.nb.activeNotebookEditor.document.fileName; + } else { + path = ownerUri.fsPath; + } + + let connection = await this.getConnectionFromUser(); + ownerUri = await azdata.connection.getUriForConnection(connection.connectionId); + this.notebookDialog = new NotebookDialog(ownerUri, { filePath: path, connection: connection }); + if (!this.notebookDialog.isOpen) { + this.notebookDialog.dialogName ? await this.notebookDialog.openDialog(this.notebookDialog.dialogName) : await this.notebookDialog.openDialog(); + } + } + else { + if (!this.notebookDialog || (this.notebookDialog && !this.notebookDialog.isOpen)) { + this.notebookDialog = new NotebookDialog(ownerUri, { notebookInfo: notebookInfo }); + } + if (!this.notebookDialog.isOpen) { + this.notebookDialog.dialogName ? await this.notebookDialog.openDialog(this.notebookDialog.dialogName) : await this.notebookDialog.openDialog(); + } + } + }); + } + + public async getConnectionFromUser(): Promise { + let connection: azdata.connection.Connection = null; + + let connections = await azdata.connection.getActiveConnections(); + if (!connections || connections.length === 0) { + connection = await azdata.connection.openConnectionDialog(); + } + else { + let sqlConnectionsPresent: boolean; + for (let i = 0; i < connections.length; i++) { + if (connections[i].providerName === 'MSSQL') { + sqlConnectionsPresent = true; + break; + } + } + let connectionNames: azdata.connection.Connection[] = []; + let connectionDisplayString: string[] = []; + for (let i = 0; i < connections.length; i++) { + let currentConnectionString = connections[i].options.server + ' (' + connections[i].options.user + ')'; + connectionNames.push(connections[i]); + connectionDisplayString.push(currentConnectionString); + } + connectionDisplayString.push(localize('agent.AddNewConnection', 'Add new connection')); + let connectionName = await vscode.window.showQuickPick(connectionDisplayString, { placeHolder: localize('agent.selectConnection', 'Select a connection') }); + if (connectionDisplayString.indexOf(connectionName) !== -1) { + if (connectionName === localize('agent.AddNewConnection', 'Add new connection')) { + connection = await azdata.connection.openConnectionDialog(); + } + else { + connection = connections[connectionDisplayString.indexOf(connectionName)]; + } + } + else { + vscode.window.showErrorMessage(localize('agent.selectValidConnection', 'Please select a valid connection'), { modal: true }); + } + } + return connection; } /** diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index a8d92047f4..ca5a19b56d 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "2.0.0-release.6", + "version": "2.0.0-release.10", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.2.zip", "Windows_64": "win-x64-netcoreapp2.2.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 7c03fbfa9c..3e67c98d0e 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -87,6 +87,68 @@ export interface DeleteAgentJobStepParams { step: azdata.AgentJobStepInfo; } +// Notebook management parameters +export interface AgentNotebookParams { + ownerUri: string; +} + +export interface AgentNotebookHistoryParams { + ownerUri: string; + jobId: string; + jobName: string; + targetDatabase: string; +} + +export interface AgentNotebookMaterializedParams { + ownerUri: string; + targetDatabase: string; + notebookMaterializedId: number; +} + +export interface AgentNotebookTemplateParams { + ownerUri: string; + targetDatabase: string; + jobId: string; +} + +export interface CreateAgentNotebookParams { + ownerUri: string; + notebook: azdata.AgentNotebookInfo; + templateFilePath: string; +} + +export interface UpdateAgentNotebookParams { + ownerUri: string; + originalNotebookName: string; + notebook: azdata.AgentJobInfo; + templateFilePath: string; +} + +export interface UpdateAgentNotebookRunPinParams { + ownerUri: string; + targetDatabase: string; + agentNotebookHistory: azdata.AgentNotebookHistoryInfo; + materializedNotebookPin: boolean; +} + +export interface UpdateAgentNotebookRunNameParams { + ownerUri: string; + targetDatabase: string; + agentNotebookHistory: azdata.AgentNotebookHistoryInfo; + materializedNotebookName: string; +} + +export interface DeleteAgentNotebookParams { + ownerUri: string; + notebook: azdata.AgentNotebookInfo; +} + +export interface DeleteAgentMaterializedNotebookParams { + ownerUri: string; + targetDatabase: string; + agentNotebookHistory: azdata.AgentNotebookHistoryInfo; +} + // Alert management parameters export interface AgentAlertsParams { ownerUri: string; @@ -218,6 +280,47 @@ export namespace DeleteAgentJobStepRequest { export const type = new RequestType('agent/deletejobstep'); } +// Notebooks request +export namespace AgentNotebooksRequest { + export const type = new RequestType('agent/notebooks'); +} + +export namespace AgentNotebookHistoryRequest { + export const type = new RequestType('agent/notebookhistory'); +} + +export namespace AgentNotebookMaterializedRequest { + export const type = new RequestType('agent/notebookmaterialized'); +} + +export namespace UpdateAgentNotebookRunNameRequest { + export const type = new RequestType('agent/updatenotebookname'); +} + +export namespace DeleteMaterializedNotebookRequest { + export const type = new RequestType('agent/deletenotebookmaterialized'); +} + +export namespace UpdateAgentNotebookRunPinRequest { + export const type = new RequestType('agent/updatenotebookpin'); +} + +export namespace AgentNotebookTemplateRequest { + export const type = new RequestType('agent/notebooktemplate'); +} + +export namespace CreateAgentNotebookRequest { + export const type = new RequestType('agent/createnotebook'); +} + +export namespace DeleteAgentNotebookRequest { + export const type = new RequestType('agent/deletenotebook'); +} + +export namespace UpdateAgentNotebookRequest { + export const type = new RequestType('agent/updatenotebook'); +} + // Alerts requests export namespace AgentAlertsRequest { export const type = new RequestType('agent/alerts'); diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index e7b0274730..3783ed2d9f 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -227,6 +227,151 @@ export class AgentServicesFeature extends SqlOpsFeature { ); }; + // Notebook Management methods + const getNotebooks = (ownerUri: string): Thenable => { + let params: contracts.AgentNotebookParams = { ownerUri: ownerUri }; + return client.sendRequest(contracts.AgentNotebooksRequest.type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.AgentNotebooksRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const getNotebookHistory = (ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable => { + let params: contracts.AgentNotebookHistoryParams = { ownerUri: ownerUri, jobId: jobID, jobName: jobName, targetDatabase: targetDatabase }; + + return client.sendRequest(contracts.AgentNotebookHistoryRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.AgentNotebookHistoryRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const getMaterializedNotebook = (ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable => { + let params: contracts.AgentNotebookMaterializedParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, notebookMaterializedId: notebookMaterializedId }; + return client.sendRequest(contracts.AgentNotebookMaterializedRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.AgentNotebookMaterializedRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const getTemplateNotebook = (ownerUri: string, targetDatabase: string, jobId: string): Thenable => { + let params: contracts.AgentNotebookTemplateParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, jobId: jobId }; + return client.sendRequest(contracts.AgentNotebookTemplateRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.AgentNotebookTemplateRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const createNotebook = (ownerUri: string, notebookInfo: azdata.AgentNotebookInfo, templateFilePath: string): Thenable => { + let params: contracts.CreateAgentNotebookParams = { + ownerUri: ownerUri, + notebook: notebookInfo, + templateFilePath: templateFilePath + }; + let requestType = contracts.CreateAgentNotebookRequest.type; + return client.sendRequest(requestType, params).then( + r => { + fireOnUpdated(); + return r; + }, + e => { + client.logFailedRequest(requestType, e); + return Promise.resolve(undefined); + } + ); + }; + + + const updateNotebook = (ownerUri: string, originalNotebookName: string, notebookInfo: azdata.AgentNotebookInfo, templateFilePath: string): Thenable => { + let params: contracts.UpdateAgentNotebookParams = { + ownerUri: ownerUri, + originalNotebookName: originalNotebookName, + notebook: notebookInfo, + templateFilePath: templateFilePath + }; + let requestType = contracts.UpdateAgentNotebookRequest.type; + return client.sendRequest(requestType, params).then( + r => { + fireOnUpdated(); + return r; + }, + e => { + client.logFailedRequest(requestType, e); + return Promise.resolve(undefined); + } + ); + }; + + const deleteNotebook = (ownerUri: string, notebookInfo: azdata.AgentNotebookInfo): Thenable => { + let params: contracts.DeleteAgentNotebookParams = { + ownerUri: ownerUri, + notebook: notebookInfo + }; + let requestType = contracts.DeleteAgentNotebookRequest.type; + return client.sendRequest(requestType, params).then( + r => { + fireOnUpdated(); + return r; + }, + e => { + client.logFailedRequest(requestType, e); + return Promise.resolve(undefined); + } + ); + }; + + const deleteMaterializedNotebook = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable => { + let params: contracts.DeleteAgentMaterializedNotebookParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory }; + return client.sendRequest(contracts.DeleteMaterializedNotebookRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.DeleteMaterializedNotebookRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const updateNotebookMaterializedName = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable => { + let params: contracts.UpdateAgentNotebookRunNameParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory, materializedNotebookName: name }; + return client.sendRequest(contracts.UpdateAgentNotebookRunNameRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.UpdateAgentNotebookRunNameRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + const updateNotebookMaterializedPin = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable => { + let params: contracts.UpdateAgentNotebookRunPinParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory, materializedNotebookPin: pin }; + return client.sendRequest(contracts.UpdateAgentNotebookRunPinRequest + .type, params).then( + r => r, + e => { + client.logFailedRequest(contracts.UpdateAgentNotebookRunPinRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + + // Alert management methods let getAlerts = (ownerUri: string): Thenable => { let params: contracts.AgentAlertsParams = { @@ -535,6 +680,16 @@ export class AgentServicesFeature extends SqlOpsFeature { createJobStep, updateJobStep, deleteJobStep, + getNotebooks, + getNotebookHistory, + getMaterializedNotebook, + getTemplateNotebook, + createNotebook, + updateNotebook, + deleteMaterializedNotebook, + updateNotebookMaterializedName, + updateNotebookMaterializedPin, + deleteNotebook, getAlerts, createAlert, updateAlert, diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index c7e7137297..cb9b3e3628 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -1387,6 +1387,20 @@ declare module 'azdata' { alerts: AgentAlertInfo[]; } + export interface AgentNotebookInfo extends AgentJobInfo { + templateId: number; + targetDatabase: string; + lastRunNotebookError: string; + executeDatabase: string; + } + + export interface AgentNotebookMaterializedInfo { + materializedId: number; + targetDatabase: string; + materializedName: string; + favorite: boolean; + } + export interface AgentJobScheduleInfo { id: number; name: string; @@ -1487,6 +1501,14 @@ declare module 'azdata' { steps: AgentJobStep[]; } + export interface AgentNotebookHistoryInfo extends AgentJobHistoryInfo { + materializedNotebookId: number; + materializedNotebookName: string; + materializedNotebookPin: boolean; + materializedNotebookErrorInfo: string; + materializedNotebookDeleted: boolean; + } + export interface AgentProxyInfo { id: number; accountName: string; @@ -1577,6 +1599,39 @@ declare module 'azdata' { categories: AgentJobCategory[]; } + export interface AgentNotebooksResult extends ResultStatus { + notebooks: AgentNotebookInfo[]; + } + + export interface AgentJobHistoryResult extends ResultStatus { + histories: AgentJobHistoryInfo[]; + schedules: AgentJobScheduleInfo[]; + alerts: AgentAlertInfo[]; + steps: AgentJobStepInfo[]; + } + + export interface AgentNotebookHistoryResult extends ResultStatus { + histories: AgentNotebookHistoryInfo[]; + schedules: AgentJobScheduleInfo[]; + steps: AgentJobStepInfo[]; + } + + export interface AgentNotebookMaterializedResult extends ResultStatus { + notebookMaterialized: string; + } + + export interface AgentNotebookTemplateResult extends ResultStatus { + notebookTemplate: string; + } + + export interface CreateAgentNotebookResult extends ResultStatus { + notebook: AgentNotebookInfo; + } + + export interface UpdateAgentNotebookResult extends ResultStatus { + notebook: AgentNotebookInfo; + } + export interface CreateAgentJobStepResult extends ResultStatus { step: AgentJobStepInfo; } @@ -1651,6 +1706,18 @@ declare module 'azdata' { deleteJob(ownerUri: string, jobInfo: AgentJobInfo): Thenable; getJobDefaults(ownerUri: string): Thenable; + // Notebook management methods + getNotebooks(ownerUri: string): Thenable; + getNotebookHistory(ownerUri: string, jobId: string, jobName: string, targetDatabase: string): Thenable; + getMaterializedNotebook(ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable; + getTemplateNotebook(ownerUri: string, targetDatabase: string, jobId: string): Thenable; + createNotebook(ownerUri: string, notebook: AgentNotebookInfo, templateFilePath: string): Thenable; + deleteNotebook(ownerUri: string, notebook: AgentNotebookInfo): Thenable; + updateNotebook(ownerUri: string, originialNotebookName: string, notebook: AgentNotebookInfo, templateFilePath: string): Thenable; + updateNotebookMaterializedName(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable; + updateNotebookMaterializedPin(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable; + deleteMaterializedNotebook(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string): Thenable; + // Job Step management methods createJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable; updateJobStep(ownerUri: string, originalJobStepName: string, stepInfo: AgentJobStepInfo): Thenable; @@ -3006,6 +3073,7 @@ declare module 'azdata' { editable?: boolean; fireOnTextChange?: boolean; ariaLabel?: string; + required?: boolean; } export interface DeclarativeTableColumn { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index ad06e92508..44b8419468 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -74,7 +74,6 @@ declare module 'azdata' { continueSerialization(requestParams: SerializeDataContinueRequestParams): Thenable; } - export namespace dataprotocol { export function registerSerializationProvider(provider: SerializationProvider): vscode.Disposable; } diff --git a/src/sql/platform/jobManagement/browser/jobActions.ts b/src/sql/platform/jobManagement/browser/jobActions.ts index 92ddf3ac21..08e1dd2b07 100644 --- a/src/sql/platform/jobManagement/browser/jobActions.ts +++ b/src/sql/platform/jobManagement/browser/jobActions.ts @@ -20,6 +20,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; import { JobManagementView } from 'sql/workbench/parts/jobManagement/browser/jobManagementView'; +import { NotebooksViewComponent } from 'sql/workbench/parts/jobManagement/browser/notebooksView.component'; +import { NotebookHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/notebookHistory.component'; export const successLabel: string = nls.localize('jobaction.successLabel', "Success"); export const errorLabel: string = nls.localize('jobaction.faillabel', "Error"); @@ -171,6 +173,22 @@ export class EditJobAction extends Action { } } +export class OpenMaterializedNotebookAction extends Action { + public static ID = 'notebookAction.openNotebook'; + public static LABEL = nls.localize('notebookAction.openNotebook', "Open"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(OpenMaterializedNotebookAction.ID, OpenMaterializedNotebookAction.LABEL, 'open'); + } + + public run(context: any): Promise { + context.component.openNotebook(context.history); + return Promise.resolve(true); + } +} + export class DeleteJobAction extends Action { public static ID = 'jobaction.deleteJob'; public static LABEL = nls.localize('jobaction.deleteJob', "Delete Job"); @@ -282,7 +300,6 @@ export class DeleteStepAction extends Action { } } - // Alert Actions export class NewAlertAction extends Action { @@ -551,3 +568,167 @@ export class DeleteProxyAction extends Action { return Promise.resolve(true); } } + +//Notebook Actions + +export class NewNotebookJobAction extends Action { + public static ID = 'notebookaction.newJob'; + public static LABEL = nls.localize('notebookaction.newJob', "New Notebook Job"); + + constructor( + ) { + super(NewNotebookJobAction.ID, NewNotebookJobAction.LABEL, 'newStepIcon'); + } + + public async run(context: IJobActionInfo): Promise { + let component = context.component as NotebooksViewComponent; + await component.openCreateNotebookDialog(); + return true; + } +} + +export class EditNotebookJobAction extends Action { + public static ID = 'notebookaction.editNotebook'; + public static LABEL = nls.localize('notebookaction.editJob', "Edit Notebook Job"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(EditNotebookJobAction.ID, EditNotebookJobAction.LABEL, 'edit'); + } + + public run(actionInfo: IJobActionInfo): Promise { + this._commandService.executeCommand( + 'agent.openNotebookDialog', + actionInfo.ownerUri, + actionInfo.targetObject.job); + return Promise.resolve(true); + } +} + +export class OpenTemplateNotebookAction extends Action { + public static ID = 'notebookaction.openTemplate'; + public static LABEL = nls.localize('notebookaction.openNotebook', "Open Template Notebook"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(OpenTemplateNotebookAction.ID, OpenTemplateNotebookAction.LABEL, 'opennotebook'); + } + + public run(actionInfo: any): Promise { + actionInfo.component.openTemplateNotebook(); + return Promise.resolve(true); + } +} + +export class DeleteNotebookAction extends Action { + public static ID = 'notebookaction.deleteNotebook'; + public static LABEL = nls.localize('notebookaction.deleteNotebook', "Delete Notebook"); + + constructor( + @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, + @IJobManagementService private _jobService: IJobManagementService, + @IInstantiationService private instantationService: IInstantiationService, + @ITelemetryService private _telemetryService: ITelemetryService + ) { + super(DeleteNotebookAction.ID, DeleteNotebookAction.LABEL); + } + + public run(actionInfo: IJobActionInfo): Promise { + let self = this; + let notebook = actionInfo.targetObject.job as azdata.AgentNotebookInfo; + let refreshAction = this.instantationService.createInstance(JobsRefreshAction); + self._notificationService.prompt( + Severity.Info, + nls.localize('jobaction.deleteNotebookConfirm', "Are you sure you'd like to delete the notebook '{0}'?", notebook.name), + [{ + label: DeleteNotebookAction.LABEL, + run: () => { + this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob); + self._jobService.deleteNotebook(actionInfo.ownerUri, actionInfo.targetObject.job).then(async (result) => { + if (!result || !result.success) { + await refreshAction.run(actionInfo); + let errorMessage = nls.localize("jobaction.failedToDeleteNotebook", "Could not delete notebook '{0}'.\nError: {1}", + notebook.name, result.errorMessage ? result.errorMessage : 'Unknown error'); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + } else { + let successMessage = nls.localize('jobaction.deletedNotebook', "The notebook was successfully deleted"); + self._notificationService.info(successMessage); + } + }); + } + }, { + label: DeleteAlertAction.CancelLabel, + run: () => { } + }] + ); + return Promise.resolve(true); + } + +} + +export class PinNotebookMaterializedAction extends Action { + public static ID = 'notebookaction.openTemplate'; + public static LABEL = nls.localize('notebookaction.pinNotebook', "Pin"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(PinNotebookMaterializedAction.ID, PinNotebookMaterializedAction.LABEL); + } + + public run(actionInfo: any): Promise { + actionInfo.component.toggleNotebookPin(actionInfo.history, true); + return Promise.resolve(true); + } +} + +export class DeleteMaterializedNotebookAction extends Action { + public static ID = 'notebookaction.deleteMaterializedNotebook'; + public static LABEL = nls.localize('notebookaction.deleteMaterializedNotebook', "Delete"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(DeleteMaterializedNotebookAction.ID, DeleteMaterializedNotebookAction.LABEL); + } + + public run(actionInfo: any): Promise { + actionInfo.component.deleteMaterializedNotebook(actionInfo.history); + return Promise.resolve(true); + } +} + +export class UnpinNotebookMaterializedAction extends Action { + public static ID = 'notebookaction.unpinNotebook'; + public static LABEL = nls.localize('notebookaction.unpinNotebook', "Unpin"); + + constructor( + @ICommandService private _commandService: ICommandService + ) { + super(UnpinNotebookMaterializedAction.ID, UnpinNotebookMaterializedAction.LABEL); + } + + public run(actionInfo: any): Promise { + actionInfo.component.toggleNotebookPin(actionInfo.history, false); + return Promise.resolve(true); + } +} + +export class RenameNotebookMaterializedAction extends Action { + public static ID = 'notebookaction.openTemplate'; + public static LABEL = nls.localize('notebookaction.renameNotebook', "Rename"); + + constructor( + @ICommandService private _commandService: ICommandService, + ) { + super(RenameNotebookMaterializedAction.ID, RenameNotebookMaterializedAction.LABEL); + } + + public run(actionInfo: any): Promise { + actionInfo.component.renameNotebook(actionInfo.history); + return Promise.resolve(true); + } +} diff --git a/src/sql/platform/jobManagement/common/interfaces.ts b/src/sql/platform/jobManagement/common/interfaces.ts index c1473ec492..f9c115ad64 100644 --- a/src/sql/platform/jobManagement/common/interfaces.ts +++ b/src/sql/platform/jobManagement/common/interfaces.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { JobCacheObject, AlertsCacheObject, ProxiesCacheObject, OperatorsCacheObject } from './jobManagementService'; +import { JobCacheObject, AlertsCacheObject, ProxiesCacheObject, OperatorsCacheObject, NotebookCacheObject } from './jobManagementService'; import { Event } from 'vs/base/common/event'; export const SERVICE_ID = 'jobManagementService'; @@ -25,6 +25,15 @@ export interface IJobManagementService { deleteJobStep(connectionUri: string, step: azdata.AgentJobStepInfo): Thenable; + getNotebooks(connectionUri: string): Thenable; + getNotebookHistory(connectionUri: string, jobId: string, jobName: string, targetDatabase: string): Thenable; + getMaterialziedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable; + getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable; + deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable; + deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable; + updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string); + updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean); + getAlerts(connectionUri: string): Thenable; deleteAlert(connectionUri: string, alert: azdata.AgentAlertInfo): Thenable; @@ -37,10 +46,11 @@ export interface IJobManagementService { getCredentials(connectionUri: string): Thenable; jobAction(connectionUri: string, jobName: string, action: string): Thenable; - addToCache(server: string, cache: JobCacheObject | OperatorsCacheObject); + addToCache(server: string, cache: JobCacheObject | OperatorsCacheObject | NotebookCacheObject); jobCacheObjectMap: { [server: string]: JobCacheObject; }; + notebookCacheObjectMap: { [server: string]: NotebookCacheObject; }; operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; }; alertsCacheObjectMap: { [server: string]: AlertsCacheObject; }; proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject }; - addToCache(server: string, cache: JobCacheObject | ProxiesCacheObject | AlertsCacheObject | OperatorsCacheObject); -} \ No newline at end of file + addToCache(server: string, cache: JobCacheObject | ProxiesCacheObject | AlertsCacheObject | OperatorsCacheObject | NotebookCacheObject); +} diff --git a/src/sql/platform/jobManagement/common/jobManagementService.ts b/src/sql/platform/jobManagement/common/jobManagementService.ts index b6ead47051..c091bf0cb6 100644 --- a/src/sql/platform/jobManagement/common/jobManagementService.ts +++ b/src/sql/platform/jobManagement/common/jobManagementService.ts @@ -20,7 +20,7 @@ export class JobManagementService implements IJobManagementService { private _operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; } = {}; private _alertsCacheObject: { [server: string]: AlertsCacheObject; } = {}; private _proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject; } = {}; - + private _notebookCacheObjectMap: { [server: string]: NotebookCacheObject; } = {}; constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService ) { @@ -62,6 +62,54 @@ export class JobManagementService implements IJobManagementService { }); } + // Notebooks + public getNotebooks(connectionUri: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getNotebooks(connectionUri); + }); + } + + public getNotebookHistory(connectionUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getNotebookHistory(connectionUri, jobID, jobName, targetDatabase); + }); + } + + public getMaterialziedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getMaterializedNotebook(connectionUri, targetDatabase, notebookMaterializedId); + }); + } + + public getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getTemplateNotebook(connectionUri, targetDatabase, jobId); + }); + } + + public deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.deleteNotebook(connectionUri, notebook); + }); + } + + public deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.deleteMaterializedNotebook(connectionUri, agentNotebookHistory, targetDatabase); + }); + } + + public updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.updateNotebookMaterializedName(connectionUri, agentNotebookHistory, targetDatabase, name); + }); + } + + public updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.updateNotebookMaterializedPin(connectionUri, agentNotebookHistory, targetDatabase, pin); + }); + } // Alerts public getAlerts(connectionUri: string): Thenable { @@ -134,6 +182,10 @@ export class JobManagementService implements IJobManagementService { return this._alertsCacheObject; } + public get notebookCacheObjectMap(): { [server: string]: NotebookCacheObject; } { + return this._notebookCacheObjectMap; + } + public get proxiesCacheObjectMap(): { [server: string]: ProxiesCacheObject; } { return this._proxiesCacheObjectMap; } @@ -142,7 +194,7 @@ export class JobManagementService implements IJobManagementService { return this._operatorsCacheObjectMap; } - public addToCache(server: string, cacheObject: JobCacheObject | OperatorsCacheObject | ProxiesCacheObject | AlertsCacheObject) { + public addToCache(server: string, cacheObject: JobCacheObject | OperatorsCacheObject | ProxiesCacheObject | AlertsCacheObject | NotebookCacheObject) { if (cacheObject instanceof JobCacheObject) { this._jobCacheObjectMap[server] = cacheObject; } else if (cacheObject instanceof OperatorsCacheObject) { @@ -151,6 +203,8 @@ export class JobManagementService implements IJobManagementService { this._alertsCacheObject[server] = cacheObject; } else if (cacheObject instanceof ProxiesCacheObject) { this._proxiesCacheObjectMap[server] = cacheObject; + } else if (cacheObject instanceof NotebookCacheObject) { + this._notebookCacheObjectMap[server] = cacheObject; } } } @@ -252,6 +306,94 @@ export class JobCacheObject { this._jobSchedules[jobID] = value; } } +/** + * Server level caching of Operators + */ +export class NotebookCacheObject { + _serviceBrand: any; + private _notebooks: azdata.AgentNotebookInfo[] = []; + private _notebookHistories: { [jobID: string]: azdata.AgentNotebookHistoryInfo[]; } = {}; + private _jobSteps: { [jobID: string]: azdata.AgentJobStepInfo[]; } = {}; + private _jobSchedules: { [jobID: string]: azdata.AgentJobScheduleInfo[]; } = {}; + private _runCharts: { [jobID: string]: string[]; } = {}; + private _prevJobID: string; + private _serverName: string; + private _dataView: Slick.Data.DataView; + + /* Getters */ + public get notebooks(): azdata.AgentNotebookInfo[] { + return this._notebooks; + } + + public get notebookHistories(): { [jobID: string]: azdata.AgentNotebookHistoryInfo[] } { + return this._notebookHistories; + } + + public get prevJobID(): string { + return this._prevJobID; + } + + public getNotebookHistory(jobID: string): azdata.AgentNotebookHistoryInfo[] { + return this._notebookHistories[jobID]; + } + + public get serverName(): string { + return this._serverName; + } + + public get dataView(): Slick.Data.DataView { + return this._dataView; + } + + public getRunChart(jobID: string): string[] { + return this._runCharts[jobID]; + } + + public getJobSteps(jobID: string): azdata.AgentJobStepInfo[] { + return this._jobSteps[jobID]; + } + + public getJobSchedules(jobID: string): azdata.AgentJobScheduleInfo[] { + return this._jobSchedules[jobID]; + } + + /* Setters */ + public set notebooks(value: azdata.AgentNotebookInfo[]) { + this._notebooks = value; + } + + public set notebookHistories(value: { [jobID: string]: azdata.AgentNotebookHistoryInfo[]; }) { + this._notebookHistories = value; + } + + public set prevJobID(value: string) { + this._prevJobID = value; + } + + public setNotebookHistory(jobID: string, value: azdata.AgentNotebookHistoryInfo[]) { + this._notebookHistories[jobID] = value; + } + + public setRunChart(jobID: string, value: string[]) { + this._runCharts[jobID] = value; + } + + public set serverName(value: string) { + this._serverName = value; + } + + public set dataView(value: Slick.Data.DataView) { + this._dataView = value; + } + + public setJobSteps(jobID: string, value: azdata.AgentJobStepInfo[]) { + this._jobSteps[jobID] = value; + } + + public setJobSchedules(jobID: string, value: azdata.AgentJobScheduleInfo[]) { + this._jobSchedules[jobID] = value; + } +} /** * Server level caching of Operators @@ -364,4 +506,4 @@ export class ProxiesCacheObject { public set serverName(value: string) { this._serverName = value; } -} \ No newline at end of file +} diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index f4c0d1d2d0..a23600d50d 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -631,6 +631,7 @@ declare module 'sqlops' { isFile?: boolean; fileContent?: string; title?: string; + fileType?: string; } export interface LoadingComponentProperties { diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 2a7ca8a59d..5fdb816d04 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -395,6 +395,30 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData deleteJobStep(connectionUri: string, stepInfo: azdata.AgentJobStepInfo): Thenable { return self._proxy.$deleteJobStep(handle, connectionUri, stepInfo); }, + getNotebooks(connectionUri: string): Thenable { + return self._proxy.$getNotebooks(handle, connectionUri); + }, + getNotebookHistory(connectionUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable { + return self._proxy.$getNotebookHistory(handle, connectionUri, jobID, jobName, targetDatabase); + }, + getMaterializedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable { + return self._proxy.$getMaterializedNotebook(handle, connectionUri, targetDatabase, notebookMaterializedId); + }, + updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable { + return self._proxy.$updateNotebookMaterializedName(handle, connectionUri, agentNotebookHistory, targetDatabase, name); + }, + deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable { + return self._proxy.$deleteMaterializedNotebook(handle, connectionUri, agentNotebookHistory, targetDatabase); + }, + updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable { + return self._proxy.$updateNotebookMaterializedPin(handle, connectionUri, agentNotebookHistory, targetDatabase, pin); + }, + getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable { + return self._proxy.$getTemplateNotebook(handle, connectionUri, targetDatabase, jobId); + }, + deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable { + return self._proxy.$deleteNotebook(handle, connectionUri, notebook); + }, getAlerts(connectionUri: string): Thenable { return self._proxy.$getAlerts(handle, connectionUri); }, diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index fa3baaaf9d..0443c40740 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -98,7 +98,7 @@ class MainThreadNotebookEditor extends Disposable { if (!input) { return false; } - return input === this.editor.notebookParams.input; + return input.notebookUri.toString() === this.editor.notebookParams.input.notebookUri.toString(); } public applyEdits(versionIdCheck: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): boolean { diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index 50f6af511c..e51b758f3e 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -679,14 +679,14 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { * Deletes a job */ $deleteJob(handle: number, ownerUri: string, job: azdata.AgentJobInfo): Thenable { - throw this._resolveProvider(handle).deleteJob(ownerUri, job); + return this._resolveProvider(handle).deleteJob(ownerUri, job); } /** * Deletes a job step */ $deleteJobStep(handle: number, ownerUri: string, step: azdata.AgentJobStepInfo): Thenable { - throw this._resolveProvider(handle).deleteJobStep(ownerUri, step); + return this._resolveProvider(handle).deleteJobStep(ownerUri, step); } /** @@ -703,6 +703,62 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return this._resolveProvider(handle).deleteAlert(ownerUri, alert); } + /** + * Get Agent Notebook list + */ + public $getNotebooks(handle: number, ownerUri: string): Thenable { + return this._resolveProvider(handle).getNotebooks(ownerUri); + } + + /** + * Get a Agent Notebook's history + */ + public $getNotebookHistory(handle: number, ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable { + return this._resolveProvider(handle).getNotebookHistory(ownerUri, jobID, jobName, targetDatabase); + } + + /** + * Get a Agent Materialized Notebook + */ + public $getMaterializedNotebook(handle: number, ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable { + return this._resolveProvider(handle).getMaterializedNotebook(ownerUri, targetDatabase, notebookMaterializedId); + } + + /** + * Get a Agent Template Notebook + */ + public $getTemplateNotebook(handle: number, ownerUri: string, targetDatabase: string, jobId: string): Thenable { + return this._resolveProvider(handle).getTemplateNotebook(ownerUri, targetDatabase, jobId); + } + + /** + * Delete a Agent Notebook + */ + public $deleteNotebook(handle: number, ownerUri: string, notebook: azdata.AgentNotebookInfo): Thenable { + return this._resolveProvider(handle).deleteNotebook(ownerUri, notebook); + } + + /** + * Update a Agent Materialized Notebook Name + */ + public $updateNotebookMaterializedName(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable { + return this._resolveProvider(handle).updateNotebookMaterializedName(ownerUri, agentNotebookHistory, targetDatabase, name); + } + + /** + * Get a Agent Materialized Notebook + */ + public $deleteMaterializedNotebook(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable { + return this._resolveProvider(handle).deleteMaterializedNotebook(ownerUri, agentNotebookHistory, targetDatabase); + } + + /** + * Update a Agent Materialized Notebook Pin + */ + public $updateNotebookMaterializedPin(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable { + return this._resolveProvider(handle).updateNotebookMaterializedPin(ownerUri, agentNotebookHistory, targetDatabase, pin); + } + /** * Get Agent Oeprators list */ diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 1171bab9b3..6fb79e8946 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -397,6 +397,47 @@ export abstract class ExtHostDataProtocolShape { */ $deleteJobStep(handle: number, ownerUri: string, step: azdata.AgentJobStepInfo): Thenable { throw ni(); } + /** + * Get Agent Notebook list + */ + $getNotebooks(handle: number, ownerUri: string): Thenable { throw ni(); } + + /** + * Get a Agent Notebook's history + */ + $getNotebookHistory(handle: number, ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable { throw ni(); } + + /** + * Get a Agent materialized notebook + */ + $getMaterializedNotebook(handle: number, ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable { throw ni(); } + + /** + * Get a Agent Template notebook + */ + $getTemplateNotebook(handle: number, ownerUri: string, targetDatabase: string, jobId: string): Thenable { throw ni(); } + + /** + * Deletes a notebook + */ + $deleteNotebook(handle: number, ownerUri: string, notebook: azdata.AgentNotebookInfo): Thenable { throw ni(); } + + /** + * Update materialzied Notebook Name + */ + $updateNotebookMaterializedName(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable { throw ni(); } + + /** + * Update materialzied Notebook Name + */ + $deleteMaterializedNotebook(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable { throw ni(); } + + /** + * Update materialzied Notebook Pin + */ + $updateNotebookMaterializedPin(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable { throw ni(); } + + /** * Get Agent Alerts list */ diff --git a/src/sql/workbench/browser/modelComponents/button.component.ts b/src/sql/workbench/browser/modelComponents/button.component.ts index 1bf48e2755..6ec33ce3c4 100644 --- a/src/sql/workbench/browser/modelComponents/button.component.ts +++ b/src/sql/workbench/browser/modelComponents/button.component.ts @@ -20,14 +20,13 @@ import { focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry' import { Button } from 'sql/base/browser/ui/button/button'; import { Color } from 'vs/base/common/color'; - @Component({ selector: 'modelview-button', template: `
@@ -37,6 +36,7 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC @Input() descriptor: IComponentDescriptor; @Input() modelStore: IModelStore; private _button: Button; + private fileType: string = '.sql'; @ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef; @ViewChild('fileInput', { read: ElementRef }) private _fileInputContainer: ElementRef; @@ -71,7 +71,10 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC self.fileContent = text.toString(); self.fireEvent({ eventType: ComponentEventType.onDidClick, - args: self.fileContent + args: { + filePath: file.path, + fileContent: self.fileContent + } }); }; reader.readAsText(file); @@ -101,6 +104,9 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC super.setProperties(properties); this._button.enabled = this.enabled; this._button.label = this.label; + if (this.properties.fileType) { + this.fileType = properties.fileType; + } this._button.title = this.title; // Button's ariaLabel gets set to the label by default. @@ -116,6 +122,7 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC this._button.setWidth(this.convertSize(this.height.toString())); } this.updateIcon(); + this._changeRef.detectChanges(); } protected updateIcon() { @@ -182,6 +189,10 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC this.setPropertyFromUI((properties, title) => { properties.title = title; }, newValue); } + private setFileType(value: string) { + this.properties.fileType = value; + } + private get ariaLabel(): string { return this.getPropertyOrDefault((properties) => properties.ariaLabel, ''); } diff --git a/src/sql/workbench/parts/dashboard/browser/dashboard.module.ts b/src/sql/workbench/parts/dashboard/browser/dashboard.module.ts index ee6441ab5a..4da0b242cd 100644 --- a/src/sql/workbench/parts/dashboard/browser/dashboard.module.ts +++ b/src/sql/workbench/parts/dashboard/browser/dashboard.module.ts @@ -56,6 +56,8 @@ import { AlertsViewComponent } from 'sql/workbench/parts/jobManagement/browser/a import { JobHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/jobHistory.component'; import { OperatorsViewComponent } from 'sql/workbench/parts/jobManagement/browser/operatorsView.component'; import { ProxiesViewComponent } from 'sql/workbench/parts/jobManagement/browser/proxiesView.component'; +import { NotebooksViewComponent } from 'sql/workbench/parts/jobManagement/browser/notebooksView.component'; +import { NotebookHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/notebookHistory.component'; import LoadingSpinner from 'sql/workbench/browser/modelComponents/loadingSpinner.component'; import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; import { SelectBox } from 'sql/platform/browser/selectBox/selectBox.component'; @@ -65,7 +67,7 @@ import { EditableDropDown } from 'sql/platform/browser/editableDropdown/editable const baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer, DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent, ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer, - JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, AlertsViewComponent, ProxiesViewComponent, OperatorsViewComponent, + JobsViewComponent, NotebooksViewComponent, AgentViewComponent, JobHistoryComponent, NotebookHistoryComponent, JobStepsViewComponent, AlertsViewComponent, ProxiesViewComponent, OperatorsViewComponent, DashboardModelViewContainer, ModelComponentWrapper, Checkbox, EditableDropDown, SelectBox, InputBox, LoadingSpinner]; /* Panel */ @@ -89,6 +91,7 @@ import { JobStepsViewComponent } from 'sql/workbench/parts/jobManagement/browser import { IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; + const widgetComponents = [ PropertiesWidgetComponent, ExplorerWidget, diff --git a/src/sql/workbench/parts/jobManagement/browser/agentView.component.html b/src/sql/workbench/parts/jobManagement/browser/agentView.component.html index 5cee6c4897..2a9e81aa95 100644 --- a/src/sql/workbench/parts/jobManagement/browser/agentView.component.html +++ b/src/sql/workbench/parts/jobManagement/browser/agentView.component.html @@ -17,6 +17,17 @@ + + +
+ +
+
+ +
+
+
diff --git a/src/sql/workbench/parts/jobManagement/browser/agentView.component.ts b/src/sql/workbench/parts/jobManagement/browser/agentView.component.ts index 629cfba617..393fbf887e 100644 --- a/src/sql/workbench/parts/jobManagement/browser/agentView.component.ts +++ b/src/sql/workbench/parts/jobManagement/browser/agentView.component.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/jobs'; import * as nls from 'vs/nls'; import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, Injectable } from '@angular/core'; -import { AgentJobInfo } from 'azdata'; +import { AgentJobInfo, AgentNotebookInfo } from 'azdata'; import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component'; import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces'; import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService'; @@ -25,17 +25,23 @@ export class AgentViewComponent { @ViewChild(PanelComponent) private _panel: PanelComponent; private _showHistory: boolean = false; + private _showNotebookHistory: boolean = false; private _jobId: string = null; + private _notebookId: string = null; private _agentJobInfo: AgentJobInfo = null; + private _agentNotebookInfo: AgentNotebookInfo = null; private _refresh: boolean = undefined; private _expanded: Map; + private _expandedNotebook: Map; public jobsIconClass: string = 'jobsview-icon'; + public notebooksIconClass: string = 'notebooksview-icon'; public alertsIconClass: string = 'alertsview-icon'; public proxiesIconClass: string = 'proxiesview-icon'; public operatorsIconClass: string = 'operatorsview-icon'; private readonly jobsComponentTitle: string = nls.localize('jobview.Jobs', "Jobs"); + private readonly notebooksComponentTitle: string = nls.localize('jobview.Notebooks', "Notebooks"); private readonly alertsComponentTitle: string = nls.localize('jobview.Alerts', "Alerts"); private readonly proxiesComponentTitle: string = nls.localize('jobview.Proxies', "Proxies"); private readonly operatorsComponentTitle: string = nls.localize('jobview.Operators', "Operators"); @@ -67,14 +73,26 @@ export class AgentViewComponent { return this._jobId; } + public get notebookId(): string { + return this._notebookId; + } + public get showHistory(): boolean { return this._showHistory; } + public get showNotebookHistory(): boolean { + return this._showNotebookHistory; + } + public get agentJobInfo(): AgentJobInfo { return this._agentJobInfo; } + public get agentNotebookInfo(): AgentNotebookInfo { + return this._agentNotebookInfo; + } + public get refresh(): boolean { return this._refresh; } @@ -83,6 +101,10 @@ export class AgentViewComponent { return this._expanded; } + public get expandedNotebook(): Map { + return this._expandedNotebook; + } + /** * Public Setters */ @@ -91,15 +113,28 @@ export class AgentViewComponent { this._jobId = value; } + public set notebookId(value: string) { + this._notebookId = value; + } + public set showHistory(value: boolean) { this._showHistory = value; this._cd.detectChanges(); } + public set showNotebookHistory(value: boolean) { + this._showNotebookHistory = value; + this._cd.detectChanges(); + } + public set agentJobInfo(value: AgentJobInfo) { this._agentJobInfo = value; } + public set agentNotebookInfo(value: AgentNotebookInfo) { + this._agentNotebookInfo = value; + } + public set refresh(value: boolean) { this._refresh = value; this._cd.detectChanges(); @@ -109,10 +144,18 @@ export class AgentViewComponent { this._expanded.set(jobId, errorMessage); } + public setExpandedNotebook(jobId: string, errorMessage: string) { + this._expandedNotebook.set(jobId, errorMessage); + } + public set expanded(value: Map) { this._expanded = value; } + public set expandedNotebook(value: Map) { + this._expandedNotebook = value; + } + public layout() { this._panel.layout(); } diff --git a/src/sql/workbench/parts/jobManagement/browser/jobManagementView.ts b/src/sql/workbench/parts/jobManagement/browser/jobManagementView.ts index 2370d9b69d..d069bb8541 100644 --- a/src/sql/workbench/parts/jobManagement/browser/jobManagementView.ts +++ b/src/sql/workbench/parts/jobManagement/browser/jobManagementView.ts @@ -130,3 +130,8 @@ export interface JobActionContext { canEdit: boolean; job: azdata.AgentJobInfo; } + +export interface NotebookActionContext { + canEdit: boolean; + notebook: azdata.AgentNotebookInfo; +} diff --git a/src/sql/workbench/parts/jobManagement/browser/jobsView.component.ts b/src/sql/workbench/parts/jobManagement/browser/jobsView.component.ts index 27edb1d5ab..f6f31b1a20 100644 --- a/src/sql/workbench/parts/jobManagement/browser/jobsView.component.ts +++ b/src/sql/workbench/parts/jobManagement/browser/jobsView.component.ts @@ -507,11 +507,11 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe if (runChart && runChart.length > 0) { return ` - - - - - + + + + +
${runChart[0] ? runChart[0] : '
'}
${runChart[1] ? runChart[1] : '
'}
${runChart[2] ? runChart[2] : '
'}
${runChart[3] ? runChart[3] : '
'}
${runChart[4] ? runChart[4] : '
'}
${runChart[0] ? runChart[0] : '
'}
${runChart[1] ? runChart[1] : '
'}
${runChart[2] ? runChart[2] : '
'}
${runChart[3] ? runChart[3] : '
'}
${runChart[4] ? runChart[4] : '
'}
`; } else { diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x.svg new file mode 100644 index 0000000000..55924e4eec --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x.svg @@ -0,0 +1,9 @@ + + Artboard 240 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x_white.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x_white.svg new file mode 100644 index 0000000000..f07e1a2baa --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookError_16x_white.svg @@ -0,0 +1,9 @@ + + Artboard 210 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x.svg new file mode 100644 index 0000000000..3e6fd7e001 --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x.svg @@ -0,0 +1,9 @@ + + Artboard 260 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x_white.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x_white.svg new file mode 100644 index 0000000000..41728df13a --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookFail_16x_white.svg @@ -0,0 +1,9 @@ + + Artboard 230 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x.svg new file mode 100644 index 0000000000..77a73a2dcb --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x.svg @@ -0,0 +1,9 @@ + + Artboard 250 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x_white.svg b/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x_white.svg new file mode 100644 index 0000000000..32104bbd5e --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/NotebookSuccess_16x_white.svg @@ -0,0 +1,9 @@ + + Artboard 220 + + + + + + + diff --git a/src/sql/workbench/parts/jobManagement/browser/media/jobHistory.css b/src/sql/workbench/parts/jobManagement/browser/media/jobHistory.css index 4a8708a20d..740e1be8cf 100644 --- a/src/sql/workbench/parts/jobManagement/browser/media/jobHistory.css +++ b/src/sql/workbench/parts/jobManagement/browser/media/jobHistory.css @@ -3,15 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - jobhistory-component .all-jobs { display: inline-block; cursor: pointer; font-size: 15px; width: 10%; + margin-top: 10px; +} +notebookhistory-component .all-jobs { + display: inline-block; + cursor: pointer; + font-size: 15px; + width: 100%; } -.overview-container .overview-tab .resultsViewCollapsible { +.overview-container .overview-tab .resultsViewCollapsible, +.overview-container .overview-tab .notebooksgridViewCollapsible { padding: 15px; display: inline; } @@ -26,6 +33,11 @@ jobhistory-component .all-jobs { padding-top: 10px; } +.history-container { + position: relative; + overflow-y: auto; +} + .vs-dark .overview-container .overview-tab { color: #fff; } @@ -42,7 +54,13 @@ jobhistory-component .all-jobs { overflow: hidden; } -input#accordion { +input#accordion{ + position: absolute; + opacity: 0; + z-index: -1; +} + +.grid-arrow{ position: absolute; opacity: 0; z-index: -1; @@ -72,7 +90,8 @@ input#accordion { background: #333333; } -.hc-black .overview-tab .accordion-content { +.hc-black .overview-tab .accordion-content, +.grid-arrow { background: #000000; border: 1px solid #2b56f2; } @@ -81,9 +100,9 @@ input#accordion { max-height: 0; overflow: hidden; background: #eaeaea; - -webkit-transition: max-height .35s; - -o-transition: max-height .35s; - transition: max-height .35s; + -webkit-transition: max-height 0.35s; + -o-transition: max-height 0.35s; + transition: max-height 0.35s; width: 100%; } @@ -92,7 +111,13 @@ input#accordion { } /* :checked */ -input#accordion:checked ~ .accordion-content { +input#accordion:checked ~ .accordion-content, +.grid-arrow:checked { + max-height: 10em; +} + +input#accordion:checked ~ .accordion-content, +.grid-arrow:checked { max-height: 10em; } @@ -106,13 +131,13 @@ input#accordion:checked ~ .accordion-content { height: 3em; line-height: 3; text-align: center; - -webkit-transition: all .3s; - -o-transition: all .3s; - transition: all .3s; + -webkit-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; } .all-jobs > .back-button-icon { - content: url('back.svg'); + content: url("back.svg"); width: 20px; margin-right: 10px; float: left; @@ -121,30 +146,43 @@ input#accordion:checked ~ .accordion-content { padding-bottom: 10px; } -.vs-dark .all-jobs >.back-button-icon, -.hc-black .all-jobs >.back-button-icon { - content: url('back_inverse.svg'); +.vs-dark .all-jobs > .back-button-icon, +.hc-black .all-jobs > .back-button-icon { + content: url("back_inverse.svg"); } .vs .action-label.icon.newStepIcon { - background-image: url('new.svg'); + background-image: url("new.svg"); } .vs-dark .action-label.icon.newStepIcon, .hc-black .action-label.icon.newStepIcon { - background-image: url('new_inverse.svg'); + background-image: url("new_inverse.svg"); +} + +.vs .action-label.icon.opennotebook { + background-image: url("open_notebook.svg"); +} + +.vs-dark .action-label.icon.opennotebook, +.hc-black .action-label.icon.newStepIcon { + background-image: url("open_notebook_inverse.svg"); } jobhistory-component .hc-black .icon.edit, -jobhistory-component .vs-dark .icon.edit { - background-image: url('edit_inverse.svg'); +jobhistory-component .vs-dark .icon.edit, +notebookhistory-component .hc-black .icon.edit, +notebookhistory-component .vs-dark .icon.edit { + background-image: url("edit_inverse.svg"); } -jobhistory-component .vs .icon.edit { - background-image: url('edit.svg'); +jobhistory-component .vs .icon.edit, +notebookhistory-component .vs .icon.edit { + background-image: url("edit.svg"); } -jobhistory-component .actions-container .icon.edit { +jobhistory-component .actions-container .icon.edit, +notebookhistory-component .actions-container .icon.edit { background-position: 0% 50%; background-repeat: no-repeat; background-size: 12px; @@ -210,12 +248,20 @@ table.step-list tr.step-row td { max-height: 200px; } -.step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before { +.step-table + .monaco-tree + .monaco-tree-rows.show-twisties + > .monaco-tree-row.has-children + > .content:before { background: none; background-image: none; } -.step-table .monaco-tree.focused .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children.selected:not(.loading) > .content:before { +.step-table + .monaco-tree.focused + .monaco-tree-rows.show-twisties + > .monaco-tree-row.has-children.selected:not(.loading) + > .content:before { background-image: none; } @@ -258,40 +304,182 @@ table.step-list tr.step-row td { flex-direction: column; } -jobhistory-component { +jobhistory-component, +notebookhistory-component { display: flex; flex-direction: column; height: 100%; width: 100%; } -jobhistory-component > .jobhistory-heading-container { +jobhistory-component > .jobhistory-heading-container, +notebookhistory-component > .jobhistory-heading-container { display: flex; } -jobhistory-component > .jobhistory-heading-container > .icon.in-progress { +jobhistory-component > .jobhistory-heading-container > .icon.in-progress, +notebookhistory-component > .jobhistory-heading-container > .icon.in-progress { width: 20px; height: 20px; padding-top: 16px; padding-left: 20px; } -jobhistory-component > .agent-actionbar-container { +jobhistory-component > .agent-actionbar-container, +notebookhistory-component > .agent-actionbar-container { border-top: 3px solid #f4f4f4; } -.vs-dark jobhistory-component > .agent-actionbar-container { +.vs-dark jobhistory-component > .agent-actionbar-container, +.vs-dark notebookhistory-component > .agent-actionbar-container { border-top: 3px solid #444444; } -.hc-black jobhistory-component > .agent-actionbar-container { +.hc-black jobhistory-component > .agent-actionbar-container, +.hc-black notebookhistory-component > .agent-actionbar-container { border-top: 3px solid #2b56f2; } -jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row { +jobhistory-component + .step-table.prev-run-list + .monaco-tree-wrapper + .monaco-tree-row, +notebookhistory-component + .step-table.prev-run-list + .monaco-tree-wrapper + .monaco-tree-row { width: 96%; } -jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar { +jobhistory-component + .agent-actionbar-container + > .monaco-toolbar.carbon-taskbar, +notebookhistory-component + .agent-actionbar-container + > .monaco-toolbar.carbon-taskbar { margin: 10px 0px 5px 0px; } +.notebook-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + grid-gap: 10px; + padding: 10px; +} + +.notebook-grid-item { + border-radius: 5px; + padding-bottom: 5px; + height: 95px; + display: block; + border-radius: 0px; +} + +.notebook-grid-item > img { + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 50px; + height: 50px; +} + +.notebook-grid-item:hover { + background: #dcdcdc !important; +} + +.vs-dark .notebook-grid-item:hover, +.hc-black .notebook-grid-item:hover { + background: #444444 !important; +} + +.notebook-grid-item > .img-success { + background-image: url(./NotebookSuccess_16x.svg); + background-position: center; + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 40px; + height: 40px; + background-size: 40px 40px; + background-repeat: no-repeat; +} + +.vs-dark .notebook-grid-item > .img-success, +.hc-black .notebook-grid-item > .img-success { + background-image: url(./NotebookSuccess_16x_white.svg); + background-position: center; +} + +.notebook-grid-item > .img-failure { + background-image: url(./NotebookFail_16x.svg); + background-position: center; + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 40px; + height: 40px; + background-size: 40px 40px; + background-repeat: no-repeat; +} + +.vs-dark .notebook-grid-item > .img-failure, +.hc-black .notebook-grid-item > .img-failure { + background-image: url(./NotebookFail_16x_white.svg); + background-position: center; +} + +.notebook-grid-item > .img-error { + background-image: url(./NotebookError_16x.svg); + background-position: center; + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 40px; + height: 40px; + background-size: 40px 40px; + background-repeat: no-repeat; +} + +.vs-dark .notebook-grid-item > .img-error, +.hc-black .notebook-grid-item > .img-error { + background-image: url(./NotebookError_16x_white.svg); + background-position: center; +} + +.notebook-grid-item > p { + display: absolute; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 100%; + text-align: center; +} + +.notebook-history-container { + flex: 1 1 auto; + position: relative; + overflow-y: auto; +} + +.grid-title { + position: relative; + display: block; + padding: 0 0 0 1em; + background: #f4f4f4; + font-weight: bold; + line-height: 3; + cursor: pointer; + width: auto; +} + +.vs-dark .grid-title { + background: #333333; +} + +.hc-black .grid-title { + background: #000000; + border: 1px solid #2b56f2; +} diff --git a/src/sql/workbench/parts/jobManagement/browser/media/jobs.css b/src/sql/workbench/parts/jobManagement/browser/media/jobs.css index 75ffa408ac..7a1f5d8c41 100644 --- a/src/sql/workbench/parts/jobManagement/browser/media/jobs.css +++ b/src/sql/workbench/parts/jobManagement/browser/media/jobs.css @@ -9,12 +9,14 @@ agentview-component { display: block; } -jobsview-component { +jobsview-component, +notebooksview-component { height: 100%; width : 100%; display: block; } + .job-heading-container { height: 50px; border-bottom: 3px solid #f4f4f4; @@ -35,6 +37,12 @@ jobsview-component { display: block; } +.jobnotebooksview-grid { + height: calc(100% - 75px); + width : 100%; + display: block; +} + .vs-dark #agentViewDiv .slick-header-column { background: #333333 !important; } @@ -45,52 +53,63 @@ jobsview-component { font-weight: bold; } -.hc-black #jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { +.hc-black #jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell, +.hc-black #notebooksDiv notebooks-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { border: 1px solid #2b56f2; } -#jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { +#jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell, +#notebooksDiv notebooksview-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { border-right: transparent !important; border-left: transparent !important; line-height: 33px !important; } -#jobsDiv .jobview-joblist { +#jobsDiv .jobview-joblist, +#notebooksDiv .jobview-joblist { height: 100%; width: 100%; } -#jobsDiv .jobview-jobnametable { +#jobsDiv .jobview-jobnametable, +#notebooksDiv .jobview-jobnametable { border: 0px; width: 100%; height: 100%; } -#jobsDiv .jobview-jobnameindicatorsuccess { +#jobsDiv .jobview-jobnameindicatorsuccess, +#notebooksDiv .jobview-jobnameindicatorsuccess { width: 5px; background: green; } -#jobsDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure { +#jobsDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure, +#notebooksDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure { width: 5px; background: red; } -#jobsDiv .jobview-jobnameindicatorcancel { +#jobsDiv .jobview-jobnameindicatorcancel, +#notebooksDiv .jobview-jobnameindicatorcancel { width: 5px; background: orange; } -#jobsDiv .jobview-grid .jobview-jobnameindicatorunknown { +#jobsDiv .jobview-grid .jobview-jobnameindicatorunknown, +#notebooks .jobnotebooksview-grid .jobview-jobnameindicatorunknown { width: 5px; background: grey; } -#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext { +#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext, +#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext, +#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row .jobview-jobnametext { width: 100%; } -#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext { +#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext, +#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1 .jobview-jobnametext { text-overflow: ellipsis; width: 250px; overflow: hidden; @@ -108,28 +127,44 @@ jobsview-component { display: inline-block; } -#jobsDiv .job-with-error { +#jobsDiv .job-with-error, +#notebooksDiv .job-with-error { border-bottom: none; } -.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row { +.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row, +.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row { width: 100%; opacity: 1; font-weight: 700; + text-overflow: ellipsis; color: orangered; } -.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row { +.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.notebook-error-row { + width: 100%; + opacity: 1; + font-weight: 700; + overflow: hidden; + text-overflow: ellipsis; + color: orange; +} + +.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row, +.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row, +.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.notebook-error-row { opacity: 1; } -#jobsDiv .jobview-splitter { +#jobsDiv .jobview-splitter, +#notebooksDiv .jobview-splitter { height: 1px; width: 100%; background-color: gray; } -#jobsDiv .jobview-jobitem { +#jobsDiv .jobview-jobitem, +#notebooksDiv .jobview-jobitem { display: flex; flex-direction: row; flex-wrap: nowrap; @@ -137,30 +172,36 @@ jobsview-component { white-space: nowrap; } -#jobsDiv .jobview-label { +#jobsDiv .jobview-label, +#notebooksDiv .jobview-label { padding-bottom: 10px; padding-top: 10px; } -#jobsDiv .jobview-highlight-none { +#jobsDiv .jobview-highlight-none, +#notebooksDiv .jobview-highlight-none { width: 5px; margin-right: 10px; } -#jobsDiv .detail-container { +#jobsDiv .detail-container, +#notebooksDiv .detail-container { max-height: 100px !important; line-height: 20px; } -#jobsDiv .detail { +#jobsDiv .detail, +#notebooksDiv .detail { padding: 5px } -#jobsDiv .preload { +#jobsDiv .preload, +#notebooksDiv .preload { font-size: 13px; } -#jobsDiv .dynamic-cell-detail > :first-child { +#jobsDiv .dynamic-cell-detail > :first-child, +#notebooksDiv .dynamic-cell-detail > :first-child { vertical-align: middle; line-height: 13px; padding: 10px; @@ -171,11 +212,22 @@ jobsview-component { background-image: url('./job.svg'); } + .vs-dark .jobsview-icon, .hc-black .jobsview-icon { background-image: url('./job_inverse.svg'); } + +.notebooksview-icon { + background-image: url('./notebook.svg'); +} + +.vs-dark .notebooksview-icon, +.hc-black .notebooksview-icon { + background-image: url('./notebook_inverse.svg'); +} + .alertsview-icon { background-image: url('./alert.svg'); } @@ -204,7 +256,7 @@ jobsview-component { } agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.even > .slick-cell, -agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell { +agentview-component .jobnotebooksview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell { cursor: pointer; } @@ -227,26 +279,37 @@ agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.od padding-left: 15px; } -#jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure { +#jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure, +#notebooksDiv notebooksview-component .jobnotebooksview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure, +#notebooksDiv notebooksview-component .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row td.jobview-jobnameindicatorfailure { width: 0; background: none; } #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell, -#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered { +#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered, +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell, +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered { background: #dcdcdc !important; } .vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, .vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered, -.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell { +.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell, +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered, +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell { background: #444444 !important; } .hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, .hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered, -.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell { +.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell, +.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, +.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered, +.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell { background: none !important; } @@ -257,7 +320,8 @@ table.jobprevruns div.bar3, table.jobprevruns div.bar4, table.jobprevruns div.ba width: 10px; } -.jobview-grid .slick-cell.l10.r10 { +.jobview-grid .slick-cell.l10.r10, +.jobnotebooksview-grid .slick-cell.l10.r10 { text-align: center; display: inline-flex; } @@ -270,6 +334,12 @@ table.jobprevruns > tbody { vertical-align: bottom; } +#notebooksDiv .jobnotebooksview-grid { + height: calc(100% - 75px); + width : 100%; + display: block; +} + #alertsDiv .jobalertsview-grid { height: calc(100% - 75px); width : 100%; @@ -289,6 +359,8 @@ table.jobprevruns > tbody { display: block; } + + .vs .action-label.icon.refreshIcon { background-image: url('refresh.svg'); } @@ -298,14 +370,52 @@ table.jobprevruns > tbody { background-image: url('refresh_inverse.svg'); } + +.vs .action-label.icon.openNotebook { + background-image: url('open_notebook.svg'); +} + +.vs-dark .action-label.icon.openNotebook, +.hc-black .action-label.icon.openNotebook { + background-image: url('open_notebook_inverse.svg'); +} + .agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item { padding-left: 20px; } -jobsview-component .jobview-grid .slick-cell.error-row { +jobsview-component .jobview-grid .slick-cell.error-row, +notebooksview-component .jobnotebooksview-grid .slick-cell.error-row, +notebooksview-component .jobnotebooksview-grid .slick-cell.notebook-error-row { opacity: 0; } +#notebooksDiv notebooksview-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { + border-right: transparent !important; + border-left: transparent !important; + line-height: 33px !important; +} + +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell, +#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered { + background: #dcdcdc !important; +} + +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell, +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered, +.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell { + background: #444444 !important; +} + +.vs-dark .jobnotebooksview-grid > .monaco-table .slick-header-columns .slick-resizable-handle { + border-left: 1px dotted white; +} + +.jobnotebooksview-grid > .monaco-table .slick-header-columns .slick-resizable-handle { + border-left: 1px dotted #444444; +} + #alertsDiv jobalertsview-component .jobalertsview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell { border-right: transparent !important; border-left: transparent !important; @@ -403,8 +513,9 @@ jobsview-component .jobview-grid .slick-cell.error-row { } #jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar, +#notebooksDiv notebooksview-component .monaco-toolbar.carbon-taskbar, #operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar, #alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar, #proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar { margin: 10px 0px 10px 0px; -} \ No newline at end of file +} diff --git a/src/sql/workbench/parts/jobManagement/browser/media/notebook.svg b/src/sql/workbench/parts/jobManagement/browser/media/notebook.svg new file mode 100644 index 0000000000..dae58b840e --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/notebook.svg @@ -0,0 +1 @@ +notebook \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/media/notebook_inverse.svg b/src/sql/workbench/parts/jobManagement/browser/media/notebook_inverse.svg new file mode 100644 index 0000000000..fb495dda69 --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/notebook_inverse.svg @@ -0,0 +1 @@ +notebook_inverse \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/media/open_notebook.svg b/src/sql/workbench/parts/jobManagement/browser/media/open_notebook.svg new file mode 100644 index 0000000000..0041ae9b21 --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/open_notebook.svg @@ -0,0 +1 @@ +open_notebook \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/media/open_notebook_inverse.svg b/src/sql/workbench/parts/jobManagement/browser/media/open_notebook_inverse.svg new file mode 100644 index 0000000000..a95750c49f --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/open_notebook_inverse.svg @@ -0,0 +1 @@ +open_notebook_inverse \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/media/status_error.svg b/src/sql/workbench/parts/jobManagement/browser/media/status_error.svg new file mode 100644 index 0000000000..abe3c65921 --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/status_error.svg @@ -0,0 +1 @@ +error_16x16 \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/media/status_success.svg b/src/sql/workbench/parts/jobManagement/browser/media/status_success.svg new file mode 100644 index 0000000000..776e1fd909 --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/media/status_success.svg @@ -0,0 +1 @@ +success_16x16 \ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.html b/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.html new file mode 100644 index 0000000000..da2cd2fc3a --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.html @@ -0,0 +1,163 @@ + +
+

Notebook | {{ this._agentNotebookInfo?.name }}

+
+
+ + +
+
+ All Notebooks +
+ + +
+ + +
+
+ + +
+ + + + + + + + + + + + + +
+ TargetDatabase: + + {{ this._agentNotebookInfo?.targetDatabase }} + + Enabled: + + {{ this._agentNotebookInfo?.enabled }} +
+ Last Run: + + {{ this._agentNotebookInfo?.lastRun }} + + Next Run: + + {{ this._agentNotebookInfo?.nextRun }} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ {{ formatDateTimetoLocaleDate(history.runDate) }} +
+ {{ formatDateTimetoLocaleTime(history.runDate) }} +

+
+
+

+ {{ history.materializedNotebookName }} +

+
+
+
+
+
+
+ + +
+ +
+

+ No Previous Runs Available +

+
+
+
+
+
diff --git a/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.ts b/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.ts new file mode 100644 index 0000000000..e7930ddb9c --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/notebookHistory.component.ts @@ -0,0 +1,608 @@ +/*--------------------------------------------------------------------------------------------- + * 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/jobHistory'; + +import * as azdata from 'azdata'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable, PipeTransform, Pipe } from '@angular/core'; +import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; +import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/browser/agentView.component'; +import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service'; +import { RunJobAction, StopJobAction, JobsRefreshAction, EditNotebookJobAction, EditJobAction, OpenMaterializedNotebookAction, OpenTemplateNotebookAction, RenameNotebookMaterializedAction, PinNotebookMaterializedAction, UnpinNotebookMaterializedAction, DeleteMaterializedNotebookAction } from 'sql/platform/jobManagement/browser/jobActions'; +import { NotebookCacheObject } from 'sql/platform/jobManagement/common/jobManagementService'; +import { JobManagementUtilities } from 'sql/platform/jobManagement/browser/jobManagementUtilities'; +import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces'; +import { JobHistoryRow } from 'sql/workbench/parts/jobManagement/browser/jobHistoryTree'; +import { JobStepsViewRow } from 'sql/workbench/parts/jobManagement/browser/jobStepsViewTree'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAction } from 'vs/base/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { JobManagementView, JobActionContext } from 'sql/workbench/parts/jobManagement/browser/jobManagementView'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; +import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +export const DASHBOARD_SELECTOR: string = 'notebookhistory-component'; +export class GridSection { + title: string; + histories: azdata.AgentNotebookHistoryInfo[]; + contextMenuType: number; + style: string; +} +@Component({ + selector: DASHBOARD_SELECTOR, + templateUrl: decodeURI(require.toUrl('./notebookHistory.component.html')), + providers: [{ provide: TabChild, useExisting: forwardRef(() => NotebookHistoryComponent) }], + changeDetection: ChangeDetectionStrategy.OnPush +}) +@Injectable() +export class NotebookHistoryComponent extends JobManagementView implements OnInit { + + + + @ViewChild('table') private _tableContainer: ElementRef; + @ViewChild('jobsteps') private _jobStepsView: ElementRef; + @ViewChild('notebookHistoryActionbarContainer') private _notebookHistoryActionbarView: ElementRef; + @ViewChild('notebookgriditem') private _notebookGridItem: ElementRef; + + @Input() public agentNotebookInfo: azdata.AgentNotebookInfo = undefined; + @Input() public agentJobHistories: azdata.AgentJobHistoryInfo[] = undefined; + public notebookHistories: azdata.AgentNotebookHistoryInfo[] = undefined; + public agentNotebookHistoryInfo: azdata.AgentNotebookHistoryInfo = undefined; + + private _isVisible: boolean = false; + private _stepRows: JobStepsViewRow[] = []; + private _showSteps: boolean = undefined; + private _showPreviousRuns: boolean = undefined; + private _runStatus: string = undefined; + private _notebookCacheObject: NotebookCacheObject; + private _agentNotebookInfo: azdata.AgentNotebookInfo; + private _noJobsAvailable: boolean = false; + protected _notebookHistoryActionBar: Taskbar; + + // Job Actions + private _editNotebookJobAction: EditNotebookJobAction; + private _runJobAction: RunJobAction; + private _stopJobAction: StopJobAction; + private _refreshAction: JobsRefreshAction; + private _openNotebookTemplateAction: OpenTemplateNotebookAction; + + private static readonly HEADING_HEIGHT: number = 24; + + private _grids: GridSection[] = []; + + + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef, + @Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface, + @Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(IInstantiationService) private instantiationService: IInstantiationService, + @Inject(IContextMenuService) contextMenuService: IContextMenuService, + @Inject(IJobManagementService) private _jobManagementService: IJobManagementService, + @Inject(ICommandService) private _commandService: ICommandService, + @Inject(IKeybindingService) keybindingService: IKeybindingService, + @Inject(IDashboardService) dashboardService: IDashboardService, + @Inject(ITelemetryService) private _telemetryService: ITelemetryService, + @Inject(IQuickInputService) private _quickInputService: IQuickInputService + ) { + super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent); + let notebookCacheObjectMap = this._jobManagementService.notebookCacheObjectMap; + this._serverName = commonService.connectionManagementService.connectionInfo.connectionProfile.serverName; + let notebookCache = notebookCacheObjectMap[this._serverName]; + if (notebookCache) { + this._notebookCacheObject = notebookCache; + } else { + this._notebookCacheObject = new NotebookCacheObject(); + this._notebookCacheObject.serverName = this._serverName; + this._jobManagementService.addToCache(this._serverName, this._notebookCacheObject); + } + } + + ngOnInit() { + // set base class elements + this._visibilityElement = this._tableContainer; + this._parentComponent = this._agentViewComponent; + this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo; + this.initActionBar(); + const self = this; + this._telemetryService.publicLog(TelemetryKeys.JobHistoryView); + } + + private loadHistory() { + const self = this; + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let jobName = this._agentViewComponent.agentNotebookInfo.name; + let jobId = this._agentViewComponent.notebookId; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + this._jobManagementService.getNotebookHistory(ownerUri, jobId, jobName, targetDatabase).then((result) => { + if (result && result.histories) { + this.notebookHistories = result.histories; + self._notebookCacheObject.setNotebookHistory(jobId, this.notebookHistories); + self._notebookCacheObject.setJobSchedules(jobId, result.schedules); + self._notebookCacheObject.setJobSteps(jobId, result.steps); + this._agentViewComponent.agentNotebookInfo.jobSteps = this._notebookCacheObject.getJobSteps(jobId); + this._agentViewComponent.agentNotebookInfo.jobSchedules = this._notebookCacheObject.getJobSchedules(jobId); + this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo; + if (result.histories.length > 0) { + self._noJobsAvailable = false; + self._showPreviousRuns = true; + } else { + self._notebookCacheObject.setNotebookHistory(self._agentViewComponent.notebookId, result.histories); + self._noJobsAvailable = true; + self._showPreviousRuns = false; + } + } else { + self._noJobsAvailable = true; + self._showPreviousRuns = false; + self._showSteps = false; + } + this._actionBar.context = { targetObject: { canEdit: true, notebook: this._agentNotebookInfo, job: this._agentNotebookInfo }, ownerUri: this.ownerUri, component: this }; + this._editNotebookJobAction.enabled = true; + this._actionBar.setContent([ + { action: this._runJobAction }, + { action: this._stopJobAction }, + { action: this._refreshAction }, + { action: this._editNotebookJobAction }, + { action: this._openNotebookTemplateAction } + ]); + + this.createGrid(); + if (self._agentViewComponent.showNotebookHistory) { + self._cd.detectChanges(); + this.collapseGrid(); + } + }); + } + + private setStepsTree(element: JobHistoryRow) { + const self = this; + let cachedHistory = self._notebookCacheObject.getNotebookHistory(element.jobID); + if (cachedHistory) { + self.agentNotebookHistoryInfo = cachedHistory.find( + history => self.formatTime(history.runDate) === self.formatTime(element.runDate)); + } + if (self.agentNotebookHistoryInfo) { + self.agentNotebookHistoryInfo.runDate = self.formatTime(self.agentNotebookHistoryInfo.runDate); + if (self.agentNotebookHistoryInfo.steps) { + let jobStepStatus = this.didJobFail(self.agentNotebookHistoryInfo); + self._stepRows = self.agentNotebookHistoryInfo.steps.map(step => { + let stepViewRow = new JobStepsViewRow(); + stepViewRow.message = step.message; + stepViewRow.runStatus = jobStepStatus ? JobManagementUtilities.convertToStatusString(0) : + JobManagementUtilities.convertToStatusString(step.runStatus); + self._runStatus = JobManagementUtilities.convertToStatusString(self.agentNotebookHistoryInfo.runStatus); + stepViewRow.stepName = step.stepDetails.stepName; + stepViewRow.stepId = step.stepDetails.id.toString(); + return stepViewRow; + }); + self._stepRows.unshift(new JobStepsViewRow()); + self._stepRows[0].rowID = 'stepsColumn' + self._agentNotebookInfo.jobId; + self._stepRows[0].stepId = nls.localize('stepRow.stepID', "Step ID"); + self._stepRows[0].stepName = nls.localize('stepRow.stepName', "Step Name"); + self._stepRows[0].message = nls.localize('stepRow.message', "Message"); + this._showSteps = self._stepRows.length > 1; + } else { + self._showSteps = false; + } + if (self._agentViewComponent.showNotebookHistory) { + self._cd.detectChanges(); + this.collapseGrid(); + } + } + } + + private didJobFail(job: azdata.AgentJobHistoryInfo): boolean { + for (let i = 0; i < job.steps.length; i++) { + if (job.steps[i].runStatus === 0) { + return true; + } + } + return false; + } + + private toggleCollapse(): void { + let arrow: HTMLElement = jQuery('.resultsViewCollapsible').get(0); + let checkbox: any = document.getElementById('accordion'); + if (arrow.className === 'resultsViewCollapsible' && checkbox.checked === false) { + arrow.className = 'resultsViewCollapsible collapsed'; + } else if (arrow.className === 'resultsViewCollapsible collapsed' && checkbox.checked === true) { + arrow.className = 'resultsViewCollapsible'; + } + + } + + private toggleGridCollapse(i): void { + let notebookGrid = document.getElementById('notebook-grid' + i); + let checkbox: any = document.getElementById('accordion' + i); + let arrow = document.getElementById('history-grid-icon' + i); + if (notebookGrid.className === 'notebook-grid ' + i && checkbox.checked === true) { + notebookGrid.className = 'notebook-grid ' + i + ' collapsed'; + notebookGrid.style.display = 'none'; + arrow.className = 'resultsViewCollapsible collapsed'; + } else if (notebookGrid.className === 'notebook-grid ' + i + ' collapsed' && checkbox.checked === false) { + notebookGrid.className = 'notebook-grid ' + i; + notebookGrid.style.display = 'grid'; + arrow.className = 'resultsViewCollapsible'; + } + + } + + private toggleHistoryDisplay(event): void { + let header = event.srcElement.attributes; + } + + private goToJobs(): void { + this._isVisible = false; + this._agentViewComponent.showNotebookHistory = false; + } + + private convertToJobHistoryRow(historyInfo: azdata.AgentJobHistoryInfo): JobHistoryRow { + let jobHistoryRow = new JobHistoryRow(); + jobHistoryRow.runDate = this.formatTime(historyInfo.runDate); + jobHistoryRow.runStatus = JobManagementUtilities.convertToStatusString(historyInfo.runStatus); + jobHistoryRow.instanceID = historyInfo.instanceId; + jobHistoryRow.jobID = historyInfo.jobId; + return jobHistoryRow; + } + + private formatTime(time: string): string { + + return time.replace('T', ' '); + } + + private formatDateTimetoLocaleDate(time: string) { + let dateInstance = new Date(time); + return dateInstance.toLocaleDateString(); + } + + private formatDateTimetoLocaleTime(time: string) { + let dateInstance = new Date(time); + return dateInstance.toLocaleTimeString(); + } + + + private showProgressWheel(): boolean { + return this._showPreviousRuns !== true && this._noJobsAvailable === false; + } + + public onFirstVisible() { + this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo; + + if (!this.agentNotebookInfo) { + this.agentNotebookInfo = this._agentNotebookInfo; + } + + if (this.isRefreshing) { + this.loadHistory(); + return; + } + else { + this.createGrid(); + } + let notebookHistories = this._notebookCacheObject.notebookHistories[this._agentViewComponent.notebookId]; + if (notebookHistories) { + if (notebookHistories.length > 0) { + const self = this; + this._noJobsAvailable = false; + if (this._notebookCacheObject.prevJobID === this._agentViewComponent.notebookId || notebookHistories[0].jobId === this._agentViewComponent.notebookId) { + this._showPreviousRuns = true; + this._agentViewComponent.agentNotebookInfo.jobSteps = this._notebookCacheObject.getJobSteps(this._agentNotebookInfo.jobId); + this._agentViewComponent.agentNotebookInfo.jobSchedules = this._notebookCacheObject.getJobSchedules(this._agentNotebookInfo.jobId); + this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo; + } + } else if (notebookHistories.length === 0) { + this._showPreviousRuns = false; + this._showSteps = false; + this._noJobsAvailable = true; + } + this._editNotebookJobAction.enabled = true; + this._actionBar.setContent([ + { action: this._runJobAction }, + { action: this._stopJobAction }, + { action: this._refreshAction }, + { action: this._editNotebookJobAction }, + { action: this._openNotebookTemplateAction } + ]); + this._cd.detectChanges(); + this.collapseGrid(); + } else { + this.loadHistory(); + } + this._notebookCacheObject.prevJobID = this._agentViewComponent.notebookId; + + } + + public layout() { + let historyDetails = jQuery('.overview-container').get(0); + let statusBar = jQuery('.part.statusbar').get(0); + if (historyDetails && statusBar) { + let historyBottom = historyDetails.getBoundingClientRect().bottom; + let statusTop = statusBar.getBoundingClientRect().top; + + let height: number = statusTop - historyBottom - NotebookHistoryComponent.HEADING_HEIGHT; + + if (this._table) { + this._table.layout(new dom.Dimension( + dom.getContentWidth(this._tableContainer.nativeElement), + height)); + } + } + } + + protected initActionBar() { + this._runJobAction = this.instantiationService.createInstance(RunJobAction); + this._stopJobAction = this.instantiationService.createInstance(StopJobAction); + this._editNotebookJobAction = this.instantiationService.createInstance(EditNotebookJobAction); + this._refreshAction = this.instantiationService.createInstance(JobsRefreshAction); + this._openNotebookTemplateAction = this.instantiationService.createInstance(OpenTemplateNotebookAction); + let taskbar = this.actionBarContainer.nativeElement; + this._actionBar = new Taskbar(taskbar); + this._editNotebookJobAction.enabled = !this.showProgressWheel(); + let targetObject: JobActionContext = { canEdit: !this.showProgressWheel(), job: this._agentNotebookInfo }; + this._actionBar.context = { targetObject: targetObject, ownerUri: this.ownerUri, component: this }; + this._actionBar.setContent([ + { action: this._runJobAction }, + { action: this._stopJobAction }, + { action: this._refreshAction }, + { action: this._editNotebookJobAction }, + { action: this._openNotebookTemplateAction } + ]); + } + + public openNotebook(history: azdata.AgentNotebookHistoryInfo) { + if (history.runStatus === 0) { + return; + } + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + this._jobManagementService.getMaterialziedNotebook(ownerUri, targetDatabase, history.materializedNotebookId).then(async (result) => { + if (result) { + let regex = /:|-/gi; + let readableDataTimeString = history.runDate.replace(regex, '').replace(' ', ''); + let tempNotebookFileName = this._agentViewComponent.agentNotebookInfo.name + '_' + readableDataTimeString; + await this._commandService.executeCommand('agent.openNotebookEditorFromJsonString', tempNotebookFileName, result.notebookMaterialized); + } + }); + } + + public deleteMaterializedNotebook(history: azdata.AgentNotebookHistoryInfo) { + //TODO: Implement deletenotebook context menu action + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + this._jobManagementService.deleteMaterializedNotebook(ownerUri, history, targetDatabase).then(async (result) => { + if (result) { + this.loadHistory(); + } + }); + } + + public openTemplateNotebook() { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + let jobId = this._agentViewComponent.agentNotebookInfo.jobId; + + this._jobManagementService.getTemplateNotebook(ownerUri, targetDatabase, jobId).then(async (result) => { + if (result) { + await this._commandService.executeCommand('agent.openNotebookEditorFromJsonString', this._agentViewComponent.agentNotebookInfo.name, result.notebookTemplate, this.agentNotebookInfo, ownerUri); + } + }); + } + + public renameNotebook(history: azdata.AgentNotebookHistoryInfo) { + const defaultDateTime = new Date(history.runDate).toLocaleDateString() + ' ' + new Date(history.runDate).toLocaleTimeString(); + let notebookRunName = (history.materializedNotebookName === '') ? defaultDateTime : history.materializedNotebookName; + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + let materializedNotebookId = history.materializedNotebookId; + this._quickInputService.input({ placeHolder: notebookRunName }).then(async (value) => { + if (value) { + if (!/\S/.test(value)) { + value = ''; + } + await this._jobManagementService.updateNotebookMaterializedName(ownerUri, history, targetDatabase, value).then(async (result) => { + if (result) { + history.materializedNotebookName = value; + this.loadHistory(); + + } + }); + } + }); + } + + public toggleNotebookPin(history: azdata.AgentNotebookHistoryInfo, pin: boolean) { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase; + let materializedNotebookId = history.materializedNotebookId; + this._jobManagementService.updateNotebookMaterializedPin(ownerUri, history, targetDatabase, pin).then(async (result) => { + if (result) { + history.materializedNotebookPin = pin; + this.loadHistory(); + } + + }); + } + + public openHistoryContextMenu(event: MouseEvent, history: azdata.AgentNotebookHistoryInfo, contextMenuType: number) { + let anchor = { + x: event.clientX, + y: event.clientY + }; + let runDate = event.target['runDate']; + let gridActions = this.getGridActions(); + let actionContext = { + component: this, + history: history + }; + this._contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => (contextMenuType === 1) ? this.getPinnedGridActions() : this.getGridActions(), + getKeyBinding: (action) => this._keybindingFor(action), + getActionsContext: () => (actionContext) + }); + } + + protected getGridActions(): IAction[] { + const openNotebookAction = this._instantiationService.createInstance(OpenMaterializedNotebookAction); + const renameNotebookAction = this._instantiationService.createInstance(RenameNotebookMaterializedAction); + const pinNotebookAction = this._instantiationService.createInstance(PinNotebookMaterializedAction); + const deleteMaterializedNotebookAction = this._instantiationService.createInstance(DeleteMaterializedNotebookAction); + return [ + openNotebookAction, + renameNotebookAction, + pinNotebookAction, + deleteMaterializedNotebookAction + ]; + } + + protected getPinnedGridActions(): IAction[] { + const openNotebookAction = this._instantiationService.createInstance(OpenMaterializedNotebookAction); + const renameNotebookAction = this._instantiationService.createInstance(RenameNotebookMaterializedAction); + const unpinNotebookAction = this._instantiationService.createInstance(UnpinNotebookMaterializedAction); + const deleteMaterializedNotebookAction = this._instantiationService.createInstance(DeleteMaterializedNotebookAction); + return [ + openNotebookAction, + renameNotebookAction, + unpinNotebookAction, + deleteMaterializedNotebookAction + ]; + } + + public createdTooltip(history: azdata.AgentNotebookHistoryInfo) { + let tooltipString: string = ''; + if (history.materializedNotebookName && history.materializedNotebookName !== '') { + tooltipString = history.materializedNotebookName; + } + let dateOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }; + + tooltipString += '\n' + nls.localize('notebookHistory.dateCreatedTooltip', "Date Created: ") + new Date(history.runDate).toLocaleDateString(undefined, dateOptions); + if (history.materializedNotebookErrorInfo && /\S/.test(history.materializedNotebookErrorInfo)) { + tooltipString += '\n' + nls.localize('notebookHistory.notebookErrorTooltip', "Notebook Error: ") + history.materializedNotebookErrorInfo; + } + if (history.runStatus === 0 && history.message && /\S/.test(history.message)) { + tooltipString += '\n' + nls.localize('notebookHistory.ErrorTooltip', "Job Error: ") + history.message; + } + return tooltipString; + } + + public createGrid() { + let histories = this._notebookCacheObject.getNotebookHistory(this._agentViewComponent.notebookId); + histories = histories.sort((h1, h2) => { + return new Date(h2.runDate).getTime() - new Date(h1.runDate).getTime(); + }); + this._grids = []; + let tempHistory: azdata.AgentNotebookHistoryInfo[] = []; + for (let i = 0; i < histories.length; i++) { + if (histories[i].materializedNotebookPin) { + tempHistory.push(histories[i]); + } + } + + this._grids.push({ + title: nls.localize('notebookHistory.pinnedTitle', "Pinned"), + histories: tempHistory, + contextMenuType: 1, + style: 'grid' + }); + // Pushing the pinned notebooks grid + tempHistory = []; + let count = 0; + let i = 0; + for (; i < histories.length; i++) { + if (!histories[i].materializedNotebookPin && count < 10) { + tempHistory.push(histories[i]); + count++; + } + if (count === 10) { + break; + } + } + this._grids.push({ + title: nls.localize('notebookHistory.recentRunsTitle', "Recent Runs"), + histories: tempHistory, + contextMenuType: 0, + style: 'grid' + }); + tempHistory = []; + for (i += 1; i < histories.length; i++) { + if (!histories[i].materializedNotebookPin) { + tempHistory.push(histories[i]); + } + } + this._grids.push({ + title: nls.localize('notebookHistory.pastRunsTitle', "Past Runs"), + histories: tempHistory, + contextMenuType: 0, + style: 'none' + }); + + } + + public collapseGrid() { + for (let i = 0; i < this._grids.length; i++) { + let notebookGrid = document.getElementById('notebook-grid' + i); + let arrow = document.getElementById('history-grid-icon' + i); + if (notebookGrid) { + let checkbox: any = document.getElementById('accordion' + i); + if (this._grids[i].style === 'none') { + notebookGrid.className = 'notebook-grid ' + i + ' collapsed'; + arrow.className = 'resultsViewCollapsible collapsed'; + notebookGrid.style.display = 'none'; + checkbox.checked = true; + } + else { + notebookGrid.className = 'notebook-grid ' + i; + notebookGrid.style.display = 'grid'; + arrow.className = 'resultsViewCollapsible'; + checkbox.checked = false; + } + } + } + } + + public refreshJobs() { + this._agentViewComponent.refresh = true; + this.loadHistory(); + } + + /** GETTERS */ + + public get showSteps(): boolean { + return this._showSteps; + } + + public get stepRows() { + return this._stepRows; + } + + public get ownerUri(): string { + return this._commonService.connectionManagementService.connectionInfo.ownerUri; + } + + public get serverName(): string { + return this._serverName; + } + + /** SETTERS */ + + public set showSteps(value: boolean) { + this._showSteps = value; + this._cd.detectChanges(); + } +} diff --git a/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.html b/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.html new file mode 100644 index 0000000000..ff6e5ab3fe --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.html @@ -0,0 +1,15 @@ + +
+

Notebook Jobs

+

No Notebooks Jobs Available

+
+
+ +
+ +
\ No newline at end of file diff --git a/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.ts b/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.ts new file mode 100644 index 0000000000..80f0df13af --- /dev/null +++ b/src/sql/workbench/parts/jobManagement/browser/notebooksView.component.ts @@ -0,0 +1,984 @@ +/*--------------------------------------------------------------------------------------------- + * 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/jobs'; + +import * as azdata from 'azdata'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; +import { Table } from 'sql/base/browser/ui/table/table'; +import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/browser/agentView.component'; +import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView'; +import { NotebookCacheObject } from 'sql/platform/jobManagement/common/jobManagementService'; +import { EditJobAction, NewNotebookJobAction, RunJobAction, EditNotebookJobAction, JobsRefreshAction, IJobActionInfo, DeleteNotebookAction } from 'sql/platform/jobManagement/browser/jobActions'; +import { JobManagementUtilities } from 'sql/platform/jobManagement/browser/jobManagementUtilities'; +import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; +import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces'; +import { JobManagementView, JobActionContext } from 'sql/workbench/parts/jobManagement/browser/jobManagementView'; +import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IAction } from 'vs/base/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService'; +import { escape } from 'sql/base/common/strings'; +import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { tableBackground, cellBackground, cellBorderColor } from 'sql/platform/theme/common/colors'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { attachButtonStyler } from 'sql/platform/theme/common/styler'; +import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; + + +export const NOTEBOOKSVIEW_SELECTOR: string = 'notebooksview-component'; +export const ROW_HEIGHT: number = 45; +export const ACTIONBAR_PADDING: number = 10; + +interface IItem extends Slick.SlickData { + notebookId?: string; + id: string; +} + +@Component({ + selector: NOTEBOOKSVIEW_SELECTOR, + templateUrl: decodeURI(require.toUrl('./notebooksView.component.html')), + providers: [{ provide: TabChild, useExisting: forwardRef(() => NotebooksViewComponent) }], +}) + +export class NotebooksViewComponent extends JobManagementView implements OnInit, OnDestroy { + + private columns: Array> = [ + { + name: nls.localize('notebookColumns.name', "Name"), + field: 'name', + formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext), + width: 150, + id: 'name' + }, + { name: nls.localize('notebookColumns.targetDatbase', "Target Database"), field: 'targetDatabase', width: 80, id: 'targetDatabase' }, + { name: nls.localize('notebookColumns.lastRun', "Last Run"), field: 'lastRun', width: 80, id: 'lastRun' }, + { name: nls.localize('notebookColumns.nextRun', "Next Run"), field: 'nextRun', width: 80, id: 'nextRun' }, + { name: nls.localize('notebookColumns.status', "Status"), field: 'currentExecutionStatus', width: 50, id: 'currentExecutionStatus' }, + { name: nls.localize('notebookColumns.lastRunOutcome', "Last Run Outcome"), field: 'lastRunOutcome', width: 100, id: 'lastRunOutcome' }, + { + name: nls.localize('notebookColumns.previousRuns', "Previous Runs"), + formatter: (row, cell, value, columnDef, dataContext) => this.renderChartsPostHistory(row, cell, value, columnDef, dataContext), + field: 'previousRuns', + width: 100, + id: 'previousRuns' + } + ]; + + private _notebookCacheObject: NotebookCacheObject; + private rowDetail: RowDetailView; + private filterPlugin: any; + private dataView: any; + private _isCloud: boolean; + private filterStylingMap: { [columnName: string]: [any]; } = {}; + private filterStack = ['start']; + private filterValueMap: { [columnName: string]: string[]; } = {}; + private sortingStylingMap: { [columnName: string]: any; } = {}; + + public notebooks: azdata.AgentNotebookInfo[]; + private notebookHistories: { [jobId: string]: azdata.AgentNotebookHistoryInfo[]; } = Object.create(null); + private jobSteps: { [jobId: string]: azdata.AgentJobStepInfo[]; } = Object.create(null); + private jobAlerts: { [jobId: string]: azdata.AgentAlertInfo[]; } = Object.create(null); + private jobSchedules: { [jobId: string]: azdata.AgentJobScheduleInfo[]; } = Object.create(null); + public contextAction = NewNotebookJobAction; + + @ViewChild('notebooksgrid') _gridEl: ElementRef; + + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) private _el: ElementRef, + @Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent, + @Inject(IJobManagementService) private _jobManagementService: IJobManagementService, + @Inject(IWorkbenchThemeService) private _themeService: IWorkbenchThemeService, + @Inject(ICommandService) private _commandService: ICommandService, + @Inject(IInstantiationService) instantiationService: IInstantiationService, + @Inject(IContextMenuService) contextMenuService: IContextMenuService, + @Inject(IKeybindingService) keybindingService: IKeybindingService, + @Inject(IDashboardService) _dashboardService: IDashboardService, + @Inject(ITelemetryService) private _telemetryService: ITelemetryService + ) { + super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent); + let notebookCacheObjectMap = this._jobManagementService.notebookCacheObjectMap; + let jobCache = notebookCacheObjectMap[this._serverName]; + if (jobCache) { + this._notebookCacheObject = jobCache; + } else { + this._notebookCacheObject = new NotebookCacheObject(); + this._notebookCacheObject.serverName = this._serverName; + this._jobManagementService.addToCache(this._serverName, this._notebookCacheObject); + } + this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud; + } + + ngOnInit() { + // set base class elements + this._visibilityElement = this._gridEl; + this._parentComponent = this._agentViewComponent; + this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e))); + this._telemetryService.publicLog(TelemetryKeys.JobsView); + } + + ngOnDestroy() { + } + + public layout() { + let jobsViewToolbar = jQuery('notebooksview-component .agent-actionbar-container').get(0); + let statusBar = jQuery('.part.statusbar').get(0); + if (jobsViewToolbar && statusBar) { + let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom + ACTIONBAR_PADDING; + let statusTop = statusBar.getBoundingClientRect().top; + this._table.layout(new dom.Dimension( + dom.getContentWidth(this._gridEl.nativeElement), + statusTop - toolbarBottom)); + } + } + + onFirstVisible() { + let self = this; + let cached: boolean = false; + if (this._notebookCacheObject.serverName === this._serverName && this._notebookCacheObject.notebooks.length > 0) { + cached = true; + this.notebooks = this._notebookCacheObject.notebooks; + } + + let columns = this.columns.map((column) => { + column.rerenderOnResize = true; + return column; + }); + let options = >{ + syncColumnCellResize: true, + enableColumnReorder: false, + rowHeight: ROW_HEIGHT, + enableCellNavigation: true, + forceFitColumns: false + }; + + this.dataView = new Slick.Data.DataView({ inlineFilters: false }); + + let rowDetail = new RowDetailView({ + cssClass: '_detail_selector', + process: (job) => { + (rowDetail).onAsyncResponse.notify({ + 'itemDetail': job + }, undefined, this); + }, + useRowClick: false, + panelRows: 1, + postTemplate: () => '', // I'm assuming these code paths are just never hit... + preTemplate: () => '', + }); + this.rowDetail = rowDetail; + columns.unshift(this.rowDetail.getColumnDefinition()); + let filterPlugin = new HeaderFilter<{ inlineFilters: false }>(); + this._register(attachButtonStyler(filterPlugin, this._themeService)); + this.filterPlugin = filterPlugin; + jQuery(this._gridEl.nativeElement).empty(); + jQuery(this.actionBarContainer.nativeElement).empty(); + this.initActionBar(); + this._table = this._register(new Table(this._gridEl.nativeElement, { columns }, options)); + this._table.grid.setData(this.dataView, true); + this._table.grid.onClick.subscribe((e, args) => { + let notebook = self.getNotebook(args); + self._agentViewComponent.notebookId = notebook.jobId; + self._agentViewComponent.agentNotebookInfo = notebook; + self._agentViewComponent.showNotebookHistory = true; + }); + this._register(this._table.onContextMenu(e => { + self.openContextMenu(e); + })); + + if (cached && this._agentViewComponent.refresh !== true) { + this.onNotebooksAvailable(null); + this._showProgressWheel = false; + if (this.isVisible) { + this._cd.detectChanges(); + } + } else { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + this._jobManagementService.getNotebooks(ownerUri).then((result) => { + if (result && result.notebooks) { + self.notebooks = result.notebooks; + self._notebookCacheObject.notebooks = self.notebooks; + self.onNotebooksAvailable(result.notebooks); + } + this._showProgressWheel = false; + if (this.isVisible) { + this._cd.detectChanges(); + } + }); + } + } + + protected initActionBar() { + let refreshAction = this._instantiationService.createInstance(JobsRefreshAction); + let newAction = this._instantiationService.createInstance(NewNotebookJobAction); + let taskbar = this.actionBarContainer.nativeElement; + this._actionBar = new Taskbar(taskbar); + this._actionBar.setContent([ + { action: refreshAction }, + { action: newAction } + ]); + let context: IJobActionInfo = { component: this, ownerUri: this._commonService.connectionManagementService.connectionInfo.ownerUri }; + this._actionBar.context = context; + } + + private onNotebooksAvailable(notebooks: azdata.AgentNotebookInfo[]) { + let jobViews: any; + let start: boolean = true; + if (!notebooks) { + let dataView = this._notebookCacheObject.dataView; + jobViews = dataView.getItems(); + start = false; + } else { + jobViews = notebooks.map((job) => { + return { + id: 'notebook' + job.jobId, + notebookId: job.jobId, + name: job.name, + targetDatabase: job.targetDatabase, + lastRun: JobManagementUtilities.convertToLastRun(job.lastRun), + nextRun: JobManagementUtilities.convertToNextRun(job.nextRun), + currentExecutionStatus: JobManagementUtilities.convertToExecutionStatusString(job.currentExecutionStatus), + lastRunOutcome: (job.lastRunNotebookError === '') ? JobManagementUtilities.convertToStatusString(job.lastRunOutcome) : 'Notebook Error' + }; + }); + } + this._table.registerPlugin(this.rowDetail); + this.filterPlugin.onFilterApplied.subscribe((e, args) => { + this.dataView.refresh(); + this._table.grid.resetActiveCell(); + let filterValues = args.column.filterValues; + if (filterValues) { + if (filterValues.length === 0) { + // if an associated styling exists with the current filters + if (this.filterStylingMap[args.column.name]) { + let filterLength = this.filterStylingMap[args.column.name].length; + // then remove the filtered styling + for (let i = 0; i < filterLength; i++) { + let lastAppliedStyle = this.filterStylingMap[args.column.name].pop(); + this._table.grid.removeCellCssStyles(lastAppliedStyle[0]); + } + delete this.filterStylingMap[args.column.name]; + let index = this.filterStack.indexOf(args.column.name, 0); + if (index > -1) { + this.filterStack.splice(index, 1); + delete this.filterValueMap[args.column.name]; + } + // apply the previous filter styling + let currentItems = this.dataView.getFilteredItems(); + let styledItems = this.filterValueMap[this.filterStack[this.filterStack.length - 1]][1]; + if (styledItems === currentItems) { + let lastColStyle = this.filterStylingMap[this.filterStack[this.filterStack.length - 1]]; + for (let i = 0; i < lastColStyle.length; i++) { + this._table.grid.setCellCssStyles(lastColStyle[i][0], lastColStyle[i][1]); + } + } else { + // style it all over again + let seenJobs = 0; + for (let i = 0; i < currentItems.length; i++) { + this._table.grid.removeCellCssStyles('error-row' + i.toString()); + this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString()); + let item = this.dataView.getFilteredItems()[i]; + if (item.lastRunOutcome === 'Failed') { + this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name); + if (this.filterStack.indexOf(args.column.name) < 0) { + this.filterStack.push(args.column.name); + this.filterValueMap[args.column.name] = [filterValues]; + } + // one expansion for the row and one for + // the error detail + seenJobs++; + i++; + } + seenJobs++; + } + this.dataView.refresh(); + this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems()); + this._table.grid.resetActiveCell(); + } + if (this.filterStack.length === 0) { + this.filterStack = ['start']; + } + } + } else { + let seenNotebooks = 0; + for (let i = 0; i < this.notebooks.length; i++) { + this._table.grid.removeCellCssStyles('error-row' + i.toString()); + this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString()); + let item = this.dataView.getItemByIdx(i); + // current filter + if (_.contains(filterValues, item[args.column.field])) { + // check all previous filters + if (this.checkPreviousFilters(item)) { + if (item.lastRunOutcome === 'Failed') { + this.addToStyleHash(seenNotebooks, false, this.filterStylingMap, args.column.name); + if (this.filterStack.indexOf(args.column.name) < 0) { + this.filterStack.push(args.column.name); + this.filterValueMap[args.column.name] = [filterValues]; + } + // one expansion for the row and one for + // the error detail + seenNotebooks++; + i++; + } + seenNotebooks++; + } + } + } + this.dataView.refresh(); + if (this.filterValueMap[args.column.name]) { + this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems()); + } else { + this.filterValueMap[args.column.name] = this.dataView.getFilteredItems(); + } + + this._table.grid.resetActiveCell(); + } + } else { + this.expandJobs(false); + } + }); + + this.filterPlugin.onCommand.subscribe((e, args: any) => { + this.columnSort(args.column.name, args.command === 'sort-asc'); + }); + this._table.registerPlugin(this.filterPlugin); + + this.dataView.beginUpdate(); + this.dataView.setItems(jobViews); + this.dataView.setFilter((item) => this.filter(item)); + this.dataView.endUpdate(); + this._table.autosizeColumns(); + this._table.resizeCanvas(); + + this.expandJobs(start); + // tooltip for job name + jQuery('.jobview-jobnamerow').hover(e => { + let currentTarget = e.currentTarget; + currentTarget.title = currentTarget.innerText; + }); + + const self = this; + this._table.grid.onColumnsResized.subscribe((e, data: any) => { + let nameWidth: number = data.grid.getColumns()[1].width; + // adjust job name when resized + jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1 .jobview-jobnametext').css('width', `${nameWidth - 10}px`); + // adjust error message when resized + jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%'); + jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row .jobview-jobnametext').css('width', '100%'); + + // generate job charts again + self.notebooks.forEach(job => { + let jobHistories = self._notebookCacheObject.getNotebookHistory(job.jobId); + if (jobHistories) { + let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length); + self.createJobChart('notebook' + job.jobId, previousRuns); + } + }); + }); + + jQuery('#notebooksDiv .jobnotebooksview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) => + this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2)); + + this._table.grid.onScroll.subscribe((e) => { + jQuery('#notebooksDiv .jobnotebooksview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) => + this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2)); + }); + + // cache the dataview for future use + this._notebookCacheObject.dataView = this.dataView; + this.filterValueMap['start'] = [[], this.dataView.getItems()]; + this.loadJobHistories(); + } + + private highlightErrorRows(e) { + // highlight the error row as well if a failing job row is hovered + if (e.currentTarget.children.item(0).classList.contains('job-with-error')) { + let target = jQuery(e.currentTarget); + let targetChildren = jQuery(e.currentTarget.children); + let siblings = target.nextAll().toArray(); + let top = parseInt(target.css('top'), 10); + for (let i = 0; i < siblings.length; i++) { + let sibling = siblings[i]; + let siblingTop = parseInt(jQuery(sibling).css('top'), 10); + if (siblingTop === top + ROW_HEIGHT) { + jQuery(sibling.children).addClass('hovered'); + sibling.onmouseenter = (e) => { + targetChildren.addClass('hovered'); + }; + sibling.onmouseleave = (e) => { + targetChildren.removeClass('hovered'); + }; + break; + } + } + } + } + + private hightlightNonErrorRows(e) { + // switch back to original background + if (e.currentTarget.children.item(0).classList.contains('job-with-error')) { + let target = jQuery(e.currentTarget); + let siblings = target.nextAll().toArray(); + let top = parseInt(target.css('top'), 10); + for (let i = 0; i < siblings.length; i++) { + let sibling = siblings[i]; + let siblingTop = parseInt(jQuery(sibling).css('top'), 10); + if (siblingTop === top + ROW_HEIGHT) { + jQuery(sibling.children).removeClass('hovered'); + break; + } + } + } + } + + private setRowWithErrorClass(hash: { [index: number]: { [id: string]: string; } }, row: number, errorClass: string) { + hash[row] = { + '_detail_selector': errorClass, + 'id': errorClass, + 'jobId': errorClass, + 'name': errorClass, + 'targetDatabase': errorClass, + 'lastRun': errorClass, + 'nextRun': errorClass, + 'currentExecutionStatus': errorClass, + 'lastRunOutcome': errorClass, + 'previousRuns': errorClass + }; + return hash; + } + + private addToStyleHash(row: number, start: boolean, map: any, columnName: string) { + let hash: { + [index: number]: { + [id: string]: string; + } + } = {}; + hash = this.setRowWithErrorClass(hash, row, 'job-with-error'); + hash = this.setRowWithErrorClass(hash, row + 1, 'error-row'); + if (start) { + if (map['start']) { + map['start'].push(['error-row' + row.toString(), hash]); + } else { + map['start'] = [['error-row' + row.toString(), hash]]; + } + } else { + if (map[columnName]) { + map[columnName].push(['error-row' + row.toString(), hash]); + } else { + map[columnName] = [['error-row' + row.toString(), hash]]; + } + } + this._table.grid.setCellCssStyles('error-row' + row.toString(), hash); + } + + private addToErrorStyleHash(row: number, start: boolean, map: any, columnName: string) { + let hash: { + [index: number]: { + [id: string]: string; + } + } = {}; + hash = this.setRowWithErrorClass(hash, row, 'job-with-error'); + hash = this.setRowWithErrorClass(hash, row + 1, 'notebook-error-row'); + if (start) { + if (map['start']) { + map['start'].push(['notebook-error-row' + row.toString(), hash]); + } else { + map['start'] = [['notebook-error-row' + row.toString(), hash]]; + } + } else { + if (map[columnName]) { + map[columnName].push(['notebook-error-row' + row.toString(), hash]); + } else { + map[columnName] = [['notebook-error-row' + row.toString(), hash]]; + } + } + this._table.grid.setCellCssStyles('notebook-error-row' + row.toString(), hash); + } + + private renderName(row, cell, value, columnDef, dataContext) { + let resultIndicatorClass: string; + switch (dataContext.lastRunOutcome) { + case ('Succeeded'): + resultIndicatorClass = 'jobview-jobnameindicatorsuccess'; + break; + case ('Failed'): + resultIndicatorClass = 'jobview-jobnameindicatorfailure'; + break; + case ('Cancelled'): + resultIndicatorClass = 'jobview-jobnameindicatorcancel'; + break; + case ('Status Unknown'): + resultIndicatorClass = 'jobview-jobnameindicatorunknown'; + break; + case ('Notebook Error'): + resultIndicatorClass = 'jobview-jobnameindicatorcancel'; + break; + default: + resultIndicatorClass = 'jobview-jobnameindicatorfailure'; + break; + } + + return '' + + '' + + '' + + '
' + escape(dataContext.name) + '
'; + } + + private renderChartsPostHistory(row, cell, value, columnDef, dataContext) { + let runChart = this._notebookCacheObject.getRunChart(dataContext.id); + if (runChart && runChart.length > 0) { + return ` + + + + + + + +
${runChart[0] ? runChart[0] : '
'}
${runChart[1] ? runChart[1] : '
'}
${runChart[2] ? runChart[2] : '
'}
${runChart[3] ? runChart[3] : '
'}
${runChart[4] ? runChart[4] : '
'}
`; + } else { + return ` + + + + + + + +
`; + } + } + + private expandJobRowDetails(rowIdx: number, message?: string): void { + let item = this.dataView.getItemByIdx(rowIdx); + item.message = this._agentViewComponent.expandedNotebook.get(item.notebookId); + this.rowDetail.applyTemplateNewLineHeight(item, true); + } + + private async loadJobHistories() { + if (this.notebooks) { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + let separatedJobs = this.separateFailingJobs(); + // grab histories of the failing jobs first + // so they can be expanded quicker + let failing = separatedJobs[0]; + let passing = separatedJobs[1]; + Promise.all([this.curateJobHistory(failing, ownerUri), this.curateJobHistory(passing, ownerUri)]); + } + } + + private separateFailingJobs(): azdata.AgentNotebookInfo[][] { + let failing = []; + let nonFailing = []; + for (let i = 0; i < this.notebooks.length; i++) { + if (this.notebooks[i].lastRunOutcome === 0) { + failing.push(this.notebooks[i]); + } else { + nonFailing.push(this.notebooks[i]); + } + } + return [failing, nonFailing]; + } + + private checkPreviousFilters(item): boolean { + for (let column in this.filterValueMap) { + if (column !== 'start' && this.filterValueMap[column][0].length > 0) { + if (!_.contains(this.filterValueMap[column][0], item[JobManagementUtilities.convertColNameToField(column)])) { + return false; + } + } + } + return true; + } + + private isErrorRow(cell: HTMLElement) { + return cell.classList.contains('error-row') || cell.classList.contains('notebook-error-row'); + } + + private getNotebook(args: Slick.OnClickEventArgs): azdata.AgentNotebookInfo { + let row = args.row; + let notebookName: string; + let cell = args.grid.getCellNode(row, 1); + if (this.isErrorRow(cell)) { + notebookName = args.grid.getCellNode(row - 1, 1).innerText.trim(); + } else { + notebookName = cell.innerText.trim(); + } + let notebook = this.notebooks.filter(job => job.name === notebookName)[0]; + return notebook; + } + + private async curateJobHistory(notebooks: azdata.AgentNotebookInfo[], ownerUri: string) { + const self = this; + for (let notebook of notebooks) { + let result = await this._jobManagementService.getNotebookHistory(ownerUri, notebook.jobId, notebook.name, notebook.targetDatabase); + if (result) { + self.jobSteps[notebook.jobId] = result.steps ? result.steps : []; + self.jobSchedules[notebook.jobId] = result.schedules ? result.schedules : []; + self.notebookHistories[notebook.jobId] = result.histories ? result.histories : []; + self._notebookCacheObject.setJobSteps(notebook.jobId, self.jobSteps[notebook.jobId]); + self._notebookCacheObject.setNotebookHistory(notebook.jobId, self.notebookHistories[notebook.jobId]); + self._notebookCacheObject.setJobSchedules(notebook.jobId, self.jobSchedules[notebook.jobId]); + let notebookHistories = self._notebookCacheObject.getNotebookHistory(notebook.jobId); + let previousRuns: azdata.AgentNotebookHistoryInfo[]; + if (notebookHistories.length >= 5) { + previousRuns = notebookHistories.slice(notebookHistories.length - 5, notebookHistories.length); + } else { + previousRuns = notebookHistories; + } + + if (self._agentViewComponent.expandedNotebook.has(notebook.jobId)) { + let lastJobHistory = notebookHistories[notebookHistories.length - 1]; + let item = self.dataView.getItemById('notebook' + notebook.jobId + '.error'); + let noStepsMessage = nls.localize('notebooksView.noSteps', "No Steps available for this job."); + let errorMessage = lastJobHistory ? lastJobHistory.message : noStepsMessage; + if (item) { + if (notebook.lastRunNotebookError.length === 0) { + item['name'] = nls.localize('notebooksView.error', "Error: ") + errorMessage; + } + else { + item['name'] = nls.localize('notebooksView.notebookError', "Notebook Error: ") + notebook.lastRunNotebookError; + } + self._agentViewComponent.setExpandedNotebook(notebook.jobId, item['name']); + self.dataView.updateItem('notebook' + notebook.jobId + '.error', item); + } + } + self.createJobChart('notebook' + notebook.jobId, previousRuns); + } + } + } + + private createJobChart(jobId: string, jobHistories: azdata.AgentNotebookHistoryInfo[]): void { + let chartHeights = this.getChartHeights(jobHistories); + let runCharts = []; + for (let i = 0; i < chartHeights.length; i++) { + let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green'; + if (jobHistories[i].materializedNotebookErrorInfo !== null && jobHistories[i].materializedNotebookErrorInfo.length > 0) { + bgColor = 'orange'; + } + let runGraph = jQuery(`table.jobprevruns#${jobId} > tbody > tr > td > div.bar${i}`); + if (runGraph.length > 0) { + + runGraph.css('height', chartHeights[i]); + + runGraph.css('background', bgColor); + runGraph.hover((e) => { + let currentTarget = e.currentTarget; + currentTarget.title = jobHistories[i].runDuration; + }); + runCharts.push(runGraph.get(0).outerHTML); + } + } + if (runCharts.length > 0) { + this._notebookCacheObject.setRunChart(jobId, runCharts); + } + this._cd.detectChanges(); + } + + // chart height normalization logic + private getChartHeights(jobHistories: azdata.AgentJobHistoryInfo[]): string[] { + if (!jobHistories || jobHistories.length === 0) { + return []; + } + let maxDuration: number = 0; + jobHistories.forEach(history => { + let historyDuration = JobManagementUtilities.convertDurationToSeconds(history.runDuration); + if (historyDuration > maxDuration) { + maxDuration = historyDuration; + } + }); + maxDuration = maxDuration === 0 ? 1 : maxDuration; + let maxBarHeight: number = 24; + let chartHeights = []; + let zeroDurationJobCount = 0; + for (let i = 0; i < jobHistories.length; i++) { + let duration = jobHistories[i].runDuration; + let chartHeight = (maxBarHeight * JobManagementUtilities.convertDurationToSeconds(duration)) / maxDuration; + chartHeights.push(`${chartHeight}px`); + if (chartHeight === 0) { + zeroDurationJobCount++; + } + } + // if the durations are all 0 secs, show minimal chart + // instead of nothing + if (zeroDurationJobCount === jobHistories.length) { + return Array(jobHistories.length).fill('5px'); + } else { + return chartHeights; + } + } + + private expandJobs(start: boolean): void { + if (start) { + this._agentViewComponent.expandedNotebook = new Map(); + } + let expandedJobs = this._agentViewComponent.expandedNotebook; + let expansions = 0; + for (let i = 0; i < this.notebooks.length; i++) { + let notebook = this.notebooks[i]; + if (notebook.lastRunOutcome === 0 && !expandedJobs.get(notebook.jobId)) { + this.expandJobRowDetails(i + expandedJobs.size); + this.addToStyleHash(i + expandedJobs.size, start, this.filterStylingMap, undefined); + this._agentViewComponent.setExpandedNotebook(notebook.jobId, 'Loading Error...'); + } else if (notebook.lastRunOutcome === 0 && expandedJobs.get(notebook.jobId)) { + this.addToStyleHash(i + expansions, start, this.filterStylingMap, undefined); + expansions++; + } else if (notebook.lastRunNotebookError !== '' && !expandedJobs.get(notebook.jobId)) { + this.expandJobRowDetails(i + expandedJobs.size); + this.addToErrorStyleHash(i + expandedJobs.size, start, this.filterStylingMap, undefined); + this._agentViewComponent.setExpandedNotebook(notebook.jobId, notebook.lastRunNotebookError); + } else if (notebook.lastRunNotebookError !== '' && expandedJobs.get(notebook.jobId)) { + this.addToErrorStyleHash(i + expansions, start, this.filterStylingMap, undefined); + expansions++; + } + } + } + + private filter(item: any) { + let columns = this._table.grid.getColumns(); + let value = true; + for (let i = 0; i < columns.length; i++) { + let col: any = columns[i]; + let filterValues = col.filterValues; + if (filterValues && filterValues.length > 0) { + if (item._parent) { + value = value && _.contains(filterValues, item._parent[col.field]); + } else { + value = value && _.contains(filterValues, item[col.field]); + } + } + } + return value; + } + + private columnSort(column: string, isAscending: boolean) { + let items = this.dataView.getItems(); + // get error items here and remove them + let jobItems = items.filter(x => x._parent === undefined); + let errorItems = items.filter(x => x._parent !== undefined); + this.sortingStylingMap[column] = items; + switch (column) { + case ('Name'): { + this.dataView.setItems(jobItems); + // sort the actual jobs + this.dataView.sort((item1, item2) => { + return item1.name.localeCompare(item2.name); + }, isAscending); + break; + } + case ('Last Run'): { + this.dataView.setItems(jobItems); + // sort the actual jobs + this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, true), isAscending); + break; + } + case ('Next Run'): { + this.dataView.setItems(jobItems); + // sort the actual jobs + this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, false), isAscending); + break; + } + case ('Status'): { + this.dataView.setItems(jobItems); + // sort the actual jobs + this.dataView.sort((item1, item2) => { + return item1.currentExecutionStatus.localeCompare(item2.currentExecutionStatus); + }, isAscending); + break; + } + case ('Last Run Outcome'): { + this.dataView.setItems(jobItems); + // sort the actual jobs + this.dataView.sort((item1, item2) => { + return item1.lastRunOutcome.localeCompare(item2.lastRunOutcome); + }, isAscending); + break; + } + } + // insert the errors back again + let jobItemsLength = jobItems.length; + for (let i = 0; i < jobItemsLength; i++) { + let item = jobItems[i]; + if (item._child) { + let child = errorItems.find(error => error === item._child); + jobItems.splice(i + 1, 0, child); + jobItemsLength++; + } + } + this.dataView.setItems(jobItems); + // remove old style + if (this.filterStylingMap[column]) { + let filterLength = this.filterStylingMap[column].length; + for (let i = 0; i < filterLength; i++) { + let lastAppliedStyle = this.filterStylingMap[column].pop(); + this._table.grid.removeCellCssStyles(lastAppliedStyle[0]); + } + } else { + for (let i = 0; i < this.notebooks.length; i++) { + this._table.grid.removeCellCssStyles('error-row' + i.toString()); + this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString()); + } + } + // add new style to the items back again + items = this.filterStack.length > 1 ? this.dataView.getFilteredItems() : this.dataView.getItems(); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (item.lastRunOutcome === 'Failed') { + this.addToStyleHash(i, false, this.sortingStylingMap, column); + } + } + } + + private dateCompare(item1: any, item2: any, lastRun: boolean): number { + let exceptionString = lastRun ? 'Never Run' : 'Not Scheduled'; + if (item2.lastRun === exceptionString && item1.lastRun !== exceptionString) { + return -1; + } else if (item1.lastRun === exceptionString && item2.lastRun !== exceptionString) { + return 1; + } else if (item1.lastRun === exceptionString && item2.lastRun === exceptionString) { + return 0; + } else { + let date1 = new Date(item1.lastRun); + let date2 = new Date(item2.lastRun); + if (date1 > date2) { + return 1; + } else if (date1 === date2) { + return 0; + } else { + return -1; + } + } + } + + private updateTheme(theme: IColorTheme) { + let bgColor = theme.getColor(tableBackground); + let cellColor = theme.getColor(cellBackground); + let borderColor = theme.getColor(cellBorderColor); + let headerColumns = jQuery('#agentViewDiv .slick-header-column'); + let cells = jQuery('.grid-canvas .ui-widget-content.slick-row .slick-cell'); + let cellDetails = jQuery('#notebooksDiv .dynamic-cell-detail'); + headerColumns.toArray().forEach(col => { + col.style.background = bgColor.toString(); + }); + cells.toArray().forEach(cell => { + cell.style.background = bgColor.toString(); + cell.style.border = borderColor ? '1px solid ' + borderColor.toString() : null; + }); + cellDetails.toArray().forEach(cellDetail => { + cellDetail.style.background = cellColor.toString(); + }); + } + + protected getTableActions(targetObject: JobActionContext): IAction[] { + const editAction = this._instantiationService.createInstance(EditJobAction); + const editNotebookAction = this._instantiationService.createInstance(EditNotebookJobAction); + const runJobAction = this._instantiationService.createInstance(RunJobAction); + return [ + runJobAction, + editNotebookAction, + editAction, + this._instantiationService.createInstance(DeleteNotebookAction) + ]; + } + + protected convertStepsToStepInfos(steps: azdata.AgentJobStep[], job: azdata.AgentJobInfo): azdata.AgentJobStepInfo[] { + let result = []; + steps.forEach(step => { + let stepInfo: azdata.AgentJobStepInfo = { + jobId: job.jobId, + jobName: job.name, + script: null, + scriptName: null, + stepName: step.stepName, + subSystem: null, + id: +step.stepId, + failureAction: null, + successAction: null, + failStepId: null, + successStepId: null, + command: null, + commandExecutionSuccessCode: null, + databaseName: null, + databaseUserName: null, + server: null, + outputFileName: null, + appendToLogFile: null, + appendToStepHist: null, + writeLogToTable: null, + appendLogToTable: null, + retryAttempts: null, + retryInterval: null, + proxyName: null + }; + result.push(stepInfo); + }); + return result; + } + + protected getCurrentTableObject(rowIndex: number): JobActionContext { + let data = this._table.grid.getData() as Slick.DataProvider; + if (!data || rowIndex >= data.getLength()) { + return undefined; + } + + let notebookID = data.getItem(rowIndex).notebookId; + if (!notebookID) { + // if we couldn't find the ID, check if it's an + // error row + let isErrorRow: boolean = data.getItem(rowIndex).id.indexOf('error') >= 0; + if (isErrorRow) { + notebookID = data.getItem(rowIndex - 1).notebookId; + } + } + + let notebook: azdata.AgentNotebookInfo[] = this.notebooks.filter(job => { + return job.jobId === notebookID; + }); + + if (notebook && notebook.length > 0) { + // add steps + if (this.jobSteps && this.jobSteps[notebookID]) { + let steps = this.jobSteps[notebookID]; + notebook[0].jobSteps = steps; + } + + // add schedules + if (this.jobSchedules && this.jobSchedules[notebookID]) { + let schedules = this.jobSchedules[notebookID]; + notebook[0].jobSchedules = schedules; + } + // add alerts + if (this.jobAlerts && this.jobAlerts[notebookID]) { + let alerts = this.jobAlerts[notebookID]; + notebook[0].alerts = alerts; + } + + if (notebook[0].jobSteps && notebook[0].jobSchedules && notebook[0].alerts) { + return { job: notebook[0], canEdit: true }; + } + return { job: notebook[0], canEdit: false }; + } + return undefined; + } + + public async openCreateJobDialog() { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + await this._commandService.executeCommand('agent.openJobDialog', ownerUri); + } + + public async openCreateNotebookDialog() { + let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + await this._commandService.executeCommand('agent.openNotebookDialog', ownerUri); + } +}