diff --git a/src/sql/base/browser/ui/table/plugins/rowDetailView.ts b/src/sql/base/browser/ui/table/plugins/rowDetailView.ts index 7365dc6a62..a2b444a9f6 100644 --- a/src/sql/base/browser/ui/table/plugins/rowDetailView.ts +++ b/src/sql/base/browser/ui/table/plugins/rowDetailView.ts @@ -269,10 +269,22 @@ export class RowDetailView { return item; } + public getErrorItem(parent, offset) { + let item: any = {}; + item.id = parent.id + '.' + offset; + item._collapsed = true; + item._isPadding = false; + item._parent = parent; + item._offset = offset; + item.jobId = parent.jobId; + item.name = parent.message ? parent.message : 'Error'; + return item; + } + ////////////////////////////////////////////////////////////// //create the detail ctr node. this belongs to the dev & can be custom-styled as per ////////////////////////////////////////////////////////////// - public applyTemplateNewLineHeight(item) { + public applyTemplateNewLineHeight(item, showError = false) { // the height seems to be calculated by the template row count (how many line of items does the template have) let rowCount = this._options.panelRows; @@ -284,11 +296,14 @@ export class RowDetailView { let idxParent = this._dataView.getIdxById(item.id); for (let idx = 1; idx <= item._sizePadding; idx++) { - this._dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx)); + if (showError) { + this._dataView.insertItem(idxParent + idx, this.getErrorItem(item, 'error')); + } else { + this._dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx)); + } } } - public getColumnDefinition() { return { id: this._options.columnId, diff --git a/src/sql/parts/jobManagement/agent/agentView.component.ts b/src/sql/parts/jobManagement/agent/agentView.component.ts index 6d2745e32c..a33c438a1c 100644 --- a/src/sql/parts/jobManagement/agent/agentView.component.ts +++ b/src/sql/parts/jobManagement/agent/agentView.component.ts @@ -38,6 +38,7 @@ export class AgentViewComponent { private _jobId: string = null; private _agentJobInfo: AgentJobInfo = null; private _refresh: boolean = undefined; + private _expanded: Map; public jobsIconClass: string = 'jobsview-icon'; @@ -50,6 +51,7 @@ export class AgentViewComponent { constructor( @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef){ + this._expanded = new Map(); } /** @@ -71,6 +73,10 @@ export class AgentViewComponent { return this._refresh; } + public get expanded(): Map { + return this._expanded; + } + /** * Public Setters */ @@ -94,4 +100,8 @@ export class AgentViewComponent { this._refresh = value; this._cd.detectChanges(); } + + public setExpanded(jobId: string, errorMessage: string) { + this._expanded.set(jobId, errorMessage); + } } diff --git a/src/sql/parts/jobManagement/common/media/jobs.css b/src/sql/parts/jobManagement/common/media/jobs.css index d30b48db97..8d0e811346 100644 --- a/src/sql/parts/jobManagement/common/media/jobs.css +++ b/src/sql/parts/jobManagement/common/media/jobs.css @@ -27,12 +27,15 @@ jobhistory-component { } #jobsDiv .jobview-grid { - padding-top: 15px; - height: 100%; + height: 96%; width : 100%; display: block; } +#jobsDiv .jobview-grid > .monaco-table > div[class^="slickgrid_"] { + overflow: scroll !important; +} + .vs-dark #jobsDiv .slick-header-column { background: #333333 !important; } @@ -54,7 +57,6 @@ jobhistory-component { border-right: transparent !important; border-left: transparent !important; line-height: 33px !important; - vertical-align: middle; } #jobsDiv .jobview-joblist { @@ -92,6 +94,25 @@ jobhistory-component { width: 100%; } +#jobsDiv .job-with-error { + border-bottom: none; +} + +#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row { + width: 100%; + opacity: 1; + font-weight: 700; + color: orangered; +} + +#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.error-row { + opacity: 0; +} + +#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row { + opacity: 1; +} + #jobsDiv .jobview-splitter { height: 1px; width: 100%; @@ -155,4 +176,10 @@ jobhistory-component { agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.even > .slick-cell, agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell { cursor: pointer; +} + +jobsview-component .jobview-grid > .monaco-table .slick-viewport > .grid-canvas + +.vs-dark .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle { + border-left: 1px dotted white; } \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.html b/src/sql/parts/jobManagement/views/jobHistory.component.html index 7fcdbb0d03..e7df4ef7bd 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.html +++ b/src/sql/parts/jobManagement/views/jobHistory.component.html @@ -82,7 +82,7 @@
-
+
@@ -93,11 +93,11 @@
-

No Previous Runs Available

+

No Previous Runs Available

-
+

{{agentJobHistoryInfo?.runDate}}

diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index 0b681128d5..9afe7e09c7 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./jobHistory'; - import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core'; import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -27,6 +26,8 @@ import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsVie import { JobStepsViewRow } from './jobStepsViewTree'; import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService'; import { AgentJobUtilities } from '../common/agentJobUtilities'; +import { ITreeOptions } from 'vs/base/parts/tree/browser/tree'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export const DASHBOARD_SELECTOR: string = 'jobhistory-component'; @@ -84,7 +85,9 @@ export class JobHistoryComponent extends Disposable implements OnInit { this._jobCacheObject.serverName = serverName; this._jobManagementService.addToCache(serverName, this._jobCacheObject); } - + $('#accordion').keypress(e => { + let meme = e; + }); } ngOnInit() { @@ -105,22 +108,24 @@ export class JobHistoryComponent extends Disposable implements OnInit { } else { tree.setFocus(element, payload); tree.setSelection([element], payload); - self.setStepsTree(element); + self.setStepsTree(element); } return true; }; this._treeController.onKeyDown = (tree, event) => { this._treeController.onKeyDownWrapper(tree, event); let element = tree.getFocus(); - self.setStepsTree(element); + if (element) { + self.setStepsTree(element); + } return true; - } + }; this._tree = new Tree(this._tableContainer.nativeElement, { controller: this._treeController, dataSource: this._treeDataSource, filter: this._treeFilter, renderer: this._treeRenderer - }); + }, {verticalScrollMode: ScrollbarVisibility.Visible}); this._register(attachListStyler(this._tree, this.bootstrapService.themeService)); this._tree.layout(1024); } @@ -154,7 +159,7 @@ export class JobHistoryComponent extends Disposable implements OnInit { } } - loadHistory() { + private loadHistory() { const self = this; let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri; this._jobManagementService.getJobHistory(ownerUri, this._agentViewComponent.jobId).then((result) => { @@ -172,7 +177,7 @@ export class JobHistoryComponent extends Disposable implements OnInit { } else { self._showPreviousRuns = false; self._showSteps = false; - this._cd.detectChanges(); + self._cd.detectChanges(); } }); } diff --git a/src/sql/parts/jobManagement/views/jobHistory.css b/src/sql/parts/jobManagement/views/jobHistory.css index 11bc888882..8e3aaf26a5 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.css +++ b/src/sql/parts/jobManagement/views/jobHistory.css @@ -177,6 +177,7 @@ table.step-list tr.step-row td { padding-left: 10px; height: 100%; width: 90%; + overflow-y: scroll; } .history-details > .job-steps { @@ -235,4 +236,8 @@ table.step-list tr.step-row td { .steps-tree .monaco-tree .monaco-tree-row { white-space: normal; min-height: 40px !important; +} + +jobhistory-component .history-details .step-table.prev-run-list .monaco-scrollable-element { + overflow-y: scroll !important; } \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobHistoryTree.ts b/src/sql/parts/jobManagement/views/jobHistoryTree.ts index 90b3fed776..79c91e51b7 100644 --- a/src/sql/parts/jobManagement/views/jobHistoryTree.ts +++ b/src/sql/parts/jobManagement/views/jobHistoryTree.ts @@ -143,7 +143,7 @@ export class JobHistoryRenderer implements tree.IRenderer { } public renderElement(tree: tree.ITree, element: JobHistoryRow, templateId: string, templateData: IListTemplate): void { - templateData.label.innerText = element.runDate + '\t\t\t' + element.runStatus; + templateData.label.innerHTML = element.runDate + '  ' + element.runStatus; let statusClass: string; if (element.runStatus === 'Succeeded') { statusClass = ' job-passed'; diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index 5ffd92c329..c2e21f2d2d 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -19,6 +19,7 @@ import { AgentJobHistoryInfo } from 'sqlops'; import { JobStepsViewController, JobStepsViewDataSource, JobStepsViewFilter, JobStepsViewRenderer, JobStepsViewRow, JobStepsViewModel} from 'sql/parts/jobManagement/views/jobStepsViewTree'; import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component'; @@ -59,7 +60,7 @@ export class JobStepsViewComponent extends Disposable implements OnInit, AfterCo dataSource: this._treeDataSource, filter: this._treeFilter, renderer: this._treeRenderer - }); + }, {verticalScrollMode: ScrollbarVisibility.Visible}); this._register(attachListStyler(this._tree, this.bootstrapService.themeService)); } this._tree.layout(JobStepsViewComponent._pageSize); @@ -74,7 +75,7 @@ export class JobStepsViewComponent extends Disposable implements OnInit, AfterCo dataSource: this._treeDataSource, filter: this._treeFilter, renderer: this._treeRenderer - }); + }, {verticalScrollMode: ScrollbarVisibility.Visible}); this._register(attachListStyler(this._tree, this.bootstrapService.themeService)); } } diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index 8bd3a7fb15..72ee8d1fbc 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -19,7 +19,6 @@ import * as tree from 'vs/base/parts/tree/browser/tree'; 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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IAction } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { generateUuid } from 'vs/base/common/uuid'; diff --git a/src/sql/parts/jobManagement/views/jobsView.component.html b/src/sql/parts/jobManagement/views/jobsView.component.html index 2a2491873d..039503144b 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.html +++ b/src/sql/parts/jobManagement/views/jobsView.component.html @@ -5,7 +5,8 @@ *--------------------------------------------------------------------------------------------*/ -->
-

Jobs

+

Jobs

+

No Jobs Available

-
+
\ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index d5f1f7c42e..b3d5b6898e 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -9,7 +9,6 @@ import 'vs/css!sql/parts/grid/media/styles'; import 'vs/css!sql/parts/grid/media/slick.grid'; import 'vs/css!sql/parts/grid/media/slickGrid'; import 'vs/css!../common/media/jobs'; -import 'vs/css!../common/media/detailview'; import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core'; import * as Utils from 'sql/parts/connection/common/utils'; @@ -51,19 +50,19 @@ export class JobsViewComponent implements AfterContentChecked { private _disposables = new Array(); private columns: Array> = [ - { name: nls.localize('jobColumns.name','Name'), field: 'name', formatter: this.renderName, width: 200 }, - { name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', minWidth: 150 }, - { name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', minWidth: 150 }, - { name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', minWidth: 70 }, - { name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', minWidth: 60 }, - { name: nls.localize('jobColumns.category','Category'), field: 'category', minWidth: 150 }, - { name: nls.localize('jobColumns.runnable','Runnable'), field: 'runnable', minWidth: 50 }, - { name: nls.localize('jobColumns.schedule','Schedule'), field: 'hasSchedule', minWidth: 50 }, - { name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', minWidth: 150 }, + { name: nls.localize('jobColumns.name','Name'), field: 'name', formatter: this.renderName, width: 200, id: 'name' }, + { name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', minWidth: 150, id: 'lastRun' }, + { name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', minWidth: 150, id: 'nextRun' }, + { name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', minWidth: 70, id: 'enabled' }, + { name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', minWidth: 60, id: 'currentExecutionStatus' }, + { name: nls.localize('jobColumns.category','Category'), field: 'category', minWidth: 150, id: 'category' }, + { name: nls.localize('jobColumns.runnable','Runnable'), field: 'runnable', minWidth: 50, id: 'runnable' }, + { name: nls.localize('jobColumns.schedule','Schedule'), field: 'hasSchedule', minWidth: 50, id: 'hasSchedule' }, + { name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', minWidth: 150, id: 'lastRunOutcome' }, ]; - private rowDetail: any; - private dataView: any; + private rowDetail: RowDetailView; + private dataView: Slick.Data.DataView; @ViewChild('jobsgrid') _gridEl: ElementRef; private isVisible: boolean = false; @@ -72,6 +71,7 @@ export class JobsViewComponent implements AfterContentChecked { public jobs: sqlops.AgentJobInfo[]; public jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = Object.create(null); private _serverName: string; + private _isCloud: boolean; constructor( @Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService, @@ -91,6 +91,7 @@ export class JobsViewComponent implements AfterContentChecked { this._jobCacheObject.serverName = this._serverName; this._jobManagementService.addToCache(this._serverName, this._jobCacheObject); } + this._isCloud = this._dashboardService.connectionManagementService.connectionInfo.serverInfo.isCloud; } ngAfterContentChecked() { @@ -126,14 +127,23 @@ export class JobsViewComponent implements AfterContentChecked { syncColumnCellResize: true, enableColumnReorder: false, rowHeight: 45, - enableCellNavigation: true, - autoHeight: false, - forceFitColumns: false + enableCellNavigation: true }; this.dataView = new Slick.Data.DataView({ inlineFilters: false }); - this.rowDetail = new RowDetailView({}); + let rowDetail = new RowDetailView({ + cssClass: '_detail_selector', + process: (job) => { + (rowDetail).onAsyncResponse.notify({ + 'itemDetail': job + }, undefined, this); + }, + useRowClick: false, + panelRows: 1 + }); + this.rowDetail = rowDetail; + columns.unshift(this.rowDetail.getColumnDefinition()); this._table = new Table(this._gridEl.nativeElement, undefined, columns, options); this._table.grid.setData(this.dataView, true); @@ -159,7 +169,7 @@ export class JobsViewComponent implements AfterContentChecked { } } - onJobsAvailable(jobs: sqlops.AgentJobInfo[]) { + private onJobsAvailable(jobs: sqlops.AgentJobInfo[]) { let jobViews = jobs.map((job) => { return { id: job.jobId, @@ -175,6 +185,14 @@ export class JobsViewComponent implements AfterContentChecked { lastRunOutcome: AgentJobUtilities.convertToStatusString(job.lastRunOutcome) }; }); + this._table.registerPlugin(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.setItems(jobViews); @@ -182,14 +200,57 @@ export class JobsViewComponent implements AfterContentChecked { this._table.resizeCanvas(); this._table.autosizeColumns(); + let expandedJobs = this._agentViewComponent.expanded; + let expansions = 0; + for (let i = 0; i < jobs.length; i++){ + let job = jobs[i]; + if (job.lastRunOutcome === 0 && !expandedJobs.get(job.jobId)) { + this.expandJobRowDetails(i+expandedJobs.size); + this.addToStyleHash(i+expandedJobs.size); + this._agentViewComponent.setExpanded(job.jobId, 'temp'); + } else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) { + this.expandJobRowDetails(i+expansions); + this.addToStyleHash(i+expansions); + expansions++; + } + } + + $('.jobview-jobnamerow').hover(e => { + let currentTarget = e.currentTarget; + currentTarget.title = currentTarget.innerText; + }); this.loadJobHistories(); } - loadingTemplate() { - return '
Loading...
'; + private setRowWithErrorClass(hash: {[index: number]: {[id: string]: string;}}, row: number, errorClass: string) { + hash[row] = { + '_detail_selector': errorClass, + 'id': errorClass, + 'jobId': errorClass, + 'name': errorClass, + 'lastRun': errorClass, + 'nextRun': errorClass, + 'enabled': errorClass, + 'currentExecutionStatus': errorClass, + 'category': errorClass, + 'runnable': errorClass, + 'hasSchedule': errorClass, + 'lastRunOutcome': errorClass + }; + return hash; } - renderName(row, cell, value, columnDef, dataContext) { + private addToStyleHash(row: number) { + let hash : { + [index: number]: { + [id: string]: string; + }} = {}; + hash = this.setRowWithErrorClass(hash, row, 'job-with-error'); + hash = this.setRowWithErrorClass(hash, row+1, 'error-row'); + this._table.grid.setCellCssStyles('error-row'+row.toString(), hash); + } + + private renderName(row, cell, value, columnDef, dataContext) { let resultIndicatorClass: string; switch (dataContext.lastRunOutcome) { case ('Succeeded'): @@ -205,7 +266,7 @@ export class JobsViewComponent implements AfterContentChecked { resultIndicatorClass = 'jobview-jobnameindicatorunknown'; break; default: - resultIndicatorClass = 'jobview-jobnameindicatorunknown'; + resultIndicatorClass = 'jobview-jobnameindicatorfailure'; break; } @@ -215,23 +276,49 @@ export class JobsViewComponent implements AfterContentChecked { ''; } - loadJobHistories() { + private expandJobRowDetails(rowIdx: number, message?: string): void { + let item = this.dataView.getItemByIdx(rowIdx); + item.message = this._agentViewComponent.expanded.get(item.jobId); + this.rowDetail.applyTemplateNewLineHeight(item, true); + } + + private loadJobHistories() { + const self = this; if (this.jobs) { - this.jobs.forEach((job) => { + let erroredJobs = 0; + for (let i = 0; i < this.jobs.length; i++) { + let job = this.jobs[i]; let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri; this._jobManagementService.getJobHistory(ownerUri, job.jobId).then((result) => { if (result.jobs) { - this.jobHistories[job.jobId] = result.jobs; - this._jobCacheObject.setJobHistory(job.jobId, result.jobs); + self.jobHistories[job.jobId] = result.jobs; + self._jobCacheObject.setJobHistory(job.jobId, result.jobs); + if (self._agentViewComponent.expanded.has(job.jobId)) { + let jobHistory = self._jobCacheObject.getJobHistory(job.jobId)[0]; + let item = self.dataView.getItemById(job.jobId + '.error'); + let noStepsMessage = nls.localize('jobsView.noSteps', 'No Steps available for this job.'); + let errorMessage = jobHistory ? jobHistory.message: noStepsMessage; + item['name'] = item['name'] + ': ' + errorMessage; + self._agentViewComponent.setExpanded(job.jobId, errorMessage); + self.dataView.updateItem(job.jobId + '.error', item); + + } } }); - }); + } } } + private isErrorRow(jobName: string) { + return jobName.includes('Error'); + } + private getJob(args: Slick.OnClickEventArgs): sqlops.AgentJobInfo { let row = args.row; let jobName = args.grid.getCellNode(row, 1).innerText.trim(); + if (this.isErrorRow(jobName)) { + jobName = args.grid.getCellNode(row-1, 1).innerText.trim(); + } let job = this.jobs.filter(job => job.name === jobName)[0]; return job; }