/*--------------------------------------------------------------------------------------------- * 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 { // 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%', placeHolder: '0' }) .component(); this.retryIntervalBox = view.modelBuilder.inputBox() .withValidation(component => Number(component.value) >= 0) .withProps({ inputType: 'number', width: '100%', placeHolder: '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 { 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; }); } }