mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Merge from master
This commit is contained in:
712
extensions/agent/src/dialogs/jobDialog.ts
Normal file
712
extensions/agent/src/dialogs/jobDialog.ts
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 sqlops from 'sqlops';
|
||||||
|
import { JobData } from '../data/jobData';
|
||||||
|
import { JobStepDialog } from './jobStepDialog';
|
||||||
|
import { PickScheduleDialog } from './pickScheduleDialog';
|
||||||
|
import { AlertDialog } from './alertDialog';
|
||||||
|
import { AgentDialog } from './agentDialog';
|
||||||
|
import { AgentUtils } from '../agentUtils';
|
||||||
|
import { JobStepData } from '../data/jobStepData';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class JobDialog extends AgentDialog<JobData> {
|
||||||
|
|
||||||
|
// TODO: localize
|
||||||
|
// Top level
|
||||||
|
private static readonly CreateDialogTitle: string = localize('jobDialog.newJob', 'New Job');
|
||||||
|
private static readonly EditDialogTitle: string = localize('jobDialog.editJob', 'Edit Job');
|
||||||
|
private readonly GeneralTabText: string = localize('jobDialog.general', 'General');
|
||||||
|
private readonly StepsTabText: string = localize('jobDialog.steps', 'Steps');
|
||||||
|
private readonly SchedulesTabText: string = localize('jobDialog.schedules', 'Schedules');
|
||||||
|
private readonly AlertsTabText: string = localize('jobDialog.alerts', 'Alerts');
|
||||||
|
private readonly NotificationsTabText: string = localize('jobDialog.notifications', 'Notifications');
|
||||||
|
private readonly BlankJobNameErrorText: string = localize('jobDialog.blankJobNameError', 'The name of the job cannot be blank.');
|
||||||
|
|
||||||
|
// General tab strings
|
||||||
|
private readonly NameTextBoxLabel: string = localize('jobDialog.name', 'Name');
|
||||||
|
private readonly OwnerTextBoxLabel: string = localize('jobDialog.owner', 'Owner');
|
||||||
|
private readonly CategoryDropdownLabel: string = localize('jobDialog.category', 'Category');
|
||||||
|
private readonly DescriptionTextBoxLabel: string = localize('jobDialog.description', 'Description');
|
||||||
|
private readonly EnabledCheckboxLabel: string = localize('jobDialog.enabled', 'Enabled');
|
||||||
|
|
||||||
|
// Steps tab strings
|
||||||
|
private readonly JobStepsTopLabelString: string = localize('jobDialog.jobStepList', 'Job step list');
|
||||||
|
private readonly StepsTable_StepColumnString: string = localize('jobDialog.step', 'Step');
|
||||||
|
private readonly StepsTable_NameColumnString: string = localize('jobDialog.name', 'Name');
|
||||||
|
private readonly StepsTable_TypeColumnString: string = localize('jobDialog.type', 'Type');
|
||||||
|
private readonly StepsTable_SuccessColumnString: string = localize('jobDialog.onSuccess', 'On Success');
|
||||||
|
private readonly StepsTable_FailureColumnString: string = localize('jobDialog.onFailure', 'On Failure');
|
||||||
|
private readonly NewStepButtonString: string = localize('jobDialog.new', 'New Step');
|
||||||
|
private readonly EditStepButtonString: string = localize('jobDialog.edit', 'Edit Step');
|
||||||
|
private readonly DeleteStepButtonString: string = localize('jobDialog.delete', 'Delete Step');
|
||||||
|
private readonly MoveStepUpButtonString: string = localize('jobDialog.moveUp', 'Move Step Up');
|
||||||
|
private readonly MoveStepDownButtonString: string = localize('jobDialog.moveDown', 'Move Step Down');
|
||||||
|
private readonly StartStepDropdownString: string = localize('jobDialog.startStepAt', 'Start step');
|
||||||
|
|
||||||
|
// Notifications tab strings
|
||||||
|
private readonly NotificationsTabTopLabelString: string = localize('jobDialog.notificationsTabTop', 'Actions to perform when the job completes');
|
||||||
|
private readonly EmailCheckBoxString: string = localize('jobDialog.email', 'Email');
|
||||||
|
private readonly PagerCheckBoxString: string = localize('jobDialog.page', 'Page');
|
||||||
|
private readonly EventLogCheckBoxString: string = localize('jobDialog.eventLogCheckBoxLabel', 'Write to the Windows Application event log');
|
||||||
|
private readonly DeleteJobCheckBoxString: string = localize('jobDialog.deleteJobLabel', 'Automatically delete job');
|
||||||
|
|
||||||
|
// Schedules tab strings
|
||||||
|
private readonly SchedulesTopLabelString: string = localize('jobDialog.schedulesaLabel', 'Schedules list');
|
||||||
|
private readonly PickScheduleButtonString: string = localize('jobDialog.pickSchedule', 'Pick Schedule');
|
||||||
|
private readonly ScheduleNameLabelString: string = localize('jobDialog.scheduleNameLabel', 'Schedule Name');
|
||||||
|
|
||||||
|
// Alerts tab strings
|
||||||
|
private readonly AlertsTopLabelString: string = localize('jobDialog.alertsList', 'Alerts list');
|
||||||
|
private readonly NewAlertButtonString: string = localize('jobDialog.newAlert', 'New Alert');
|
||||||
|
private readonly AlertNameLabelString: string = localize('jobDialog.alertNameLabel', 'Alert Name');
|
||||||
|
private readonly AlertEnabledLabelString: string = localize('jobDialog.alertEnabledLabel', 'Enabled');
|
||||||
|
private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type');
|
||||||
|
|
||||||
|
// Event Name strings
|
||||||
|
private readonly NewJobDialogEvent: string = 'NewJobDialogOpened';
|
||||||
|
private readonly EditJobDialogEvent: string = 'EditJobDialogOpened';
|
||||||
|
|
||||||
|
// UI Components
|
||||||
|
private generalTab: sqlops.window.DialogTab;
|
||||||
|
private stepsTab: sqlops.window.DialogTab;
|
||||||
|
private alertsTab: sqlops.window.DialogTab;
|
||||||
|
private schedulesTab: sqlops.window.DialogTab;
|
||||||
|
private notificationsTab: sqlops.window.DialogTab;
|
||||||
|
|
||||||
|
// General tab controls
|
||||||
|
private nameTextBox: sqlops.InputBoxComponent;
|
||||||
|
private ownerTextBox: sqlops.InputBoxComponent;
|
||||||
|
private categoryDropdown: sqlops.DropDownComponent;
|
||||||
|
private descriptionTextBox: sqlops.InputBoxComponent;
|
||||||
|
private enabledCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
|
||||||
|
// Steps tab controls
|
||||||
|
private stepsTable: sqlops.TableComponent;
|
||||||
|
private newStepButton: sqlops.ButtonComponent;
|
||||||
|
private moveStepUpButton: sqlops.ButtonComponent;
|
||||||
|
private moveStepDownButton: sqlops.ButtonComponent;
|
||||||
|
private editStepButton: sqlops.ButtonComponent;
|
||||||
|
private deleteStepButton: sqlops.ButtonComponent;
|
||||||
|
|
||||||
|
// Schedule tab controls
|
||||||
|
private removeScheduleButton: sqlops.ButtonComponent;
|
||||||
|
|
||||||
|
// Notifications tab controls
|
||||||
|
private notificationsTabTopLabel: sqlops.TextComponent;
|
||||||
|
private emailCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
private emailOperatorDropdown: sqlops.DropDownComponent;
|
||||||
|
private emailConditionDropdown: sqlops.DropDownComponent;
|
||||||
|
private pagerCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
private pagerOperatorDropdown: sqlops.DropDownComponent;
|
||||||
|
private pagerConditionDropdown: sqlops.DropDownComponent;
|
||||||
|
private eventLogCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
private eventLogConditionDropdown: sqlops.DropDownComponent;
|
||||||
|
private deleteJobCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
private deleteJobConditionDropdown: sqlops.DropDownComponent;
|
||||||
|
private startStepDropdown: sqlops.DropDownComponent;
|
||||||
|
|
||||||
|
// Schedule tab controls
|
||||||
|
private schedulesTable: sqlops.TableComponent;
|
||||||
|
private pickScheduleButton: sqlops.ButtonComponent;
|
||||||
|
|
||||||
|
// Alert tab controls
|
||||||
|
private alertsTable: sqlops.TableComponent;
|
||||||
|
private newAlertButton: sqlops.ButtonComponent;
|
||||||
|
private isEdit: boolean = false;
|
||||||
|
|
||||||
|
// Job objects
|
||||||
|
private steps: sqlops.AgentJobStepInfo[];
|
||||||
|
private schedules: sqlops.AgentJobScheduleInfo[];
|
||||||
|
private alerts: sqlops.AgentAlertInfo[] = [];
|
||||||
|
private startStepDropdownValues: sqlops.CategoryValue[] = [];
|
||||||
|
|
||||||
|
constructor(ownerUri: string, jobInfo: sqlops.AgentJobInfo = undefined) {
|
||||||
|
super(
|
||||||
|
ownerUri,
|
||||||
|
new JobData(ownerUri, jobInfo),
|
||||||
|
jobInfo ? JobDialog.EditDialogTitle : JobDialog.CreateDialogTitle);
|
||||||
|
this.steps = this.model.jobSteps ? this.model.jobSteps : [];
|
||||||
|
this.schedules = this.model.jobSchedules ? this.model.jobSchedules : [];
|
||||||
|
this.alerts = this.model.alerts ? this.model.alerts : [];
|
||||||
|
this.isEdit = jobInfo ? true : false;
|
||||||
|
this.dialogName = this.isEdit ? this.EditJobDialogEvent : this.NewJobDialogEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async initializeDialog() {
|
||||||
|
this.generalTab = sqlops.window.createTab(this.GeneralTabText);
|
||||||
|
this.stepsTab = sqlops.window.createTab(this.StepsTabText);
|
||||||
|
this.alertsTab = sqlops.window.createTab(this.AlertsTabText);
|
||||||
|
this.schedulesTab = sqlops.window.createTab(this.SchedulesTabText);
|
||||||
|
this.notificationsTab = sqlops.window.createTab(this.NotificationsTabText);
|
||||||
|
this.initializeGeneralTab();
|
||||||
|
this.initializeStepsTab();
|
||||||
|
this.initializeAlertsTab();
|
||||||
|
this.initializeSchedulesTab();
|
||||||
|
this.initializeNotificationsTab();
|
||||||
|
this.dialog.content = [this.generalTab, this.stepsTab, this.schedulesTab, this.alertsTab, this.notificationsTab];
|
||||||
|
this.dialog.registerCloseValidator(() => {
|
||||||
|
this.updateModel();
|
||||||
|
let validationResult = this.model.validate();
|
||||||
|
if (!validationResult.valid) {
|
||||||
|
// TODO: Show Error Messages
|
||||||
|
console.error(validationResult.errorMessages.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResult.valid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeGeneralTab() {
|
||||||
|
this.generalTab.registerContent(async view => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.ownerTextBox = view.modelBuilder.inputBox().component();
|
||||||
|
this.categoryDropdown = view.modelBuilder.dropDown().component();
|
||||||
|
this.descriptionTextBox = view.modelBuilder.inputBox().withProperties({
|
||||||
|
multiline: true,
|
||||||
|
height: 200
|
||||||
|
}).component();
|
||||||
|
this.enabledCheckBox = view.modelBuilder.checkBox()
|
||||||
|
.withProperties({
|
||||||
|
label: this.EnabledCheckboxLabel
|
||||||
|
}).component();
|
||||||
|
let formModel = view.modelBuilder.formContainer()
|
||||||
|
.withFormItems([{
|
||||||
|
component: this.nameTextBox,
|
||||||
|
title: this.NameTextBoxLabel
|
||||||
|
}, {
|
||||||
|
component: this.ownerTextBox,
|
||||||
|
title: this.OwnerTextBoxLabel
|
||||||
|
}, {
|
||||||
|
component: this.categoryDropdown,
|
||||||
|
title: this.CategoryDropdownLabel
|
||||||
|
}, {
|
||||||
|
component: this.descriptionTextBox,
|
||||||
|
title: this.DescriptionTextBoxLabel
|
||||||
|
}, {
|
||||||
|
component: this.enabledCheckBox,
|
||||||
|
title: ''
|
||||||
|
}]).withLayout({ width: '100%' }).component();
|
||||||
|
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
|
||||||
|
this.nameTextBox.value = this.model.name;
|
||||||
|
this.ownerTextBox.value = this.model.defaultOwner;
|
||||||
|
this.categoryDropdown.values = this.model.jobCategories;
|
||||||
|
|
||||||
|
let idx: number = undefined;
|
||||||
|
if (this.model.category && this.model.category !== '') {
|
||||||
|
idx = this.model.jobCategories.indexOf(this.model.category);
|
||||||
|
}
|
||||||
|
this.categoryDropdown.value = this.model.jobCategories[idx > 0 ? idx : 0];
|
||||||
|
|
||||||
|
this.enabledCheckBox.checked = this.model.enabled;
|
||||||
|
this.descriptionTextBox.value = this.model.description;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeStepsTab() {
|
||||||
|
this.stepsTab.registerContent(async view => {
|
||||||
|
let data = this.steps ? this.convertStepsToData(this.steps) : [];
|
||||||
|
this.stepsTable = view.modelBuilder.table()
|
||||||
|
.withProperties({
|
||||||
|
columns: [
|
||||||
|
this.StepsTable_StepColumnString,
|
||||||
|
this.StepsTable_NameColumnString,
|
||||||
|
this.StepsTable_TypeColumnString,
|
||||||
|
this.StepsTable_SuccessColumnString,
|
||||||
|
this.StepsTable_FailureColumnString
|
||||||
|
],
|
||||||
|
data: data,
|
||||||
|
height: 650
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.startStepDropdown = view.modelBuilder.dropDown().withProperties({ width: 180 }).component();
|
||||||
|
this.startStepDropdown.enabled = this.steps.length >= 1;
|
||||||
|
this.steps.forEach((step) => {
|
||||||
|
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
|
||||||
|
});
|
||||||
|
this.startStepDropdown.values = this.startStepDropdownValues;
|
||||||
|
|
||||||
|
this.moveStepUpButton = view.modelBuilder.button()
|
||||||
|
.withProperties({
|
||||||
|
label: this.MoveStepUpButtonString,
|
||||||
|
width: 120
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.moveStepDownButton = view.modelBuilder.button()
|
||||||
|
.withProperties({
|
||||||
|
label: this.MoveStepDownButtonString,
|
||||||
|
width: 120
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.moveStepUpButton.enabled = false;
|
||||||
|
this.moveStepDownButton.enabled = false;
|
||||||
|
|
||||||
|
this.newStepButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: this.NewStepButtonString,
|
||||||
|
width: 140
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.newStepButton.onDidClick((e)=>{
|
||||||
|
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
|
||||||
|
let stepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, null, true);
|
||||||
|
stepDialog.onSuccess((step) => {
|
||||||
|
let stepInfo = JobStepData.convertToAgentJobStepInfo(step);
|
||||||
|
this.steps.push(stepInfo);
|
||||||
|
this.stepsTable.data = this.convertStepsToData(this.steps);
|
||||||
|
this.startStepDropdownValues = [];
|
||||||
|
this.steps.forEach((step) => {
|
||||||
|
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
|
||||||
|
});
|
||||||
|
this.startStepDropdown.values = this.startStepDropdownValues;
|
||||||
|
this.startStepDropdown.enabled = true;
|
||||||
|
this.model.jobSteps = this.steps;
|
||||||
|
});
|
||||||
|
stepDialog.jobName = this.nameTextBox.value;
|
||||||
|
stepDialog.openDialog();
|
||||||
|
} else {
|
||||||
|
this.dialog.message = { text: this.BlankJobNameErrorText };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editStepButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: this.EditStepButtonString,
|
||||||
|
width: 140
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.deleteStepButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: this.DeleteStepButtonString,
|
||||||
|
width: 140
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.stepsTable.enabled = false;
|
||||||
|
this.editStepButton.enabled = false;
|
||||||
|
this.deleteStepButton.enabled = false;
|
||||||
|
|
||||||
|
this.moveStepUpButton.onDidClick(() => {
|
||||||
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
|
let previousRow = rowNumber - 1;
|
||||||
|
let previousStep = this.steps[previousRow];
|
||||||
|
let previousStepId = this.steps[previousRow].id;
|
||||||
|
let currentStep = this.steps[rowNumber];
|
||||||
|
let currentStepId = this.steps[rowNumber].id;
|
||||||
|
this.steps[previousRow] = currentStep;
|
||||||
|
this.steps[rowNumber] = previousStep;
|
||||||
|
this.stepsTable.data = this.convertStepsToData(this.steps);
|
||||||
|
this.steps[previousRow].id = previousStepId;
|
||||||
|
this.steps[rowNumber].id = currentStepId;
|
||||||
|
this.stepsTable.selectedRows = [previousRow];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.moveStepDownButton.onDidClick(() => {
|
||||||
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
|
let nextRow = rowNumber + 1;
|
||||||
|
let nextStep = this.steps[nextRow];
|
||||||
|
let nextStepId = this.steps[nextRow].id;
|
||||||
|
let currentStep = this.steps[rowNumber];
|
||||||
|
let currentStepId = this.steps[rowNumber].id;
|
||||||
|
this.steps[nextRow] = currentStep;
|
||||||
|
this.steps[rowNumber] = nextStep;
|
||||||
|
this.stepsTable.data = this.convertStepsToData(this.steps);
|
||||||
|
this.steps[nextRow].id = nextStepId;
|
||||||
|
this.steps[rowNumber].id = currentStepId;
|
||||||
|
this.stepsTable.selectedRows = [nextRow];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editStepButton.onDidClick(() => {
|
||||||
|
if (this.stepsTable.selectedRows.length === 1) {
|
||||||
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
|
let stepData = this.model.jobSteps[rowNumber];
|
||||||
|
let editStepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, stepData, true);
|
||||||
|
editStepDialog.onSuccess((step) => {
|
||||||
|
let stepInfo = JobStepData.convertToAgentJobStepInfo(step);
|
||||||
|
for (let i = 0; i < this.steps.length; i++) {
|
||||||
|
if (this.steps[i].id === stepInfo.id) {
|
||||||
|
this.steps[i] = stepInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stepsTable.data = this.convertStepsToData(this.steps);
|
||||||
|
this.startStepDropdownValues = [];
|
||||||
|
this.steps.forEach((step) => {
|
||||||
|
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
|
||||||
|
});
|
||||||
|
this.startStepDropdown.values = this.startStepDropdownValues;
|
||||||
|
this.model.jobSteps = this.steps;
|
||||||
|
});
|
||||||
|
editStepDialog.openDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.deleteStepButton.onDidClick(() => {
|
||||||
|
if (this.stepsTable.selectedRows.length === 1) {
|
||||||
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
|
AgentUtils.getAgentService().then((agentService) => {
|
||||||
|
let stepData = this.steps[rowNumber];
|
||||||
|
if (stepData.jobId) {
|
||||||
|
agentService.deleteJobStep(this.ownerUri, stepData).then((result) => {
|
||||||
|
if (result && result.success) {
|
||||||
|
this.steps.splice(rowNumber, 1);
|
||||||
|
let data = this.convertStepsToData(this.steps);
|
||||||
|
this.stepsTable.data = data;
|
||||||
|
this.startStepDropdownValues = [];
|
||||||
|
this.steps.forEach((step) => {
|
||||||
|
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
|
||||||
|
});
|
||||||
|
this.startStepDropdown.values = this.startStepDropdownValues;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.steps.splice(rowNumber, 1);
|
||||||
|
let data = this.convertStepsToData(this.steps);
|
||||||
|
this.stepsTable.data = data;
|
||||||
|
this.startStepDropdownValues = [];
|
||||||
|
this.steps.forEach((step) => {
|
||||||
|
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
|
||||||
|
});
|
||||||
|
this.startStepDropdown.values = this.startStepDropdownValues;
|
||||||
|
this.startStepDropdown.enabled = this.steps.length >= 1;
|
||||||
|
}
|
||||||
|
this.model.jobSteps = this.steps;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stepsTable.onRowSelected((row) => {
|
||||||
|
// only let edit or delete steps if there's
|
||||||
|
// one step selection
|
||||||
|
if (this.stepsTable.selectedRows.length === 1) {
|
||||||
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
|
// if it's not the last step
|
||||||
|
if (this.steps.length !== rowNumber + 1) {
|
||||||
|
this.moveStepDownButton.enabled = true;
|
||||||
|
}
|
||||||
|
// if it's not the first step
|
||||||
|
if (rowNumber !== 0) {
|
||||||
|
this.moveStepUpButton.enabled = true;
|
||||||
|
}
|
||||||
|
this.deleteStepButton.enabled = true;
|
||||||
|
this.editStepButton.enabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stepMoveContainer = this.createRowContainer(view).withItems([this.startStepDropdown, this.moveStepUpButton, this.moveStepDownButton]).component();
|
||||||
|
let stepsDialogContainer = this.createRowContainer(view).withItems([this.newStepButton, this.editStepButton, this.deleteStepButton]).component();
|
||||||
|
let formModel = view.modelBuilder.formContainer().withFormItems([
|
||||||
|
{
|
||||||
|
component: this.stepsTable,
|
||||||
|
title: this.JobStepsTopLabelString
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: stepMoveContainer,
|
||||||
|
title: this.StartStepDropdownString
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: stepsDialogContainer,
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
]).withLayout({ width: '100%' }).component();
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
this.setConditionDropdownSelectedValue(this.startStepDropdown, this.model.startStepId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeAlertsTab() {
|
||||||
|
this.alertsTab.registerContent(async view => {
|
||||||
|
let alerts = this.model.alerts ? this.model.alerts : [];
|
||||||
|
let data = this.convertAlertsToData(alerts);
|
||||||
|
this.alertsTable = view.modelBuilder.table()
|
||||||
|
.withProperties({
|
||||||
|
columns: [
|
||||||
|
this.AlertNameLabelString,
|
||||||
|
this.AlertEnabledLabelString,
|
||||||
|
this.AlertTypeLabelString
|
||||||
|
],
|
||||||
|
data: data,
|
||||||
|
height: 750,
|
||||||
|
width: 400
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.newAlertButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: this.NewAlertButtonString,
|
||||||
|
width: 80
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
let alertDialog = new AlertDialog(this.model.ownerUri, this.model, null, true);
|
||||||
|
alertDialog.onSuccess((alert) => {
|
||||||
|
let alertInfo = alert.toAgentAlertInfo();
|
||||||
|
this.alerts.push(alertInfo);
|
||||||
|
this.alertsTable.data = this.convertAlertsToData(this.alerts);
|
||||||
|
});
|
||||||
|
this.newAlertButton.onDidClick(()=>{
|
||||||
|
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
|
||||||
|
alertDialog.jobId = this.model.jobId;
|
||||||
|
alertDialog.jobName = this.model.name ? this.model.name : this.nameTextBox.value;
|
||||||
|
alertDialog.openDialog();
|
||||||
|
} else {
|
||||||
|
this.dialog.message = { text: this.BlankJobNameErrorText };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let formModel = view.modelBuilder.formContainer()
|
||||||
|
.withFormItems([{
|
||||||
|
component: this.alertsTable,
|
||||||
|
title: this.AlertsTopLabelString,
|
||||||
|
actions: [this.newAlertButton]
|
||||||
|
}]).withLayout({ width: '100%' }).component();
|
||||||
|
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeSchedulesTab() {
|
||||||
|
this.schedulesTab.registerContent(async view => {
|
||||||
|
this.schedulesTable = view.modelBuilder.table()
|
||||||
|
.withProperties({
|
||||||
|
columns: [
|
||||||
|
PickScheduleDialog.SchedulesIDText,
|
||||||
|
PickScheduleDialog.ScheduleNameLabelText,
|
||||||
|
PickScheduleDialog.ScheduleDescription
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
height: 750,
|
||||||
|
width: 420
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.pickScheduleButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: this.PickScheduleButtonString,
|
||||||
|
width: 80
|
||||||
|
}).component();
|
||||||
|
this.removeScheduleButton = view.modelBuilder.button().withProperties({
|
||||||
|
label: 'Remove schedule',
|
||||||
|
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([{
|
||||||
|
component: this.schedulesTable,
|
||||||
|
title: this.SchedulesTopLabelString,
|
||||||
|
actions: [this.pickScheduleButton, this.removeScheduleButton]
|
||||||
|
}]).withLayout({ width: '100%' }).component();
|
||||||
|
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
|
||||||
|
this.populateScheduleTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private populateScheduleTable() {
|
||||||
|
let data = this.convertSchedulesToData(this.schedules);
|
||||||
|
this.schedulesTable.data = data;
|
||||||
|
this.schedulesTable.height = 750;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeNotificationsTab() {
|
||||||
|
this.notificationsTab.registerContent(async view => {
|
||||||
|
|
||||||
|
this.notificationsTabTopLabel = view.modelBuilder.text().withProperties({ value: this.NotificationsTabTopLabelString }).component();
|
||||||
|
this.emailCheckBox = view.modelBuilder.checkBox().withProperties({
|
||||||
|
label: this.EmailCheckBoxString,
|
||||||
|
width: 80
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.pagerCheckBox = view.modelBuilder.checkBox().withProperties({
|
||||||
|
label: this.PagerCheckBoxString,
|
||||||
|
width: 80
|
||||||
|
}).component();
|
||||||
|
this.eventLogCheckBox = view.modelBuilder.checkBox().withProperties({
|
||||||
|
label: this.EventLogCheckBoxString,
|
||||||
|
width: 250
|
||||||
|
}).component();
|
||||||
|
this.deleteJobCheckBox = view.modelBuilder.checkBox().withProperties({
|
||||||
|
label: this.DeleteJobCheckBoxString,
|
||||||
|
width: 250
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.emailCheckBox.onChanged(() => {
|
||||||
|
this.emailConditionDropdown.enabled = this.emailCheckBox.checked;
|
||||||
|
this.emailOperatorDropdown.enabled = this.emailCheckBox.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pagerCheckBox.onChanged(() => {
|
||||||
|
this.pagerConditionDropdown.enabled = this.pagerCheckBox.checked;
|
||||||
|
this.pagerOperatorDropdown.enabled = this.pagerCheckBox.checked;
|
||||||
|
});
|
||||||
|
this.eventLogCheckBox.onChanged(() => {
|
||||||
|
this.eventLogConditionDropdown.enabled = this.eventLogCheckBox.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.deleteJobCheckBox.onChanged(() => {
|
||||||
|
this.deleteJobConditionDropdown.enabled = this.deleteJobCheckBox.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emailOperatorDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
this.pagerOperatorDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
this.emailConditionDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
this.pagerConditionDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
this.eventLogConditionDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
this.deleteJobConditionDropdown = view.modelBuilder.dropDown().withProperties({ width: 150 }).component();
|
||||||
|
|
||||||
|
let emailContainer = this.createRowContainer(view).withItems([this.emailCheckBox, this.emailOperatorDropdown, this.emailConditionDropdown]).component();
|
||||||
|
|
||||||
|
let pagerContainer = this.createRowContainer(view).withItems([this.pagerCheckBox, this.pagerOperatorDropdown, this.pagerConditionDropdown]).component();
|
||||||
|
|
||||||
|
let eventLogContainer = this.createRowContainer(view).withItems([this.eventLogCheckBox, this.eventLogConditionDropdown]).component();
|
||||||
|
|
||||||
|
let deleteJobContainer = this.createRowContainer(view).withItems([this.deleteJobCheckBox, this.deleteJobConditionDropdown]).component();
|
||||||
|
|
||||||
|
let formModel = view.modelBuilder.formContainer().withFormItems([
|
||||||
|
{
|
||||||
|
components:
|
||||||
|
[{
|
||||||
|
component: emailContainer,
|
||||||
|
title: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: pagerContainer,
|
||||||
|
title: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: eventLogContainer,
|
||||||
|
title: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: deleteJobContainer,
|
||||||
|
title: ''
|
||||||
|
}], title: this.NotificationsTabTopLabelString}]).withLayout({ width: '100%' }).component();
|
||||||
|
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
this.emailConditionDropdown.values = this.model.JobCompletionActionConditions;
|
||||||
|
this.pagerConditionDropdown.values = this.model.JobCompletionActionConditions;
|
||||||
|
this.eventLogConditionDropdown.values = this.model.JobCompletionActionConditions;
|
||||||
|
this.deleteJobConditionDropdown.values = this.model.JobCompletionActionConditions;
|
||||||
|
this.setConditionDropdownSelectedValue(this.emailConditionDropdown, this.model.emailLevel);
|
||||||
|
this.setConditionDropdownSelectedValue(this.pagerConditionDropdown, this.model.pageLevel);
|
||||||
|
this.setConditionDropdownSelectedValue(this.eventLogConditionDropdown, this.model.eventLogLevel);
|
||||||
|
this.setConditionDropdownSelectedValue(this.deleteJobConditionDropdown, this.model.deleteLevel);
|
||||||
|
this.emailOperatorDropdown.values = this.model.operators;
|
||||||
|
this.pagerOperatorDropdown.values = this.model.operators;
|
||||||
|
this.emailCheckBox.checked = false;
|
||||||
|
this.pagerCheckBox.checked = false;
|
||||||
|
this.eventLogCheckBox.checked = false;
|
||||||
|
this.deleteJobCheckBox.checked = false;
|
||||||
|
this.emailOperatorDropdown.enabled = false;
|
||||||
|
this.pagerOperatorDropdown.enabled = false;
|
||||||
|
this.emailConditionDropdown.enabled = false;
|
||||||
|
this.pagerConditionDropdown.enabled = false;
|
||||||
|
this.eventLogConditionDropdown.enabled = false;
|
||||||
|
this.deleteJobConditionDropdown.enabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRowContainer(view: sqlops.ModelView): sqlops.FlexBuilder {
|
||||||
|
return view.modelBuilder.flexContainer().withLayout({
|
||||||
|
flexFlow: 'row',
|
||||||
|
alignItems: 'left',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertStepsToData(jobSteps: sqlops.AgentJobStepInfo[]): any[][] {
|
||||||
|
let result = [];
|
||||||
|
jobSteps.forEach(jobStep => {
|
||||||
|
let cols = [];
|
||||||
|
cols.push(jobStep.id);
|
||||||
|
cols.push(jobStep.stepName);
|
||||||
|
cols.push(jobStep.subSystem);
|
||||||
|
cols.push(jobStep.successAction);
|
||||||
|
cols.push(jobStep.failureAction);
|
||||||
|
result.push(cols);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertSchedulesToData(jobSchedules: sqlops.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertAlertsToData(alerts: sqlops.AgentAlertInfo[]): any[][] {
|
||||||
|
let result = [];
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
let cols = [];
|
||||||
|
cols.push(alert.name);
|
||||||
|
cols.push(alert.isEnabled);
|
||||||
|
cols.push(alert.alertType.toString());
|
||||||
|
result.push(cols);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateModel() {
|
||||||
|
this.model.name = this.nameTextBox.value;
|
||||||
|
this.model.owner = this.ownerTextBox.value;
|
||||||
|
this.model.enabled = this.enabledCheckBox.checked;
|
||||||
|
this.model.description = this.descriptionTextBox.value;
|
||||||
|
this.model.category = this.getDropdownValue(this.categoryDropdown);
|
||||||
|
this.model.emailLevel = this.getActualConditionValue(this.emailCheckBox, this.emailConditionDropdown);
|
||||||
|
this.model.operatorToEmail = this.getDropdownValue(this.emailOperatorDropdown);
|
||||||
|
this.model.operatorToPage = this.getDropdownValue(this.pagerOperatorDropdown);
|
||||||
|
this.model.pageLevel = this.getActualConditionValue(this.pagerCheckBox, this.pagerConditionDropdown);
|
||||||
|
this.model.eventLogLevel = this.getActualConditionValue(this.eventLogCheckBox, this.eventLogConditionDropdown);
|
||||||
|
this.model.deleteLevel = this.getActualConditionValue(this.deleteJobCheckBox, this.deleteJobConditionDropdown);
|
||||||
|
this.model.startStepId = this.startStepDropdown.enabled ? +this.getDropdownValue(this.startStepDropdown) : 1;
|
||||||
|
if (!this.model.jobSteps) {
|
||||||
|
this.model.jobSteps = [];
|
||||||
|
}
|
||||||
|
this.model.jobSteps = this.steps;
|
||||||
|
if (!this.model.jobSchedules) {
|
||||||
|
this.model.jobSchedules = [];
|
||||||
|
}
|
||||||
|
this.model.jobSchedules = this.schedules;
|
||||||
|
if (!this.model.alerts) {
|
||||||
|
this.model.alerts = [];
|
||||||
|
}
|
||||||
|
this.model.alerts = this.alerts;
|
||||||
|
this.model.categoryId = +this.model.jobCategoryIdsMap.find(cat => cat.name === this.model.category).id;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
extensions/azurecore/src/azureResource/azure-resource.d.ts
vendored
Normal file
29
extensions/azurecore/src/azureResource/azure-resource.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { TreeDataProvider } from 'vscode';
|
||||||
|
import { DataProvider, Account } from 'sqlops';
|
||||||
|
import { TreeItem } from 'sqlops';
|
||||||
|
|
||||||
|
export namespace azureResource {
|
||||||
|
export interface IAzureResourceProvider extends DataProvider {
|
||||||
|
getTreeDataProvider(): IAzureResourceTreeDataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAzureResourceTreeDataProvider extends TreeDataProvider<IAzureResourceNode> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAzureResourceNode {
|
||||||
|
readonly account: Account;
|
||||||
|
readonly subscription: AzureResourceSubscription;
|
||||||
|
readonly tenantId: string;
|
||||||
|
readonly treeItem: TreeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AzureResourceSubscription {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { AzureResource } from 'sqlops';
|
||||||
|
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
|
||||||
|
import { TokenCredentials } from 'ms-rest';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
import { azureResource } from '../../azure-resource';
|
||||||
|
import { IAzureResourceDatabaseService, IAzureResourceDatabaseNode } from './interfaces';
|
||||||
|
import { AzureResourceDatabase } from './models';
|
||||||
|
import { AzureResourceItemType } from '../../../azureResource/constants';
|
||||||
|
import { ApiWrapper } from '../../../apiWrapper';
|
||||||
|
import { generateGuid } from '../../utils';
|
||||||
|
|
||||||
|
export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider {
|
||||||
|
public constructor(
|
||||||
|
databaseService: IAzureResourceDatabaseService,
|
||||||
|
apiWrapper: ApiWrapper,
|
||||||
|
extensionContext: ExtensionContext
|
||||||
|
) {
|
||||||
|
this._databaseService = databaseService;
|
||||||
|
this._apiWrapper = apiWrapper;
|
||||||
|
this._extensionContext = extensionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable<TreeItem> {
|
||||||
|
return element.treeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getChildren(element?: azureResource.IAzureResourceNode): Promise<azureResource.IAzureResourceNode[]> {
|
||||||
|
if (!element) {
|
||||||
|
return [this.createContainerNode()];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement);
|
||||||
|
const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType);
|
||||||
|
|
||||||
|
const databases: AzureResourceDatabase[] = (await this._databaseService.getDatabases(element.subscription, credential)) || <AzureResourceDatabase[]>[];
|
||||||
|
|
||||||
|
return databases.map((database) => <IAzureResourceDatabaseNode>{
|
||||||
|
account: element.account,
|
||||||
|
subscription: element.subscription,
|
||||||
|
tenantId: element.tenantId,
|
||||||
|
database: database,
|
||||||
|
treeItem: {
|
||||||
|
id: `databaseServer_${database.serverFullName}.database_${database.name}`,
|
||||||
|
label: `${database.name} (${database.serverName})`,
|
||||||
|
iconPath: {
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'),
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg')
|
||||||
|
},
|
||||||
|
collapsibleState: process.env.NODE_ENV === 'development' ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None,
|
||||||
|
contextValue: AzureResourceItemType.database,
|
||||||
|
payload: {
|
||||||
|
id: generateGuid(),
|
||||||
|
connectionName: undefined,
|
||||||
|
serverName: database.serverFullName,
|
||||||
|
databaseName: database.name,
|
||||||
|
userName: database.loginName,
|
||||||
|
password: '',
|
||||||
|
authenticationType: 'SqlLogin',
|
||||||
|
savePassword: true,
|
||||||
|
groupFullName: '',
|
||||||
|
groupId: '',
|
||||||
|
providerName: 'MSSQL',
|
||||||
|
saveProfile: false,
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
childProvider: 'MSSQL'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContainerNode(): azureResource.IAzureResourceNode {
|
||||||
|
return {
|
||||||
|
account: undefined,
|
||||||
|
subscription: undefined,
|
||||||
|
tenantId: undefined,
|
||||||
|
treeItem: {
|
||||||
|
id: AzureResourceDatabaseTreeDataProvider.containerId,
|
||||||
|
label: AzureResourceDatabaseTreeDataProvider.containerLabel,
|
||||||
|
iconPath: {
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||||
|
},
|
||||||
|
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||||
|
contextValue: AzureResourceItemType.databaseContainer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _databaseService: IAzureResourceDatabaseService = undefined;
|
||||||
|
private _apiWrapper: ApiWrapper = undefined;
|
||||||
|
private _extensionContext: ExtensionContext = undefined;
|
||||||
|
|
||||||
|
private static readonly containerId = 'azure.resource.providers.database.treeDataProvider.databaseContainer';
|
||||||
|
private static readonly containerLabel = localize('azure.resource.providers.database.treeDataProvider.databaseContainerLabel', 'SQL Databases');
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { AzureResource } from 'sqlops';
|
||||||
|
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
|
||||||
|
import { TokenCredentials } from 'ms-rest';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
import { azureResource } from '../../azure-resource';
|
||||||
|
import { IAzureResourceDatabaseServerService, IAzureResourceDatabaseServerNode } from './interfaces';
|
||||||
|
import { AzureResourceDatabaseServer } from './models';
|
||||||
|
import { AzureResourceItemType } from '../../../azureResource/constants';
|
||||||
|
import { ApiWrapper } from '../../../apiWrapper';
|
||||||
|
import { generateGuid } from '../../utils';
|
||||||
|
|
||||||
|
export class AzureResourceDatabaseServerTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider {
|
||||||
|
public constructor(
|
||||||
|
databaseServerService: IAzureResourceDatabaseServerService,
|
||||||
|
apiWrapper: ApiWrapper,
|
||||||
|
extensionContext: ExtensionContext
|
||||||
|
) {
|
||||||
|
this._databaseServerService = databaseServerService;
|
||||||
|
this._apiWrapper = apiWrapper;
|
||||||
|
this._extensionContext = extensionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable<TreeItem> {
|
||||||
|
return element.treeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getChildren(element?: azureResource.IAzureResourceNode): Promise<azureResource.IAzureResourceNode[]> {
|
||||||
|
if (!element) {
|
||||||
|
return [this.createContainerNode()];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement);
|
||||||
|
const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType);
|
||||||
|
|
||||||
|
const databaseServers: AzureResourceDatabaseServer[] = (await this._databaseServerService.getDatabaseServers(element.subscription, credential)) || <AzureResourceDatabaseServer[]>[];
|
||||||
|
|
||||||
|
return databaseServers.map((databaseServer) => <IAzureResourceDatabaseServerNode>{
|
||||||
|
account: element.account,
|
||||||
|
subscription: element.subscription,
|
||||||
|
tenantId: element.tenantId,
|
||||||
|
databaseServer: databaseServer,
|
||||||
|
treeItem: {
|
||||||
|
id: `databaseServer_${databaseServer.name}`,
|
||||||
|
label: databaseServer.name,
|
||||||
|
iconPath: {
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
|
||||||
|
},
|
||||||
|
collapsibleState: process.env.NODE_ENV === 'development' ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None,
|
||||||
|
contextValue: AzureResourceItemType.databaseServer,
|
||||||
|
payload: {
|
||||||
|
id: generateGuid(),
|
||||||
|
connectionName: undefined,
|
||||||
|
serverName: databaseServer.fullName,
|
||||||
|
databaseName: databaseServer.defaultDatabaseName,
|
||||||
|
userName: databaseServer.loginName,
|
||||||
|
password: '',
|
||||||
|
authenticationType: 'SqlLogin',
|
||||||
|
savePassword: true,
|
||||||
|
groupFullName: '',
|
||||||
|
groupId: '',
|
||||||
|
providerName: 'MSSQL',
|
||||||
|
saveProfile: false,
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
childProvider: 'MSSQL'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContainerNode(): azureResource.IAzureResourceNode {
|
||||||
|
return {
|
||||||
|
account: undefined,
|
||||||
|
subscription: undefined,
|
||||||
|
tenantId: undefined,
|
||||||
|
treeItem: {
|
||||||
|
id: AzureResourceDatabaseServerTreeDataProvider.containerId,
|
||||||
|
label: AzureResourceDatabaseServerTreeDataProvider.containerLabel,
|
||||||
|
iconPath: {
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||||
|
},
|
||||||
|
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||||
|
contextValue: AzureResourceItemType.databaseServerContainer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _databaseServerService: IAzureResourceDatabaseServerService = undefined;
|
||||||
|
private _apiWrapper: ApiWrapper = undefined;
|
||||||
|
private _extensionContext: ExtensionContext = undefined;
|
||||||
|
|
||||||
|
private static readonly containerId = 'azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer';
|
||||||
|
private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainerLabel', 'SQL Servers');
|
||||||
|
}
|
||||||
0
extensions/git/src/commands.ts
Normal file → Executable file
0
extensions/git/src/commands.ts
Normal file → Executable file
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal file
399
extensions/notebook/src/jupyter/serverInstance.ts
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { spawn, ExecOptions, SpawnOptions, ChildProcess } from 'child_process';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
import { IServerInstance } from './common';
|
||||||
|
import JupyterServerInstallation from './jupyterServerInstallation';
|
||||||
|
import * as utils from '../common/utils';
|
||||||
|
import * as constants from '../common/constants';
|
||||||
|
import * as notebookUtils from '../common/notebookUtils';
|
||||||
|
import * as ports from '../common/ports';
|
||||||
|
|
||||||
|
const NotebookConfigFilename = 'jupyter_notebook_config.py';
|
||||||
|
const CustomJsFilename = 'custom.js';
|
||||||
|
const defaultPort = 8888;
|
||||||
|
const JupyterStartedMessage = 'The Jupyter Notebook is running';
|
||||||
|
|
||||||
|
type MessageListener = (data: string | Buffer) => void;
|
||||||
|
type ErrorListener = (err: any) => void;
|
||||||
|
|
||||||
|
export interface IInstanceOptions {
|
||||||
|
/**
|
||||||
|
* The path to the initial document we want to start this server for
|
||||||
|
*/
|
||||||
|
documentPath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base install information needed in order to start the server instance
|
||||||
|
*/
|
||||||
|
install: JupyterServerInstallation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional start directory for the notebook server. If none is set, will use a
|
||||||
|
* path relative to the initial document
|
||||||
|
*/
|
||||||
|
notebookDirectory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to enable testing without calling into file system or
|
||||||
|
* commandline shell APIs
|
||||||
|
*/
|
||||||
|
export class ServerInstanceUtils {
|
||||||
|
public mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
|
||||||
|
return utils.mkDir(dirPath, outputChannel);
|
||||||
|
}
|
||||||
|
public removeDir(dirPath: string): Promise<void> {
|
||||||
|
return fs.remove(dirPath);
|
||||||
|
}
|
||||||
|
public pathExists(dirPath: string): Promise<boolean> {
|
||||||
|
return fs.pathExists(dirPath);
|
||||||
|
}
|
||||||
|
public copy(src: string, dest: string): Promise<void> {
|
||||||
|
return fs.copy(src, dest);
|
||||||
|
}
|
||||||
|
public existsSync(dirPath: string): boolean {
|
||||||
|
return fs.existsSync(dirPath);
|
||||||
|
}
|
||||||
|
public generateUuid(): string {
|
||||||
|
return UUID.generateUuid();
|
||||||
|
}
|
||||||
|
public executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
|
||||||
|
return utils.executeBufferedCommand(cmd, options, outputChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess {
|
||||||
|
return spawn(command, args, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkProcessDied(childProcess: ChildProcess): void {
|
||||||
|
if (!childProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wait 10 seconds and then force kill. Jupyter stop is slow so this seems a reasonable time limit
|
||||||
|
setTimeout(() => {
|
||||||
|
// Test if the process is still alive. Throws an exception if not
|
||||||
|
try {
|
||||||
|
process.kill(childProcess.pid, <any>0);
|
||||||
|
} catch (error) {
|
||||||
|
// All is fine.
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerNotebookServerInstance implements IServerInstance {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root of the jupyter directory structure. Config and data roots will be
|
||||||
|
* under this, in order to simplify deletion of folders on stop of the instance
|
||||||
|
*/
|
||||||
|
private baseDir: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to configuration folder for this instance. Typically:
|
||||||
|
* %extension_path%/jupyter_config/%server%_config
|
||||||
|
*/
|
||||||
|
private instanceConfigRoot: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to data folder for this instance. Typically:
|
||||||
|
* %extension_path%/jupyter_config/%server%_data
|
||||||
|
*/
|
||||||
|
private instanceDataRoot: string;
|
||||||
|
|
||||||
|
private _systemJupyterDir: string;
|
||||||
|
private _port: string;
|
||||||
|
private _uri: vscode.Uri;
|
||||||
|
private _isStarted: boolean = false;
|
||||||
|
private utils: ServerInstanceUtils;
|
||||||
|
private childProcess: ChildProcess;
|
||||||
|
private errorHandler: ErrorHandler = new ErrorHandler();
|
||||||
|
|
||||||
|
constructor(private options: IInstanceOptions, fsUtils?: ServerInstanceUtils) {
|
||||||
|
this.utils = fsUtils || new ServerInstanceUtils();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isStarted(): boolean {
|
||||||
|
return this._isStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get port(): string {
|
||||||
|
return this._port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get uri(): vscode.Uri {
|
||||||
|
return this._uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async configure(): Promise<void> {
|
||||||
|
await this.configureJupyter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
await this.startInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.baseDir) {
|
||||||
|
let exists = await this.utils.pathExists(this.baseDir);
|
||||||
|
if (exists) {
|
||||||
|
await this.utils.removeDir(this.baseDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.isStarted) {
|
||||||
|
let install = this.options.install;
|
||||||
|
let stopCommand = `${install.pythonExecutable} -m jupyter notebook stop ${this._port}`;
|
||||||
|
await this.utils.executeBufferedCommand(stopCommand, install.execOptions, install.outputChannel);
|
||||||
|
this._isStarted = false;
|
||||||
|
this.utils.checkProcessDied(this.childProcess);
|
||||||
|
this.handleConnectionClosed();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// For now, we don't care as this is non-critical
|
||||||
|
this.notify(this.options.install, localize('serverStopError', 'Error stopping Notebook Server: {0}', utils.getErrorMessage(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async configureJupyter(): Promise<void> {
|
||||||
|
await this.createInstanceFolders();
|
||||||
|
let resourcesFolder = path.join(this.options.install.extensionPath, 'resources', constants.jupyterConfigRootFolder);
|
||||||
|
await this.copyInstanceConfig(resourcesFolder);
|
||||||
|
await this.CopyCustomJs(resourcesFolder);
|
||||||
|
await this.copyKernelsToSystemJupyterDirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createInstanceFolders(): Promise<void> {
|
||||||
|
this.baseDir = path.join(this.options.install.configRoot, 'instances', `${this.utils.generateUuid()}`);
|
||||||
|
this.instanceConfigRoot = path.join(this.baseDir, 'config');
|
||||||
|
this.instanceDataRoot = path.join(this.baseDir, 'data');
|
||||||
|
await this.utils.mkDir(this.baseDir, this.options.install.outputChannel);
|
||||||
|
await this.utils.mkDir(this.instanceConfigRoot, this.options.install.outputChannel);
|
||||||
|
await this.utils.mkDir(this.instanceDataRoot, this.options.install.outputChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyInstanceConfig(resourcesFolder: string): Promise<void> {
|
||||||
|
let configSource = path.join(resourcesFolder, NotebookConfigFilename);
|
||||||
|
let configDest = path.join(this.instanceConfigRoot, NotebookConfigFilename);
|
||||||
|
await this.utils.copy(configSource, configDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async CopyCustomJs(resourcesFolder: string): Promise<void> {
|
||||||
|
let customPath = path.join(this.instanceConfigRoot, 'custom');
|
||||||
|
await this.utils.mkDir(customPath, this.options.install.outputChannel);
|
||||||
|
let customSource = path.join(resourcesFolder, CustomJsFilename);
|
||||||
|
let customDest = path.join(customPath, CustomJsFilename);
|
||||||
|
await this.utils.copy(customSource, customDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyKernelsToSystemJupyterDirs(): Promise<void> {
|
||||||
|
let kernelsExtensionSource = path.join(this.options.install.extensionPath, 'kernels');
|
||||||
|
this._systemJupyterDir = this.getSystemJupyterKernelDir();
|
||||||
|
if (!this.utils.existsSync(this._systemJupyterDir)) {
|
||||||
|
await this.utils.mkDir(this._systemJupyterDir, this.options.install.outputChannel);
|
||||||
|
}
|
||||||
|
await this.utils.copy(kernelsExtensionSource, this._systemJupyterDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSystemJupyterKernelDir(): string {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
let appDataWindows = process.env['APPDATA'];
|
||||||
|
return appDataWindows + '\\jupyter\\kernels';
|
||||||
|
case 'darwin':
|
||||||
|
return path.resolve(os.homedir(), 'Library/Jupyter/kernels');
|
||||||
|
default:
|
||||||
|
return path.resolve(os.homedir(), '.local/share/jupyter/kernels');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a Jupyter instance using the provided a start command. Server is determined to have
|
||||||
|
* started when the log message with URL to connect to is emitted.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
protected async startInternal(): Promise<void> {
|
||||||
|
if (this.isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let notebookDirectory = this.getNotebookDirectory();
|
||||||
|
// Find a port in a given range. If run into trouble, got up 100 in range and search inside a larger range
|
||||||
|
let port = await ports.strictFindFreePort(new ports.StrictPortFindOptions(defaultPort, defaultPort + 100, defaultPort + 1000));
|
||||||
|
let token = await notebookUtils.getRandomToken();
|
||||||
|
this._uri = vscode.Uri.parse(`http://localhost:${port}/?token=${token}`);
|
||||||
|
this._port = port.toString();
|
||||||
|
let startCommand = `${this.options.install.pythonExecutable} -m jupyter notebook --no-browser --notebook-dir "${notebookDirectory}" --port=${port} --NotebookApp.token=${token}`;
|
||||||
|
this.notifyStarting(this.options.install, startCommand);
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
await this.executeStartCommand(startCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeStartCommand(startCommand: string): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
let install = this.options.install;
|
||||||
|
this.childProcess = this.spawnJupyterProcess(install, startCommand);
|
||||||
|
|
||||||
|
// Add listeners for the process exiting prematurely
|
||||||
|
let onErrorBeforeStartup = (err) => reject(err);
|
||||||
|
let onExitBeforeStart = (err) => {
|
||||||
|
if (!this.isStarted) {
|
||||||
|
reject(localize('notebookStartProcessExitPremature', 'Notebook process exited prematurely with error: {0}', err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.childProcess.on('error', onErrorBeforeStartup);
|
||||||
|
this.childProcess.on('exit', onExitBeforeStart);
|
||||||
|
|
||||||
|
// Add listener for the process to emit its web address
|
||||||
|
let handleStdout = (data: string | Buffer) => { install.outputChannel.appendLine(data.toString()); };
|
||||||
|
let handleStdErr = (data: string | Buffer) => {
|
||||||
|
// For some reason, URL info is sent on StdErr
|
||||||
|
let [url, port] = this.matchUrlAndPort(data);
|
||||||
|
if (url) {
|
||||||
|
// For now, will verify port matches
|
||||||
|
if (url.authority !== this._uri.authority
|
||||||
|
|| url.query !== this._uri.query) {
|
||||||
|
this._uri = url;
|
||||||
|
this._port = port;
|
||||||
|
}
|
||||||
|
this.notifyStarted(install, url.toString());
|
||||||
|
this._isStarted = true;
|
||||||
|
|
||||||
|
this.updateListeners(handleStdout, handleStdErr, onErrorBeforeStartup, onExitBeforeStart);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.childProcess.stdout.on('data', handleStdout);
|
||||||
|
this.childProcess.stderr.on('data', handleStdErr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateListeners(handleStdout: MessageListener, handleStdErr: MessageListener, onErrorBeforeStartup: ErrorListener, onExitBeforeStart: ErrorListener): void {
|
||||||
|
this.childProcess.stdout.removeListener('data', handleStdout);
|
||||||
|
this.childProcess.stderr.removeListener('data', handleStdErr);
|
||||||
|
this.childProcess.removeListener('error', onErrorBeforeStartup);
|
||||||
|
this.childProcess.removeListener('exit', onExitBeforeStart);
|
||||||
|
|
||||||
|
this.childProcess.addListener('error', this.handleConnectionError);
|
||||||
|
this.childProcess.addListener('exit', this.handleConnectionClosed);
|
||||||
|
|
||||||
|
// TODO #897 covers serializing stdout and stderr to a location where we can read from so that user can see if they run into trouble
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleConnectionError(error: Error): void {
|
||||||
|
let action = this.errorHandler.handleError(error);
|
||||||
|
if (action === ErrorAction.Shutdown) {
|
||||||
|
this.notify(this.options.install, localize('jupyterError', 'Error sent from Jupyter: {0}', utils.getErrorMessage(error)));
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private handleConnectionClosed(): void {
|
||||||
|
this.childProcess = undefined;
|
||||||
|
this._isStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotebookDirectory(): string {
|
||||||
|
if (this.options.notebookDirectory) {
|
||||||
|
if (this.options.notebookDirectory.endsWith('\\')) {
|
||||||
|
return this.options.notebookDirectory.substr(0, this.options.notebookDirectory.length - 1) + '/';
|
||||||
|
}
|
||||||
|
return this.options.notebookDirectory;
|
||||||
|
}
|
||||||
|
return path.dirname(this.options.documentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchUrlAndPort(data: string | Buffer): [vscode.Uri, string] {
|
||||||
|
// regex: Looks for the successful startup log message like:
|
||||||
|
// [C 12:08:51.947 NotebookApp]
|
||||||
|
//
|
||||||
|
// Copy/paste this URL into your browser when you connect for the first time,
|
||||||
|
// to login with a token:
|
||||||
|
// http://localhost:8888/?token=f5ee846e9bd61c3a8d835ecd9b965591511a331417b997b7
|
||||||
|
let dataString = data.toString();
|
||||||
|
let urlMatch = dataString.match(/\[C[\s\S]+ {8}(.+:(\d+)\/.*)$/m);
|
||||||
|
|
||||||
|
if (urlMatch) {
|
||||||
|
// Legacy case: manually parse token info if no token/port were passed
|
||||||
|
return [vscode.Uri.parse(urlMatch[1]), urlMatch[2]];
|
||||||
|
} else if (this._uri && dataString.indexOf(JupyterStartedMessage) > -1) {
|
||||||
|
// Default case: detect the notebook started message, indicating our preferred port and token were used
|
||||||
|
return [this._uri, this._port];
|
||||||
|
}
|
||||||
|
return [undefined, undefined];
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyStarted(install: JupyterServerInstallation, jupyterUri: string): void {
|
||||||
|
install.outputChannel.appendLine(localize('jupyterOutputMsgStartSuccessful', '... Jupyter is running at {0}', jupyterUri));
|
||||||
|
}
|
||||||
|
private notify(install: JupyterServerInstallation, message: string): void {
|
||||||
|
install.outputChannel.appendLine(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyStarting(install: JupyterServerInstallation, startCommand: string): void {
|
||||||
|
install.outputChannel.appendLine(localize('jupyterOutputMsgStart', '... Starting Notebook server'));
|
||||||
|
install.outputChannel.appendLine(` > ${startCommand}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnJupyterProcess(install: JupyterServerInstallation, startCommand: string): ChildProcess {
|
||||||
|
// Specify the global environment variables
|
||||||
|
let env = this.getEnvWithConfigPaths();
|
||||||
|
// Setting the PATH variable here for the jupyter command. Apparently setting it above will cause the
|
||||||
|
// notebook process to die even though we don't override it with the for loop logic above.
|
||||||
|
let pathVariableSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
|
env['PATH'] = install.pythonEnvVarPath + pathVariableSeparator + env['PATH'];
|
||||||
|
|
||||||
|
// 'MSHOST_TELEMETRY_ENABLED' and 'MSHOST_ENVIRONMENT' environment variables are set
|
||||||
|
// for telemetry purposes used by PROSE in the process where the Jupyter kernel runs
|
||||||
|
if (vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
|
||||||
|
env['MSHOST_TELEMETRY_ENABLED'] = true;
|
||||||
|
} else {
|
||||||
|
env['MSHOST_TELEMETRY_ENABLED'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
env['MSHOST_ENVIRONMENT'] = 'ADSClient-' + vscode.version;
|
||||||
|
|
||||||
|
// Start the notebook process
|
||||||
|
let options = {
|
||||||
|
shell: true,
|
||||||
|
env: env
|
||||||
|
};
|
||||||
|
let childProcess = this.utils.spawn(startCommand, [], options);
|
||||||
|
return childProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEnvWithConfigPaths(): any {
|
||||||
|
let env = Object.assign({}, process.env);
|
||||||
|
env['JUPYTER_CONFIG_DIR'] = this.instanceConfigRoot;
|
||||||
|
env['JUPYTER_PATH'] = this.instanceDataRoot;
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorHandler {
|
||||||
|
private numErrors: number = 0;
|
||||||
|
|
||||||
|
public handleError(error: Error): ErrorAction {
|
||||||
|
this.numErrors++;
|
||||||
|
return this.numErrors > 3 ? ErrorAction.Shutdown : ErrorAction.Continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ErrorAction {
|
||||||
|
Continue = 1,
|
||||||
|
Shutdown = 2
|
||||||
|
}
|
||||||
260
src/sql/parts/common/customInputConverter.ts
Normal file
260
src/sql/parts/common/customInputConverter.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.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||||
|
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||||
|
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
|
||||||
|
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||||
|
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||||
|
import { IQueryEditorOptions } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||||
|
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
||||||
|
import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput';
|
||||||
|
import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||||
|
import { getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils';
|
||||||
|
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
////// Exported public functions/vars
|
||||||
|
|
||||||
|
// prefix for untitled sql editors
|
||||||
|
export const untitledFilePrefix = 'SQLQuery';
|
||||||
|
|
||||||
|
// mode identifier for SQL mode
|
||||||
|
export const sqlModeId = 'sql';
|
||||||
|
export const notebookModeId = 'notebook';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
||||||
|
* to that type.
|
||||||
|
* @param input The input to check for conversion
|
||||||
|
* @param options Editor options for controlling the conversion
|
||||||
|
* @param instantiationService The instantiation service to use to create the new input types
|
||||||
|
*/
|
||||||
|
export function convertEditorInput(input: EditorInput, options: IQueryEditorOptions, instantiationService: IInstantiationService): EditorInput {
|
||||||
|
let denyQueryEditor = options && options.denyQueryEditor;
|
||||||
|
if (input && !denyQueryEditor) {
|
||||||
|
//QueryInput
|
||||||
|
let uri: URI = getQueryEditorFileUri(input);
|
||||||
|
if (uri) {
|
||||||
|
const queryResultsInput: QueryResultsInput = instantiationService.createInstance(QueryResultsInput, uri.toString());
|
||||||
|
let queryInput: QueryInput = instantiationService.createInstance(QueryInput, '', input, queryResultsInput, undefined);
|
||||||
|
return queryInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
//QueryPlanInput
|
||||||
|
uri = getQueryPlanEditorUri(input);
|
||||||
|
if (uri) {
|
||||||
|
let queryPlanXml: string = fs.readFileSync(uri.fsPath);
|
||||||
|
let queryPlanInput: QueryPlanInput = instantiationService.createInstance(QueryPlanInput, queryPlanXml, 'aaa', undefined);
|
||||||
|
return queryPlanInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Notebook
|
||||||
|
uri = getNotebookEditorUri(input, instantiationService);
|
||||||
|
if (uri) {
|
||||||
|
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
|
||||||
|
let fileName: string = 'untitled';
|
||||||
|
let providerIds: string[] = [DEFAULT_NOTEBOOK_PROVIDER];
|
||||||
|
if (input) {
|
||||||
|
fileName = input.getName();
|
||||||
|
providerIds = getProvidersForFileName(fileName, notebookService);
|
||||||
|
}
|
||||||
|
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||||
|
notebookInputModel.providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0];
|
||||||
|
notebookInputModel.providers = providerIds;
|
||||||
|
notebookInputModel.providers.forEach(provider => {
|
||||||
|
let standardKernels = getStandardKernelsForProvider(provider, notebookService);
|
||||||
|
notebookInputModel.standardKernels = standardKernels;
|
||||||
|
});
|
||||||
|
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||||
|
return notebookInput;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the resource of the input if it's one of the ones we support.
|
||||||
|
* @param input The IEditorInput to get the resource of
|
||||||
|
*/
|
||||||
|
export function getSupportedInputResource(input: IEditorInput): URI {
|
||||||
|
if (input instanceof UntitledEditorInput) {
|
||||||
|
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||||
|
if (untitledCast) {
|
||||||
|
return untitledCast.getResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input instanceof FileEditorInput) {
|
||||||
|
let fileCast: FileEditorInput = <FileEditorInput>input;
|
||||||
|
if (fileCast) {
|
||||||
|
return fileCast.getResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input instanceof ResourceEditorInput) {
|
||||||
|
let resourceCast: ResourceEditorInput = <ResourceEditorInput>input;
|
||||||
|
if (resourceCast) {
|
||||||
|
return resourceCast.getResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
////// Non-Exported Private functions/vars
|
||||||
|
|
||||||
|
// file extensions for the inputs we support (should be all upper case for comparison)
|
||||||
|
const sqlFileTypes = ['SQL'];
|
||||||
|
const sqlPlanFileTypes = ['SQLPLAN'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If input is a supported query editor file, return it's URI. Otherwise return undefined.
|
||||||
|
* @param input The EditorInput to retrieve the URI of
|
||||||
|
*/
|
||||||
|
function getQueryEditorFileUri(input: EditorInput): URI {
|
||||||
|
if (!input || !input.getName()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this editor is not already of type queryinput
|
||||||
|
if (!(input instanceof QueryInput)) {
|
||||||
|
|
||||||
|
// If this editor has a URI
|
||||||
|
let uri: URI = getSupportedInputResource(input);
|
||||||
|
if (uri) {
|
||||||
|
let isValidUri: boolean = !!uri && !!uri.toString;
|
||||||
|
|
||||||
|
if (isValidUri && (hasFileExtension(sqlFileTypes, input, true) || hasSqlFileMode(input))) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If input is a supported query plan editor file (.sqlplan), return it's URI. Otherwise return undefined.
|
||||||
|
* @param input The EditorInput to get the URI of
|
||||||
|
*/
|
||||||
|
function getQueryPlanEditorUri(input: EditorInput): URI {
|
||||||
|
if (!input || !input.getName()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this editor is not already of type queryinput
|
||||||
|
if (!(input instanceof QueryPlanInput)) {
|
||||||
|
let uri: URI = getSupportedInputResource(input);
|
||||||
|
if (uri) {
|
||||||
|
if (hasFileExtension(sqlPlanFileTypes, input, false)) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
|
||||||
|
* @param input The EditorInput to get the URI of.
|
||||||
|
*/
|
||||||
|
function getNotebookEditorUri(input: EditorInput, instantiationService: IInstantiationService): URI {
|
||||||
|
if (!input || !input.getName()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this editor is not already of type notebook input
|
||||||
|
if (!(input instanceof NotebookInput)) {
|
||||||
|
let uri: URI = getSupportedInputResource(input);
|
||||||
|
if (uri) {
|
||||||
|
if (hasFileExtension(getNotebookFileExtensions(instantiationService), input, false) || hasNotebookFileMode(input)) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotebookFileExtensions(instantiationService: IInstantiationService): string[] {
|
||||||
|
return withService<INotebookService, string[]>(instantiationService, INotebookService, notebookService => {
|
||||||
|
return notebookService.getSupportedFileExtensions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given EditorInput is set to either undefined or notebook mode
|
||||||
|
* @param input The EditorInput to check the mode of
|
||||||
|
*/
|
||||||
|
function hasNotebookFileMode(input: EditorInput): boolean {
|
||||||
|
if (input instanceof UntitledEditorInput) {
|
||||||
|
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||||
|
return (untitledCast && untitledCast.getModeId() === notebookModeId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withService<TService, TResult>(instantiationService: IInstantiationService, serviceId: ServiceIdentifier<TService>, action: (service: TService) => TResult, ): TResult {
|
||||||
|
return instantiationService.invokeFunction(accessor => {
|
||||||
|
let service = accessor.get(serviceId);
|
||||||
|
return action(service);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given EditorInput is set to either undefined or sql mode
|
||||||
|
* @param input The EditorInput to check the mode of
|
||||||
|
*/
|
||||||
|
function hasSqlFileMode(input: EditorInput): boolean {
|
||||||
|
if (input instanceof UntitledEditorInput) {
|
||||||
|
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||||
|
return untitledCast && (untitledCast.getModeId() === undefined || untitledCast.getModeId() === sqlModeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the name of the specified input has an extension that is
|
||||||
|
* @param extensions The extensions to check for
|
||||||
|
* @param input The input to check for the specified extensions
|
||||||
|
*/
|
||||||
|
function hasFileExtension(extensions: string[], input: EditorInput, checkUntitledFileType: boolean): boolean {
|
||||||
|
// Check the extension type
|
||||||
|
let lastPeriodIndex = input.getName().lastIndexOf('.');
|
||||||
|
if (lastPeriodIndex > -1) {
|
||||||
|
let extension: string = input.getName().substr(lastPeriodIndex + 1).toUpperCase();
|
||||||
|
return !!extensions.find(x => x === extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for untitled file type
|
||||||
|
if (checkUntitledFileType && input.getName().includes(untitledFilePrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false if not a queryEditor file
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns file mode - notebookModeId or sqlModeId
|
||||||
|
export function getFileMode(instantiationService: IInstantiationService, resource: URI): string {
|
||||||
|
if (!resource) {
|
||||||
|
return sqlModeId;
|
||||||
|
}
|
||||||
|
return withService<INotebookService, string>(instantiationService, INotebookService, notebookService => {
|
||||||
|
for (const editor of notebookService.listNotebookEditors()) {
|
||||||
|
if (editor.notebookParams.notebookUri === resource) {
|
||||||
|
return notebookModeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sqlModeId;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user