mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Agent Notebooks Scheduler (#6786)
* added agent notebooks, notebook history view and view materialized notebook button * Got a basic UI running for viewing notebook history * made some changes to make UI look good * Added new notebook dialog * Added new notebook Dialog * Added create notebook dialog * Added edit and delete notebook job * Added some notebook history features * Added new notebook job icons, fixed a minor bug in openmaterializednotebookAPI and added fixed the schedule Picker API. * Fixed Bugs in Notebook Grid expansion * Fixed Notebook table highlighting and grid generation is done using code. * fixed some UI bugs * Added changes to reflect sqltoolservice api * Fixed some localize keys * Made changes in the PR and added ability to open Template Notebooks from notebook history view. * Added pin and renaming to notebook history * made some library calls async * fixed an import bug caused by merging from master * Validation in NotebookJobDialog * Added entry points for scheduling notebooks on file explorer and notebook editor * Handled no active connections and a small bug in collapsing grid * fix a bug in scheduling notebook from explorer and toolbar * setting up agent providers from connection now * changed modals * Reupload edited template * Add dialog info, solved an edit bug and localized UI strings. * Bug fixes in UI, notebook renaming and editing template on fly. * fixed a bug that failed editing notebook jobs from notebook jobs table * Fixed a cyclic dependency, made strings const and some other changes in the PR * Made some cyclic dependency and some fixes from PR * made some changes mentioned in the PR * Changed storage database health text * Changed the sqltoolservice version to the point to the latest build.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
1
extensions/agent/resources/dark/notebook_inverse.svg
Normal file
1
extensions/agent/resources/dark/notebook_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>notebook_inverse</title><path class="cls-1" d="M15.46,2V15H.46V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.46,1h3V2Zm-14,12h6.3a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.46,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.46,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31,4.43,4.43,0,0,0-.51.43h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 734 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#0095d7;}</style></defs><title>open_notebook_inverse</title><path class="cls-1" d="M12.55,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9.18a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.29,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.93,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.67,3.2H2v.9H.15V15.85H13.72V5.48ZM2.86,4.1H4.67a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.86ZM1,15V5H2v9H4.67a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39ZM12.8,15H7.11a2.7,2.7,0,0,1,.47-.39A2.83,2.83,0,0,1,8,14.28a3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9.18,14h2.73V5h.89Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><path class="cls-2" d="M13.19,3.57h0v0Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><polygon class="cls-2" points="14.21 1.65 14.19 1.65 14.19 1.63 14.21 1.65"/><path class="cls-2" d="M15.91,2.1,14.2,3.81l-.38.38-.62-.61v0l1-1H12.79a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47A3.41,3.41,0,0,0,9.5,6.11H8.6a4.68,4.68,0,0,1,.16-1.19A4.74,4.74,0,0,1,9,4.26a2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16,2.79,2.79,0,0,1,.34-.18l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.82,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
extensions/agent/resources/light/notebook.svg
Normal file
1
extensions/agent/resources/light/notebook.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>notebook</title><path d="M15.5,2V15H.5V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.5,1h3V2ZM1.5,14H7.8a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.5,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.5,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31A4.43,4.43,0,0,0,8.2,14h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 661 B |
1
extensions/agent/resources/light/open_notebook.svg
Normal file
1
extensions/agent/resources/light/open_notebook.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#00539c;}</style></defs><title>open_notebook</title><path d="M12.4,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.14,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.78,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.52,3.2H1.81v.9H0V15.85H13.57V5.48ZM2.71,4.1H4.52a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.71ZM.9,15V5h.91v9H4.52a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39Zm11.75,0H7a2.7,2.7,0,0,1,.47-.39,2.83,2.83,0,0,1,.47-.29,3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9,14h2.73V5h.89Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><path class="cls-1" d="M13,3.57h0v0Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><polygon class="cls-1" points="14.06 1.65 14.04 1.65 14.04 1.63 14.06 1.65"/><path class="cls-1" d="M15.76,2.1,14,3.81l-.38.38L13,3.58v0l1-1H12.64a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47,3.41,3.41,0,0,0-.27,1.39h-.9a4.68,4.68,0,0,1,.16-1.19,4.74,4.74,0,0,1,.25-.66,2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16A2.79,2.79,0,0,1,11,2.08l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.67,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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<azdata.AgentServicesProvider>(connection.providerName, azdata.DataProviderType.AgentServicesProvider);
|
||||
this._connectionService = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(connection.providerName, azdata.DataProviderType.ConnectionProvider);
|
||||
this._queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(connection.providerName, azdata.DataProviderType.QueryProvider);
|
||||
}
|
||||
|
||||
public static async getAgentService(): Promise<azdata.AgentServicesProvider> {
|
||||
if (!AgentUtils._agentService) {
|
||||
let currentConnection = await azdata.connection.getCurrentConnection();
|
||||
@@ -41,4 +50,20 @@ export class AgentUtils {
|
||||
return this._queryProvider;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function exists(path: string): Promise<boolean> {
|
||||
return promisify(fs.exists)(path);
|
||||
}
|
||||
|
||||
export function mkdir(path: string): Promise<void> {
|
||||
return promisify(fs.mkdir)(path);
|
||||
}
|
||||
|
||||
export function unlink(path: string): Promise<void> {
|
||||
return promisify(fs.unlink)(path);
|
||||
}
|
||||
|
||||
export function writeFile(path: string, data: string): Promise<void> {
|
||||
return promisify(fs.writeFile)(path, data);
|
||||
}
|
||||
|
||||
260
extensions/agent/src/data/notebookData.ts
Normal file
260
extensions/agent/src/data/notebookData.ts
Normal file
@@ -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: '',
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
isFile: false
|
||||
}).component();
|
||||
this.openButton.onDidClick(e => {
|
||||
let queryContent = e;
|
||||
let queryContent = e.fileContent;
|
||||
this.commandTextBox.value = queryContent;
|
||||
});
|
||||
this.parseButton.onDidClick(e => {
|
||||
|
||||
340
extensions/agent/src/dialogs/notebookDialog.ts
Normal file
340
extensions/agent/src/dialogs/notebookDialog.ts
Normal file
@@ -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<NotebookData> {
|
||||
|
||||
// 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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<string, TemplateMapObject>();
|
||||
// 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, <NotebookDialogOptions>{ 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, <NotebookDialogOptions>{ notebookInfo: notebookInfo });
|
||||
}
|
||||
if (!this.notebookDialog.isOpen) {
|
||||
this.notebookDialog.dialogName ? await this.notebookDialog.openDialog(this.notebookDialog.dialogName) : await this.notebookDialog.openDialog();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getConnectionFromUser(): Promise<azdata.connection.Connection> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<DeleteAgentJobStepParams, azdata.ResultStatus, void, void>('agent/deletejobstep');
|
||||
}
|
||||
|
||||
// Notebooks request
|
||||
export namespace AgentNotebooksRequest {
|
||||
export const type = new RequestType<AgentNotebookParams, azdata.AgentNotebooksResult, void, void>('agent/notebooks');
|
||||
}
|
||||
|
||||
export namespace AgentNotebookHistoryRequest {
|
||||
export const type = new RequestType<AgentNotebookHistoryParams, azdata.AgentNotebookHistoryResult, void, void>('agent/notebookhistory');
|
||||
}
|
||||
|
||||
export namespace AgentNotebookMaterializedRequest {
|
||||
export const type = new RequestType<AgentNotebookMaterializedParams, azdata.AgentNotebookMaterializedResult, void, void>('agent/notebookmaterialized');
|
||||
}
|
||||
|
||||
export namespace UpdateAgentNotebookRunNameRequest {
|
||||
export const type = new RequestType<UpdateAgentNotebookRunNameParams, azdata.UpdateAgentNotebookResult, void, void>('agent/updatenotebookname');
|
||||
}
|
||||
|
||||
export namespace DeleteMaterializedNotebookRequest {
|
||||
export const type = new RequestType<DeleteAgentMaterializedNotebookParams, azdata.ResultStatus, void, void>('agent/deletenotebookmaterialized');
|
||||
}
|
||||
|
||||
export namespace UpdateAgentNotebookRunPinRequest {
|
||||
export const type = new RequestType<UpdateAgentNotebookRunPinParams, azdata.ResultStatus, void, void>('agent/updatenotebookpin');
|
||||
}
|
||||
|
||||
export namespace AgentNotebookTemplateRequest {
|
||||
export const type = new RequestType<AgentNotebookTemplateParams, azdata.ResultStatus, void, void>('agent/notebooktemplate');
|
||||
}
|
||||
|
||||
export namespace CreateAgentNotebookRequest {
|
||||
export const type = new RequestType<CreateAgentNotebookParams, azdata.CreateAgentNotebookResult, void, void>('agent/createnotebook');
|
||||
}
|
||||
|
||||
export namespace DeleteAgentNotebookRequest {
|
||||
export const type = new RequestType<DeleteAgentNotebookParams, azdata.ResultStatus, void, void>('agent/deletenotebook');
|
||||
}
|
||||
|
||||
export namespace UpdateAgentNotebookRequest {
|
||||
export const type = new RequestType<UpdateAgentNotebookParams, azdata.UpdateAgentNotebookResult, void, void>('agent/updatenotebook');
|
||||
}
|
||||
|
||||
// Alerts requests
|
||||
export namespace AgentAlertsRequest {
|
||||
export const type = new RequestType<CreateAgentAlertParams, azdata.AgentAlertsResult, void, void>('agent/alerts');
|
||||
|
||||
@@ -227,6 +227,151 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
||||
);
|
||||
};
|
||||
|
||||
// Notebook Management methods
|
||||
const getNotebooks = (ownerUri: string): Thenable<azdata.AgentNotebooksResult> => {
|
||||
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<azdata.AgentNotebookHistoryResult> => {
|
||||
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<azdata.AgentNotebookMaterializedResult> => {
|
||||
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<azdata.AgentNotebookTemplateResult> => {
|
||||
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<azdata.CreateAgentNotebookResult> => {
|
||||
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<azdata.UpdateAgentNotebookResult> => {
|
||||
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<azdata.ResultStatus> => {
|
||||
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<azdata.ResultStatus> => {
|
||||
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<azdata.ResultStatus> => {
|
||||
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<azdata.ResultStatus> => {
|
||||
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<azdata.AgentAlertsResult> => {
|
||||
let params: contracts.AgentAlertsParams = {
|
||||
@@ -535,6 +680,16 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
||||
createJobStep,
|
||||
updateJobStep,
|
||||
deleteJobStep,
|
||||
getNotebooks,
|
||||
getNotebookHistory,
|
||||
getMaterializedNotebook,
|
||||
getTemplateNotebook,
|
||||
createNotebook,
|
||||
updateNotebook,
|
||||
deleteMaterializedNotebook,
|
||||
updateNotebookMaterializedName,
|
||||
updateNotebookMaterializedPin,
|
||||
deleteNotebook,
|
||||
getAlerts,
|
||||
createAlert,
|
||||
updateAlert,
|
||||
|
||||
Reference in New Issue
Block a user