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 _operators: string[];
private _defaultOwner: string; private _defaultOwner: string;
private _jobCompletionActionConditions: sqlops.CategoryValue[]; private _jobCompletionActionConditions: sqlops.CategoryValue[];
private _jobCategoryIdsMap: sqlops.AgentJobCategory[];
public dialogMode: AgentDialogMode = AgentDialogMode.CREATE; public dialogMode: AgentDialogMode = AgentDialogMode.CREATE;
public name: string; public name: string;
@@ -46,6 +47,7 @@ export class JobData implements IAgentDialogData {
public alerts: sqlops.AgentAlertInfo[]; public alerts: sqlops.AgentAlertInfo[];
public jobId: string; public jobId: string;
public startStepId: number; public startStepId: number;
public categoryType: number;
constructor( constructor(
ownerUri: string, ownerUri: string,
@@ -66,6 +68,8 @@ export class JobData implements IAgentDialogData {
this.alerts = jobInfo.alerts; this.alerts = jobInfo.alerts;
this.jobId = jobInfo.jobId; this.jobId = jobInfo.jobId;
this.startStepId = jobInfo.startStepId; this.startStepId = jobInfo.startStepId;
this.categoryId = jobInfo.categoryId;
this.categoryType = jobInfo.categoryType;
} }
} }
@@ -73,6 +77,10 @@ export class JobData implements IAgentDialogData {
return this._jobCategories; return this._jobCategories;
} }
public get jobCategoryIdsMap(): sqlops.AgentJobCategory[] {
return this._jobCategoryIdsMap;
}
public get operators(): string[] { public get operators(): string[] {
return this._operators; return this._operators;
} }
@@ -96,7 +104,7 @@ export class JobData implements IAgentDialogData {
this._jobCategories = jobDefaults.categories.map((cat) => { this._jobCategories = jobDefaults.categories.map((cat) => {
return cat.name; return cat.name;
}); });
this._jobCategoryIdsMap = jobDefaults.categories;
this._defaultOwner = jobDefaults.owner; this._defaultOwner = jobDefaults.owner;
this._operators = ['', this._defaultOwner]; this._operators = ['', this._defaultOwner];
@@ -164,8 +172,8 @@ export class JobData implements IAgentDialogData {
hasSchedule: false, hasSchedule: false,
hasStep: false, hasStep: false,
runnable: true, runnable: true,
categoryId: 0, categoryId: this.categoryId,
categoryType: 1, // LocalJob, hard-coding the value, corresponds to the target tab in SSMS categoryType: this.categoryType,
lastRun: '', lastRun: '',
nextRun: '', nextRun: '',
jobId: this.jobId, jobId: this.jobId,

View File

@@ -94,6 +94,9 @@ export class JobDialog extends AgentDialog<JobData> {
private editStepButton: sqlops.ButtonComponent; private editStepButton: sqlops.ButtonComponent;
private deleteStepButton: sqlops.ButtonComponent; private deleteStepButton: sqlops.ButtonComponent;
// Schedule tab controls
private removeScheduleButton: sqlops.ButtonComponent;
// Notifications tab controls // Notifications tab controls
private notificationsTabTopLabel: sqlops.TextComponent; private notificationsTabTopLabel: sqlops.TextComponent;
private emailCheckBox: sqlops.CheckBoxComponent; private emailCheckBox: sqlops.CheckBoxComponent;
@@ -302,6 +305,7 @@ export class JobDialog extends AgentDialog<JobData> {
this.stepsTable.data = this.convertStepsToData(this.steps); this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[previousRow].id = previousStepId; this.steps[previousRow].id = previousStepId;
this.steps[rowNumber].id = currentStepId; this.steps[rowNumber].id = currentStepId;
this.stepsTable.selectedRows = [previousRow];
}); });
this.moveStepDownButton.onDidClick(() => { this.moveStepDownButton.onDidClick(() => {
@@ -316,6 +320,7 @@ export class JobDialog extends AgentDialog<JobData> {
this.stepsTable.data = this.convertStepsToData(this.steps); this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[nextRow].id = nextStepId; this.steps[nextRow].id = nextStepId;
this.steps[rowNumber].id = currentStepId; this.steps[rowNumber].id = currentStepId;
this.stepsTable.selectedRows = [nextRow];
}); });
this.editStepButton.onDidClick(() => { this.editStepButton.onDidClick(() => {
@@ -346,20 +351,30 @@ export class JobDialog extends AgentDialog<JobData> {
if (this.stepsTable.selectedRows.length === 1) { if (this.stepsTable.selectedRows.length === 1) {
let rowNumber = this.stepsTable.selectedRows[0]; let rowNumber = this.stepsTable.selectedRows[0];
AgentUtils.getAgentService().then((agentService) => { AgentUtils.getAgentService().then((agentService) => {
let steps = this.model.jobSteps ? this.model.jobSteps : []; let stepData = this.steps[rowNumber];
let stepData = this.model.jobSteps[rowNumber]; if (stepData.jobId) {
agentService.deleteJobStep(this.ownerUri, stepData).then((result) => { agentService.deleteJobStep(this.ownerUri, stepData).then((result) => {
if (result && result.success) { if (result && result.success) {
delete steps[rowNumber]; this.steps.splice(rowNumber, 1);
let data = this.convertStepsToData(steps); let data = this.convertStepsToData(this.steps);
this.stepsTable.data = data; this.stepsTable.data = data;
this.startStepDropdownValues = []; this.startStepDropdownValues = [];
this.steps.forEach((step) => { this.steps.forEach((step) => {
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() }); this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
}); });
this.startStepDropdown.values = this.startStepDropdownValues; 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, label: this.PickScheduleButtonString,
width: 80 width: 80
}).component(); }).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); let pickScheduleDialog = new PickScheduleDialog(this.model.ownerUri, this.model.name);
pickScheduleDialog.onSuccess((dialogModel) => { pickScheduleDialog.onSuccess((dialogModel) => {
let selectedSchedule = dialogModel.selectedSchedule; let selectedSchedule = dialogModel.selectedSchedule;
@@ -483,12 +502,23 @@ export class JobDialog extends AgentDialog<JobData> {
}); });
pickScheduleDialog.showDialog(); 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() let formModel = view.modelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
component: this.schedulesTable, component: this.schedulesTable,
title: this.SchedulesTopLabelString, title: this.SchedulesTopLabelString,
actions: [this.pickScheduleButton] actions: [this.pickScheduleButton, this.removeScheduleButton]
}]).withLayout({ width: '100%' }).component(); }]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel); await view.initializeModel(formModel);
@@ -499,10 +529,9 @@ export class JobDialog extends AgentDialog<JobData> {
private populateScheduleTable() { private populateScheduleTable() {
let data = this.convertSchedulesToData(this.schedules); let data = this.convertSchedulesToData(this.schedules);
if (data.length > 0) { this.schedulesTable.data = data;
this.schedulesTable.data = data; this.schedulesTable.height = 750;
this.schedulesTable.height = 750;
}
} }
private initializeNotificationsTab() { private initializeNotificationsTab() {
@@ -674,5 +703,6 @@ export class JobDialog extends AgentDialog<JobData> {
this.model.alerts = []; this.model.alerts = [];
} }
this.model.alerts = this.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 AdvancedTabText: string = localize('jobStepDialog.advanced', 'Advanced');
private readonly OpenCommandText: string = localize('jobStepDialog.open', 'Open...'); private readonly OpenCommandText: string = localize('jobStepDialog.open', 'Open...');
private readonly ParseCommandText: string = localize('jobStepDialog.parse','Parse'); 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 SuccessfulParseText: string = localize('jobStepDialog.successParse', 'The command was successfully parsed.');
private readonly FailureParseText: string = localize('jobStepDialog.failParse', 'The command failed.'); 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 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 // General Control Titles
private readonly StepNameLabelString: string = localize('jobStepDialog.stepNameLabel', 'Step Name'); private readonly StepNameLabelString: string = localize('jobStepDialog.stepNameLabel', 'Step Name');
@@ -62,6 +61,8 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
// Dropdown options // Dropdown options
private readonly TSQLScript: string = localize('jobStepDialog.TSQL', 'Transact-SQL script (T-SQL)'); 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 AgentServiceAccount: string = localize('jobStepDialog.agentServiceAccount', 'SQL Server Agent Service Account');
private readonly NextStep: string = localize('jobStepDialog.nextStep', 'Go to the next step'); private readonly NextStep: string = localize('jobStepDialog.nextStep', 'Go to the next step');
private readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success'); 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 outputFileNameBox: sqlops.InputBoxComponent;
private fileBrowserNameBox: sqlops.InputBoxComponent; private fileBrowserNameBox: sqlops.InputBoxComponent;
private userInputBox: sqlops.InputBoxComponent; private userInputBox: sqlops.InputBoxComponent;
private processExitCodeBox: sqlops.InputBoxComponent;
// Dropdowns // Dropdowns
private typeDropdown: sqlops.DropDownComponent; private typeDropdown: sqlops.DropDownComponent;
@@ -100,8 +102,6 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
// Buttons // Buttons
private openButton: sqlops.ButtonComponent; private openButton: sqlops.ButtonComponent;
private parseButton: sqlops.ButtonComponent; private parseButton: sqlops.ButtonComponent;
private nextButton: sqlops.ButtonComponent;
private previousButton: sqlops.ButtonComponent;
private outputFileBrowserButton: sqlops.ButtonComponent; private outputFileBrowserButton: sqlops.ButtonComponent;
// Checkbox // Checkbox
@@ -179,18 +179,6 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
inputType: 'text' inputType: 'text'
}) })
.component(); .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) { private createGeneralTab(databases: string[], queryProvider: sqlops.QueryProvider) {
@@ -208,7 +196,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
this.typeDropdown = view.modelBuilder.dropDown() this.typeDropdown = view.modelBuilder.dropDown()
.withProperties({ .withProperties({
value: this.TSQLScript, value: this.TSQLScript,
values: [this.TSQLScript] values: [this.TSQLScript, this.CmdExec, this.Powershell]
}) })
.component(); .component();
this.runAsDropdown = view.modelBuilder.dropDown() this.runAsDropdown = view.modelBuilder.dropDown()
@@ -218,33 +206,20 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
}) })
.component(); .component();
this.runAsDropdown.enabled = false; 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() this.databaseDropdown = view.modelBuilder.dropDown()
.withProperties({ .withProperties({
value: databases[0], value: databases[0],
values: databases values: databases
}).component(); }).component();
this.processExitCodeBox = view.modelBuilder.inputBox()
.withProperties({
}).component();
this.processExitCodeBox.enabled = false;
// create the commands section // create the commands section
this.createCommands(view, queryProvider); 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() let formModel = view.modelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
component: this.nameTextBox, component: this.nameTextBox,
@@ -258,14 +233,52 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
}, { }, {
component: this.databaseDropdown, component: this.databaseDropdown,
title: this.DatabaseLabelString title: this.DatabaseLabelString
}, {
component: this.processExitCodeBox,
title: this.ProcessExitCodeText
}, { }, {
component: this.commandTextBox, component: this.commandTextBox,
title: this.CommandLabelString, title: this.CommandLabelString,
actions: [buttonContainer] actions: [this.openButton, this.parseButton]
}], { }], {
horizontal: false, horizontal: false,
componentWidth: 420 componentWidth: 420
}).component(); }).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(); let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component();
formWrapper.loading = false; formWrapper.loading = false;
await view.initializeModel(formWrapper); await view.initializeModel(formWrapper);
@@ -524,6 +537,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
this.model.outputFileName = this.outputFileNameBox.value; this.model.outputFileName = this.outputFileNameBox.value;
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked; this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
this.model.command = this.commandTextBox.value ? this.commandTextBox.value : ''; this.model.command = this.commandTextBox.value ? this.commandTextBox.value : '';
this.model.commandExecutionSuccessCode = this.processExitCodeBox.value ? +this.processExitCodeBox.value : 0;
} }
public async initializeDialog() { 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys'; 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 { export enum JobActions {
Run = 'run', Run = 'run',
Stop = 'stop' Stop = 'stop'
} }
export interface IJobActionInfo { export class IJobActionInfo {
ownerUri: string; ownerUri: string;
targetObject: any; targetObject: any;
jobHistoryComponent?: JobHistoryComponent; jobHistoryComponent?: JobHistoryComponent;
jobViewComponent?: JobsViewComponent;
} }
// Job actions // Job actions
@@ -43,10 +47,10 @@ export class JobsRefreshAction extends Action {
super(JobsRefreshAction.ID, JobsRefreshAction.LABEL, 'refreshIcon'); 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) => { return new TPromise<boolean>((resolve, reject) => {
if (context) { if (context) {
context.refreshJobs(); context.jobHistoryComponent.refreshJobs();
resolve(true); resolve(true);
} else { } else {
reject(false); reject(false);
@@ -82,33 +86,28 @@ export class RunJobAction extends Action {
constructor( constructor(
@INotificationService private notificationService: INotificationService, @INotificationService private notificationService: INotificationService,
@IErrorMessageService private errorMessageService: IErrorMessageService,
@IJobManagementService private jobManagementService: IJobManagementService, @IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService, @IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService @ITelemetryService private telemetryService: ITelemetryService
) { ) {
super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon'); super(RunJobAction.ID, RunJobAction.LABEL, 'start');
} }
public run(context: JobHistoryComponent): TPromise<boolean> { public run(context: IJobActionInfo): TPromise<boolean> {
let jobName = context.agentJobInfo.name; let jobName = context.targetObject.name;
let ownerUri = context.ownerUri; let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction); let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob); this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
return new TPromise<boolean>((resolve, reject) => { return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => { this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
if (result.success) { if (result.success) {
refreshAction.run(context);
var startMsg = nls.localize('jobSuccessfullyStarted', ': The job was successfully started.'); var startMsg = nls.localize('jobSuccessfullyStarted', ': The job was successfully started.');
this.notificationService.notify({ this.notificationService.info(jobName+startMsg);
severity: Severity.Info, refreshAction.run(context);
message: jobName+ startMsg
});
resolve(true); resolve(true);
} else { } else {
this.notificationService.notify({ this.errorMessageService.showDialog(Severity.Error, errorLabel, result.errorMessage);
severity: Severity.Error,
message: result.errorMessage
});
resolve(false); resolve(false);
} }
}); });
@@ -122,15 +121,16 @@ export class StopJobAction extends Action {
constructor( constructor(
@INotificationService private notificationService: INotificationService, @INotificationService private notificationService: INotificationService,
@IErrorMessageService private errorMessageService: IErrorMessageService,
@IJobManagementService private jobManagementService: IJobManagementService, @IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService, @IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService @ITelemetryService private telemetryService: ITelemetryService
) { ) {
super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon'); super(StopJobAction.ID, StopJobAction.LABEL, 'stop');
} }
public run(context: JobHistoryComponent): TPromise<boolean> { public run(context: IJobActionInfo): TPromise<boolean> {
let jobName = context.agentJobInfo.name; let jobName = context.targetObject.name;
let ownerUri = context.ownerUri; let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction); let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.StopAgentJob); this.telemetryService.publicLog(TelemetryKeys.StopAgentJob);
@@ -139,16 +139,10 @@ export class StopJobAction extends Action {
if (result.success) { if (result.success) {
refreshAction.run(context); refreshAction.run(context);
var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.'); var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.');
this.notificationService.notify({ this.notificationService.info(jobName+stopMsg);
severity: Severity.Info,
message: jobName+ stopMsg
});
resolve(true); resolve(true);
} else { } else {
this.notificationService.notify({ this.errorMessageService.showDialog(Severity.Error, 'Error', result.errorMessage);
severity: Severity.Error,
message: result.errorMessage
});
resolve(false); resolve(false);
} }
}); });
@@ -163,7 +157,7 @@ export class EditJobAction extends Action {
constructor( constructor(
@ICommandService private _commandService: ICommandService @ICommandService private _commandService: ICommandService
) { ) {
super(EditJobAction.ID, EditJobAction.LABEL); super(EditJobAction.ID, EditJobAction.LABEL, 'edit');
} }
public run(actionInfo: IJobActionInfo): TPromise<boolean> { public run(actionInfo: IJobActionInfo): TPromise<boolean> {
@@ -181,6 +175,7 @@ export class DeleteJobAction extends Action {
constructor( constructor(
@INotificationService private _notificationService: INotificationService, @INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService, @IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService @ITelemetryService private _telemetryService: ITelemetryService
) { ) {
@@ -201,7 +196,10 @@ export class DeleteJobAction extends Action {
if (!result || !result.success) { if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}", let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
job.name, result.errorMessage ? result.errorMessage : 'Unknown error'); 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( constructor(
@INotificationService private _notificationService: INotificationService, @INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService, @IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService, @IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private _telemetryService: ITelemetryService @ITelemetryService private _telemetryService: ITelemetryService
@@ -262,11 +261,13 @@ export class DeleteStepAction extends Action {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep); this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep);
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => { self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) { 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'); step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error');
self._notificationService.error(errorMessage); self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
refreshAction.run(actionInfo);
} else { } 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( constructor(
@INotificationService private _notificationService: INotificationService, @INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService, @IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService @ITelemetryService private _telemetryService: ITelemetryService
) { ) {
@@ -349,7 +351,10 @@ export class DeleteAlertAction extends Action {
if (!result || !result.success) { if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}", let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
alert.name, result.errorMessage ? result.errorMessage : 'Unknown error'); 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( constructor(
@INotificationService private _notificationService: INotificationService, @INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService, @IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService @ITelemetryService private _telemetryService: ITelemetryService
) { ) {
@@ -417,7 +423,7 @@ export class DeleteOperatorAction extends Action {
} }
public run(actionInfo: IJobActionInfo): TPromise<boolean> { public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this; const self = this;
let operator = actionInfo.targetObject as sqlops.AgentOperatorInfo; let operator = actionInfo.targetObject as sqlops.AgentOperatorInfo;
self._notificationService.prompt( self._notificationService.prompt(
Severity.Info, Severity.Info,
@@ -425,12 +431,15 @@ export class DeleteOperatorAction extends Action {
[{ [{
label: DeleteOperatorAction.LABEL, label: DeleteOperatorAction.LABEL,
run: () => { run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator); self._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator);
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => { self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) { if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}", let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
operator.name, result.errorMessage ? result.errorMessage : 'Unknown error'); 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( constructor(
@INotificationService private _notificationService: INotificationService, @INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService, @IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService @ITelemetryService private _telemetryService: ITelemetryService
) { ) {
@@ -512,7 +522,10 @@ export class DeleteProxyAction extends Action {
if (!result || !result.success) { if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}", let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",
proxy.accountName, result.errorMessage ? result.errorMessage : 'Unknown error'); 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 --> <!-- Job History details -->
<div class='history-details'> <div class='history-details'>
<!-- Previous run list --> <!-- 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"> <table *ngIf="_showPreviousRuns === true">
<tr> <tr>
<td class="date-column"> <td class="date-column">

View File

@@ -7,12 +7,13 @@ import 'vs/css!./jobHistory';
import 'vs/css!sql/media/icons/common-icons'; import 'vs/css!sql/media/icons/common-icons';
import * as sqlops from 'sqlops'; import * as sqlops from 'sqlops';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom'; import * as dom from 'vs/base/browser/dom';
import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core'; import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component'; import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; 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 { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService';
import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities';
import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces';
@@ -103,6 +104,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
this._parentComponent = this._agentViewComponent; this._parentComponent = this._agentViewComponent;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
const self = this; const self = this;
this._treeController.onClick = (tree, element, event, origin = 'mouse') => { this._treeController.onClick = (tree, element, event, origin = 'mouse') => {
const payload = { origin: origin }; const payload = { origin: origin };
@@ -143,16 +145,25 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
}, {verticalScrollMode: ScrollbarVisibility.Visible}); }, {verticalScrollMode: ScrollbarVisibility.Visible});
this._register(attachListStyler(this._tree, this.themeService)); this._register(attachListStyler(this._tree, this.themeService));
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement)); this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
this.initActionBar();
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView); this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
this.initActionBar();
} }
private loadHistory() { private loadHistory() {
const self = this; const self = this;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let jobName = this._agentViewComponent.agentJobInfo.name; 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) { 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) { if (result.histories.length > 0) {
self._showPreviousRuns = true; self._showPreviousRuns = true;
self.buildHistoryTree(self, result.histories); self.buildHistoryTree(self, result.histories);
@@ -189,9 +200,14 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
stepViewRow.stepId = step.stepDetails.id.toString(); stepViewRow.stepId = step.stepDetails.id.toString();
return stepViewRow; 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 { } else {
this._showSteps = false; self._showSteps = false;
} }
self._cd.detectChanges(); self._cd.detectChanges();
} }
@@ -208,7 +224,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
private buildHistoryTree(self: any, jobHistories: sqlops.AgentJobHistoryInfo[]) { private buildHistoryTree(self: any, jobHistories: sqlops.AgentJobHistoryInfo[]) {
self._treeController.jobHistories = jobHistories; self._treeController.jobHistories = jobHistories;
self._jobCacheObject.setJobHistory(self._agentViewComponent.jobId, jobHistories);
let jobHistoryRows = this._treeController.jobHistories.map(job => self.convertToJobHistoryRow(job)); let jobHistoryRows = this._treeController.jobHistories.map(job => self.convertToJobHistoryRow(job));
self._treeDataSource.data = jobHistoryRows; self._treeDataSource.data = jobHistoryRows;
self._tree.setInput(new JobHistoryModel()); self._tree.setInput(new JobHistoryModel());
@@ -216,6 +231,13 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
if (self.agentJobHistoryInfo) { if (self.agentJobHistoryInfo) {
self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate); 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 { private toggleCollapse(): void {
@@ -249,17 +271,10 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
return this._showPreviousRuns !== true && this._noJobsAvailable === false; 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() { public onFirstVisible() {
this._agentJobInfo = this._agentViewComponent.agentJobInfo; this._agentJobInfo = this._agentViewComponent.agentJobInfo;
if (!this.agentJobInfo) { if (!this.agentJobInfo) {
this.agentJobInfo = this._agentJobInfo; this.agentJobInfo = this._agentJobInfo;
this.setActions();
} }
if (this.isRefreshing ) { if (this.isRefreshing ) {
@@ -272,9 +287,12 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
const self = this; const self = this;
if (this._jobCacheObject.prevJobID === this._agentViewComponent.jobId || jobHistories[0].jobId === this._agentViewComponent.jobId) { if (this._jobCacheObject.prevJobID === this._agentViewComponent.jobId || jobHistories[0].jobId === this._agentViewComponent.jobId) {
this._showPreviousRuns = true; 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); this.buildHistoryTree(self, jobHistories);
$('jobhistory-component .history-details .prev-run-list .monaco-tree').attr('tabIndex', '-1'); this._actionBar.context = { targetObject: this._agentJobInfo, ownerUri: this.ownerUri, jobHistoryComponent: this };
$('jobhistory-component .history-details .prev-run-list .monaco-tree-row').attr('tabIndex', '0');
this._cd.detectChanges(); this._cd.detectChanges();
} }
} else if (jobHistories && jobHistories.length === 0 ){ } else if (jobHistories && jobHistories.length === 0 ){
@@ -316,14 +334,25 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
protected initActionBar() { protected initActionBar() {
let runJobAction = this.instantiationService.createInstance(RunJobAction); let runJobAction = this.instantiationService.createInstance(RunJobAction);
let stopJobAction = this.instantiationService.createInstance(StopJobAction); 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; let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService); 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([ this._actionBar.setContent([
{ action: runJobAction }, { action: runJobAction },
{ action: stopJobAction }, { action: stopJobAction },
{ action: newStepAction } { action: refreshAction },
{ action: editJobAction }
]); ]);
} }

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
.all-jobs { jobhistory-component .all-jobs {
display: inline; display: inline-block;
font-size: 15px; font-size: 15px;
} }
@@ -17,6 +17,7 @@
.job-heading { .job-heading {
text-align: left; text-align: left;
padding-left: 13px; padding-left: 13px;
font-size: 1.5vw;
} }
.overview-container { .overview-container {
@@ -123,19 +124,6 @@ input#accordion:checked ~ .accordion-content {
content: url('../common/media/back_inverse.svg'); 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 { .vs .action-label.icon.newStepIcon {
background-image: url('../common/media/new.svg'); background-image: url('../common/media/new.svg');
} }
@@ -145,6 +133,21 @@ input#accordion:checked ~ .accordion-content {
background-image: url('../common/media/new_inverse.svg'); 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 { a.action-label.icon.runJobIcon.non-runnable {
opacity: 0.4; opacity: 0.4;
cursor: default; cursor: default;
@@ -197,8 +200,12 @@ table.step-list tr.step-row td {
border-left: 3px solid #2b56f2; border-left: 3px solid #2b56f2;
} }
.history-details > .job-steps > .step-list { .history-details > .job-steps > table.step-list {
padding-bottom: 10px; 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 { .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 { jobhistory-component > .jobhistory-heading-container {
display: flex; display: inline-block;
} }
jobhistory-component > .jobhistory-heading-container > .icon.in-progress { 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) { } else if (event.code === 'ArrowUp' || event.keyCode === 38) {
super.onUp(tree, event); super.onUp(tree, event);
return super.onEnter(tree, event); return super.onEnter(tree, event);
} else { } else if (event.code !== 'Tab' && event.keyCode !== 2) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return true; return true;
} }
return false;
} }
} }
@@ -96,6 +97,10 @@ export class JobHistoryDataSource implements tree.IDataSource {
public set data(data: JobHistoryRow[]) { public set data(data: JobHistoryRow[]) {
this._data = data; this._data = data;
} }
public getFirstElement() {
return this._data[0];
}
} }
export interface IListTemplate { export interface IListTemplate {

View File

@@ -9,19 +9,6 @@
<div class="steps-icon"></div> <div class="steps-icon"></div>
<h1 style="display: inline">Steps</h1> <h1 style="display: inline">Steps</h1>
</div> </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 class='steps-tree' style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%" ></div> <div #table style="position: absolute; height: 100%; width: 100%" ></div>
</div> </div>

View File

@@ -6,7 +6,7 @@
import 'vs/css!./jobStepsView'; import 'vs/css!./jobStepsView';
import * as dom from 'vs/base/browser/dom'; 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 { attachListStyler } from 'vs/platform/theme/common/styler';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ScrollbarVisibility } from 'vs/base/common/scrollable';
@@ -57,23 +57,51 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
} }
ngAfterContentChecked() { ngAfterContentChecked() {
if (this._jobHistoryComponent.stepRows.length > 0) { $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
this._treeDataSource.data = this._jobHistoryComponent.stepRows; this.layout();
this._tree.setInput(new JobStepsViewModel()); this._tree.setInput(new JobStepsViewModel());
this.layout(); this._tree.onDidScroll(() => {
$('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); $('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
$('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); });
} 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() { ngOnInit() {
this._treeDataSource.data = this._jobHistoryComponent.stepRows;
this._tree = new Tree(this._tableContainer.nativeElement, { this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController, controller: this._treeController,
dataSource: this._treeDataSource, dataSource: this._treeDataSource,
filter: this._treeFilter, filter: this._treeFilter,
renderer: this._treeRenderer renderer: this._treeRenderer
}, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible }); }, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible });
this.layout();
this._register(attachListStyler(this._tree, this.themeService)); this._register(attachListStyler(this._tree, this.themeService));
this._telemetryService.publicLog(TelemetryKeys.JobStepsView); this._telemetryService.publicLog(TelemetryKeys.JobStepsView);
} }

View File

@@ -21,14 +21,6 @@
user-select: initial; 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 { .steps-tree .list-row {
display: inline-flex; display: inline-flex;
height: 20px height: 20px
@@ -39,26 +31,31 @@
padding-top: 10px; padding-top: 10px;
} }
.step-columns .step-id-col, .steps-tree .tree-id-col { .steps-tree .tree-id-col {
padding-left: 10px; padding-left: 10px;
white-space: normal; white-space: normal;
text-align: center; text-align: left;
width: 60px; width: 60px;
} }
.step-columns .step-name-col, .steps-tree .tree-name-col { .steps-tree .tree-name-col {
padding-right: 10px; padding-right: 10px;
white-space: normal; white-space: normal;
text-align: center; text-align: left;
width: 350px; width: 300px;
} }
.step-columns .step-message-col, .steps-tree .tree-message-col { .steps-tree .tree-message-col {
padding-right: 10px; padding-right: 10px;
white-space: normal; white-space: normal;
text-align: left;
width: 700px;
}
.steps-tree .step-column-heading {
font-weight: bold;
text-align: center; text-align: center;
width: 680px; height: 40px;
} }
.steps-header > .steps-icon { .steps-header > .steps-icon {
@@ -87,4 +84,27 @@ jobstepsview-component {
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row { jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row {
width: 99.2%; 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 { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { generateUuid } from 'vs/base/common/uuid'; 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 { export class JobStepsViewRow {
public stepId: string; public stepId: string;
@@ -36,6 +36,20 @@ export class JobStepsViewController extends TreeDefaults.DefaultController {
return true; 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 { export class JobStepsViewDataSource implements tree.IDataSource {
@@ -84,7 +98,6 @@ export interface IListTemplate {
} }
export class JobStepsViewRenderer implements tree.IRenderer { export class JobStepsViewRenderer implements tree.IRenderer {
private _statusIcon: HTMLElement;
public getHeight(tree: tree.ITree, element: JobStepsViewRow): number { public getHeight(tree: tree.ITree, element: JobStepsViewRow): number {
return 40; return 40;
@@ -101,11 +114,10 @@ export class JobStepsViewRenderer implements tree.IRenderer {
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate { public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
let row = DOM.$('.list-row'); let row = DOM.$('.list-row');
let label = DOM.$('.label'); let label = DOM.$('.label');
this._statusIcon = this.createStatusIcon(); let statusIcon = this.createStatusIcon();
row.appendChild(this._statusIcon); row.appendChild(statusIcon);
row.appendChild(label); row.appendChild(label);
container.appendChild(row); container.appendChild(row);
let statusIcon = this._statusIcon;
return { statusIcon, label }; return { statusIcon, label };
} }
@@ -119,19 +131,26 @@ export class JobStepsViewRenderer implements tree.IRenderer {
let stepMessageCol: HTMLElement = DOM.$('div'); let stepMessageCol: HTMLElement = DOM.$('div');
stepMessageCol.className = 'tree-message-col'; stepMessageCol.className = 'tree-message-col';
stepMessageCol.innerText = element.message; 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).empty();
templateData.label.appendChild(stepIdCol); templateData.label.appendChild(stepIdCol);
templateData.label.appendChild(stepNameCol); templateData.label.appendChild(stepNameCol);
templateData.label.appendChild(stepMessageCol); templateData.label.appendChild(stepMessageCol);
let statusClass: string; if (element.runStatus) {
if (element.runStatus === 'Succeeded') { if (element.runStatus === 'Succeeded') {
statusClass = ' step-passed'; templateData.statusIcon.className = 'status-icon step-passed';
} else if (element.runStatus === 'Failed') { } else if (element.runStatus === 'Failed') {
statusClass = ' step-failed'; templateData.statusIcon.className = 'status-icon step-failed';
} else {
templateData.statusIcon.className = 'status-icon step-unknown';
}
} else { } else {
statusClass = ' step-unknown'; templateData.statusIcon.className = '';
} }
this._statusIcon.className += statusClass;
} }
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void { public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
@@ -140,7 +159,6 @@ export class JobStepsViewRenderer implements tree.IRenderer {
private createStatusIcon(): HTMLElement { private createStatusIcon(): HTMLElement {
let statusIcon: HTMLElement = DOM.$('div'); let statusIcon: HTMLElement = DOM.$('div');
statusIcon.className += 'status-icon';
return statusIcon; return statusIcon;
} }
} }

View File

@@ -42,6 +42,7 @@ import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component'; export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
export const ROW_HEIGHT: number = 45; export const ROW_HEIGHT: number = 45;
export const ACTIONBAR_PADDING: number = 10;
@Component({ @Component({
selector: JOBSVIEW_SELECTOR, 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 jobsViewToolbar = $('jobsview-component .agent-actionbar-container').get(0);
let statusBar = $('.part.statusbar').get(0); let statusBar = $('.part.statusbar').get(0);
if (jobsViewToolbar && statusBar) { if (jobsViewToolbar && statusBar) {
let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom; let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom + ACTIONBAR_PADDING;
let statusTop = statusBar.getBoundingClientRect().top; let statusTop = statusBar.getBoundingClientRect().top;
this._table.layout(new dom.Dimension( this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement), 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.jobHistories[job.jobId] = result.histories ? result.histories : [];
self._jobCacheObject.setJobSteps(job.jobId, self.jobSteps[job.jobId]); self._jobCacheObject.setJobSteps(job.jobId, self.jobSteps[job.jobId]);
self._jobCacheObject.setJobHistory(job.jobId, self.jobHistories[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 jobHistories = self._jobCacheObject.getJobHistory(job.jobId);
let previousRuns: sqlops.AgentJobHistoryInfo[]; let previousRuns: sqlops.AgentJobHistoryInfo[];
if (jobHistories.length >= 5) { if (jobHistories.length >= 5) {
@@ -630,14 +633,14 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
let runCharts = []; let runCharts = [];
for (let i = 0; i < chartHeights.length; i++) { for (let i = 0; i < chartHeights.length; i++) {
let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`); let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`);
runGraph.css('height', chartHeights[i]); if (runGraph.length > 0) {
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green'; runGraph.css('height', chartHeights[i]);
runGraph.css('background', bgColor); let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
runGraph.hover((e) => { runGraph.css('background', bgColor);
let currentTarget = e.currentTarget; runGraph.hover((e) => {
currentTarget.title = jobHistories[i].runDuration; let currentTarget = e.currentTarget;
}); currentTarget.title = jobHistories[i].runDuration;
if (runGraph.get(0)) { });
runCharts.push(runGraph.get(0).outerHTML); runCharts.push(runGraph.get(0).outerHTML);
} }
} }