Agent: Filtering & Sorting (#1441)

* have a working filter

* fixed error details when filtering

* filtering with styling done

* fix transition styling

* fixed more styling issues

* optimized errors when switching pages

* added sorting functionality

* removed dead code

* fixed styling issues when sorting

* added sorting with styling for every column

* code review comments

* fixed styling issue when a bigger filter was applied, followed by a smaller one and then cleared

* use absolute paths in imports

* fixed issues with styling when sorting is performed on a filtered dataset
This commit is contained in:
Aditya Bist
2018-06-08 17:08:01 -07:00
committed by GitHub
parent 3afd3b0ff3
commit e870a309c0
12 changed files with 888 additions and 70 deletions

View File

@@ -92,4 +92,52 @@ export class AgentJobUtilities {
return;
}
}
public static convertColFieldToName(colField: string) {
switch(colField) {
case('name'):
return 'Name';
case('lastRun'):
return 'Last Run';
case('nextRun'):
return 'Next Run';
case('enabled'):
return 'Enabled';
case('status'):
return 'Status';
case('category'):
return 'Category';
case('runnable'):
return 'Runnable';
case('schedule'):
return 'Schedule';
case('lastRunOutcome'):
return 'Last Run Outcome';
}
return '';
}
public static convertColNameToField(columnName: string) {
switch(columnName) {
case('Name'):
return 'name';
case('Last Run'):
return 'lastRun';
case('Next Run'):
return 'nextRun';
case('Enabled'):
return 'enabled';
case('Status'):
return 'status';
case('Category'):
return 'category';
case('Runnable'):
return 'runnable';
case('Schedule'):
return 'schedule';
case('Last Run Outcome'):
return 'lastRunOutcome';
}
return '';
}
}

View File

@@ -82,6 +82,7 @@ export class JobCacheObject {
private _jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = {};
private _prevJobID: string;
private _serverName: string;
private _dataView: Slick.Data.DataView<any>;
/* Getters */
public get jobs(): sqlops.AgentJobInfo[] {
@@ -104,6 +105,10 @@ export class JobCacheObject {
return this._serverName;
}
public get dataView(): Slick.Data.DataView<any> {
return this._dataView;
}
/* Setters */
public set jobs(value: sqlops.AgentJobInfo[]) {
this._jobs = value;
@@ -125,4 +130,7 @@ export class JobCacheObject {
this._serverName = value;
}
}
public set dataView(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
}

View File

@@ -301,7 +301,4 @@ export class JobHistoryComponent extends Disposable implements OnInit {
this._showSteps = value;
this._cd.detectChanges();
}
}

View File

@@ -10,6 +10,7 @@ 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!sql/media/icons/common-icons';
import 'vs/css!sql/base/browser/ui/table/media/table';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core';
@@ -17,18 +18,27 @@ import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from '../agent/agentView.component';
import { attachTableStyler } from 'sql/common/theme/styler';
import { JobHistoryComponent } from 'src/sql/parts/jobManagement/views/jobHistory.component';
import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowdetailview';
import { JobCacheObject } from 'sql/parts/jobManagement/common/jobManagementService';
import { AgentJobUtilities } from '../common/agentJobUtilities';
import { AgentJobUtilities } from 'sql/parts/jobManagement/common/agentJobUtilities';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { BaseFocusDirectionTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import * as Utils from 'sql/parts/connection/common/utils';
import * as dom from 'vs/base/browser/dom';
import { IJobManagementService } from '../common/interfaces';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
@Component({
@@ -44,19 +54,28 @@ export class JobsViewComponent implements AfterContentChecked {
private _disposables = new Array<vscode.Disposable>();
private columns: Array<Slick.Column<any>> = [
{ name: nls.localize('jobColumns.name', 'Name'), field: 'name', formatter: this.renderName, width: 200, id: 'name' },
{ name: nls.localize('jobColumns.lastRun', 'Last Run'), field: 'lastRun', width: 150, id: 'lastRun' },
{ name: nls.localize('jobColumns.nextRun', 'Next Run'), field: 'nextRun', width: 150, id: 'nextRun' },
{ name: nls.localize('jobColumns.enabled', 'Enabled'), field: 'enabled', width: 70, id: 'enabled' },
{ name: nls.localize('jobColumns.status', 'Status'), field: 'currentExecutionStatus', width: 60, id: 'currentExecutionStatus' },
{ name: nls.localize('jobColumns.category', 'Category'), field: 'category', width: 150, id: 'category' },
{ name: nls.localize('jobColumns.runnable', 'Runnable'), field: 'runnable', width: 50, id: 'runnable' },
{ name: nls.localize('jobColumns.schedule', 'Schedule'), field: 'hasSchedule', width: 50, id: 'hasSchedule' },
{ name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', width: 150, id: 'lastRunOutcome' },
{ name: nls.localize('jobColumns.name','Name'), field: 'name', formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext), 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 options: Slick.GridOptions<any> = {
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: 45,
enableCellNavigation: true,
editable: true
};
private rowDetail: RowDetailView;
private dataView: Slick.Data.DataView<any>;
private filterPlugin: any;
private dataView: any;
@ViewChild('jobsgrid') _gridEl: ElementRef;
private isVisible: boolean = false;
@@ -68,13 +87,18 @@ export class JobsViewComponent implements AfterContentChecked {
private _isCloud: boolean;
private _showProgressWheel: boolean;
private _tabHeight: number;
private filterStylingMap: { [columnName: string]: [any] ;} = {};
private filterStack = ['start'];
private filterValueMap: { [columnName: string]: string[] ;} = {};
private sortingStylingMap: { [columnName: string]: any; } = {};
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) private _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IThemeService) private _themeService: IThemeService
) {
let jobCacheObjectMap = this._jobManagementService.jobCacheObjectMap;
this._serverName = _dashboardService.connectionManagementService.connectionInfo.connectionProfile.serverName;
@@ -126,16 +150,8 @@ export class JobsViewComponent implements AfterContentChecked {
column.rerenderOnResize = true;
return column;
});
let options = <Slick.GridOptions<any>>{
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: 45,
enableCellNavigation: true,
forceFitColumns: true
};
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
// create the table
this.dataView = new Slick.Data.DataView();
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
process: (job) => {
@@ -147,9 +163,13 @@ export class JobsViewComponent implements AfterContentChecked {
panelRows: 1
});
this.rowDetail = rowDetail;
columns.unshift(this.rowDetail.getColumnDefinition());
this._table = new Table(this._gridEl.nativeElement, undefined, columns, options);
let filterPlugin = new HeaderFilter({}, this._themeService);
this.filterPlugin = filterPlugin;
this._table = new Table(this._gridEl.nativeElement, undefined, columns, this.options);
this._table.grid.setData(this.dataView, true);
this._table.grid.onClick.subscribe((e, args) => {
let job = self.getJob(args);
@@ -158,7 +178,7 @@ export class JobsViewComponent implements AfterContentChecked {
self._agentViewComponent.showHistory = true;
});
if (cached && this._agentViewComponent.refresh !== true) {
this.onJobsAvailable(this._jobCacheObject.jobs);
this.onJobsAvailable(null);
} else {
let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getJobs(ownerUri).then((result) => {
@@ -172,50 +192,135 @@ export class JobsViewComponent implements AfterContentChecked {
}
private onJobsAvailable(jobs: sqlops.AgentJobInfo[]) {
let jobViews = jobs.map((job) => {
return {
id: job.jobId,
jobId: job.jobId,
name: job.name,
lastRun: AgentJobUtilities.convertToLastRun(job.lastRun),
nextRun: AgentJobUtilities.convertToNextRun(job.nextRun),
enabled: AgentJobUtilities.convertToResponse(job.enabled),
currentExecutionStatus: AgentJobUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
category: job.category,
runnable: AgentJobUtilities.convertToResponse(job.runnable),
hasSchedule: AgentJobUtilities.convertToResponse(job.hasSchedule),
lastRunOutcome: AgentJobUtilities.convertToStatusString(job.lastRunOutcome)
};
});
let jobViews: any;
if (!jobs) {
let dataView = this._jobCacheObject.dataView;
jobViews = dataView.getItems();
} else {
jobViews = jobs.map((job) => {
return {
id: job.jobId,
jobId: job.jobId,
name: job.name,
lastRun: AgentJobUtilities.convertToLastRun(job.lastRun),
nextRun: AgentJobUtilities.convertToNextRun(job.nextRun),
enabled: AgentJobUtilities.convertToResponse(job.enabled),
currentExecutionStatus: AgentJobUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
category: job.category,
runnable: AgentJobUtilities.convertToResponse(job.runnable),
hasSchedule: AgentJobUtilities.convertToResponse(job.hasSchedule),
lastRunOutcome: AgentJobUtilities.convertToStatusString(job.lastRunOutcome)
};
});
}
this._table.registerPlugin(<any>this.rowDetail);
this.filterPlugin.onFilterApplied.subscribe((e, args) => {
this.dataView.refresh();
this._table.grid.resetActiveCell();
let filterValues = args.column.filterValues;
if (filterValues) {
if (filterValues.length === 0) {
// if an associated styling exists with the current filters
if (this.filterStylingMap[args.column.name]) {
let filterLength = this.filterStylingMap[args.column.name].length;
// then remove the filtered styling
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[args.column.name].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
delete this.filterStylingMap[args.column.name];
let index = this.filterStack.indexOf(args.column.name, 0);
if (index > -1) {
this.filterStack.splice(index, 1);
delete this.filterValueMap[args.column.name];
}
// apply the previous filter styling
let currentItems = this.dataView.getFilteredItems();
let styledItems = this.filterValueMap[this.filterStack[this.filterStack.length-1]][1];
if (styledItems === currentItems) {
let lastColStyle = this.filterStylingMap[this.filterStack[this.filterStack.length-1]];
for (let i = 0; i < lastColStyle.length; i++) {
this._table.grid.setCellCssStyles(lastColStyle[i][0], lastColStyle[i][1]);
}
} else {
// style it all over again
let seenJobs = 0;
for (let i = 0; i < currentItems.length; i++) {
this._table.grid.removeCellCssStyles('error-row'+i.toString());
let item = this.dataView.getFilteredItems()[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenJobs ++;
i++;
}
seenJobs++;
}
this.dataView.refresh();
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
this._table.grid.resetActiveCell();
}
if (this.filterStack.length === 0) {
this.filterStack = ['start'];
}
}
} else {
let seenJobs = 0;
for (let i = 0; i < this.jobs.length; i++) {
this._table.grid.removeCellCssStyles('error-row'+i.toString());
let item = this.dataView.getItemByIdx(i);
// current filter
if (_.contains(filterValues, item[args.column.field])) {
// check all previous filters
if (this.checkPreviousFilters(item)) {
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenJobs ++;
i++;
}
seenJobs++;
}
}
}
this.dataView.refresh();
if (this.filterValueMap[args.column.name]) {
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
} else {
this.filterValueMap[args.column.name] = this.dataView.getFilteredItems();
}
this.rowDetail.onBeforeRowDetailToggle.subscribe(function (e, args) {
this._table.grid.resetActiveCell();
}
} else {
this.expandJobs(false);
}
});
this.rowDetail.onAfterRowDetailToggle.subscribe(function (e, args) {
});
this.rowDetail.onAsyncEndUpdate.subscribe(function (e, args) {
this.filterPlugin.onCommand.subscribe((e, args: any) => {
this.columnSort(args.column.name, args.command === 'sort-asc');
});
this._table.registerPlugin(<HeaderFilter>this.filterPlugin);
this.dataView.beginUpdate();
this.dataView.setItems(jobViews);
this.dataView.setFilter((item) => this.filter(item));
this.dataView.endUpdate();
this._table.autosizeColumns();
this._table.resizeCanvas();
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, 'Loading Error...');
} else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) {
this.expandJobRowDetails(i + expansions);
this.addToStyleHash(i + expansions);
expansions++;
}
}
this.expandJobs(true);
// tooltip for job name
$('.jobview-jobnamerow').hover(e => {
let currentTarget = e.currentTarget;
currentTarget.title = currentTarget.innerText;
@@ -245,6 +350,9 @@ export class JobsViewComponent implements AfterContentChecked {
// adjust error message when resized
$('#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%');
});
// cache the dataview for future use
this._jobCacheObject.dataView = this.dataView;
this.filterValueMap['start'] = [[], this.dataView.getItems()];
this.loadJobHistories();
}
@@ -266,15 +374,28 @@ export class JobsViewComponent implements AfterContentChecked {
return hash;
}
private addToStyleHash(row: number) {
let hash: {
private addToStyleHash(row: number, start: boolean, map: any, columnName: string) {
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);
hash = this.setRowWithErrorClass(hash, row+1, 'error-row');
if (start) {
if (map['start']) {
map['start'].push(['error-row'+row.toString(), hash]);
} else {
map['start'] = [['error-row'+row.toString(), hash]];
}
} else {
if (map[columnName]) {
map[columnName].push(['error-row'+row.toString(), hash]);
} else {
map[columnName] = [['error-row'+row.toString(), hash]];
}
}
this._table.grid.setCellCssStyles('error-row'+row.toString(), hash);
}
private renderName(row, cell, value, columnDef, dataContext) {
@@ -336,6 +457,17 @@ export class JobsViewComponent implements AfterContentChecked {
return [failing, nonFailing];
}
private checkPreviousFilters(item): boolean {
for (let column in this.filterValueMap) {
if (column !== 'start' && this.filterValueMap[column][0].length > 0) {
if (!_.contains(this.filterValueMap[column][0], item[AgentJobUtilities.convertColNameToField(column)])) {
return false;
}
}
}
return true;
}
private isErrorRow(cell: HTMLElement) {
return cell.classList.contains('error-row');
}
@@ -374,4 +506,167 @@ export class JobsViewComponent implements AfterContentChecked {
});
}
}
private expandJobs(start: boolean): void {
let expandedJobs = this._agentViewComponent.expanded;
let expansions = 0;
for (let i = 0; i < this.jobs.length; i++){
let job = this.jobs[i];
if (job.lastRunOutcome === 0 && !expandedJobs.get(job.jobId)) {
this.expandJobRowDetails(i+expandedJobs.size);
this.addToStyleHash(i+expandedJobs.size, start, this.filterStylingMap, undefined);
this._agentViewComponent.setExpanded(job.jobId, 'Loading Error...');
} else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) {
this.addToStyleHash(i+expansions, start, this.filterStylingMap, undefined);
expansions++;
}
}
}
private filter(item: any) {
let columns = this._table.grid.getColumns();
let value = true;
for (let i = 0; i < columns.length; i++) {
let col: any = columns[i];
let filterValues = col.filterValues;
if (filterValues && filterValues.length > 0) {
if (item._parent) {
value = value && _.contains(filterValues, item._parent[col.field]);
} else {
value = value && _.contains(filterValues, item[col.field]);
}
}
}
return value;
}
private columnSort(column: string, isAscending: boolean) {
let items = this.dataView.getItems();
// get error items here and remove them
let jobItems = items.filter(x => x._parent === undefined);
let errorItems = items.filter(x => x._parent !== undefined);
this.sortingStylingMap[column] = items;
switch(column) {
case('Name'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.name.localeCompare(item2.name);
}, isAscending);
break;
}
case('Last Run'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, true), isAscending);
break;
}
case ('Next Run') : {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, false), isAscending);
break;
}
case ('Enabled'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.enabled.localeCompare(item2.enabled);
}, isAscending);
break;
}
case ('Status'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.currentExecutionStatus.localeCompare(item2.currentExecutionStatus);
}, isAscending);
break;
}
case ('Category'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.category.localeCompare(item2.category);
}, isAscending);
break;
}
case ('Runnable'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.runnable.localeCompare(item2.runnable);
}, isAscending);
break;
}
case ('Schedule'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.hasSchedule.localeCompare(item2.hasSchedule);
}, isAscending);
break;
}
case ('Last Run Outcome'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.lastRunOutcome.localeCompare(item2.lastRunOutcome);
}, isAscending);
break;
}
}
// insert the errors back again
let jobItemsLength = jobItems.length;
for (let i = 0; i < jobItemsLength; i++) {
let item = jobItems[i];
if (item._child) {
let child = errorItems.find(error => error === item._child);
jobItems.splice(i+1, 0, child);
jobItemsLength++;
}
}
this.dataView.setItems(jobItems);
// remove old style
if (this.filterStylingMap[column]) {
let filterLength = this.filterStylingMap[column].length;
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[column].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
} else {
for (let i = 0; i < this.jobs.length; i++) {
this._table.grid.removeCellCssStyles('error-row'+i.toString());
}
}
// add new style to the items back again
items = this.filterStack.length > 1 ? this.dataView.getFilteredItems() : this.dataView.getItems();
for (let i = 0; i < items.length; i ++) {
let item = items[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(i, false, this.sortingStylingMap, column);
}
}
}
private dateCompare(item1: any, item2: any, lastRun: boolean): number {
let exceptionString = lastRun ? 'Never Run' : 'Not Scheduled';
if (item2.lastRun === exceptionString && item1.lastRun !== exceptionString) {
return -1;
} else if (item1.lastRun === exceptionString && item2.lastRun !== exceptionString) {
return 1;
} else if (item1.lastRun === exceptionString && item2.lastRun === exceptionString) {
return 0;
} else {
let date1 = new Date(item1.lastRun);
let date2 = new Date(item2.lastRun);
if (date1 > date2) {
return 1;
} else if (date1 === date2) {
return 0;
} else {
return -1;
}
}
}
}