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:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user