Feature/agent finishes (#1149)

* added icons for steps and fixed bugs in jobs view page

* put svgs in common folder

* added steps header logo

* added full path for import

* changed codes to text

* removed cat id, changed bools to yes/no and fixed steps theme

* localized the strings

* set the jobs table column widths

* added indicators for failure, unknown and canceled jobs in jobs view

* fixed jobs panel style and jobs view scrolling

* fixed jobs view page styling

* fixed job history tree size rows

* made error messages copy-able

* made job history tree work with keyboard
This commit is contained in:
Aditya Bist
2018-04-17 16:45:22 -07:00
committed by GitHub
parent b9c877a109
commit 134f76c17f
19 changed files with 273 additions and 138 deletions

View File

@@ -5,7 +5,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<panel class="fullsize" [options]="panelOpt"> <panel class="dashboard-panel" [options]="panelOpt">
<tab [title]="jobsComponentTitle" class="fullsize" [identifier]="jobsTabIdentifier" <tab [title]="jobsComponentTitle" class="fullsize" [identifier]="jobsTabIdentifier"
[iconClass]="jobsIconClass"> [iconClass]="jobsIconClass">
<div id="jobsDiv" class="fullsize" *ngIf="showHistory === false"> <div id="jobsDiv" class="fullsize" *ngIf="showHistory === false">

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!../common/media/jobs'; import 'vs/css!../common/media/jobs';
import 'sql/parts/dashboard/common/dashboardPanelStyles';
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable} from '@angular/core'; import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable} from '@angular/core';
import * as Utils from 'sql/parts/connection/common/utils'; import * as Utils from 'sql/parts/connection/common/utils';

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
export class AgentJobUtilities {
public static convertToStatusString(status: number): string {
switch(status) {
case(0): return nls.localize('agentUtilities.failed','Failed');
case(1): return nls.localize('agentUtilities.succeeded', 'Succeeded');
case(3): return nls.localize('agentUtilities.canceled', 'Canceled');
case(5): return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
}
}
public static convertToExecutionStatusString(status: number): string {
switch(status) {
case(1): return nls.localize('agentUtilities.executing', 'Executing');
case(2): return nls.localize('agentUtilities.waitingForThread', 'Waiting for Thread');
case(3): return nls.localize('agentUtilities.betweenRetries', 'Between Retries');
case(4): return nls.localize('agentUtilities.idle', 'Idle');
case(5): return nls.localize('agentUtilities.suspended', 'Suspended');
case(6): return nls.localize('agentUtilities.obsolete', '[Obsolete]');
case(7): return nls.localize('agentUtilities.performingCompletionActions', 'PerformingCompletionActions');
default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
}
}
public static convertToResponse(bool: boolean) {
return bool ? nls.localize('agentUtilities.yes', 'Yes') : nls.localize('agentUtilities.no', 'No');
}
public static convertToNextRun(date: string) {
if (date.includes('1/1/0001')) {
return nls.localize('agentUtilities.notScheduled', 'Not Scheduled');
} else {
return date;
}
}
public static convertToLastRun(date: string) {
if (date.includes('1/1/0001')) {
return nls.localize('agentUtilities.neverRun', 'Never Run');
} else {
return date;
}
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#d02e00;}</style><clipPath id="clip-path"><path class="cls-1" d="M8.88,8l2.67-2.67-.88-.88L8,7.12,5.33,4.45l-.88.88L7.12,8,4.45,10.67l.88.88L8,8.88l2.67,2.67.88-.88ZM8,0a7.92,7.92,0,0,1,4,1.09A8.15,8.15,0,0,1,14.91,4a8,8,0,0,1,0,8.07A8.15,8.15,0,0,1,12,14.91a8,8,0,0,1-8.07,0A8.15,8.15,0,0,1,1.09,12,8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.09,7.92,7.92,0,0,1,8,0Z"/></clipPath></defs><title>failed</title><g class="cls-2"><rect class="cls-3" x="-5" y="-5" width="26" height="26"/></g></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@@ -21,10 +21,16 @@ jobhistory-component {
display: block; display: block;
} }
.vs-dark .job-heading-container {
height: 32px;
border-bottom: 3px solid #444444;
}
#jobsDiv .jobview-grid { #jobsDiv .jobview-grid {
padding-top: 15px;
height: 100%; height: 100%;
width : 100%; width : 100%;
display: block; display: block;
} }
.vs-dark #jobsDiv .slick-header-column { .vs-dark #jobsDiv .slick-header-column {
@@ -67,6 +73,21 @@ jobhistory-component {
background: green; background: green;
} }
#jobsDiv .jobview-jobnameindicatorfailure {
width: 5px;
background: red;
}
#jobsDiv .jobview-jobnameindicatorcancel {
width: 5px;
background: orange;
}
#jobsDiv .jobview-jobnameindicatorunknown {
width: 5px;
background: yellow;
}
#jobsDiv .jobview-jobnametext { #jobsDiv .jobview-jobnametext {
width: 100%; width: 100%;
} }
@@ -123,16 +144,15 @@ jobhistory-component {
background: #faf5f8 !important; background: #faf5f8 !important;
} }
.jobsview-icon { .jobsview-icon {
content: url('./job.svg'); background-image: url('./job.svg');
width: 25px;
} }
.vs-dark .jobsview-icon { .vs-dark .jobsview-icon {
content: url('./job_inverse.svg'); background-image: url('./job_inverse.svg');
} }
agentview-component .tabbedPanel .tabList .tab .tabLabel.icon { agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.even > .slick-cell,
padding: 20px 15px !important; agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell {
cursor: pointer;
} }

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><path class="cls-1" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/></clipPath></defs><title>step</title><g class="cls-2"><rect x="-5.71" y="-5.71" width="27.43" height="27.43"/></g></svg>

After

Width:  |  Height:  |  Size: 590 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2,.cls-4{fill:#fff;}.cls-1{clip-rule:evenodd;}.cls-2{fill-rule:evenodd;}.cls-3{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><path class="cls-1" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/></clipPath></defs><title>step_inverse</title><path class="cls-2" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/><g class="cls-3"><rect class="cls-4" x="-5.71" y="-5.71" width="27.43" height="27.43"/></g></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#3bb44a;}.cls-4{clip-path:url(#clip-path-2);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><path class="cls-1" d="M16,8a7.92,7.92,0,0,1-1.09,4A8.15,8.15,0,0,1,12,14.91a8,8,0,0,1-8.07,0A8.15,8.15,0,0,1,1.09,12,8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.09a8,8,0,0,1,8.07,0A8.15,8.15,0,0,1,14.91,4,7.92,7.92,0,0,1,16,8Z"/></clipPath><clipPath id="clip-path-2"><polygon class="cls-1" points="10.9 4.9 11.6 5.6 6.5 10.71 3.65 7.85 4.35 7.15 6.5 9.29 10.9 4.9"/></clipPath></defs><title>success_complete</title><g class="cls-2"><rect class="cls-3" x="-5" y="-5" width="26" height="26"/></g><g class="cls-4"><rect class="cls-5" x="-1.35" y="-0.1" width="17.95" height="15.81"/></g></svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -82,7 +82,7 @@
<!-- Job History details --> <!-- Job History details -->
<div class='history-details'> <div class='history-details'>
<!-- Previous run list --> <!-- Previous run list -->
<div style="width: 20%"> <div style="width: 20%; overflow-y: scroll">
<table *ngIf="_showPreviousRuns === true"> <table *ngIf="_showPreviousRuns === true">
<tr> <tr>
<td class="date-column"> <td class="date-column">
@@ -103,39 +103,39 @@
</h1> </h1>
<table class="step-list"> <table class="step-list">
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
<h3>Status:</h3> <h3>Status:</h3>
</td> </td>
<td height="30"> <td height="20">
<h3>{{_runStatus}}</h3> <h3>{{_runStatus}}</h3>
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
Job ID: Job ID:
</td> </td>
<td height="30"> <td height="20" style="user-select: initial">
{{agentJobHistoryInfo?.jobId || agentJobInfo?.jobId}} {{agentJobHistoryInfo?.jobId || agentJobInfo?.jobId}}
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
Message: Message:
</td> </td>
<td height="30"> <td height="20" style="user-select: initial">
{{agentJobHistoryInfo?.message}} {{agentJobHistoryInfo?.message}}
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
Duration: Duration:
</td> </td>
<td height="30"> <td height="20">
{{agentJobHistoryInfo?.runDuration}} {{agentJobHistoryInfo?.runDuration}}
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
Server: Server:
</td> </td>
<td> <td>
@@ -143,23 +143,23 @@
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
SQL message ID: SQL message ID:
</td> </td>
<td height="30"> <td height="20">
{{agentJobHistoryInfo?.sqlMessageId}} {{agentJobHistoryInfo?.sqlMessageId}}
</td> </td>
</tr> </tr>
<tr class="step-row"> <tr class="step-row">
<td height="30"> <td height="20">
Retries Attempted: Retries Attempted:
</td> </td>
<td height="30"> <td height="20">
{{agentJobHistoryInfo?.retriesAttempted}} {{agentJobHistoryInfo?.retriesAttempted}}
</td> </td>
</tr> </tr>
</table> </table>
<jobstepsview-component *ngIf="showSteps === true" [stepRows]="_stepRows"></jobstepsview-component> <jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
<h3 *ngIf="showSteps === false">No Steps Available</h3> <h3 *ngIf="showSteps === false">No Steps Available</h3>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@
import 'vs/css!./jobHistory'; import 'vs/css!./jobHistory';
import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy } from '@angular/core'; import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops'; import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachListStyler } from 'vs/platform/theme/common/styler'; import { attachListStyler } from 'vs/platform/theme/common/styler';
@@ -25,6 +25,7 @@ import { JobHistoryController, JobHistoryDataSource,
import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsView.component'; import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsView.component';
import { JobStepsViewRow } from './jobStepsViewTree'; import { JobStepsViewRow } from './jobStepsViewTree';
import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService'; import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService';
import { AgentJobUtilities } from '../common/agentJobUtilities';
export const DASHBOARD_SELECTOR: string = 'jobhistory-component'; export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
@@ -33,6 +34,7 @@ export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
templateUrl: decodeURI(require.toUrl('./jobHistory.component.html')), templateUrl: decodeURI(require.toUrl('./jobHistory.component.html')),
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
@Injectable()
export class JobHistoryComponent extends Disposable implements OnInit { export class JobHistoryComponent extends Disposable implements OnInit {
private _jobManagementService: IJobManagementService; private _jobManagementService: IJobManagementService;
@@ -102,28 +104,16 @@ export class JobHistoryComponent extends Disposable implements OnInit {
} else { } else {
tree.setFocus(element, payload); tree.setFocus(element, payload);
tree.setSelection([element], payload); tree.setSelection([element], payload);
self.agentJobHistoryInfo = self._treeController.jobHistories.filter(history => history.instanceId === element.instanceID)[0]; self.setStepsTree(element);
if (self.agentJobHistoryInfo) {
self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate);
if (self.agentJobHistoryInfo.steps) {
self._stepRows = self.agentJobHistoryInfo.steps.map(step => {
let stepViewRow = new JobStepsViewRow();
stepViewRow.message = step.message;
stepViewRow.runStatus = JobHistoryRow.convertToStatusString(self.agentJobHistoryInfo.runStatus);
self._runStatus = stepViewRow.runStatus;
stepViewRow.stepName = step.stepName;
stepViewRow.stepID = step.stepId.toString();
return stepViewRow;
});
this._showSteps = true;
} else {
this._showSteps = false;
}
self._cd.detectChanges();
}
} }
return true; return true;
}; };
this._treeController.onKeyDown = (tree, event) => {
this._treeController.onKeyDownWrapper(tree, event);
let element = tree.getFocus();
self.setStepsTree(element);
return true;
}
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,
@@ -145,6 +135,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
if (jobHistories && jobHistories.length > 0) { if (jobHistories && jobHistories.length > 0) {
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.buildHistoryTree(self, jobHistories); this.buildHistoryTree(self, jobHistories);
this._cd.detectChanges(); this._cd.detectChanges();
} }
@@ -185,6 +176,29 @@ export class JobHistoryComponent extends Disposable implements OnInit {
}); });
} }
private setStepsTree(element: any) {
const self = this;
self.agentJobHistoryInfo = self._treeController.jobHistories.filter(history => history.instanceId === element.instanceID)[0];
if (self.agentJobHistoryInfo) {
self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate);
if (self.agentJobHistoryInfo.steps) {
self._stepRows = self.agentJobHistoryInfo.steps.map(step => {
let stepViewRow = new JobStepsViewRow();
stepViewRow.message = step.message;
stepViewRow.runStatus = AgentJobUtilities.convertToStatusString(step.runStatus);
self._runStatus = AgentJobUtilities.convertToStatusString(self.agentJobHistoryInfo.runStatus);
stepViewRow.stepName = step.stepName;
stepViewRow.stepID = step.stepId.toString();
return stepViewRow;
});
this._showSteps = true;
} else {
this._showSteps = false;
}
self._cd.detectChanges();
}
}
private buildHistoryTree(self: any, jobHistories: AgentJobHistoryInfo[]) { private buildHistoryTree(self: any, jobHistories: AgentJobHistoryInfo[]) {
self._treeController.jobHistories = jobHistories; self._treeController.jobHistories = jobHistories;
self._jobCacheObject.setJobHistory(self._agentViewComponent.jobId, jobHistories); self._jobCacheObject.setJobHistory(self._agentViewComponent.jobId, jobHistories);
@@ -247,7 +261,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
private convertToJobHistoryRow(historyInfo: AgentJobHistoryInfo): JobHistoryRow { private convertToJobHistoryRow(historyInfo: AgentJobHistoryInfo): JobHistoryRow {
let jobHistoryRow = new JobHistoryRow(); let jobHistoryRow = new JobHistoryRow();
jobHistoryRow.runDate = historyInfo.runDate; jobHistoryRow.runDate = historyInfo.runDate;
jobHistoryRow.runStatus = JobHistoryRow.convertToStatusString(historyInfo.runStatus); jobHistoryRow.runStatus = AgentJobUtilities.convertToStatusString(historyInfo.runStatus);
jobHistoryRow.instanceID = historyInfo.instanceId; jobHistoryRow.instanceID = historyInfo.instanceId;
return jobHistoryRow; return jobHistoryRow;
} }
@@ -264,5 +278,9 @@ export class JobHistoryComponent extends Disposable implements OnInit {
this._showSteps = value; this._showSteps = value;
this._cd.detectChanges(); this._cd.detectChanges();
} }
public get stepRows() {
return this._stepRows;
}
} }

View File

@@ -186,6 +186,10 @@ table.step-list tr.step-row td {
height: 100%; height: 100%;
} }
.history-details > .job-steps > .step-list {
padding-bottom: 10px;
}
.vs-dark .step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before { .vs-dark .step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
background-image: none; background-image: none;
} }
@@ -202,11 +206,13 @@ table.step-list tr.step-row td {
height: 10px; height: 10px;
width: 10px; width: 10px;
display: inline-block; display: inline-block;
vertical-align: middle;
} }
.step-table .list-row .label { .step-table .list-row .label {
padding-left: 10px; padding-left: 10px;
display: inline-block; display: inline-block;
vertical-align: middle;
} }
.job-passed { .job-passed {
@@ -228,5 +234,5 @@ table.step-list tr.step-row td {
.steps-tree .monaco-tree .monaco-tree-row { .steps-tree .monaco-tree .monaco-tree-row {
white-space: normal; white-space: normal;
height: 40px !important; min-height: 40px !important;
} }

View File

@@ -28,20 +28,14 @@ import { OEAction } from 'sql/parts/objectExplorer/viewlet/objectExplorerActions
import { Builder, $, withElementById } from 'vs/base/browser/builder'; import { Builder, $, withElementById } from 'vs/base/browser/builder';
import { AgentJobHistoryInfo } from 'sqlops'; import { AgentJobHistoryInfo } from 'sqlops';
import { Agent } from 'vs/base/node/request'; import { Agent } from 'vs/base/node/request';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { JobHistoryComponent } from './jobHistory.component';
export class JobHistoryRow { export class JobHistoryRow {
runDate: string; runDate: string;
runStatus: string; runStatus: string;
instanceID: number; instanceID: number;
rowID: string = generateUuid(); rowID: string = generateUuid();
public static convertToStatusString(status: number): string {
switch(status) {
case(1): return 'Succeeded';
case(0): return 'Failed';
default: return 'Unknown';
}
}
} }
// Empty class just for tree input // Empty class just for tree input
@@ -56,10 +50,6 @@ export class JobHistoryController extends TreeDefaults.DefaultController {
return true; return true;
} }
public onContextMenu(tree: tree.ITree, element: JobHistoryRow, event: tree.ContextMenuEvent): boolean {
return true;
}
public set jobHistories(value: AgentJobHistoryInfo[]) { public set jobHistories(value: AgentJobHistoryInfo[]) {
this._jobHistories = value; this._jobHistories = value;
} }
@@ -68,6 +58,17 @@ export class JobHistoryController extends TreeDefaults.DefaultController {
return this._jobHistories; return this._jobHistories;
} }
public onKeyDownWrapper(tree: tree.ITree, event: IKeyboardEvent): boolean {
if (event.code === 'ArrowDown' || event.keyCode === 40) {
return super.onDown(tree, event);
} else if (event.code === 'ArrowUp' || event.keyCode === 38) {
return super.onUp(tree, event);
} else {
event.preventDefault();
event.stopPropagation();
return true;
}
}
} }
export class JobHistoryDataSource implements tree.IDataSource { export class JobHistoryDataSource implements tree.IDataSource {
@@ -119,7 +120,7 @@ export class JobHistoryRenderer implements tree.IRenderer {
private _statusIcon: HTMLElement; private _statusIcon: HTMLElement;
public getHeight(tree: tree.ITree, element: JobHistoryRow): number { public getHeight(tree: tree.ITree, element: JobHistoryRow): number {
return 22; return 30;
} }
public getTemplateId(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): string { public getTemplateId(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): string {

View File

@@ -5,7 +5,10 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<h1>Steps</h1> <div class="steps-header">
<div class="steps-icon"></div>
<h1 style="display: inline">Steps</h1>
</div>
<table class='step-columns'> <table class='step-columns'>
<tr> <tr>
<td class='step-id-col'> <td class='step-id-col'>

View File

@@ -5,7 +5,7 @@
import 'vs/css!./jobStepsView'; import 'vs/css!./jobStepsView';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnChanges, ViewChild, Input, Injectable } from '@angular/core'; import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable, AfterContentChecked } from '@angular/core';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
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';
@@ -17,6 +17,7 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import { AgentJobHistoryInfo } from 'sqlops'; import { AgentJobHistoryInfo } from 'sqlops';
import { JobStepsViewController, JobStepsViewDataSource, JobStepsViewFilter, import { JobStepsViewController, JobStepsViewDataSource, JobStepsViewFilter,
JobStepsViewRenderer, JobStepsViewRow, JobStepsViewModel} from 'sql/parts/jobManagement/views/jobStepsViewTree'; JobStepsViewRenderer, JobStepsViewRow, JobStepsViewModel} from 'sql/parts/jobManagement/views/jobStepsViewTree';
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component'; export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
@@ -24,7 +25,7 @@ export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
selector: JOBSTEPSVIEW_SELECTOR, selector: JOBSTEPSVIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./jobStepsView.component.html')) templateUrl: decodeURI(require.toUrl('./jobStepsView.component.html'))
}) })
export class JobStepsViewComponent extends Disposable implements OnInit, OnChanges { export class JobStepsViewComponent extends Disposable implements OnInit, AfterContentChecked {
private _jobManagementService: IJobManagementService; private _jobManagementService: IJobManagementService;
private _tree: Tree; private _tree: Tree;
@@ -36,18 +37,35 @@ export class JobStepsViewComponent extends Disposable implements OnInit, OnChang
@ViewChild('table') private _tableContainer: ElementRef; @ViewChild('table') private _tableContainer: ElementRef;
@Input() public stepRows: JobStepsViewRow[] = [];
constructor( constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService, @Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => DashboardServiceInterface)) private _dashboardService: DashboardServiceInterface @Inject(forwardRef(() => DashboardServiceInterface)) private _dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => JobHistoryComponent)) private _jobHistoryComponent: JobHistoryComponent
) { ) {
super(); super();
this._jobManagementService = bootstrapService.jobManagementService; this._jobManagementService = bootstrapService.jobManagementService;
} }
ngAfterContentChecked() {
if (this._jobHistoryComponent.stepRows.length > 0) {
this._treeDataSource.data = this._jobHistoryComponent.stepRows;
if (!this._tree) {
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
});
this._register(attachListStyler(this._tree, this.bootstrapService.themeService));
}
this._tree.layout(JobStepsViewComponent._pageSize);
this._tree.setInput(new JobStepsViewModel());
}
}
ngOnInit() { ngOnInit() {
let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri; let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri;
this._tree = new Tree(this._tableContainer.nativeElement, { this._tree = new Tree(this._tableContainer.nativeElement, {
@@ -56,22 +74,7 @@ export class JobStepsViewComponent extends Disposable implements OnInit, OnChang
filter: this._treeFilter, filter: this._treeFilter,
renderer: this._treeRenderer renderer: this._treeRenderer
}); });
} this._register(attachListStyler(this._tree, this.bootstrapService.themeService));
ngOnChanges() {
if (this.stepRows.length > 0) {
this._treeDataSource.data = this.stepRows;
if (!this._tree) {
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
});
}
this._tree.layout(JobStepsViewComponent._pageSize);
this._tree.setInput(new JobStepsViewModel());
}
} }
} }

View File

@@ -3,28 +3,29 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
.steps-tree .list-row .status-icon { .steps-tree .list-row .status-icon.step-passed {
height: 10px; content: url("../common/media/success_complete.svg");
width: 10px; height: 100%;
display: inline-block; }
margin-top: 4px;
.steps-tree .list-row .status-icon.step-failed {
content: url("../common/media/failed.svg");
height: 100%;
} }
.steps-tree .list-row .label { .steps-tree .list-row .label {
padding-left: 10px; padding-left: 10px;
display: flex; display: flex;
text-align: center; text-align: center;
vertical-align: middle;
user-select: initial;
} }
.step-passed { .steps-tree .list-row .status-icon.step-unknown {
background: green; height: 10px;
} width: 10px;
display: inline-block;
.step-failed { margin-top: 4px;
background: red;
}
.step-unknown {
background: yellow; background: yellow;
} }
@@ -35,6 +36,7 @@
.step-columns { .step-columns {
padding-left: 50px; padding-left: 50px;
padding-top: 10px;
} }
.step-columns .step-id-col, .steps-tree .tree-id-col { .step-columns .step-id-col, .steps-tree .tree-id-col {
@@ -58,3 +60,23 @@
text-align: center; text-align: center;
width: 680px; width: 680px;
} }
.steps-header > .steps-icon {
height: 25px;
padding-right: 10px;
display: inline;
vertical-align: middle;
}
.vs-dark .steps-header > .steps-icon,
.hc-black .steps-header > .steps-icon {
content: url("../common/media/step_inverse.svg");
}
.steps-header > .steps-icon {
content: url("../common/media/step.svg");
}
jobstepsview-component {
padding-top: 10px;
}

View File

@@ -164,7 +164,7 @@ 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'; statusIcon.className += 'status-icon';
return statusIcon; return statusIcon;
} }
} }

View File

@@ -4,6 +4,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<h1 class="job-heading">Jobs</h1> <div class="job-heading-container">
<h1 class="job-heading">Jobs</h1>
</div>
<div #jobsgrid class="jobview-grid"></div> <div #jobsgrid class="jobview-grid"></div>

View File

@@ -32,6 +32,7 @@ import { JobHistoryComponent } from './jobHistory.component';
import { AgentViewComponent } from '../agent/agentView.component'; import { AgentViewComponent } from '../agent/agentView.component';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowdetailview'; import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowdetailview';
import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService'; import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService';
import { AgentJobUtilities } from '../common/agentJobUtilities';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component'; export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
@@ -49,16 +50,15 @@ export class JobsViewComponent implements AfterContentChecked {
private _disposables = new Array<vscode.Disposable>(); private _disposables = new Array<vscode.Disposable>();
private columns: Array<Slick.Column<any>> = [ private columns: Array<Slick.Column<any>> = [
{ name: 'Name', field: 'name', formatter: this.renderName, width: 200, }, { name: nls.localize('jobColumns.name','Name'), field: 'name', formatter: this.renderName, width: 200 },
{ name: 'Last Run', field: 'lastRun' }, { name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', minWidth: 150 },
{ name: 'Next Run', field: 'nextRun' }, { name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', minWidth: 150 },
{ name: 'Enabled', field: 'enabled' }, { name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', minWidth: 70 },
{ name: 'Status', field: 'currentExecutionStatus' }, { name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', minWidth: 60 },
{ name: 'Category', field: 'category' }, { name: nls.localize('jobColumns.category','Category'), field: 'category', minWidth: 150 },
{ name: 'Runnable', field: 'runnable' }, { name: nls.localize('jobColumns.runnable','Runnable'), field: 'runnable', minWidth: 50 },
{ name: 'Schedule', field: 'hasSchedule' }, { name: nls.localize('jobColumns.schedule','Schedule'), field: 'hasSchedule', minWidth: 50 },
{ name: 'Category ID', field: 'categoryId' }, { name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', minWidth: 150 },
{ name: 'Last Run Outcome', field: 'lastRunOutcome' },
]; ];
private rowDetail: any; private rowDetail: any;
@@ -125,24 +125,14 @@ export class JobsViewComponent implements AfterContentChecked {
syncColumnCellResize: true, syncColumnCellResize: true,
enableColumnReorder: false, enableColumnReorder: false,
rowHeight: 45, rowHeight: 45,
enableCellNavigation: true enableCellNavigation: true,
autoHeight: false,
forceFitColumns: false
}; };
this.dataView = new Slick.Data.DataView({ inlineFilters: false }); this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: 'detailView-toggle',
preTemplate: this.loadingTemplate,
process: (job) => {
(<any>rowDetail).onAsyncResponse.notify({
'itemDetail': job,
}, undefined, null);
},
panelRows: 2,
postTemplate: () => ''
});
this.rowDetail = rowDetail;
this.rowDetail = new RowDetailView({});
columns.unshift(this.rowDetail.getColumnDefinition()); columns.unshift(this.rowDetail.getColumnDefinition());
this._table = new Table(this._gridEl.nativeElement, undefined, columns, options); this._table = new Table(this._gridEl.nativeElement, undefined, columns, options);
this._table.grid.setData(this.dataView, true); this._table.grid.setData(this.dataView, true);
@@ -174,27 +164,17 @@ export class JobsViewComponent implements AfterContentChecked {
id: job.jobId, id: job.jobId,
jobId: job.jobId, jobId: job.jobId,
name: job.name, name: job.name,
lastRun: job.lastRun, lastRun: AgentJobUtilities.convertToLastRun(job.lastRun),
nextRun: job.nextRun, nextRun: AgentJobUtilities.convertToNextRun(job.nextRun),
enabled: job.enabled, enabled: AgentJobUtilities.convertToResponse(job.enabled),
currentExecutionStatus: job.currentExecutionStatus, currentExecutionStatus: AgentJobUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
category: job.category, category: job.category,
runnable: job.runnable, runnable: AgentJobUtilities.convertToResponse(job.runnable),
hasSchedule: job.hasSchedule, hasSchedule: AgentJobUtilities.convertToResponse(job.hasSchedule),
categoryId: job.categoryId, lastRunOutcome: AgentJobUtilities.convertToStatusString(job.lastRunOutcome)
lastRunOutcome: job.lastRunOutcome
}; };
}); });
this._table.registerPlugin(<any>this.rowDetail);
this.rowDetail.onBeforeRowDetailToggle.subscribe(function(e, args) {
});
this.rowDetail.onAfterRowDetailToggle.subscribe(function(e, args) {
});
this.rowDetail.onAsyncEndUpdate.subscribe(function(e, args) {
});
this.dataView.beginUpdate(); this.dataView.beginUpdate();
this.dataView.setItems(jobViews); this.dataView.setItems(jobViews);
this.dataView.endUpdate(); this.dataView.endUpdate();
@@ -209,8 +189,27 @@ export class JobsViewComponent implements AfterContentChecked {
} }
renderName(row, cell, value, columnDef, dataContext) { renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass: string;
switch (dataContext.lastRunOutcome) {
case ('Succeeded'):
resultIndicatorClass = 'jobview-jobnameindicatorsuccess';
break;
case ('Failed'):
resultIndicatorClass = 'jobview-jobnameindicatorfailure';
break;
case ('Canceled'):
resultIndicatorClass = 'jobview-jobnameindicatorcancel';
break;
case ('Status Unknown'):
resultIndicatorClass = 'jobview-jobnameindicatorunknown';
break;
default:
resultIndicatorClass = 'jobview-jobnameindicatorunknown';
break;
}
return '<table class="jobview-jobnametable"><tr class="jobview-jobnamerow">' + return '<table class="jobview-jobnametable"><tr class="jobview-jobnamerow">' +
'<td nowrap class="jobview-jobnameindicatorsuccess"></td>' + '<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="jobview-jobnametext">' + dataContext.name + '</td>' + '<td nowrap class="jobview-jobnametext">' + dataContext.name + '</td>' +
'</tr></table>'; '</tr></table>';
} }

1
src/sql/sqlops.d.ts vendored
View File

@@ -1065,6 +1065,7 @@ declare module 'sqlops' {
stepName: string; stepName: string;
message: string; message: string;
runDate: string; runDate: string;
runStatus: number;
} }
export interface AgentJobHistoryInfo { export interface AgentJobHistoryInfo {