mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
1
src/sql/base/browser/ui/table/media/down-inverse.svg
Normal file
1
src/sql/base/browser/ui/table/media/down-inverse.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>CollapseChevronDown_md_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M15.444,6.061,8,13.5.556,6.061,3.03,3.586,8,8.556l4.97-4.97Z" style="display: none;"/><path class="icon-vs-bg" d="M14.03,6.061,8,12.091,1.97,6.061,3.03,5,8,9.97,12.97,5Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
1
src/sql/base/browser/ui/table/media/down.svg
Normal file
1
src/sql/base/browser/ui/table/media/down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>CollapseChevronDown_md_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M15.444,6.061,8,13.5.556,6.061,3.03,3.586,8,8.556l4.97-4.97Z" style="display: none;"/><path class="icon-vs-bg" d="M14.03,6.061,8,12.091,1.97,6.061,3.03,5,8,9.97,12.97,5Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
1
src/sql/base/browser/ui/table/media/filter.svg
Normal file
1
src/sql/base/browser/ui/table/media/filter.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}</style></defs><title>filter_16x16</title><text class="cls-1" transform="translate(0 12)"> </text><path d="M0,1.53H16V3.24l-6,6v6.27H6V9.22l-6-6ZM15,2.82V2.53H1v.29l6,6v5.69H9V8.8Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 363 B |
1
src/sql/base/browser/ui/table/media/filter_inverse.svg
Normal file
1
src/sql/base/browser/ui/table/media/filter_inverse.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}.cls-1,.cls-2{fill:#fff;}</style></defs><title>filter_inverse_16x16</title><text class="cls-1" transform="translate(0.03 12.1)"> </text><path class="cls-2" d="M.05,1.63H16V3.33l-6,6v6.27H6V9.31l-6-6ZM15,2.91V2.62H1v.29l6,6v5.69H9V8.89Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 418 B |
@@ -32,3 +32,116 @@
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slick-header-menubutton {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
bottom: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 0;
|
||||||
|
width: 18px;
|
||||||
|
background-image: url('down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-dark .slick-header-menubutton {
|
||||||
|
background-image: url('down-inverse.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menubutton.filtered {
|
||||||
|
background-image: url('filter.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-dark .slick-header-menubutton.filtered {
|
||||||
|
background-image: url('filter_inverse.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menu {
|
||||||
|
background: none repeat scroll 0 0 white;
|
||||||
|
border: 1px solid #BFBDBD;
|
||||||
|
min-width: 175px;
|
||||||
|
padding: 4px;
|
||||||
|
z-index: 100000;
|
||||||
|
cursor: default;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-dark .slick-header-menu {
|
||||||
|
background: none repeat scroll 0 0 #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menu a.monaco-button.monaco-text-button {
|
||||||
|
width: 60px;
|
||||||
|
margin: 6px 6px 6px 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menu .filter
|
||||||
|
{
|
||||||
|
border: 1px solid #BFBDBD;
|
||||||
|
font-size: 8pt;
|
||||||
|
height: 400px;
|
||||||
|
margin-top: 6px;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menuitem
|
||||||
|
{
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none outside none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menuicon
|
||||||
|
{
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menuicon.ascending {
|
||||||
|
background: url('sort-asc.gif');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menuicon.descending {
|
||||||
|
background: url('sort-desc.gif');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menucontent {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slick-header-menuitem:hover {
|
||||||
|
border-color: #BFBDBD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-overlay, .cell-overlay, .selection-cell-overlay {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-dark .slick-header-menu > input.input {
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
353
src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts
Normal file
353
src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
// Adopted and converted to typescript from https://github.com/danny-sg/slickgrid-spreadsheet-plugins/blob/master/ext.headerfilter.js
|
||||||
|
// heavily modified
|
||||||
|
import 'vs/css!sql/base/browser/ui/table/media/table';
|
||||||
|
|
||||||
|
import { mixin } from 'vs/base/common/objects';
|
||||||
|
import { SlickGrid } from 'angular2-slickgrid';
|
||||||
|
import { Button } from '../../button/button';
|
||||||
|
import { attachButtonStyler } from 'sql/common/theme/styler';
|
||||||
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
|
|
||||||
|
export class HeaderFilter {
|
||||||
|
|
||||||
|
public onFilterApplied = new Slick.Event();
|
||||||
|
public onCommand = new Slick.Event();
|
||||||
|
|
||||||
|
private grid;
|
||||||
|
private handler = new Slick.EventHandler();
|
||||||
|
private defaults = {
|
||||||
|
filterImage: 'src/sql/media/icons/filter.svg',
|
||||||
|
sortAscImage: 'sort-asc.gif',
|
||||||
|
sortDescImage: 'sort-desc.gif'
|
||||||
|
};
|
||||||
|
|
||||||
|
private $menu;
|
||||||
|
private options: any;
|
||||||
|
private okButton: Button;
|
||||||
|
private clearButton: Button;
|
||||||
|
private cancelButton: Button;
|
||||||
|
private workingFilters: any;
|
||||||
|
private columnDef: any;
|
||||||
|
|
||||||
|
constructor(options: any, private _themeService: IThemeService) {
|
||||||
|
this.options = mixin(options, this.defaults, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(grid: Slick.Grid<any>): void {
|
||||||
|
this.grid = grid;
|
||||||
|
this.handler.subscribe(this.grid.onHeaderCellRendered, (e, args) => this.handleHeaderCellRendered(e , args))
|
||||||
|
.subscribe(this.grid.onBeforeHeaderCellDestroy, (e, args) => this.handleBeforeHeaderCellDestroy(e, args))
|
||||||
|
.subscribe(this.grid.onClick, (e) => this.handleBodyMouseDown)
|
||||||
|
.subscribe(this.grid.onColumnsResized, () => this.columnsResized());
|
||||||
|
|
||||||
|
this.grid.setColumns(this.grid.getColumns());
|
||||||
|
|
||||||
|
$(document.body).bind('mousedown', this.handleBodyMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.handler.unsubscribeAll();
|
||||||
|
$(document.body).unbind('mousedown', this.handleBodyMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBodyMouseDown = (e) => {
|
||||||
|
if (this.$menu && this.$menu[0] !== e.target && !$.contains(this.$menu[0], e.target)) {
|
||||||
|
this.hideMenu();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hideMenu() {
|
||||||
|
if (this.$menu) {
|
||||||
|
this.$menu.remove();
|
||||||
|
this.$menu = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleHeaderCellRendered(e, args) {
|
||||||
|
let column = args.column;
|
||||||
|
if (column.id === '_detail_selector') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let $el = $('<div></div>')
|
||||||
|
.addClass('slick-header-menubutton')
|
||||||
|
.data('column', column);
|
||||||
|
|
||||||
|
$el.bind('click', (e) => this.showFilter(e)).appendTo(args.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBeforeHeaderCellDestroy(e, args) {
|
||||||
|
$(args.node)
|
||||||
|
.find('.slick-header-menubutton')
|
||||||
|
.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private addMenuItem(menu, columnDef, title, command, image) {
|
||||||
|
let $item = $('<div class="slick-header-menuitem">')
|
||||||
|
.data('command', command)
|
||||||
|
.data('column', columnDef)
|
||||||
|
.bind('click', (e) => this.handleMenuItemClick(e, command, columnDef))
|
||||||
|
.appendTo(menu);
|
||||||
|
|
||||||
|
let $icon = $('<div class="slick-header-menuicon">')
|
||||||
|
.appendTo($item);
|
||||||
|
|
||||||
|
if (title === 'Sort Ascending') {
|
||||||
|
$icon.get(0).className += ' ascending';
|
||||||
|
} else if (title === 'Sort Descending') {
|
||||||
|
$icon.get(0).className += ' descending';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('<span class="slick-header-menucontent">')
|
||||||
|
.text(title)
|
||||||
|
.appendTo($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addMenuInput(menu, columnDef) {
|
||||||
|
const self = this;
|
||||||
|
$('<input class="input" placeholder="Search" style="margin-top: 5px; width: 206px">')
|
||||||
|
.data('column', columnDef)
|
||||||
|
.bind('keyup', (e) => {
|
||||||
|
let filterVals = this.getFilterValuesByInput($(e.target));
|
||||||
|
self.updateFilterInputs(menu, columnDef, filterVals);
|
||||||
|
})
|
||||||
|
.appendTo(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFilterInputs(menu, columnDef, filterItems) {
|
||||||
|
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
|
||||||
|
columnDef.filterValues = columnDef.filterValues || [];
|
||||||
|
|
||||||
|
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
|
||||||
|
this.workingFilters = columnDef.filterValues.slice(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < filterItems.length; i++) {
|
||||||
|
let filtered = _.contains(this.workingFilters, filterItems[i]);
|
||||||
|
|
||||||
|
filterOptions += '<label><input type="checkbox" value="' + i + '"'
|
||||||
|
+ (filtered ? ' checked="checked"' : '')
|
||||||
|
+ '/>' + filterItems[i] + '</label>';
|
||||||
|
}
|
||||||
|
let $filter = menu.find('.filter');
|
||||||
|
$filter.empty().append($(filterOptions));
|
||||||
|
|
||||||
|
$(':checkbox', $filter).bind('click', (e) => {
|
||||||
|
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, $(e.target));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showFilter(e) {
|
||||||
|
let $menuButton = $(e.target);
|
||||||
|
this.columnDef = $menuButton.data('column');
|
||||||
|
|
||||||
|
this.columnDef.filterValues = this.columnDef.filterValues || [];
|
||||||
|
|
||||||
|
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
|
||||||
|
this.workingFilters = this.columnDef.filterValues.slice(0);
|
||||||
|
|
||||||
|
let filterItems;
|
||||||
|
|
||||||
|
if (this.workingFilters.length === 0) {
|
||||||
|
// Filter based all available values
|
||||||
|
filterItems = this.getFilterValues(this.grid.getData(), this.columnDef);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Filter based on current dataView subset
|
||||||
|
filterItems = this.getAllFilterValues(this.grid.getData().getItems(), this.columnDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.$menu) {
|
||||||
|
this.$menu = $('<div class="slick-header-menu">').appendTo(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$menu.empty();
|
||||||
|
|
||||||
|
this.addMenuItem(this.$menu, this.columnDef, 'Sort Ascending', 'sort-asc', this.options.sortAscImage);
|
||||||
|
this.addMenuItem(this.$menu, this.columnDef, 'Sort Descending', 'sort-desc', this.options.sortDescImage);
|
||||||
|
this.addMenuInput(this.$menu, this.columnDef);
|
||||||
|
|
||||||
|
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
|
||||||
|
|
||||||
|
for (let i = 0; i < filterItems.length; i++) {
|
||||||
|
let filtered = _.contains(this.workingFilters, filterItems[i]);
|
||||||
|
if (filterItems[i] && filterItems[i].indexOf('Error:') < 0) {
|
||||||
|
filterOptions += '<label><input type="checkbox" value="' + i + '"'
|
||||||
|
+ (filtered ? ' checked="checked"' : '')
|
||||||
|
+ '/>' + filterItems[i] + '</label>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let $filter = $('<div class="filter">')
|
||||||
|
.append($(filterOptions))
|
||||||
|
.appendTo(this.$menu);
|
||||||
|
|
||||||
|
this.okButton = new Button(this.$menu.get(0));
|
||||||
|
this.okButton.label = 'OK';
|
||||||
|
this.okButton.title = 'OK';
|
||||||
|
this.okButton.element.id = 'filter-ok-button';
|
||||||
|
let okElement = $('#filter-ok-button');
|
||||||
|
okElement.bind('click', (ev) => {
|
||||||
|
this.columnDef.filterValues = this.workingFilters.splice(0);
|
||||||
|
this.setButtonImage($menuButton, this.columnDef.filterValues.length > 0);
|
||||||
|
this.handleApply(ev, this.columnDef);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clearButton = new Button(this.$menu.get(0));
|
||||||
|
this.clearButton.label = 'Clear';
|
||||||
|
this.clearButton.title = 'Clear';
|
||||||
|
this.clearButton.element.id = 'filter-clear-button';
|
||||||
|
let clearElement = $('#filter-clear-button');
|
||||||
|
clearElement.bind('click', (ev) => {
|
||||||
|
this.columnDef.filterValues.length = 0;
|
||||||
|
this.setButtonImage($menuButton, false);
|
||||||
|
this.handleApply(ev, this.columnDef);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cancelButton = new Button(this.$menu.get(0));
|
||||||
|
this.cancelButton.label = 'Cancel';
|
||||||
|
this.cancelButton.title = 'Cancel';
|
||||||
|
this.cancelButton.element.id = 'filter-cancel-button';
|
||||||
|
let cancelElement = $('#filter-cancel-button');
|
||||||
|
cancelElement.bind('click', () => this.hideMenu());
|
||||||
|
attachButtonStyler(this.okButton, this._themeService);
|
||||||
|
attachButtonStyler(this.clearButton, this._themeService);
|
||||||
|
attachButtonStyler(this.cancelButton, this._themeService);
|
||||||
|
|
||||||
|
$(':checkbox', $filter).bind('click', (e) => {
|
||||||
|
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, $(e.target));
|
||||||
|
});
|
||||||
|
|
||||||
|
let offset = $(e.target).offset();
|
||||||
|
let left = offset.left - this.$menu.width() + $(e.target).width() - 8;
|
||||||
|
|
||||||
|
let menutop = offset.top + $(e.target).height();
|
||||||
|
|
||||||
|
if (menutop + offset.top > $(window).height()) {
|
||||||
|
menutop -= (this.$menu.height() + $(e.target).height() + 8);
|
||||||
|
}
|
||||||
|
this.$menu.css('top', menutop)
|
||||||
|
.css('left', (left > 0 ? left : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private columnsResized() {
|
||||||
|
this.hideMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private changeWorkingFilter(filterItems, workingFilters, $checkbox) {
|
||||||
|
let value = $checkbox.val();
|
||||||
|
let $filter = $checkbox.parent().parent();
|
||||||
|
|
||||||
|
if ($checkbox.val() < 0) {
|
||||||
|
// Select All
|
||||||
|
if ($checkbox.prop('checked')) {
|
||||||
|
$(':checkbox', $filter).prop('checked', true);
|
||||||
|
workingFilters = filterItems.slice(0);
|
||||||
|
} else {
|
||||||
|
$(':checkbox', $filter).prop('checked', false);
|
||||||
|
workingFilters.length = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let index = _.indexOf(workingFilters, filterItems[value]);
|
||||||
|
|
||||||
|
if ($checkbox.prop('checked') && index < 0) {
|
||||||
|
workingFilters.push(filterItems[value]);
|
||||||
|
let nextRow = filterItems[(parseInt(value)+1).toString()];
|
||||||
|
if (nextRow && nextRow.indexOf('Error:') >= 0) {
|
||||||
|
workingFilters.push(nextRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (index > -1) {
|
||||||
|
workingFilters.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workingFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setButtonImage($el, filtered) {
|
||||||
|
let element: HTMLElement = $el.get(0);
|
||||||
|
if (filtered) {
|
||||||
|
element.className += ' filtered';
|
||||||
|
} else {
|
||||||
|
let classList = element.classList;
|
||||||
|
if (classList.contains('filtered')) {
|
||||||
|
classList.remove('filtered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleApply(e, columnDef) {
|
||||||
|
this.hideMenu();
|
||||||
|
|
||||||
|
this.onFilterApplied.notify({ 'grid': this.grid, 'column': columnDef }, e, self);
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilterValues(dataView, column) {
|
||||||
|
let seen = [];
|
||||||
|
for (let i = 0; i < dataView.getLength() ; i++) {
|
||||||
|
let value = dataView.getItem(i)[column.field];
|
||||||
|
|
||||||
|
if (!_.contains(seen, value)) {
|
||||||
|
seen.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilterValuesByInput($input) {
|
||||||
|
let column = $input.data('column'),
|
||||||
|
filter = $input.val(),
|
||||||
|
dataView = this.grid.getData(),
|
||||||
|
seen = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < dataView.getLength() ; i++) {
|
||||||
|
let value = dataView.getItem(i)[column.field];
|
||||||
|
|
||||||
|
if (filter.length > 0) {
|
||||||
|
let itemValue = !value ? '' : value;
|
||||||
|
let lowercaseFilter = filter.toString().toLowerCase();
|
||||||
|
let lowercaseVal = itemValue.toString().toLowerCase();
|
||||||
|
if (!_.contains(seen, value) && lowercaseVal.indexOf(lowercaseFilter) > -1) {
|
||||||
|
seen.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!_.contains(seen, value)) {
|
||||||
|
seen.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.sortBy(seen, (v) => { return v; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllFilterValues(data, column) {
|
||||||
|
let seen = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let value = data[i][column.field];
|
||||||
|
|
||||||
|
if (!_.contains(seen, value)) {
|
||||||
|
seen.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.sortBy(seen, (v) => { return v; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMenuItemClick(e, command, columnDef) {
|
||||||
|
this.hideMenu();
|
||||||
|
|
||||||
|
this.onCommand.notify({
|
||||||
|
'grid': this.grid,
|
||||||
|
'column': columnDef,
|
||||||
|
'command': command
|
||||||
|
}, e, self);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,8 +277,8 @@ export class RowDetailView {
|
|||||||
item._isPadding = false;
|
item._isPadding = false;
|
||||||
item._parent = parent;
|
item._parent = parent;
|
||||||
item._offset = offset;
|
item._offset = offset;
|
||||||
item.jobId = parent.jobId;
|
|
||||||
item.name = parent.message ? parent.message : nls.localize('rowDetailView.loadError','Loading Error...');
|
item.name = parent.message ? parent.message : nls.localize('rowDetailView.loadError','Loading Error...');
|
||||||
|
parent._child = item;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface IFindPosition {
|
|||||||
row: number;
|
row: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T> {
|
export function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T> {
|
||||||
let field = args.sortCol.field;
|
let field = args.sortCol.field;
|
||||||
let sign = args.sortAsc ? 1 : -1;
|
let sign = args.sortAsc ? 1 : -1;
|
||||||
return data.sort((a, b) => (a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1)) * sign);
|
return data.sort((a, b) => (a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1)) * sign);
|
||||||
|
|||||||
@@ -92,4 +92,52 @@ export class AgentJobUtilities {
|
|||||||
return;
|
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 '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +82,7 @@ export class JobCacheObject {
|
|||||||
private _jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = {};
|
private _jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = {};
|
||||||
private _prevJobID: string;
|
private _prevJobID: string;
|
||||||
private _serverName: string;
|
private _serverName: string;
|
||||||
|
private _dataView: Slick.Data.DataView<any>;
|
||||||
|
|
||||||
/* Getters */
|
/* Getters */
|
||||||
public get jobs(): sqlops.AgentJobInfo[] {
|
public get jobs(): sqlops.AgentJobInfo[] {
|
||||||
@@ -104,6 +105,10 @@ export class JobCacheObject {
|
|||||||
return this._serverName;
|
return this._serverName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get dataView(): Slick.Data.DataView<any> {
|
||||||
|
return this._dataView;
|
||||||
|
}
|
||||||
|
|
||||||
/* Setters */
|
/* Setters */
|
||||||
public set jobs(value: sqlops.AgentJobInfo[]) {
|
public set jobs(value: sqlops.AgentJobInfo[]) {
|
||||||
this._jobs = value;
|
this._jobs = value;
|
||||||
@@ -125,4 +130,7 @@ export class JobCacheObject {
|
|||||||
this._serverName = value;
|
this._serverName = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set dataView(value: Slick.Data.DataView<any>) {
|
||||||
|
this._dataView = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,4 @@ export class JobHistoryComponent extends Disposable implements OnInit {
|
|||||||
this._showSteps = value;
|
this._showSteps = value;
|
||||||
this._cd.detectChanges();
|
this._cd.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'vs/css!sql/parts/grid/media/slick.grid';
|
|||||||
import 'vs/css!sql/parts/grid/media/slickGrid';
|
import 'vs/css!sql/parts/grid/media/slickGrid';
|
||||||
import 'vs/css!../common/media/jobs';
|
import 'vs/css!../common/media/jobs';
|
||||||
import 'vs/css!sql/media/icons/common-icons';
|
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';
|
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 vscode from 'vscode';
|
||||||
|
|
||||||
import * as nls from 'vs/nls';
|
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 { 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 { 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';
|
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 { IJobManagementService } from '../common/interfaces';
|
||||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
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';
|
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
|
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -44,19 +54,28 @@ 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: nls.localize('jobColumns.name', 'Name'), field: 'name', formatter: this.renderName, width: 200, id: 'name' },
|
{ 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', width: 150, id: 'lastRun' },
|
{ name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', minWidth: 150, id: 'lastRun' },
|
||||||
{ name: nls.localize('jobColumns.nextRun', 'Next Run'), field: 'nextRun', width: 150, id: 'nextRun' },
|
{ name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', minWidth: 150, id: 'nextRun' },
|
||||||
{ name: nls.localize('jobColumns.enabled', 'Enabled'), field: 'enabled', width: 70, id: 'enabled' },
|
{ name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', minWidth: 70, id: 'enabled' },
|
||||||
{ name: nls.localize('jobColumns.status', 'Status'), field: 'currentExecutionStatus', width: 60, id: 'currentExecutionStatus' },
|
{ name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', minWidth: 60, id: 'currentExecutionStatus' },
|
||||||
{ name: nls.localize('jobColumns.category', 'Category'), field: 'category', width: 150, id: 'category' },
|
{ name: nls.localize('jobColumns.category','Category'), field: 'category', minWidth: 150, id: 'category' },
|
||||||
{ name: nls.localize('jobColumns.runnable', 'Runnable'), field: 'runnable', width: 50, id: 'runnable' },
|
{ name: nls.localize('jobColumns.runnable','Runnable'), field: 'runnable', minWidth: 50, id: 'runnable' },
|
||||||
{ name: nls.localize('jobColumns.schedule', 'Schedule'), field: 'hasSchedule', width: 50, id: 'hasSchedule' },
|
{ name: nls.localize('jobColumns.schedule','Schedule'), field: 'hasSchedule', minWidth: 50, id: 'hasSchedule' },
|
||||||
{ name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', width: 150, id: 'lastRunOutcome' },
|
{ 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 rowDetail: RowDetailView;
|
||||||
private dataView: Slick.Data.DataView<any>;
|
private filterPlugin: any;
|
||||||
|
private dataView: any;
|
||||||
|
|
||||||
@ViewChild('jobsgrid') _gridEl: ElementRef;
|
@ViewChild('jobsgrid') _gridEl: ElementRef;
|
||||||
private isVisible: boolean = false;
|
private isVisible: boolean = false;
|
||||||
@@ -68,13 +87,18 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
private _isCloud: boolean;
|
private _isCloud: boolean;
|
||||||
private _showProgressWheel: boolean;
|
private _showProgressWheel: boolean;
|
||||||
private _tabHeight: number;
|
private _tabHeight: number;
|
||||||
|
private filterStylingMap: { [columnName: string]: [any] ;} = {};
|
||||||
|
private filterStack = ['start'];
|
||||||
|
private filterValueMap: { [columnName: string]: string[] ;} = {};
|
||||||
|
private sortingStylingMap: { [columnName: string]: any; } = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
|
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
|
||||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
|
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
|
||||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||||
@Inject(forwardRef(() => AgentViewComponent)) private _agentViewComponent: AgentViewComponent,
|
@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;
|
let jobCacheObjectMap = this._jobManagementService.jobCacheObjectMap;
|
||||||
this._serverName = _dashboardService.connectionManagementService.connectionInfo.connectionProfile.serverName;
|
this._serverName = _dashboardService.connectionManagementService.connectionInfo.connectionProfile.serverName;
|
||||||
@@ -126,16 +150,8 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
column.rerenderOnResize = true;
|
column.rerenderOnResize = true;
|
||||||
return column;
|
return column;
|
||||||
});
|
});
|
||||||
let options = <Slick.GridOptions<any>>{
|
// create the table
|
||||||
syncColumnCellResize: true,
|
this.dataView = new Slick.Data.DataView();
|
||||||
enableColumnReorder: false,
|
|
||||||
rowHeight: 45,
|
|
||||||
enableCellNavigation: true,
|
|
||||||
forceFitColumns: true
|
|
||||||
};
|
|
||||||
|
|
||||||
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
|
|
||||||
|
|
||||||
let rowDetail = new RowDetailView({
|
let rowDetail = new RowDetailView({
|
||||||
cssClass: '_detail_selector',
|
cssClass: '_detail_selector',
|
||||||
process: (job) => {
|
process: (job) => {
|
||||||
@@ -147,9 +163,13 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
panelRows: 1
|
panelRows: 1
|
||||||
});
|
});
|
||||||
this.rowDetail = rowDetail;
|
this.rowDetail = rowDetail;
|
||||||
|
|
||||||
columns.unshift(this.rowDetail.getColumnDefinition());
|
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.setData(this.dataView, true);
|
||||||
this._table.grid.onClick.subscribe((e, args) => {
|
this._table.grid.onClick.subscribe((e, args) => {
|
||||||
let job = self.getJob(args);
|
let job = self.getJob(args);
|
||||||
@@ -158,7 +178,7 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
self._agentViewComponent.showHistory = true;
|
self._agentViewComponent.showHistory = true;
|
||||||
});
|
});
|
||||||
if (cached && this._agentViewComponent.refresh !== true) {
|
if (cached && this._agentViewComponent.refresh !== true) {
|
||||||
this.onJobsAvailable(this._jobCacheObject.jobs);
|
this.onJobsAvailable(null);
|
||||||
} else {
|
} else {
|
||||||
let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri;
|
let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri;
|
||||||
this._jobManagementService.getJobs(ownerUri).then((result) => {
|
this._jobManagementService.getJobs(ownerUri).then((result) => {
|
||||||
@@ -172,50 +192,135 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onJobsAvailable(jobs: sqlops.AgentJobInfo[]) {
|
private onJobsAvailable(jobs: sqlops.AgentJobInfo[]) {
|
||||||
let jobViews = jobs.map((job) => {
|
let jobViews: any;
|
||||||
return {
|
if (!jobs) {
|
||||||
id: job.jobId,
|
let dataView = this._jobCacheObject.dataView;
|
||||||
jobId: job.jobId,
|
jobViews = dataView.getItems();
|
||||||
name: job.name,
|
} else {
|
||||||
lastRun: AgentJobUtilities.convertToLastRun(job.lastRun),
|
jobViews = jobs.map((job) => {
|
||||||
nextRun: AgentJobUtilities.convertToNextRun(job.nextRun),
|
return {
|
||||||
enabled: AgentJobUtilities.convertToResponse(job.enabled),
|
id: job.jobId,
|
||||||
currentExecutionStatus: AgentJobUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
|
jobId: job.jobId,
|
||||||
category: job.category,
|
name: job.name,
|
||||||
runnable: AgentJobUtilities.convertToResponse(job.runnable),
|
lastRun: AgentJobUtilities.convertToLastRun(job.lastRun),
|
||||||
hasSchedule: AgentJobUtilities.convertToResponse(job.hasSchedule),
|
nextRun: AgentJobUtilities.convertToNextRun(job.nextRun),
|
||||||
lastRunOutcome: AgentJobUtilities.convertToStatusString(job.lastRunOutcome)
|
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._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.filterPlugin.onCommand.subscribe((e, args: any) => {
|
||||||
});
|
this.columnSort(args.column.name, args.command === 'sort-asc');
|
||||||
this.rowDetail.onAsyncEndUpdate.subscribe(function (e, args) {
|
|
||||||
});
|
});
|
||||||
|
this._table.registerPlugin(<HeaderFilter>this.filterPlugin);
|
||||||
|
|
||||||
this.dataView.beginUpdate();
|
this.dataView.beginUpdate();
|
||||||
this.dataView.setItems(jobViews);
|
this.dataView.setItems(jobViews);
|
||||||
|
this.dataView.setFilter((item) => this.filter(item));
|
||||||
|
|
||||||
this.dataView.endUpdate();
|
this.dataView.endUpdate();
|
||||||
this._table.autosizeColumns();
|
this._table.autosizeColumns();
|
||||||
this._table.resizeCanvas();
|
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 => {
|
$('.jobview-jobnamerow').hover(e => {
|
||||||
let currentTarget = e.currentTarget;
|
let currentTarget = e.currentTarget;
|
||||||
currentTarget.title = currentTarget.innerText;
|
currentTarget.title = currentTarget.innerText;
|
||||||
@@ -245,6 +350,9 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
// adjust error message when resized
|
// adjust error message when resized
|
||||||
$('#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%');
|
$('#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();
|
this.loadJobHistories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,15 +374,28 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToStyleHash(row: number) {
|
private addToStyleHash(row: number, start: boolean, map: any, columnName: string) {
|
||||||
let hash: {
|
let hash : {
|
||||||
[index: number]: {
|
[index: number]: {
|
||||||
[id: string]: string;
|
[id: string]: string;
|
||||||
}
|
}
|
||||||
} = {};
|
} = {};
|
||||||
hash = this.setRowWithErrorClass(hash, row, 'job-with-error');
|
hash = this.setRowWithErrorClass(hash, row, 'job-with-error');
|
||||||
hash = this.setRowWithErrorClass(hash, row + 1, 'error-row');
|
hash = this.setRowWithErrorClass(hash, row+1, 'error-row');
|
||||||
this._table.grid.setCellCssStyles('error-row' + row.toString(), hash);
|
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) {
|
private renderName(row, cell, value, columnDef, dataContext) {
|
||||||
@@ -336,6 +457,17 @@ export class JobsViewComponent implements AfterContentChecked {
|
|||||||
return [failing, nonFailing];
|
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) {
|
private isErrorRow(cell: HTMLElement) {
|
||||||
return cell.classList.contains('error-row');
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user