From 7c39268fe567d29cc0da41fa469c315cc2703608 Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Tue, 22 Jan 2019 10:01:13 -0800 Subject: [PATCH] Agent - bug fixes and mini features (#3637) * fixed scrollbar in jobs * show steps tree when job history is opened * cleaned and added edit job to job history * scrollbars on step details * steps scrolling done * fixed styling * fixed keyboard selection, navigation and UI * fixed tabbing accessibility * added refresh action to job history * fixed focus on move step * added remove schedule button * fixed various bugs * added errors for all actions * review comments --- extensions/agent/src/data/jobData.ts | 14 ++- extensions/agent/src/dialogs/jobDialog.ts | 72 ++++++++++----- extensions/agent/src/dialogs/jobStepDialog.ts | 86 ++++++++++-------- .../parts/jobManagement/common/jobActions.ts | 87 +++++++++++-------- .../jobManagement/common/media/start.svg | 1 - .../parts/jobManagement/common/media/stop.svg | 1 - .../views/jobHistory.component.html | 2 +- .../views/jobHistory.component.ts | 65 ++++++++++---- .../parts/jobManagement/views/jobHistory.css | 41 +++++---- .../jobManagement/views/jobHistoryTree.ts | 7 +- .../views/jobStepsView.component.html | 13 --- .../views/jobStepsView.component.ts | 46 ++++++++-- .../jobManagement/views/jobStepsView.css | 52 +++++++---- .../jobManagement/views/jobStepsViewTree.ts | 44 +++++++--- .../jobManagement/views/jobsView.component.ts | 21 +++-- 15 files changed, 356 insertions(+), 196 deletions(-) delete mode 100644 src/sql/parts/jobManagement/common/media/start.svg delete mode 100644 src/sql/parts/jobManagement/common/media/stop.svg diff --git a/extensions/agent/src/data/jobData.ts b/extensions/agent/src/data/jobData.ts index fc995fd59b..68b31cd23b 100644 --- a/extensions/agent/src/data/jobData.ts +++ b/extensions/agent/src/data/jobData.ts @@ -26,6 +26,7 @@ export class JobData implements IAgentDialogData { private _operators: string[]; private _defaultOwner: string; private _jobCompletionActionConditions: sqlops.CategoryValue[]; + private _jobCategoryIdsMap: sqlops.AgentJobCategory[]; public dialogMode: AgentDialogMode = AgentDialogMode.CREATE; public name: string; @@ -46,6 +47,7 @@ export class JobData implements IAgentDialogData { public alerts: sqlops.AgentAlertInfo[]; public jobId: string; public startStepId: number; + public categoryType: number; constructor( ownerUri: string, @@ -66,6 +68,8 @@ export class JobData implements IAgentDialogData { this.alerts = jobInfo.alerts; this.jobId = jobInfo.jobId; this.startStepId = jobInfo.startStepId; + this.categoryId = jobInfo.categoryId; + this.categoryType = jobInfo.categoryType; } } @@ -73,6 +77,10 @@ export class JobData implements IAgentDialogData { return this._jobCategories; } + public get jobCategoryIdsMap(): sqlops.AgentJobCategory[] { + return this._jobCategoryIdsMap; + } + public get operators(): string[] { return this._operators; } @@ -96,7 +104,7 @@ export class JobData implements IAgentDialogData { this._jobCategories = jobDefaults.categories.map((cat) => { return cat.name; }); - + this._jobCategoryIdsMap = jobDefaults.categories; this._defaultOwner = jobDefaults.owner; this._operators = ['', this._defaultOwner]; @@ -164,8 +172,8 @@ export class JobData implements IAgentDialogData { hasSchedule: false, hasStep: false, runnable: true, - categoryId: 0, - categoryType: 1, // LocalJob, hard-coding the value, corresponds to the target tab in SSMS + categoryId: this.categoryId, + categoryType: this.categoryType, lastRun: '', nextRun: '', jobId: this.jobId, diff --git a/extensions/agent/src/dialogs/jobDialog.ts b/extensions/agent/src/dialogs/jobDialog.ts index 8b438c5e80..c01a75e585 100644 --- a/extensions/agent/src/dialogs/jobDialog.ts +++ b/extensions/agent/src/dialogs/jobDialog.ts @@ -94,6 +94,9 @@ export class JobDialog extends AgentDialog { 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; @@ -302,6 +305,7 @@ export class JobDialog extends AgentDialog { this.stepsTable.data = this.convertStepsToData(this.steps); this.steps[previousRow].id = previousStepId; this.steps[rowNumber].id = currentStepId; + this.stepsTable.selectedRows = [previousRow]; }); this.moveStepDownButton.onDidClick(() => { @@ -316,6 +320,7 @@ export class JobDialog extends AgentDialog { this.stepsTable.data = this.convertStepsToData(this.steps); this.steps[nextRow].id = nextStepId; this.steps[rowNumber].id = currentStepId; + this.stepsTable.selectedRows = [nextRow]; }); this.editStepButton.onDidClick(() => { @@ -346,20 +351,30 @@ export class JobDialog extends AgentDialog { if (this.stepsTable.selectedRows.length === 1) { let rowNumber = this.stepsTable.selectedRows[0]; AgentUtils.getAgentService().then((agentService) => { - let steps = this.model.jobSteps ? this.model.jobSteps : []; - let stepData = this.model.jobSteps[rowNumber]; - agentService.deleteJobStep(this.ownerUri, stepData).then((result) => { - if (result && result.success) { - delete steps[rowNumber]; - let data = this.convertStepsToData(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; - } - }); + 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; + } }); } }); @@ -468,7 +483,11 @@ export class JobDialog extends AgentDialog { label: this.PickScheduleButtonString, width: 80 }).component(); - this.pickScheduleButton.onDidClick((e)=>{ + 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; @@ -483,12 +502,23 @@ export class JobDialog extends AgentDialog { }); 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] + actions: [this.pickScheduleButton, this.removeScheduleButton] }]).withLayout({ width: '100%' }).component(); await view.initializeModel(formModel); @@ -499,10 +529,9 @@ export class JobDialog extends AgentDialog { private populateScheduleTable() { let data = this.convertSchedulesToData(this.schedules); - if (data.length > 0) { - this.schedulesTable.data = data; - this.schedulesTable.height = 750; - } + this.schedulesTable.data = data; + this.schedulesTable.height = 750; + } private initializeNotificationsTab() { @@ -674,5 +703,6 @@ export class JobDialog extends AgentDialog { this.model.alerts = []; } this.model.alerts = this.alerts; + this.model.categoryId = +this.model.jobCategoryIdsMap.find(cat => cat.name === this.model.category).id; } } \ No newline at end of file diff --git a/extensions/agent/src/dialogs/jobStepDialog.ts b/extensions/agent/src/dialogs/jobStepDialog.ts index 7fede4c2e1..0f6ead5bc8 100644 --- a/extensions/agent/src/dialogs/jobStepDialog.ts +++ b/extensions/agent/src/dialogs/jobStepDialog.ts @@ -29,11 +29,10 @@ export class JobStepDialog extends AgentDialog { 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 NextButtonText: string = localize('jobStepDialog.next', 'Next'); - private readonly PreviousButtonText: string = localize('jobStepDialog.previous','Previous'); 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'); @@ -62,6 +61,8 @@ export class JobStepDialog extends AgentDialog { // Dropdown options private readonly TSQLScript: string = localize('jobStepDialog.TSQL', 'Transact-SQL script (T-SQL)'); + private readonly Powershell: string = localize('jobStepDialog.powershell', 'PowerShell'); + private readonly CmdExec: string = localize('jobStepDialog.CmdExec', 'Operating system (CmdExec)'); private readonly AgentServiceAccount: string = localize('jobStepDialog.agentServiceAccount', 'SQL Server Agent Service Account'); private readonly NextStep: string = localize('jobStepDialog.nextStep', 'Go to the next step'); private readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success'); @@ -88,6 +89,7 @@ export class JobStepDialog extends AgentDialog { private outputFileNameBox: sqlops.InputBoxComponent; private fileBrowserNameBox: sqlops.InputBoxComponent; private userInputBox: sqlops.InputBoxComponent; + private processExitCodeBox: sqlops.InputBoxComponent; // Dropdowns private typeDropdown: sqlops.DropDownComponent; @@ -100,8 +102,6 @@ export class JobStepDialog extends AgentDialog { // Buttons private openButton: sqlops.ButtonComponent; private parseButton: sqlops.ButtonComponent; - private nextButton: sqlops.ButtonComponent; - private previousButton: sqlops.ButtonComponent; private outputFileBrowserButton: sqlops.ButtonComponent; // Checkbox @@ -179,18 +179,6 @@ export class JobStepDialog extends AgentDialog { inputType: 'text' }) .component(); - this.nextButton = view.modelBuilder.button() - .withProperties({ - label: this.NextButtonText, - enabled: false, - width: '80px' - }).component(); - this.previousButton = view.modelBuilder.button() - .withProperties({ - label: this.PreviousButtonText, - enabled: false, - width: '80px' - }).component(); } private createGeneralTab(databases: string[], queryProvider: sqlops.QueryProvider) { @@ -208,7 +196,7 @@ export class JobStepDialog extends AgentDialog { this.typeDropdown = view.modelBuilder.dropDown() .withProperties({ value: this.TSQLScript, - values: [this.TSQLScript] + values: [this.TSQLScript, this.CmdExec, this.Powershell] }) .component(); this.runAsDropdown = view.modelBuilder.dropDown() @@ -218,33 +206,20 @@ export class JobStepDialog extends AgentDialog { }) .component(); this.runAsDropdown.enabled = false; - this.typeDropdown.onValueChanged((type) => { - if (type.selected !== this.TSQLScript) { - this.runAsDropdown.value = this.AgentServiceAccount; - this.runAsDropdown.values = [this.runAsDropdown.value]; - } else { - this.runAsDropdown.value = ''; - this.runAsDropdown.values = ['']; - } - }); this.databaseDropdown = view.modelBuilder.dropDown() .withProperties({ value: databases[0], values: databases }).component(); + this.processExitCodeBox = view.modelBuilder.inputBox() + .withProperties({ + }).component(); + this.processExitCodeBox.enabled = false; + // create the commands section this.createCommands(view, queryProvider); - let buttonContainer = view.modelBuilder.flexContainer() - .withLayout({ - flexFlow: 'row', - justifyContent: 'space-between', - width: 420 - }).withItems([this.openButton, this.parseButton, this.previousButton, this.nextButton], { - flex: '1 1 50%' - }).component(); - let formModel = view.modelBuilder.formContainer() .withFormItems([{ component: this.nameTextBox, @@ -258,14 +233,52 @@ export class JobStepDialog extends AgentDialog { }, { component: this.databaseDropdown, title: this.DatabaseLabelString + }, { + component: this.processExitCodeBox, + title: this.ProcessExitCodeText }, { component: this.commandTextBox, title: this.CommandLabelString, - actions: [buttonContainer] + actions: [this.openButton, this.parseButton] }], { horizontal: false, componentWidth: 420 }).component(); + this.typeDropdown.onValueChanged((type) => { + switch (type.selected) { + case(this.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(this.Powershell): + this.runAsDropdown.value = this.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(this.CmdExec): + this.databaseDropdown.enabled = false; + this.databaseDropdown.values = ['']; + this.databaseDropdown.value = ''; + this.runAsDropdown.value = this.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); @@ -524,6 +537,7 @@ export class JobStepDialog extends AgentDialog { 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() { diff --git a/src/sql/parts/jobManagement/common/jobActions.ts b/src/sql/parts/jobManagement/common/jobActions.ts index a065b87cf0..3a253cfe0f 100644 --- a/src/sql/parts/jobManagement/common/jobActions.ts +++ b/src/sql/parts/jobManagement/common/jobActions.ts @@ -19,17 +19,21 @@ import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView. import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/common/telemetryKeys'; -import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement'; + +export const successLabel: string = nls.localize('jobaction.successLabel', 'Success'); +export const errorLabel: string = nls.localize('jobaction.faillabel', 'Error'); export enum JobActions { Run = 'run', Stop = 'stop' } -export interface IJobActionInfo { +export class IJobActionInfo { ownerUri: string; targetObject: any; jobHistoryComponent?: JobHistoryComponent; + jobViewComponent?: JobsViewComponent; } // Job actions @@ -43,10 +47,10 @@ export class JobsRefreshAction extends Action { super(JobsRefreshAction.ID, JobsRefreshAction.LABEL, 'refreshIcon'); } - public run(context: JobsViewComponent | JobHistoryComponent): TPromise { + public run(context: IJobActionInfo): TPromise { return new TPromise((resolve, reject) => { if (context) { - context.refreshJobs(); + context.jobHistoryComponent.refreshJobs(); resolve(true); } else { reject(false); @@ -82,33 +86,28 @@ export class RunJobAction extends Action { constructor( @INotificationService private notificationService: INotificationService, + @IErrorMessageService private errorMessageService: IErrorMessageService, @IJobManagementService private jobManagementService: IJobManagementService, @IInstantiationService private instantationService: IInstantiationService, @ITelemetryService private telemetryService: ITelemetryService ) { - super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon'); + super(RunJobAction.ID, RunJobAction.LABEL, 'start'); } - public run(context: JobHistoryComponent): TPromise { - let jobName = context.agentJobInfo.name; + public run(context: IJobActionInfo): TPromise { + let jobName = context.targetObject.name; let ownerUri = context.ownerUri; let refreshAction = this.instantationService.createInstance(JobsRefreshAction); this.telemetryService.publicLog(TelemetryKeys.RunAgentJob); return new TPromise((resolve, reject) => { this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => { if (result.success) { - refreshAction.run(context); var startMsg = nls.localize('jobSuccessfullyStarted', ': The job was successfully started.'); - this.notificationService.notify({ - severity: Severity.Info, - message: jobName+ startMsg - }); + this.notificationService.info(jobName+startMsg); + refreshAction.run(context); resolve(true); } else { - this.notificationService.notify({ - severity: Severity.Error, - message: result.errorMessage - }); + this.errorMessageService.showDialog(Severity.Error, errorLabel, result.errorMessage); resolve(false); } }); @@ -122,15 +121,16 @@ export class StopJobAction extends Action { constructor( @INotificationService private notificationService: INotificationService, + @IErrorMessageService private errorMessageService: IErrorMessageService, @IJobManagementService private jobManagementService: IJobManagementService, @IInstantiationService private instantationService: IInstantiationService, @ITelemetryService private telemetryService: ITelemetryService ) { - super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon'); + super(StopJobAction.ID, StopJobAction.LABEL, 'stop'); } - public run(context: JobHistoryComponent): TPromise { - let jobName = context.agentJobInfo.name; + public run(context: IJobActionInfo): TPromise { + let jobName = context.targetObject.name; let ownerUri = context.ownerUri; let refreshAction = this.instantationService.createInstance(JobsRefreshAction); this.telemetryService.publicLog(TelemetryKeys.StopAgentJob); @@ -139,16 +139,10 @@ export class StopJobAction extends Action { if (result.success) { refreshAction.run(context); var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.'); - this.notificationService.notify({ - severity: Severity.Info, - message: jobName+ stopMsg - }); + this.notificationService.info(jobName+stopMsg); resolve(true); } else { - this.notificationService.notify({ - severity: Severity.Error, - message: result.errorMessage - }); + this.errorMessageService.showDialog(Severity.Error, 'Error', result.errorMessage); resolve(false); } }); @@ -163,7 +157,7 @@ export class EditJobAction extends Action { constructor( @ICommandService private _commandService: ICommandService ) { - super(EditJobAction.ID, EditJobAction.LABEL); + super(EditJobAction.ID, EditJobAction.LABEL, 'edit'); } public run(actionInfo: IJobActionInfo): TPromise { @@ -181,6 +175,7 @@ export class DeleteJobAction extends Action { constructor( @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, @IJobManagementService private _jobService: IJobManagementService, @ITelemetryService private _telemetryService: ITelemetryService ) { @@ -201,7 +196,10 @@ export class DeleteJobAction extends Action { if (!result || !result.success) { let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}", job.name, result.errorMessage ? result.errorMessage : 'Unknown error'); - self._notificationService.error(errorMessage); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + } else { + let successMessage = nls.localize('jobaction.deletedJob', 'The job was successfully deleted'); + self._notificationService.info(successMessage); } }); } @@ -242,6 +240,7 @@ export class DeleteStepAction extends Action { constructor( @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, @IJobManagementService private _jobService: IJobManagementService, @IInstantiationService private instantationService: IInstantiationService, @ITelemetryService private _telemetryService: ITelemetryService @@ -262,11 +261,13 @@ export class DeleteStepAction extends Action { this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep); self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => { if (!result || !result.success) { - let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}", + let errorMessage = nls.localize('jobaction.failedToDeleteStep', "Could not delete step '{0}'.\nError: {1}", step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error'); - self._notificationService.error(errorMessage); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + refreshAction.run(actionInfo); } else { - refreshAction.run(actionInfo.jobHistoryComponent); + let successMessage = nls.localize('jobaction.deletedStep', 'The job step was successfully deleted'); + self._notificationService.info(successMessage); } }); } @@ -329,6 +330,7 @@ export class DeleteAlertAction extends Action { constructor( @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, @IJobManagementService private _jobService: IJobManagementService, @ITelemetryService private _telemetryService: ITelemetryService ) { @@ -349,7 +351,10 @@ export class DeleteAlertAction extends Action { if (!result || !result.success) { let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}", alert.name, result.errorMessage ? result.errorMessage : 'Unknown error'); - self._notificationService.error(errorMessage); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + } else { + let successMessage = nls.localize('jobaction.deletedAlert', 'The alert was successfully deleted'); + self._notificationService.info(successMessage); } }); } @@ -410,6 +415,7 @@ export class DeleteOperatorAction extends Action { constructor( @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, @IJobManagementService private _jobService: IJobManagementService, @ITelemetryService private _telemetryService: ITelemetryService ) { @@ -417,7 +423,7 @@ export class DeleteOperatorAction extends Action { } public run(actionInfo: IJobActionInfo): TPromise { - let self = this; + const self = this; let operator = actionInfo.targetObject as sqlops.AgentOperatorInfo; self._notificationService.prompt( Severity.Info, @@ -425,12 +431,15 @@ export class DeleteOperatorAction extends Action { [{ label: DeleteOperatorAction.LABEL, run: () => { - this._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator); + self._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator); self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => { if (!result || !result.success) { let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}", operator.name, result.errorMessage ? result.errorMessage : 'Unknown error'); - self._notificationService.error(errorMessage); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + } else { + let successMessage = nls.localize('joaction.deletedOperator', 'The operator was deleted successfully'); + self._notificationService.info(successMessage); } }); } @@ -492,6 +501,7 @@ export class DeleteProxyAction extends Action { constructor( @INotificationService private _notificationService: INotificationService, + @IErrorMessageService private _errorMessageService: IErrorMessageService, @IJobManagementService private _jobService: IJobManagementService, @ITelemetryService private _telemetryService: ITelemetryService ) { @@ -512,7 +522,10 @@ export class DeleteProxyAction extends Action { if (!result || !result.success) { let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}", proxy.accountName, result.errorMessage ? result.errorMessage : 'Unknown error'); - self._notificationService.error(errorMessage); + self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage); + } else { + let successMessage = nls.localize('jobaction.deletedProxy', 'The proxy was deleted successfully'); + self._notificationService.info(successMessage); } }); } diff --git a/src/sql/parts/jobManagement/common/media/start.svg b/src/sql/parts/jobManagement/common/media/start.svg deleted file mode 100644 index 2ceb9e2292..0000000000 --- a/src/sql/parts/jobManagement/common/media/start.svg +++ /dev/null @@ -1 +0,0 @@ -run \ No newline at end of file diff --git a/src/sql/parts/jobManagement/common/media/stop.svg b/src/sql/parts/jobManagement/common/media/stop.svg deleted file mode 100644 index b3ac86d210..0000000000 --- a/src/sql/parts/jobManagement/common/media/stop.svg +++ /dev/null @@ -1 +0,0 @@ -stop \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.html b/src/sql/parts/jobManagement/views/jobHistory.component.html index 4e2534151d..cecebd7964 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.html +++ b/src/sql/parts/jobManagement/views/jobHistory.component.html @@ -77,7 +77,7 @@
-
+
diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index 4e536c6ae6..daa6aa21d6 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -7,12 +7,13 @@ import 'vs/css!./jobHistory'; import 'vs/css!sql/media/icons/common-icons'; import * as sqlops from 'sqlops'; +import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; -import { RunJobAction, StopJobAction, NewStepAction } from 'sql/parts/jobManagement/common/jobActions'; +import { RunJobAction, StopJobAction, EditJobAction, JobsRefreshAction } from 'sql/parts/jobManagement/common/jobActions'; import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; @@ -103,6 +104,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { this._parentComponent = this._agentViewComponent; let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; + this._agentJobInfo = this._agentViewComponent.agentJobInfo; const self = this; this._treeController.onClick = (tree, element, event, origin = 'mouse') => { const payload = { origin: origin }; @@ -143,16 +145,25 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { }, {verticalScrollMode: ScrollbarVisibility.Visible}); this._register(attachListStyler(this._tree, this.themeService)); this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement)); - this.initActionBar(); this._telemetryService.publicLog(TelemetryKeys.JobHistoryView); + this.initActionBar(); } private loadHistory() { const self = this; let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; let jobName = this._agentViewComponent.agentJobInfo.name; - this._jobManagementService.getJobHistory(ownerUri, this._agentViewComponent.jobId, jobName).then((result) => { + let jobId = this._agentViewComponent.jobId; + this._jobManagementService.getJobHistory(ownerUri, jobId, jobName).then((result) => { if (result && result.histories) { + self._jobCacheObject.setJobHistory(jobId, result.histories); + self._jobCacheObject.setJobAlerts(jobId, result.alerts); + self._jobCacheObject.setJobSchedules(jobId, result.schedules); + self._jobCacheObject.setJobSteps(jobId, result.steps); + this._agentViewComponent.agentJobInfo.jobSteps = this._jobCacheObject.getJobSteps(jobId); + this._agentViewComponent.agentJobInfo.jobSchedules = this._jobCacheObject.getJobSchedules(jobId); + this._agentViewComponent.agentJobInfo.alerts = this._jobCacheObject.getJobAlerts(jobId); + this._agentJobInfo = this._agentViewComponent.agentJobInfo; if (result.histories.length > 0) { self._showPreviousRuns = true; self.buildHistoryTree(self, result.histories); @@ -189,9 +200,14 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { stepViewRow.stepId = step.stepDetails.id.toString(); return stepViewRow; }); - this._showSteps = self._stepRows.length > 0; + self._stepRows.unshift(new JobStepsViewRow()); + self._stepRows[0].rowID = 'stepsColumn' + self._agentJobInfo.jobId; + self._stepRows[0].stepId = nls.localize('stepRow.stepID','Step ID'); + self._stepRows[0].stepName = nls.localize('stepRow.stepName', 'Step Name'); + self._stepRows[0].message = nls.localize('stepRow.message', 'Message'); + this._showSteps = self._stepRows.length > 1; } else { - this._showSteps = false; + self._showSteps = false; } self._cd.detectChanges(); } @@ -208,7 +224,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { private buildHistoryTree(self: any, jobHistories: sqlops.AgentJobHistoryInfo[]) { self._treeController.jobHistories = jobHistories; - self._jobCacheObject.setJobHistory(self._agentViewComponent.jobId, jobHistories); let jobHistoryRows = this._treeController.jobHistories.map(job => self.convertToJobHistoryRow(job)); self._treeDataSource.data = jobHistoryRows; self._tree.setInput(new JobHistoryModel()); @@ -216,6 +231,13 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { if (self.agentJobHistoryInfo) { self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate); } + const payload = { origin: 'origin' }; + let element = this._treeDataSource.getFirstElement(); + this._tree.setFocus(element, payload); + this._tree.setSelection([element], payload); + if (element.rowID) { + self.setStepsTree(element); + } } private toggleCollapse(): void { @@ -249,17 +271,10 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { return this._showPreviousRuns !== true && this._noJobsAvailable === false; } - private setActions(): void { - let startIcon: HTMLElement = $('.action-label.icon.runJobIcon').get(0); - let stopIcon: HTMLElement = $('.action-label.icon.stopJobIcon').get(0); - JobManagementUtilities.getActionIconClassName(startIcon, stopIcon, this.agentJobInfo.currentExecutionStatus); - } - public onFirstVisible() { this._agentJobInfo = this._agentViewComponent.agentJobInfo; if (!this.agentJobInfo) { this.agentJobInfo = this._agentJobInfo; - this.setActions(); } if (this.isRefreshing ) { @@ -272,9 +287,12 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { const self = this; if (this._jobCacheObject.prevJobID === this._agentViewComponent.jobId || jobHistories[0].jobId === this._agentViewComponent.jobId) { this._showPreviousRuns = true; + this._agentViewComponent.agentJobInfo.jobSteps = this._jobCacheObject.getJobSteps(this._agentJobInfo.jobId); + this._agentViewComponent.agentJobInfo.jobSchedules = this._jobCacheObject.getJobSchedules(this._agentJobInfo.jobId); + this._agentViewComponent.agentJobInfo.alerts = this._jobCacheObject.getJobAlerts(this._agentJobInfo.jobId); + this._agentJobInfo = this._agentViewComponent.agentJobInfo; this.buildHistoryTree(self, jobHistories); - $('jobhistory-component .history-details .prev-run-list .monaco-tree').attr('tabIndex', '-1'); - $('jobhistory-component .history-details .prev-run-list .monaco-tree-row').attr('tabIndex', '0'); + this._actionBar.context = { targetObject: this._agentJobInfo, ownerUri: this.ownerUri, jobHistoryComponent: this }; this._cd.detectChanges(); } } else if (jobHistories && jobHistories.length === 0 ){ @@ -316,14 +334,25 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { protected initActionBar() { let runJobAction = this.instantiationService.createInstance(RunJobAction); let stopJobAction = this.instantiationService.createInstance(StopJobAction); - let newStepAction = this.instantiationService.createInstance(NewStepAction); + switch(this._agentJobInfo.currentExecutionStatus) { + case(1): + case(2): + case(3): + stopJobAction.enabled = true; + break; + default: + stopJobAction.enabled = false; + } + let editJobAction = this.instantiationService.createInstance(EditJobAction); + let refreshAction = this.instantiationService.createInstance(JobsRefreshAction); let taskbar = this.actionBarContainer.nativeElement; this._actionBar = new Taskbar(taskbar, this.contextMenuService); - this._actionBar.context = this; + this._actionBar.context = { targetObject: this._agentJobInfo, ownerUri: this.ownerUri, jobHistoryComponent: this }; this._actionBar.setContent([ { action: runJobAction }, { action: stopJobAction }, - { action: newStepAction } + { action: refreshAction }, + { action: editJobAction } ]); } diff --git a/src/sql/parts/jobManagement/views/jobHistory.css b/src/sql/parts/jobManagement/views/jobHistory.css index ae3e94e079..38a2a011ce 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.css +++ b/src/sql/parts/jobManagement/views/jobHistory.css @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ -.all-jobs { - display: inline; +jobhistory-component .all-jobs { + display: inline-block; font-size: 15px; } @@ -17,6 +17,7 @@ .job-heading { text-align: left; padding-left: 13px; + font-size: 1.5vw; } .overview-container { @@ -123,19 +124,6 @@ input#accordion:checked ~ .accordion-content { content: url('../common/media/back_inverse.svg'); } -.vs .action-label.icon.runJobIcon, -.vs-dark .action-label.icon.runJobIcon, -.hc-black .action-label.icon.runJobIcon { - background-image: url('../common/media/start.svg'); - padding-right: 5px; -} - -.vs .action-label.icon.stopJobIcon, -.vs-dark .action-label.icon.stopJobIcon, -.hc-black .action-label.icon.stopJobIcon { - background-image: url('../common/media/stop.svg'); -} - .vs .action-label.icon.newStepIcon { background-image: url('../common/media/new.svg'); } @@ -145,6 +133,21 @@ input#accordion:checked ~ .accordion-content { background-image: url('../common/media/new_inverse.svg'); } +jobhistory-component .hc-black .icon.edit, +jobhistory-component .vs-dark .icon.edit { + background-image: url('../../../media/icons/edit_inverse.svg'); +} + +jobhistory-component .vs .icon.edit { + background-image: url('../../../media/icons/edit.svg'); +} + +jobhistory-component .actions-container .icon.edit { + background-position: 0% 50%; + background-repeat: no-repeat; + background-size: 12px; +} + a.action-label.icon.runJobIcon.non-runnable { opacity: 0.4; cursor: default; @@ -197,8 +200,12 @@ table.step-list tr.step-row td { border-left: 3px solid #2b56f2; } -.history-details > .job-steps > .step-list { +.history-details > .job-steps > table.step-list { padding-bottom: 10px; + display: flex; + flex: 1 1; + overflow: scroll; + max-height: 200px; } .step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before { @@ -255,7 +262,7 @@ jobhistory-component { } jobhistory-component > .jobhistory-heading-container { - display: flex; + display: inline-block; } jobhistory-component > .jobhistory-heading-container > .icon.in-progress { diff --git a/src/sql/parts/jobManagement/views/jobHistoryTree.ts b/src/sql/parts/jobManagement/views/jobHistoryTree.ts index e4215f216c..6785bd2f99 100644 --- a/src/sql/parts/jobManagement/views/jobHistoryTree.ts +++ b/src/sql/parts/jobManagement/views/jobHistoryTree.ts @@ -50,11 +50,12 @@ export class JobHistoryController extends TreeDefaults.DefaultController { } else if (event.code === 'ArrowUp' || event.keyCode === 38) { super.onUp(tree, event); return super.onEnter(tree, event); - } else { + } else if (event.code !== 'Tab' && event.keyCode !== 2) { event.preventDefault(); event.stopPropagation(); return true; } + return false; } } @@ -96,6 +97,10 @@ export class JobHistoryDataSource implements tree.IDataSource { public set data(data: JobHistoryRow[]) { this._data = data; } + + public getFirstElement() { + return this._data[0]; + } } export interface IListTemplate { diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.html b/src/sql/parts/jobManagement/views/jobStepsView.component.html index 2ed7bad2db..bf420227e6 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.html +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.html @@ -9,19 +9,6 @@

Steps

- - - - - - -
- Step ID - - Step Name - - Message -
diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index d51862585f..a41cee8c38 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -6,7 +6,7 @@ import 'vs/css!./jobStepsView'; import * as dom from 'vs/base/browser/dom'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable, AfterContentChecked } from '@angular/core'; +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; @@ -57,23 +57,51 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, } ngAfterContentChecked() { - if (this._jobHistoryComponent.stepRows.length > 0) { - this._treeDataSource.data = this._jobHistoryComponent.stepRows; - this._tree.setInput(new JobStepsViewModel()); - this.layout(); - $('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); - $('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); - } + $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row'); + this.layout(); + this._tree.setInput(new JobStepsViewModel()); + this._tree.onDidScroll(() => { + $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row'); + }); + this._treeController.onClick = (tree, element, event, origin = 'mouse') => { + const payload = { origin: origin }; + const isDoubleClick = (origin === 'mouse' && event.detail === 2); + // Cancel Event + const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; + if (!isMouseDown) { + event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise + } + event.stopPropagation(); + tree.setFocus(element, payload); + if (element && isDoubleClick) { + event.preventDefault(); // focus moves to editor, we need to prevent default + } else { + tree.setFocus(element, payload); + tree.setSelection([element], payload); + } + $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row'); + return true; + }; + this._treeController.onKeyDown = (tree, event) => { + this._treeController.onKeyDownWrapper(tree, event); + $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row'); + return true; + }; + this._tree.onDidFocus(() => { + this._tree.focusNth(1); + let element = this._tree.getFocus(); + this._tree.select(element); + }); } ngOnInit() { + this._treeDataSource.data = this._jobHistoryComponent.stepRows; this._tree = new Tree(this._tableContainer.nativeElement, { controller: this._treeController, dataSource: this._treeDataSource, filter: this._treeFilter, renderer: this._treeRenderer }, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible }); - this.layout(); this._register(attachListStyler(this._tree, this.themeService)); this._telemetryService.publicLog(TelemetryKeys.JobStepsView); } diff --git a/src/sql/parts/jobManagement/views/jobStepsView.css b/src/sql/parts/jobManagement/views/jobStepsView.css index aadff202c2..bff5448e0e 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.css +++ b/src/sql/parts/jobManagement/views/jobStepsView.css @@ -21,14 +21,6 @@ user-select: initial; } -.steps-tree .list-row .status-icon.step-unknown { - height: 10px; - width: 10px; - display: inline-block; - margin-top: 4px; - background: yellow; -} - .steps-tree .list-row { display: inline-flex; height: 20px @@ -39,26 +31,31 @@ padding-top: 10px; } -.step-columns .step-id-col, .steps-tree .tree-id-col { +.steps-tree .tree-id-col { padding-left: 10px; white-space: normal; - text-align: center; + text-align: left; width: 60px; - } -.step-columns .step-name-col, .steps-tree .tree-name-col { +.steps-tree .tree-name-col { padding-right: 10px; white-space: normal; - text-align: center; - width: 350px; + text-align: left; + width: 300px; } -.step-columns .step-message-col, .steps-tree .tree-message-col { +.steps-tree .tree-message-col { padding-right: 10px; white-space: normal; + text-align: left; + width: 700px; +} + +.steps-tree .step-column-heading { + font-weight: bold; text-align: center; - width: 680px; + height: 40px; } .steps-header > .steps-icon { @@ -87,4 +84,27 @@ jobstepsview-component { jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row { width: 99.2%; +} + +.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row, +.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused, +.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected, +.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected { + background: #444444 !important; +} + +.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row, +.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected, +.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused, +.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected { + background: none !important; +} + +jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row, +jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused, +jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected, +jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected { + background: #dcdcdc !important; + cursor: none; + padding-left: 0px; } \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index e9bbc8b193..0f2d506dfc 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -11,7 +11,7 @@ import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { generateUuid } from 'vs/base/common/uuid'; -import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; export class JobStepsViewRow { public stepId: string; @@ -36,6 +36,20 @@ export class JobStepsViewController extends TreeDefaults.DefaultController { return true; } + public onKeyDownWrapper(tree: tree.ITree, event: IKeyboardEvent): boolean { + if (event.code === 'ArrowDown' || event.keyCode === 40) { + super.onDown(tree, event); + return super.onEnter(tree, event); + } else if (event.code === 'ArrowUp' || event.keyCode === 38) { + super.onUp(tree, event); + return super.onEnter(tree, event); + } else if (event.code !== 'Tab' && event.keyCode !== 2) { + event.preventDefault(); + event.stopPropagation(); + return true; + } + return false; + } } export class JobStepsViewDataSource implements tree.IDataSource { @@ -84,7 +98,6 @@ export interface IListTemplate { } export class JobStepsViewRenderer implements tree.IRenderer { - private _statusIcon: HTMLElement; public getHeight(tree: tree.ITree, element: JobStepsViewRow): number { return 40; @@ -101,11 +114,10 @@ export class JobStepsViewRenderer implements tree.IRenderer { public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate { let row = DOM.$('.list-row'); let label = DOM.$('.label'); - this._statusIcon = this.createStatusIcon(); - row.appendChild(this._statusIcon); + let statusIcon = this.createStatusIcon(); + row.appendChild(statusIcon); row.appendChild(label); container.appendChild(row); - let statusIcon = this._statusIcon; return { statusIcon, label }; } @@ -119,19 +131,26 @@ export class JobStepsViewRenderer implements tree.IRenderer { let stepMessageCol: HTMLElement = DOM.$('div'); stepMessageCol.className = 'tree-message-col'; stepMessageCol.innerText = element.message; + if (element.rowID.includes('stepsColumn')) { + stepNameCol.className += ' step-column-heading'; + stepIdCol.className += ' step-column-heading'; + stepMessageCol.className += ' step-column-heading'; + } $(templateData.label).empty(); templateData.label.appendChild(stepIdCol); templateData.label.appendChild(stepNameCol); templateData.label.appendChild(stepMessageCol); - let statusClass: string; - if (element.runStatus === 'Succeeded') { - statusClass = ' step-passed'; - } else if (element.runStatus === 'Failed') { - statusClass = ' step-failed'; + if (element.runStatus) { + if (element.runStatus === 'Succeeded') { + templateData.statusIcon.className = 'status-icon step-passed'; + } else if (element.runStatus === 'Failed') { + templateData.statusIcon.className = 'status-icon step-failed'; + } else { + templateData.statusIcon.className = 'status-icon step-unknown'; + } } else { - statusClass = ' step-unknown'; + templateData.statusIcon.className = ''; } - this._statusIcon.className += statusClass; } public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void { @@ -140,7 +159,6 @@ export class JobStepsViewRenderer implements tree.IRenderer { private createStatusIcon(): HTMLElement { let statusIcon: HTMLElement = DOM.$('div'); - statusIcon.className += 'status-icon'; return statusIcon; } } diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 67079872ec..ab587bbf66 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -42,6 +42,7 @@ import * as TelemetryKeys from 'sql/common/telemetryKeys'; export const JOBSVIEW_SELECTOR: string = 'jobsview-component'; export const ROW_HEIGHT: number = 45; +export const ACTIONBAR_PADDING: number = 10; @Component({ selector: JOBSVIEW_SELECTOR, @@ -141,7 +142,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe let jobsViewToolbar = $('jobsview-component .agent-actionbar-container').get(0); let statusBar = $('.part.statusbar').get(0); if (jobsViewToolbar && statusBar) { - let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom; + let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom + ACTIONBAR_PADDING; let statusTop = statusBar.getBoundingClientRect().top; this._table.layout(new dom.Dimension( dom.getContentWidth(this._gridEl.nativeElement), @@ -600,6 +601,8 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe self.jobHistories[job.jobId] = result.histories ? result.histories : []; self._jobCacheObject.setJobSteps(job.jobId, self.jobSteps[job.jobId]); self._jobCacheObject.setJobHistory(job.jobId, self.jobHistories[job.jobId]); + self._jobCacheObject.setJobAlerts(job.jobId, self.jobAlerts[job.jobId]); + self._jobCacheObject.setJobSchedules(job.jobId, self.jobSchedules[job.jobId]); let jobHistories = self._jobCacheObject.getJobHistory(job.jobId); let previousRuns: sqlops.AgentJobHistoryInfo[]; if (jobHistories.length >= 5) { @@ -630,14 +633,14 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe let runCharts = []; for (let i = 0; i < chartHeights.length; i++) { let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`); - runGraph.css('height', chartHeights[i]); - let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green'; - runGraph.css('background', bgColor); - runGraph.hover((e) => { - let currentTarget = e.currentTarget; - currentTarget.title = jobHistories[i].runDuration; - }); - if (runGraph.get(0)) { + if (runGraph.length > 0) { + runGraph.css('height', chartHeights[i]); + let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green'; + runGraph.css('background', bgColor); + runGraph.hover((e) => { + let currentTarget = e.currentTarget; + currentTarget.title = jobHistories[i].runDuration; + }); runCharts.push(runGraph.get(0).outerHTML); } }