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

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