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:
Aasim Khan
2019-09-04 15:12:35 -07:00
committed by GitHub
parent 0a393400b2
commit fbb2accacb
48 changed files with 3948 additions and 149 deletions

View File

@@ -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);
}

View 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: '',
};
}
}

View File

@@ -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 => {

View 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;
}
}

View File

@@ -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() {

View File

@@ -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;
}
/**