Files
azuredatastudio/extensions/agent/src/dialogs/jobStepDialog.ts

583 lines
24 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import { JobStepData } from '../data/jobStepData';
import { AgentUtils } from '../agentUtils';
import { JobData } from '../data/jobData';
import { AgentDialog } from './agentDialog';
import { AgentDialogMode } from '../interfaces';
import * as path from 'path';
const localize = nls.loadMessageBundle();
export class JobStepDialog extends AgentDialog<JobStepData> {
// TODO: localize
// Top level
//
private static readonly NewDialogTitle: string = localize('jobStepDialog.newJobStep', "New Job Step");
private static readonly EditDialogTitle: string = localize('jobStepDialog.editJobStep', "Edit Job Step");
private readonly FileBrowserDialogTitle: string = localize('jobStepDialog.fileBrowserTitle', "Locate Database Files - ");
private readonly OkButtonText: string = localize('jobStepDialog.ok', "OK");
private readonly CancelButtonText: string = localize('jobStepDialog.cancel', "Cancel");
private readonly GeneralTabText: string = localize('jobStepDialog.general', "General");
private readonly AdvancedTabText: string = localize('jobStepDialog.advanced', "Advanced");
private readonly OpenCommandText: string = localize('jobStepDialog.open', "Open...");
private readonly ParseCommandText: string = localize('jobStepDialog.parse', "Parse");
private readonly SuccessfulParseText: string = localize('jobStepDialog.successParse', "The command was successfully parsed.");
private readonly FailureParseText: string = localize('jobStepDialog.failParse', "The command failed.");
private readonly BlankStepNameErrorText: string = localize('jobStepDialog.blankStepName', "The step name cannot be left blank");
private readonly ProcessExitCodeText: string = localize('jobStepDialog.processExitCode', "Process exit code of a successful command:");
// General Control Titles
private readonly StepNameLabelString: string = localize('jobStepDialog.stepNameLabel', "Step Name");
private readonly TypeLabelString: string = localize('jobStepDialog.typeLabel', "Type");
private readonly RunAsLabelString: string = localize('jobStepDialog.runAsLabel', "Run as");
private readonly DatabaseLabelString: string = localize('jobStepDialog.databaseLabel', "Database");
private readonly CommandLabelString: string = localize('jobStepDialog.commandLabel', "Command");
// Advanced Control Titles
private readonly SuccessActionLabel: string = localize('jobStepDialog.successAction', "On success action");
private readonly FailureActionLabel: string = localize('jobStepDialog.failureAction', "On failure action");
private readonly RunAsUserLabel: string = localize('jobStepDialog.runAsUser', "Run as user");
private readonly RetryAttemptsLabel: string = localize('jobStepDialog.retryAttempts', "Retry Attempts");
private readonly RetryIntervalLabel: string = localize('jobStepDialog.retryInterval', "Retry Interval (minutes)");
private readonly LogToTableLabel: string = localize('jobStepDialog.logToTable', "Log to table");
private readonly AppendExistingTableEntryLabel: string = localize('jobStepDialog.appendExistingTableEntry', "Append output to exisiting entry in table");
private readonly IncludeStepOutputHistoryLabel: string = localize('jobStepDialog.includeStepOutputHistory', "Include step output in history");
private readonly OutputFileNameLabel: string = localize('jobStepDialog.outputFile', "Output File");
private readonly AppendOutputToFileLabel: string = localize('jobStepDialog.appendOutputToFile', "Append output to existing file");
// File Browser Control Titles
private readonly SelectedPathLabelString: string = localize('jobStepDialog.selectedPath', "Selected path");
private readonly FilesOfTypeLabelString: string = localize('jobStepDialog.filesOfType', "Files of type");
private readonly FileNameLabelString: string = localize('jobStepDialog.fileName', "File name");
private readonly AllFilesLabelString: string = localize('jobStepDialog.allFiles', "All Files (*)");
// Dropdown options
public static readonly TSQLScript: string = localize('jobStepDialog.TSQL', "Transact-SQL script (T-SQL)");
public static readonly Powershell: string = localize('jobStepDialog.powershell', "PowerShell");
public static readonly CmdExec: string = localize('jobStepDialog.CmdExec', "Operating system (CmdExec)");
public static readonly ReplicationDistributor: string = localize('jobStepDialog.replicationDistribution', "Replication Distributor");
public static readonly ReplicationMerge: string = localize('jobStepDialog.replicationMerge', "Replication Merge");
public static readonly ReplicationQueueReader: string = localize('jobStepDialog.replicationQueueReader', "Replication Queue Reader");
public static readonly ReplicationSnapshot: string = localize('jobStepDialog.replicationSnapshot', "Replication Snapshot");
public static readonly ReplicationTransactionLogReader: string = localize('jobStepDialog.replicationTransactionLogReader', "Replication Transaction-Log Reader");
public static readonly AnalysisServicesCommand: string = localize('jobStepDialog.analysisCommand', "SQL Server Analysis Services Command");
public static readonly AnalysisServicesQuery: string = localize('jobStepDialog.analysisQuery', "SQL Server Analysis Services Query");
public static readonly ServicesPackage: string = localize('jobStepDialog.servicesPackage', "SQL Server Integration Service Package");
public static readonly AgentServiceAccount: string = localize('jobStepDialog.agentServiceAccount', "SQL Server Agent Service Account");
public static readonly NextStep: string = localize('jobStepDialog.nextStep', "Go to the next step");
public static readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', "Quit the job reporting success");
public static readonly QuitJobReportingFailure: string = localize('jobStepDialog.quitJobFailure', "Quit the job reporting failure");
// Event Name strings
private readonly NewStepDialog = 'NewStepDialogOpened';
private readonly EditStepDialog = 'EditStepDialogOpened';
// UI Components
// Dialogs
private fileBrowserDialog: azdata.window.Dialog;
// Dialog tabs
private generalTab: azdata.window.DialogTab;
private advancedTab: azdata.window.DialogTab;
//Input boxes
private nameTextBox: azdata.InputBoxComponent;
private commandTextBox: azdata.InputBoxComponent;
private selectedPathTextBox: azdata.InputBoxComponent;
private retryAttemptsBox: azdata.InputBoxComponent;
private retryIntervalBox: azdata.InputBoxComponent;
private outputFileNameBox: azdata.InputBoxComponent;
private fileBrowserNameBox: azdata.InputBoxComponent;
private userInputBox: azdata.InputBoxComponent;
private processExitCodeBox: azdata.InputBoxComponent;
// Dropdowns
private typeDropdown: azdata.DropDownComponent;
private runAsDropdown: azdata.DropDownComponent;
private databaseDropdown: azdata.DropDownComponent;
private successActionDropdown: azdata.DropDownComponent;
private failureActionDropdown: azdata.DropDownComponent;
private fileTypeDropdown: azdata.DropDownComponent;
// Buttons
private openButton: azdata.ButtonComponent;
private parseButton: azdata.ButtonComponent;
private outputFileBrowserButton: azdata.ButtonComponent;
// Checkbox
private appendToExistingFileCheckbox: azdata.CheckBoxComponent;
private logToTableCheckbox: azdata.CheckBoxComponent;
private logStepOutputHistoryCheckbox: azdata.CheckBoxComponent;
private fileBrowserTree: azdata.FileBrowserTreeComponent;
private jobModel: JobData;
public jobName: string;
private server: string;
private stepId: number;
private isEdit: boolean;
constructor(
ownerUri: string,
server: string,
jobModel: JobData,
jobStepInfo?: azdata.AgentJobStepInfo,
viaJobDialog: boolean = false
) {
super(ownerUri,
jobStepInfo ? JobStepData.convertToJobStepData(jobStepInfo, jobModel) : new JobStepData(ownerUri, jobModel, viaJobDialog),
jobStepInfo ? JobStepDialog.EditDialogTitle : JobStepDialog.NewDialogTitle);
this.stepId = jobStepInfo ?
jobStepInfo.id : jobModel.jobSteps ?
jobModel.jobSteps.length + 1 : 1;
this.isEdit = jobStepInfo ? true : false;
this.model.dialogMode = this.isEdit ? AgentDialogMode.EDIT : AgentDialogMode.CREATE;
this.jobModel = jobModel;
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
this.server = server;
this.dialogName = this.isEdit ? this.EditStepDialog : this.NewStepDialog;
}
private initializeUIComponents() {
this.generalTab = azdata.window.createTab(this.GeneralTabText);
this.advancedTab = azdata.window.createTab(this.AdvancedTabText);
this.dialog.content = [this.generalTab, this.advancedTab];
}
private createCommands(view: azdata.ModelView, queryProvider: azdata.QueryProvider) {
this.openButton = view.modelBuilder.button()
.withProps({
label: this.OpenCommandText,
title: this.OpenCommandText,
width: '80px',
isFile: true,
secondary: true
}).component();
this.parseButton = view.modelBuilder.button()
.withProps({
label: this.ParseCommandText,
title: this.ParseCommandText,
width: '80px',
isFile: false,
secondary: true
}).component();
this.openButton.onDidClick(e => {
let queryContent = e.fileContent;
this.commandTextBox.value = queryContent;
});
this.parseButton.onDidClick(e => {
if (this.commandTextBox.value) {
queryProvider.parseSyntax(this.ownerUri, this.commandTextBox.value).then(result => {
if (result && result.parseable) {
this.dialog.message = { text: this.SuccessfulParseText, level: 2 };
} else if (result && !result.parseable) {
this.dialog.message = { text: this.FailureParseText };
}
});
}
});
this.commandTextBox = view.modelBuilder.inputBox()
.withProps({
height: 300,
width: 400,
multiline: true,
inputType: 'text',
ariaLabel: this.CommandLabelString,
placeHolder: this.CommandLabelString
})
.component();
}
private createGeneralTab(databases: string[], queryProvider: azdata.QueryProvider) {
this.generalTab.registerContent(async (view) => {
this.nameTextBox = view.modelBuilder.inputBox()
.withProps({
ariaLabel: this.StepNameLabelString,
placeHolder: this.StepNameLabelString
}).component();
this.nameTextBox.required = true;
this.nameTextBox.onTextChanged(() => {
if (this.nameTextBox.value.length > 0) {
this.dialog.message = null;
}
});
this.typeDropdown = view.modelBuilder.dropDown()
.withProps({
value: JobStepDialog.TSQLScript,
values: [JobStepDialog.TSQLScript, JobStepDialog.CmdExec, JobStepDialog.Powershell]
})
.component();
this.runAsDropdown = view.modelBuilder.dropDown()
.withProps({
value: '',
values: ['']
})
.component();
this.runAsDropdown.enabled = false;
this.databaseDropdown = view.modelBuilder.dropDown()
.withProps({
value: databases[0],
values: databases
}).component();
this.processExitCodeBox = view.modelBuilder.inputBox()
.withProps({
ariaLabel: this.ProcessExitCodeText,
placeHolder: this.ProcessExitCodeText
}).component();
this.processExitCodeBox.enabled = false;
// create the commands section
this.createCommands(view, queryProvider);
let formModel = view.modelBuilder.formContainer()
.withFormItems([{
component: this.nameTextBox,
title: this.StepNameLabelString
}, {
component: this.typeDropdown,
title: this.TypeLabelString
}, {
component: this.runAsDropdown,
title: this.RunAsLabelString
}, {
component: this.databaseDropdown,
title: this.DatabaseLabelString
}, {
component: this.processExitCodeBox,
title: this.ProcessExitCodeText
}, {
component: this.commandTextBox,
title: this.CommandLabelString,
actions: [this.openButton, this.parseButton]
}], {
horizontal: false,
componentWidth: 420
}).component();
this.typeDropdown.onValueChanged((type) => {
switch (type.selected) {
case (JobStepDialog.TSQLScript):
this.runAsDropdown.value = '';
this.runAsDropdown.values = [''];
this.runAsDropdown.enabled = false;
this.databaseDropdown.enabled = true;
this.databaseDropdown.values = databases;
this.databaseDropdown.value = databases[0];
this.processExitCodeBox.value = '';
this.processExitCodeBox.enabled = false;
break;
case (JobStepDialog.Powershell):
this.runAsDropdown.value = JobStepDialog.AgentServiceAccount;
this.runAsDropdown.values = [this.runAsDropdown.value];
this.runAsDropdown.enabled = true;
this.databaseDropdown.enabled = false;
this.databaseDropdown.values = [''];
this.databaseDropdown.value = '';
this.processExitCodeBox.value = '';
this.processExitCodeBox.enabled = false;
break;
case (JobStepDialog.CmdExec):
this.databaseDropdown.enabled = false;
this.databaseDropdown.values = [''];
this.databaseDropdown.value = '';
this.runAsDropdown.value = JobStepDialog.AgentServiceAccount;
this.runAsDropdown.values = [this.runAsDropdown.value];
this.runAsDropdown.enabled = true;
this.processExitCodeBox.enabled = true;
this.processExitCodeBox.value = '0';
break;
}
});
let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component();
formWrapper.loading = false;
await view.initializeModel(formWrapper);
// Load values for edit scenario
if (this.isEdit) {
this.nameTextBox.value = this.model.stepName;
this.typeDropdown.value = JobStepData.convertToSubSystemDisplayName(this.model.subSystem);
this.databaseDropdown.value = this.model.databaseName;
this.commandTextBox.value = this.model.command;
}
});
}
private createAdvancedTab() {
this.advancedTab.registerContent(async (view) => {
this.successActionDropdown = view.modelBuilder.dropDown()
.withProps({
width: '100%',
value: JobStepDialog.NextStep,
values: [JobStepDialog.NextStep, JobStepDialog.QuitJobReportingSuccess, JobStepDialog.QuitJobReportingFailure]
})
.component();
let retryFlexContainer = this.createRetryCounters(view);
this.failureActionDropdown = view.modelBuilder.dropDown()
.withProps({
value: JobStepDialog.QuitJobReportingFailure,
values: [JobStepDialog.QuitJobReportingFailure, JobStepDialog.NextStep, JobStepDialog.QuitJobReportingSuccess]
})
.component();
let optionsGroup = this.createTSQLOptions(view);
this.logToTableCheckbox = view.modelBuilder.checkBox()
.withProps({
label: this.LogToTableLabel
}).component();
let appendToExistingEntryInTableCheckbox = view.modelBuilder.checkBox()
.withProps({ label: this.AppendExistingTableEntryLabel }).component();
appendToExistingEntryInTableCheckbox.enabled = false;
this.logToTableCheckbox.onChanged(e => {
appendToExistingEntryInTableCheckbox.enabled = e;
});
let appendCheckboxContainer = view.modelBuilder.groupContainer()
.withItems([appendToExistingEntryInTableCheckbox]).component();
let logToTableContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'row', justifyContent: 'space-between', width: 300 })
.withItems([this.logToTableCheckbox]).component();
this.logStepOutputHistoryCheckbox = view.modelBuilder.checkBox()
.withProps({ label: this.IncludeStepOutputHistoryLabel }).component();
this.userInputBox = view.modelBuilder.inputBox()
.withProps({ inputType: 'text', width: '100%' }).component();
let formModel = view.modelBuilder.formContainer()
.withFormItems(
[{
component: this.successActionDropdown,
title: this.SuccessActionLabel
}, {
component: retryFlexContainer,
title: ''
}, {
component: this.failureActionDropdown,
title: this.FailureActionLabel
}, {
component: optionsGroup,
title: JobStepDialog.TSQLScript
}, {
component: logToTableContainer,
title: ''
}, {
component: appendCheckboxContainer,
title: ' '
}, {
component: this.logStepOutputHistoryCheckbox,
title: ''
}, {
component: this.userInputBox,
title: this.RunAsUserLabel
}], {
componentWidth: 400
}).component();
let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component();
formWrapper.loading = false;
await view.initializeModel(formWrapper);
if (this.isEdit) {
this.successActionDropdown.value = JobStepData.convertToCompletionActionDisplayName(this.model.successAction);
this.retryAttemptsBox.value = this.model.retryAttempts.toString();
this.retryIntervalBox.value = this.model.retryInterval.toString();
this.failureActionDropdown.value = JobStepData.convertToCompletionActionDisplayName(this.model.failureAction);
this.outputFileNameBox.value = this.model.outputFileName;
this.appendToExistingFileCheckbox.checked = this.model.appendToLogFile;
this.logToTableCheckbox.checked = this.model.appendLogToTable;
this.logStepOutputHistoryCheckbox.checked = this.model.appendToStepHist;
this.userInputBox.value = this.model.databaseUserName;
}
});
}
private createRetryCounters(view: azdata.ModelView) {
this.retryAttemptsBox = view.modelBuilder.inputBox()
.withValidation(component => Number(component.value) >= 0)
.withProps({
inputType: 'number',
width: '100%',
value: '0'
})
.component();
this.retryIntervalBox = view.modelBuilder.inputBox()
.withValidation(component => Number(component.value) >= 0)
.withProps({
inputType: 'number',
width: '100%',
value: '0'
}).component();
let retryAttemptsContainer = view.modelBuilder.formContainer()
.withFormItems(
[{
component: this.retryAttemptsBox,
title: this.RetryAttemptsLabel
}], {
horizontal: false,
componentWidth: '100%'
})
.component();
let retryIntervalContainer = view.modelBuilder.formContainer()
.withFormItems(
[{
component: this.retryIntervalBox,
title: this.RetryIntervalLabel
}], {
horizontal: false
})
.component();
let retryFlexContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
}).withItems([retryAttemptsContainer, retryIntervalContainer]).component();
return retryFlexContainer;
}
private openFileBrowserDialog() {
let fileBrowserTitle = this.FileBrowserDialogTitle + `${this.server}`;
this.fileBrowserDialog = azdata.window.createModelViewDialog(fileBrowserTitle);
let fileBrowserTab = azdata.window.createTab('File Browser');
this.fileBrowserDialog.content = [fileBrowserTab];
fileBrowserTab.registerContent(async (view) => {
this.fileBrowserTree = view.modelBuilder.fileBrowserTree()
.withProps({ ownerUri: this.ownerUri, width: 420, height: 700 })
.component();
this.selectedPathTextBox = view.modelBuilder.inputBox()
.withProps({ inputType: 'text' })
.component();
this.fileBrowserTree.onDidChange((args) => {
this.selectedPathTextBox.value = args.fullPath;
this.fileBrowserNameBox.value = args.isFile ? path.win32.basename(args.fullPath) : '';
});
this.fileTypeDropdown = view.modelBuilder.dropDown()
.withProps({
value: this.AllFilesLabelString,
values: [this.AllFilesLabelString]
})
.component();
this.fileBrowserNameBox = view.modelBuilder.inputBox()
.withProps({})
.component();
let fileBrowserContainer = view.modelBuilder.formContainer()
.withFormItems([{
component: this.fileBrowserTree,
title: ''
}, {
component: this.selectedPathTextBox,
title: this.SelectedPathLabelString
}, {
component: this.fileTypeDropdown,
title: this.FilesOfTypeLabelString
}, {
component: this.fileBrowserNameBox,
title: this.FileNameLabelString
}
]).component();
view.initializeModel(fileBrowserContainer);
});
this.fileBrowserDialog.okButton.onClick(() => {
this.outputFileNameBox.value = path.join(path.dirname(this.selectedPathTextBox.value), this.fileBrowserNameBox.value);
});
this.fileBrowserDialog.okButton.label = this.OkButtonText;
this.fileBrowserDialog.cancelButton.label = this.CancelButtonText;
azdata.window.openDialog(this.fileBrowserDialog);
}
private createTSQLOptions(view: azdata.ModelView) {
this.outputFileBrowserButton = view.modelBuilder.button()
.withProps({
width: '20px',
label: '...',
secondary: true
}).component();
this.outputFileBrowserButton.onDidClick(() => this.openFileBrowserDialog());
this.outputFileNameBox = view.modelBuilder.inputBox()
.withProps({
width: 250,
inputType: 'text'
}).component();
let outputButtonContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
textAlign: 'right',
width: '100%'
}).withItems([this.outputFileBrowserButton], { flex: '1 1 50%' }).component();
let outputFlexBox = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
width: 350
}).withItems([this.outputFileNameBox, outputButtonContainer], {
flex: '1 1 50%'
}).component();
this.appendToExistingFileCheckbox = view.modelBuilder.checkBox()
.withProps({
label: this.AppendOutputToFileLabel
}).component();
this.appendToExistingFileCheckbox.enabled = false;
this.outputFileNameBox.onTextChanged((input) => {
if (input !== '') {
this.appendToExistingFileCheckbox.enabled = true;
} else {
this.appendToExistingFileCheckbox.enabled = false;
}
});
let outputFileForm = view.modelBuilder.formContainer()
.withFormItems([{
component: outputFlexBox,
title: this.OutputFileNameLabel
}, {
component: this.appendToExistingFileCheckbox,
title: ''
}], { horizontal: false, componentWidth: 200 }).component();
return outputFileForm;
}
protected async updateModel(): Promise<void> {
this.model.stepName = this.nameTextBox.value;
if (!this.model.stepName || this.model.stepName.length === 0) {
this.dialog.message = this.dialog.message = { text: this.BlankStepNameErrorText };
return;
}
this.model.jobName = this.jobName;
this.model.id = this.stepId;
this.model.server = this.server;
this.model.subSystem = JobStepData.convertToAgentSubSystem(this.typeDropdown.value as string);
this.model.databaseName = this.databaseDropdown.value as string;
this.model.script = this.commandTextBox.value;
this.model.successAction = JobStepData.convertToStepCompletionAction(this.successActionDropdown.value as string);
this.model.retryAttempts = this.retryAttemptsBox.value ? +this.retryAttemptsBox.value : 0;
this.model.retryInterval = +this.retryIntervalBox.value ? +this.retryIntervalBox.value : 0;
this.model.failureAction = JobStepData.convertToStepCompletionAction(this.failureActionDropdown.value as string);
this.model.outputFileName = this.outputFileNameBox.value;
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
this.model.command = this.commandTextBox.value ? this.commandTextBox.value : '';
this.model.commandExecutionSuccessCode = this.processExitCodeBox.value ? +this.processExitCodeBox.value : 0;
}
public async initializeDialog() {
let databases = await AgentUtils.getDatabases(this.ownerUri);
let queryProvider = await AgentUtils.getQueryProvider();
this.initializeUIComponents();
this.createGeneralTab(databases, queryProvider);
this.createAdvancedTab();
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;
});
}
}