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
This commit is contained in:
Aditya Bist
2019-01-22 10:01:13 -08:00
committed by GitHub
parent eb67b299de
commit 7c39268fe5
15 changed files with 356 additions and 196 deletions

View File

@@ -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,

View File

@@ -94,6 +94,9 @@ export class JobDialog extends AgentDialog<JobData> {
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<JobData> {
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<JobData> {
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<JobData> {
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<JobData> {
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<JobData> {
});
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<JobData> {
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<JobData> {
this.model.alerts = [];
}
this.model.alerts = this.alerts;
this.model.categoryId = +this.model.jobCategoryIdsMap.find(cat => cat.name === this.model.category).id;
}
}

View File

@@ -29,11 +29,10 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
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<JobStepData> {
// 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<JobStepData> {
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<JobStepData> {
// 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<JobStepData> {
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<JobStepData> {
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<JobStepData> {
})
.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<JobStepData> {
}, {
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<JobStepData> {
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() {

View File

@@ -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<boolean> {
public run(context: IJobActionInfo): TPromise<boolean> {
return new TPromise<boolean>((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<boolean> {
let jobName = context.agentJobInfo.name;
public run(context: IJobActionInfo): TPromise<boolean> {
let jobName = context.targetObject.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
return new TPromise<boolean>((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<boolean> {
let jobName = context.agentJobInfo.name;
public run(context: IJobActionInfo): TPromise<boolean> {
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<boolean> {
@@ -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<boolean> {
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);
}
});
}

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#3bb44a;}</style></defs><title>run</title><path class="cls-1" d="M3.24,0,14.61,8,3.24,16Zm2,12.07L11.13,8,5.24,3.88Z"/><path class="cls-1" d="M3.74,1l10,7-10,7Zm1,1.92V13.07L12,8Z"/></svg>

Before

Width:  |  Height:  |  Size: 306 B

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#d02e00;}</style></defs><title>stop</title><path class="cls-1" d="M.5,15.3V.3h15v15Zm13-2V2.3H2.5v11Z"/><path class="cls-1" d="M1,.8H15v14H1Zm13,13V1.8H2v12Z"/></svg>

Before

Width:  |  Height:  |  Size: 284 B

View File

@@ -77,7 +77,7 @@
<!-- Job History details -->
<div class='history-details'>
<!-- Previous run list -->
<div class="prev-run-list-container" style="min-width: 250px">
<div class="prev-run-list-container" style="min-width: 270px">
<table *ngIf="_showPreviousRuns === true">
<tr>
<td class="date-column">

View File

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

View File

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

View File

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

View File

@@ -9,19 +9,6 @@
<div class="steps-icon"></div>
<h1 style="display: inline">Steps</h1>
</div>
<table class='step-columns'>
<tr>
<td class='step-id-col'>
<b>Step ID</b>
</td>
<td class='step-name-col'>
<b>Step Name</b>
</td>
<td class='step-message-col'>
<b>Message</b>
</td>
</tr>
</table>
<div class='steps-tree' style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%" ></div>
</div>

View File

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

View File

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

View File

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

View File

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