diff --git a/src/sql/base/browser/ui/scrollableView/scrollableView.ts b/src/sql/base/browser/ui/scrollableView/scrollableView.ts index 98e14caed3..26f8917e38 100644 --- a/src/sql/base/browser/ui/scrollableView/scrollableView.ts +++ b/src/sql/base/browser/ui/scrollableView/scrollableView.ts @@ -36,7 +36,7 @@ export interface IView { readonly element: HTMLElement; readonly minimumSize: number; readonly maximumSize: number; - onDidInsert?(): void; + onDidInsert?(): Promise; onDidRemove?(): void; } @@ -276,10 +276,10 @@ export class ScrollableView extends Disposable { this.updateItemInDOM(item, index, false); item.onDidRemoveDisposable?.dispose(); - item.onDidInsertDisposable = DOM.scheduleAtNextAnimationFrame(() => { + item.onDidInsertDisposable = DOM.scheduleAtNextAnimationFrame(async () => { // we don't trust the items to be performant so don't interrupt our operations if (item.view.onDidInsert) { - item.view.onDidInsert(); + await item.view.onDidInsert(); } item.view.layout(item.size, this.width); }); diff --git a/src/sql/base/browser/ui/table/asyncDataView.ts b/src/sql/base/browser/ui/table/asyncDataView.ts index f35bfe3d86..fca027f62b 100644 --- a/src/sql/base/browser/ui/table/asyncDataView.ts +++ b/src/sql/base/browser/ui/table/asyncDataView.ts @@ -3,8 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; export interface IObservableCollection { getLength(): number; @@ -201,8 +202,38 @@ export class VirtualizedCollection implements IObserv export class AsyncDataProvider implements IDisposableDataProvider { + private _onFilterStateChange = new Emitter(); + get onFilterStateChange(): Event { return this._onFilterStateChange.event; } + + private _onSortComplete = new Emitter>(); + get onSortComplete(): Event> { return this._onSortComplete.event; } + constructor(public dataRows: IObservableCollection) { } + public get isDataInMemory(): boolean { + return false; + } + + getRangeAsync(startIndex: number, length: number): Promise { + throw new Error('Method not implemented.'); + } + + getFilteredColumnValues(column: Slick.Column): Promise { + throw new Error('Method not implemented.'); + } + + getColumnValues(column: Slick.Column): Promise { + throw new Error('Method not implemented.'); + } + + sort(options: Slick.OnSortEventArgs): Promise { + throw new Error('Method not implemented.'); + } + + filter(columns?: Slick.Column[]): Promise { + throw new Error('Method not implemented.'); + } + public getLength(): number { return this.dataRows.getLength(); } diff --git a/src/sql/base/browser/ui/table/hybridDataProvider.ts b/src/sql/base/browser/ui/table/hybridDataProvider.ts new file mode 100644 index 0000000000..1f2258b6c1 --- /dev/null +++ b/src/sql/base/browser/ui/table/hybridDataProvider.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AsyncDataProvider, IObservableCollection } from 'sql/base/browser/ui/table/asyncDataView'; +import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; +import { CellValueGetter, TableDataView, TableFilterFunc, TableSortFunc } from 'sql/base/browser/ui/table/tableDataView'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; +import { Event, Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export interface HybridDataProviderOptions { + inMemoryDataProcessing: boolean; + inMemoryDataCountThreshold?: number; +} + +/** + * Used to abstract the underlying data provider, based on the options, if we are allowing in-memory data processing and the threshold is not reached the + * a TableDataView will be used to provide in memory data source, otherwise it will be using the async data provider. + */ +export class HybridDataProvider implements IDisposableDataProvider { + private _asyncDataProvider: AsyncDataProvider; + private _tableDataProvider: TableDataView; + private _dataCached: boolean = false; + private _disposableStore = new DisposableStore(); + + private _onFilterStateChange = new Emitter(); + get onFilterStateChange(): Event { return this._onFilterStateChange.event; } + + private _onSortComplete = new Emitter>(); + get onSortComplete(): Event> { return this._onSortComplete.event; } + + constructor(dataRows: IObservableCollection, + private _loadDataFn: (offset: number, count: number) => Thenable, + filterFn: TableFilterFunc, + sortFn: TableSortFunc, + valueGetter: CellValueGetter, + private readonly _options: HybridDataProviderOptions) { + this._asyncDataProvider = new AsyncDataProvider(dataRows); + this._tableDataProvider = new TableDataView(undefined, undefined, sortFn, filterFn, valueGetter); + this._disposableStore.add(this._asyncDataProvider.onFilterStateChange(() => { + this._onFilterStateChange.fire(); + })); + this._disposableStore.add(this._asyncDataProvider.onSortComplete((args) => { + this._onSortComplete.fire(args); + })); + this._disposableStore.add(this._tableDataProvider.onFilterStateChange(() => { + this._onFilterStateChange.fire(); + })); + this._disposableStore.add(this._tableDataProvider.onSortComplete((args) => { + this._onSortComplete.fire(args); + })); + this._disposableStore.add(this._asyncDataProvider); + this._disposableStore.add(this._tableDataProvider); + } + + public get isDataInMemory(): boolean { + return this._dataCached; + } + + async getRangeAsync(startIndex: number, length: number): Promise { + return this.provider.getRangeAsync(startIndex, length); + } + + public async getColumnValues(column: Slick.Column): Promise { + await this.initializeCacheIfNeeded(); + return this.provider.getColumnValues(column); + } + + public async getFilteredColumnValues(column: Slick.Column): Promise { + await this.initializeCacheIfNeeded(); + return this.provider.getFilteredColumnValues(column); + } + + public get dataRows(): IObservableCollection { + return this._asyncDataProvider.dataRows; + } + + public set dataRows(value: IObservableCollection) { + this._asyncDataProvider.dataRows = value; + } + + public dispose(): void { + this._disposableStore.dispose(); + } + + public getLength(): number { + return this.provider.getLength(); + } + + public getItem(index: number): T { + return this.provider.getItem(index); + } + + public getItems(): T[] { + throw new Error('Method not implemented.'); + } + + public get length(): number { + return this.provider.getLength(); + } + + public set length(value: number) { + this._asyncDataProvider.length = value; + } + + public async filter(columns: FilterableColumn[]) { + await this.initializeCacheIfNeeded(); + this.provider.filter(columns); + } + + public async sort(options: Slick.OnSortEventArgs) { + await this.initializeCacheIfNeeded(); + this.provider.sort(options); + } + + private get thresholdReached(): boolean { + return this._options.inMemoryDataCountThreshold !== undefined && this.length > this._options.inMemoryDataCountThreshold; + } + + private get provider(): IDisposableDataProvider { + return this._dataCached ? this._tableDataProvider : this._asyncDataProvider; + } + + private async initializeCacheIfNeeded() { + if (!this._options.inMemoryDataProcessing) { + return; + } + if (this.thresholdReached) { + return; + } + if (!this._dataCached) { + const data = await this._loadDataFn(0, this.length); + this._dataCached = true; + this._tableDataProvider.push(data); + } + } +} diff --git a/src/sql/base/browser/ui/table/interfaces.ts b/src/sql/base/browser/ui/table/interfaces.ts index 65df5bd48c..a936823f6b 100644 --- a/src/sql/base/browser/ui/table/interfaces.ts +++ b/src/sql/base/browser/ui/table/interfaces.ts @@ -3,13 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { Color } from 'vs/base/common/color'; -export interface IDisposableDataProvider extends Slick.DataProvider { - dispose(): void; -} - export interface ITableMouseEvent { anchor: HTMLElement | { x: number, y: number }; cell?: { row: number, cell: number }; @@ -32,4 +29,5 @@ export interface ITableConfiguration { export interface FilterableColumn extends Slick.Column { filterable?: boolean; + filterValues?: Array; } diff --git a/src/sql/base/browser/ui/table/media/table.css b/src/sql/base/browser/ui/table/media/table.css index e1a3c7ed72..590e7c792c 100644 --- a/src/sql/base/browser/ui/table/media/table.css +++ b/src/sql/base/browser/ui/table/media/table.css @@ -18,15 +18,15 @@ height: 100%; } -.slick-sort-indicator-asc { +.monaco-table .slick-sort-indicator-asc { background: url('sort-asc.gif'); } -.slick-sort-indicator-desc { +.monaco-table .slick-sort-indicator-desc { background: url('sort-desc.gif'); } -.slick-sort-indicator { +.monaco-table .slick-sort-indicator { display: inline-block; width: 8px; height: 5px; @@ -98,7 +98,6 @@ padding: 4px; white-space: nowrap; width: 200px; - display: grid; align-content: flex-start; } diff --git a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts index 5f692caec1..0767b68177 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -10,28 +10,20 @@ import { escape } from 'sql/base/common/strings'; import { addDisposableListener } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; -export interface IExtendedColumn extends Slick.Column { - filterValues?: Array; -} - +export type HeaderFilterCommands = 'sort-asc' | 'sort-desc'; export interface CommandEventArgs { grid: Slick.Grid, column: Slick.Column, - command: string -} - -export type CellValueGetter = (data: any) => string; - -function GetCellValue(data: any): string { - return data?.toString(); + command: HeaderFilterCommands } const ShowFilterText: string = localize('headerFilter.showFilter', "Show Filter"); export class HeaderFilter { - public onFilterApplied = new Slick.Event<{ grid: Slick.Grid, column: IExtendedColumn }>(); + public onFilterApplied = new Slick.Event<{ grid: Slick.Grid, column: FilterableColumn }>(); public onCommand = new Slick.Event>(); private grid!: Slick.Grid; @@ -42,12 +34,12 @@ export class HeaderFilter { private clearButton?: Button; private cancelButton?: Button; private workingFilters!: Array; - private columnDef!: IExtendedColumn; + private columnDef!: FilterableColumn; private buttonStyles?: IButtonStyles; - private disposableStore = new DisposableStore(); + public enabled: boolean = true; - constructor(private cellValueExtrator: CellValueGetter = GetCellValue) { + constructor() { } public init(grid: Slick.Grid): void { @@ -92,7 +84,10 @@ export class HeaderFilter { } private handleHeaderCellRendered(e: Event, args: Slick.OnHeaderCellRenderedEventArgs) { - const column = args.column; + if (!this.enabled) { + return; + } + const column = args.column as FilterableColumn; if (column.id === '_detail_selector') { return; } @@ -107,11 +102,12 @@ export class HeaderFilter { const $el = jQuery(``) .addClass('slick-header-menubutton') .data('column', column); + this.setButtonImage($el, column.filterValues?.length > 0); - $el.click((e: JQuery.Event) => { + $el.click(async (e: JQuery.Event) => { e.stopPropagation(); e.preventDefault(); - this.showFilter($el[0]); + await this.showFilter($el[0]); }); $el.appendTo(args.node); } @@ -122,11 +118,11 @@ export class HeaderFilter { .remove(); } - private addMenuItem(menu: JQuery, columnDef: Slick.Column, title: string, command: string) { + private addMenuItem(menu: JQuery, columnDef: Slick.Column, title: string, command: HeaderFilterCommands) { const $item = jQuery('
') .data('command', command) .data('column', columnDef) - .bind('click', (e) => this.handleMenuItemClick(e, command, columnDef)) + .bind('click', async (e) => await this.handleMenuItemClick(e, command, columnDef)) .appendTo(menu); const $icon = jQuery('
') @@ -154,7 +150,7 @@ export class HeaderFilter { .appendTo(menu); } - private updateFilterInputs(menu: JQuery, columnDef: IExtendedColumn, filterItems: Array) { + private updateFilterInputs(menu: JQuery, columnDef: FilterableColumn, filterItems: Array) { let filterOptions = ''; columnDef.filterValues = columnDef.filterValues || []; @@ -176,7 +172,7 @@ export class HeaderFilter { }); } - private showFilter(element: HTMLElement) { + private async showFilter(element: HTMLElement) { const target = withNullAsUndefined(element); const $menuButton = jQuery(target); this.columnDef = $menuButton.data('column'); @@ -188,13 +184,23 @@ export class HeaderFilter { let filterItems: Array; - if (this.workingFilters.length === 0) { - // Filter based all available values - filterItems = this.getFilterValues(this.grid.getData() as Slick.DataProvider, this.columnDef); - } - else { - // Filter based on current dataView subset - filterItems = this.getAllFilterValues((this.grid.getData() as Slick.DataProvider).getItems(), this.columnDef); + const provider = this.grid.getData() as IDisposableDataProvider; + + if (provider.getColumnValues) { + if (this.workingFilters.length === 0) { + filterItems = await provider.getColumnValues(this.columnDef); + } else { + filterItems = await provider.getFilteredColumnValues(this.columnDef); + } + } else { + if (this.workingFilters.length === 0) { + // Filter based all available values + filterItems = this.getFilterValues(this.grid.getData() as Slick.DataProvider, this.columnDef); + } + else { + // Filter based on current dataView subset + filterItems = this.getAllFilterValues((this.grid.getData() as Slick.DataProvider).getItems(), this.columnDef); + } } if (!this.$menu) { @@ -203,8 +209,8 @@ export class HeaderFilter { this.$menu.empty(); - this.addMenuItem(this.$menu, this.columnDef, 'Sort Ascending', 'sort-asc'); - this.addMenuItem(this.$menu, this.columnDef, 'Sort Descending', 'sort-desc'); + this.addMenuItem(this.$menu, this.columnDef, localize('table.sortAscending', "Sort Ascending"), 'sort-asc'); + this.addMenuItem(this.$menu, this.columnDef, localize('table.sortDescending', "Sort Descending"), 'sort-desc'); this.addMenuInput(this.$menu, this.columnDef); let filterOptions = ''; @@ -345,7 +351,14 @@ export class HeaderFilter { private handleApply(e: JQuery.Event, columnDef: Slick.Column) { this.hideMenu(); + const provider = this.grid.getData() as IDisposableDataProvider; + if (provider.filter) { + provider.filter(this.grid.getColumns()); + this.grid.invalidateAllRows(); + this.grid.updateRowCount(); + this.grid.render(); + } this.onFilterApplied.notify({ grid: this.grid, column: columnDef }, e, self); e.preventDefault(); e.stopPropagation(); @@ -356,7 +369,7 @@ export class HeaderFilter { dataView.getItems().forEach(items => { const value = items[column.field!]; const valueArr = value instanceof Array ? value : [value]; - valueArr.forEach(v => seen.add(this.cellValueExtrator(v))); + valueArr.forEach(v => seen.add(v)); }); return Array.from(seen); @@ -398,9 +411,21 @@ export class HeaderFilter { return Array.from(seen).sort((v) => { return v; }); } - private handleMenuItemClick(e: JQuery.Event, command: string, columnDef: Slick.Column) { + private async handleMenuItemClick(e: JQuery.Event, command: HeaderFilterCommands, columnDef: Slick.Column) { this.hideMenu(); + const provider = this.grid.getData() as IDisposableDataProvider; + if (provider.sort && (command === 'sort-asc' || command === 'sort-desc')) { + await provider.sort({ + grid: this.grid, + multiColumnSort: false, + sortCol: this.columnDef, + sortAsc: command === 'sort-asc' + }); + this.grid.invalidateAllRows(); + this.grid.updateRowCount(); + this.grid.render(); + } this.onCommand.notify({ grid: this.grid, column: columnDef, diff --git a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts index 65ce016052..6d303f0f0e 100644 --- a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; + export interface IRowNumberColumnOptions { numberOfRows: number; cssClass?: string; @@ -45,7 +47,7 @@ export class RowNumberColumn implements Slick.Plugin { } } - public getColumnDefinition(): Slick.Column { + public getColumnDefinition(): FilterableColumn { // that smallest we can make it is 22 due to padding and margins in the cells return { id: 'rowNumber', @@ -56,6 +58,7 @@ export class RowNumberColumn implements Slick.Plugin { cssClass: this.options.cssClass, focusable: false, selectable: false, + filterable: false, formatter: r => this.formatter(r) }; } diff --git a/src/sql/base/browser/ui/table/table.ts b/src/sql/base/browser/ui/table/table.ts index e7b4086f7b..3214d07c7a 100644 --- a/src/sql/base/browser/ui/table/table.ts +++ b/src/sql/base/browser/ui/table/table.ts @@ -8,7 +8,7 @@ import 'vs/css!./media/slick.grid'; import 'vs/css!./media/slickColorTheme'; import { TableDataView } from './tableDataView'; -import { IDisposableDataProvider, ITableSorter, ITableMouseEvent, ITableConfiguration, ITableStyles } from 'sql/base/browser/ui/table/interfaces'; +import { ITableSorter, ITableMouseEvent, ITableConfiguration, ITableStyles } from 'sql/base/browser/ui/table/interfaces'; import * as DOM from 'vs/base/browser/dom'; import { mixin } from 'vs/base/common/objects'; @@ -19,6 +19,7 @@ import { isArray, isBoolean } from 'vs/base/common/types'; import { Event, Emitter } from 'vs/base/common/event'; import { range } from 'vs/base/common/arrays'; import { AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; function getDefaultOptions(): Slick.GridOptions { return >{ @@ -122,7 +123,7 @@ export class Table extends Widget implements IDisposa this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire()); } - public rerenderGrid(start: number, end: number) { + public rerenderGrid() { this._grid.updateRowCount(); this._grid.setColumns(this._grid.getColumns()); this._grid.invalidateAllRows(); diff --git a/src/sql/base/browser/ui/table/tableDataView.ts b/src/sql/base/browser/ui/table/tableDataView.ts index d81f0481d6..ac9e7bae83 100644 --- a/src/sql/base/browser/ui/table/tableDataView.ts +++ b/src/sql/base/browser/ui/table/tableDataView.ts @@ -7,40 +7,58 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as types from 'vs/base/common/types'; import { compare as stringCompare } from 'vs/base/common/strings'; -import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces'; +import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; export interface IFindPosition { col: number; row: number; } -function defaultSort(args: Slick.OnSortEventArgs, data: Array): Array { +export type CellValueGetter = (data: any) => any; +export type TableFilterFunc = (data: Array, columns: Slick.Column[]) => Array; +export type TableSortFunc = (args: Slick.OnSortEventArgs, data: Array) => Array; +export type TableFindFunc = (val: T, exp: string) => Array; + +function defaultCellValueGetter(data: any): any { + return data; +} + +function defaultSort(args: Slick.OnSortEventArgs, data: Array, cellValueGetter: CellValueGetter = defaultCellValueGetter): Array { if (!args.sortCol || !args.sortCol.field || data.length === 0) { return data; } const field = args.sortCol.field; const sign = args.sortAsc ? 1 : -1; + let sampleData = data[0][field]; + sampleData = types.isObject(sampleData) ? cellValueGetter(sampleData) : sampleData; let comparer: (a: T, b: T) => number; - if (types.isString(data[0][field])) { - if (!isNaN(Number(data[0][field]))) { - comparer = (a: T, b: T) => { - let anum = Number(a[field]); - let bnum = Number(b[field]); - return anum === bnum ? 0 : anum > bnum ? 1 : -1; - }; - } else { - comparer = (a: T, b: T) => { - return stringCompare(a[field], b[field]); - }; - } + if (!isNaN(Number(sampleData))) { + comparer = (a: T, b: T) => { + let anum = Number(cellValueGetter(a[field])); + let bnum = Number(cellValueGetter(b[field])); + return anum === bnum ? 0 : anum > bnum ? 1 : -1; + }; } else { comparer = (a: T, b: T) => { - return a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1); + return stringCompare(cellValueGetter(a[field]), cellValueGetter(b[field])); }; } return data.sort((a, b) => comparer(a, b) * sign); } +function defaultFilter(data: T[], columns: FilterableColumn[], cellValueGetter: CellValueGetter = defaultCellValueGetter): T[] { + let filteredData = data; + columns?.forEach(column => { + if (column.filterValues?.length > 0 && column.field) { + filteredData = filteredData.filter((item) => { + return column.filterValues.includes(cellValueGetter(item[column.field!])); + }); + } + }); + return filteredData; +} + export class TableDataView implements IDisposableDataProvider { //The data exposed publicly, when filter is enabled, _data holds the filtered data. private _data: Array; @@ -49,6 +67,7 @@ export class TableDataView implements IDisposableData private _findArray?: Array; private _findIndex?: number; private _filterEnabled: boolean; + private _currentColumnFilters: FilterableColumn[]; private _onRowCountChange = new Emitter(); get onRowCountChange(): Event { return this._onRowCountChange.event; } @@ -59,14 +78,15 @@ export class TableDataView implements IDisposableData private _onFilterStateChange = new Emitter(); get onFilterStateChange(): Event { return this._onFilterStateChange.event; } - private _filterFn: (data: Array) => Array; - private _sortFn: (args: Slick.OnSortEventArgs, data: Array) => Array; + private _onSortComplete = new Emitter>(); + get onSortComplete(): Event> { return this._onSortComplete.event; } constructor( data?: Array, - private _findFn?: (val: T, exp: string) => Array, - _sortFn?: (args: Slick.OnSortEventArgs, data: Array) => Array, - _filterFn?: (data: Array) => Array + private _findFn?: TableFindFunc, + private _sortFn?: TableSortFunc, + private _filterFn?: TableFilterFunc, + private _cellValueGetter: CellValueGetter = defaultCellValueGetter ) { if (data) { this._data = data; @@ -75,25 +95,59 @@ export class TableDataView implements IDisposableData } // @todo @anthonydresser 5/1/19 theres a lot we could do by just accepting a regex as a exp rather than accepting a full find function - this._sortFn = _sortFn ? _sortFn : defaultSort; - - this._filterFn = _filterFn ? _filterFn : (dataToFilter) => dataToFilter; + this._sortFn = _sortFn ? _sortFn : (args, data) => { + return defaultSort(args, data, _cellValueGetter); + }; + this._filterFn = _filterFn ? _filterFn : (data, columns) => { + return defaultFilter(data, columns, _cellValueGetter); + }; this._filterEnabled = false; + this._cellValueGetter = this._cellValueGetter ? this._cellValueGetter : (cellValue) => cellValue?.toString(); + } + + public get isDataInMemory(): boolean { + return true; + } + + async getRangeAsync(startIndex: number, length: number): Promise { + return this._data.slice(startIndex, startIndex + length); + } + + public async getFilteredColumnValues(column: Slick.Column): Promise { + return this.getDistinctColumnValues(this._data, column); + } + + public async getColumnValues(column: Slick.Column): Promise { + return this.getDistinctColumnValues(this.filterEnabled ? this._allData : this._data, column); + } + + private getDistinctColumnValues(source: T[], column: Slick.Column): string[] { + const distinctValues: Set = new Set(); + source.forEach(items => { + const value = items[column.field!]; + const valueArr = value instanceof Array ? value : [value]; + valueArr.forEach(v => distinctValues.add(this._cellValueGetter(v))); + }); + + return Array.from(distinctValues); } public get filterEnabled(): boolean { return this._filterEnabled; } - public filter() { + public async filter(columns?: Slick.Column[]) { if (!this.filterEnabled) { this._allData = new Array(...this._data); - this._data = this._filterFn(this._allData); this._filterEnabled = true; } - - this._data = this._filterFn(this._allData); - this._onFilterStateChange.fire(); + this._currentColumnFilters = columns; + this._data = this._filterFn(this._allData, columns); + if (this._data.length === this._allData.length) { + this.clearFilter(); + } else { + this._onFilterStateChange.fire(); + } } public clearFilter() { @@ -105,8 +159,9 @@ export class TableDataView implements IDisposableData } } - sort(args: Slick.OnSortEventArgs) { + async sort(args: Slick.OnSortEventArgs): Promise { this._data = this._sortFn(args, this._data); + this._onSortComplete.fire(args); } getLength(): number { @@ -137,7 +192,7 @@ export class TableDataView implements IDisposableData if (this._filterEnabled) { this._allData.push(...inputArray); - let filteredArray = this._filterFn(inputArray); + let filteredArray = this._filterFn(inputArray, this._currentColumnFilters); if (filteredArray.length !== 0) { this._data.push(...filteredArray); } diff --git a/src/sql/base/common/dataProvider.ts b/src/sql/base/common/dataProvider.ts new file mode 100644 index 0000000000..7c984f2268 --- /dev/null +++ b/src/sql/base/common/dataProvider.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; + +/** + * Interface for table data providers + */ +export interface IDisposableDataProvider extends Slick.DataProvider { + /** + * Disposes the data provider object + */ + dispose(): void; + + /** + * Gets the rows of the giving range + * @param startIndex Start index of the range + * @param length Length of the rows to retrieve + */ + getRangeAsync(startIndex: number, length: number): Promise; + + /** + * Gets unique values of all the cells in the given column + * @param column the column information + */ + getColumnValues(column: Slick.Column): Promise; + + /** + * Gets the unique values of the filtered cells in the given column + * @param column + */ + getFilteredColumnValues(column: Slick.Column): Promise; + + /** + * Filters the data + * @param columns columns to be filtered, the + */ + filter(columns?: Slick.Column[]): Promise; + + /** + * Sorts the data + * @param args sort arguments + */ + sort(args: Slick.OnSortEventArgs): Promise; + + /** + * Event fired when the filters changed + */ + readonly onFilterStateChange: Event; + + /** + * Event fired when the sorting is completed + */ + readonly onSortComplete: Event>; + + /** + * Gets a boolean value indicating whether the data is current in memory + */ + readonly isDataInMemory: boolean; +} diff --git a/src/sql/base/test/browser/ui/scrollableView/scrollableView.test.ts b/src/sql/base/test/browser/ui/scrollableView/scrollableView.test.ts index a5426e7d8a..027dd7c9eb 100644 --- a/src/sql/base/test/browser/ui/scrollableView/scrollableView.test.ts +++ b/src/sql/base/test/browser/ui/scrollableView/scrollableView.test.ts @@ -33,7 +33,7 @@ class TestView extends Disposable implements IView { private readonly _onDidInsertEmitter = this._register(new Emitter()); public readonly onDidInsertEvent = this._onDidInsertEmitter.event; - onDidInsert?(): void { + async onDidInsert?(): Promise { this._onDidInsertEmitter.fire(); } diff --git a/src/sql/platform/query/common/query.ts b/src/sql/platform/query/common/query.ts index 2b9f88c02c..0e6947c4d1 100644 --- a/src/sql/platform/query/common/query.ts +++ b/src/sql/platform/query/common/query.ts @@ -19,7 +19,8 @@ export interface IQueryEditorConfiguration { readonly streaming: boolean, readonly copyIncludeHeaders: boolean, readonly copyRemoveNewLine: boolean, - readonly optimizedTable: boolean + readonly optimizedTable: boolean, + readonly inMemoryDataProcessingThreshold: number }, readonly messages: { readonly showBatchTime: boolean, diff --git a/src/sql/workbench/common/editor/query/gridTableState.ts b/src/sql/workbench/common/editor/query/gridTableState.ts index faad446519..d318a8a8d1 100644 --- a/src/sql/workbench/common/editor/query/gridTableState.ts +++ b/src/sql/workbench/common/editor/query/gridTableState.ts @@ -16,6 +16,16 @@ export class GridPanelState { } } +export interface GridColumnFilter { + field: string; + filterValues: string[]; +} + +export interface GridSortState { + field: string; + sortAsc: boolean; +} + export class GridTableState extends Disposable { private _maximized?: boolean; @@ -28,6 +38,9 @@ export class GridTableState extends Disposable { private _canBeMaximized?: boolean; + private _columnFilters: GridColumnFilter[] | undefined; + private _sortState: GridSortState | undefined; + /* The top row of the current scroll */ public scrollPositionY = 0; public scrollPositionX = 0; @@ -62,4 +75,20 @@ export class GridTableState extends Disposable { this._maximized = val; this._onMaximizedChange.fire(val); } + + public get columnFilters(): GridColumnFilter[] | undefined { + return this._columnFilters; + } + + public set columnFilters(value: GridColumnFilter[] | undefined) { + this._columnFilters = value; + } + + public get sortState(): GridSortState | undefined { + return this._sortState; + } + + public set sortState(value: GridSortState | undefined) { + this._sortState = value; + } } diff --git a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts index ca1361cbf2..b8deb2b1c2 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts @@ -981,7 +981,7 @@ export class EditDataGridPanel extends GridParentComponent { || (changes['blurredColumns'] && !equals(changes['blurredColumns'].currentValue, changes['blurredColumns'].previousValue)) || (changes['columnsLoading'] && !equals(changes['columnsLoading'].currentValue, changes['columnsLoading'].previousValue))) { this.setCallbackOnDataRowsChanged(); - this.table.rerenderGrid(0, this.dataSet.dataRows.getLength()); + this.table.rerenderGrid(); hasGridStructureChanges = true; } diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index 4fc2838409..51dd9c6d47 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -27,7 +27,7 @@ import { ICellModel } from 'sql/workbench/services/notebook/browser/models/model import { MimeModel } from 'sql/workbench/services/notebook/browser/outputs/mimemodel'; import { GridTableState } from 'sql/workbench/common/editor/query/gridTableState'; import { GridTableBase } from 'sql/workbench/contrib/query/browser/gridPanel'; -import { getErrorMessage } from 'vs/base/common/errors'; +import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors'; import { ISerializationService, SerializeDataParams } from 'sql/platform/serialization/common/serializationService'; import { SaveResultAction, IGridActionContext } from 'sql/workbench/contrib/query/browser/actions'; import { SaveFormat, ResultSerializer, SaveResultsResponse } from 'sql/workbench/services/query/common/resultSerializer'; @@ -43,6 +43,7 @@ import { URI } from 'vs/base/common/uri'; import { assign } from 'vs/base/common/objects'; import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell'; import { equals } from 'vs/base/common/arrays'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; @Component({ selector: GridOutputComponent.SELECTOR, template: `
` @@ -71,7 +72,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo @Input() set bundleOptions(value: MimeModel.IOptions) { this._bundleOptions = value; if (this._initialized) { - this.renderGrid(); + this.renderGrid().catch(onUnexpectedError); } } @@ -84,7 +85,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo @Input() set cellModel(value: ICellModel) { this._cellModel = value; if (this._initialized) { - this.renderGrid(); + this.renderGrid().catch(onUnexpectedError); } } @@ -96,7 +97,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo this._cellOutput = value; } - ngOnInit() { + async ngOnInit() { if (this.cellModel) { let outputId: QueryResultId = this.cellModel.getOutputId(this._cellOutput); if (outputId) { @@ -109,10 +110,10 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo } })); } - this.renderGrid(); + await this.renderGrid(); } - renderGrid(): void { + async renderGrid(): Promise { if (!this._bundleOptions || !this._cellModel || !this.mimeType) { return; } @@ -124,7 +125,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo let outputElement = this.output.nativeElement; outputElement.appendChild(this._table.element); this._register(attachTableStyler(this._table, this.themeService)); - this._table.onDidInsert(); + await this._table.onDidInsert(); this.layout(); this._initialized = true; } @@ -200,9 +201,13 @@ class DataResourceTable extends GridTableBase { @IEditorService editorService: IEditorService, @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, @IConfigurationService configurationService: IConfigurationService, - @IQueryModelService queryModelService: IQueryModelService + @IQueryModelService queryModelService: IQueryModelService, + @IThemeService themeService: IThemeService ) { - super(state, createResultSet(source), { actionOrientation: ActionsOrientation.HORIZONTAL }, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService, queryModelService); + super(state, createResultSet(source), { + actionOrientation: ActionsOrientation.HORIZONTAL, + inMemoryDataProcessing: true + }, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService, queryModelService, themeService); this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, this.cellModel); this._chart = this.instantiationService.createInstance(ChartView, false); @@ -352,13 +357,13 @@ export class DataResourceDataProvider implements IGridDataProvider { return Promise.resolve(resultSubset); } - async copyResults(selection: Slick.Range[], includeHeaders?: boolean): Promise { - return this.copyResultsAsync(selection, includeHeaders); + async copyResults(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { + return this.copyResultsAsync(selection, includeHeaders, tableView); } - private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise { + private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { try { - let results = await getResultsString(this, selection, includeHeaders); + let results = await getResultsString(this, selection, includeHeaders, tableView); this._clipboardService.writeText(results); } catch (error) { this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error))); diff --git a/src/sql/workbench/contrib/query/browser/actions.ts b/src/sql/workbench/contrib/query/browser/actions.ts index 67c02b614f..8c6452da0c 100644 --- a/src/sql/workbench/contrib/query/browser/actions.ts +++ b/src/sql/workbench/contrib/query/browser/actions.ts @@ -110,15 +110,10 @@ export class CopyResultAction extends Action { super(id, label); } - public run(context: IGridActionContext): Promise { - if (this.accountForNumberColumn) { - context.gridDataProvider.copyResults( - mapForNumberColumn(context.selection), - this.copyHeader); - } else { - context.gridDataProvider.copyResults(context.selection, this.copyHeader); - } - return Promise.resolve(true); + public async run(context: IGridActionContext): Promise { + const selection = this.accountForNumberColumn ? mapForNumberColumn(context.selection) : context.selection; + context.gridDataProvider.copyResults(selection, this.copyHeader, context.table.getData()); + return true; } } diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index 14cb3fda26..083123f676 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -5,11 +5,11 @@ import 'vs/css!./media/gridPanel'; -import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; +import { ITableStyles, ITableMouseEvent, FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; import { attachTableStyler } from 'sql/platform/theme/common/styler'; import QueryRunner, { QueryGridDataProvider } from 'sql/workbench/services/query/common/queryRunner'; -import { ResultSetSummary, IColumn } from 'sql/workbench/services/query/common/query'; -import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView'; +import { ResultSetSummary, IColumn, ICellValue } from 'sql/workbench/services/query/common/query'; +import { VirtualizedCollection } from 'sql/base/browser/ui/table/asyncDataView'; import { Table } from 'sql/base/browser/ui/table/table'; import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin'; import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin'; @@ -49,6 +49,9 @@ import { ScrollableView, IView } from 'sql/base/browser/ui/scrollableView/scroll import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; +import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; +import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { HybridDataProvider } from 'sql/base/browser/ui/table/hybridDataProvider'; const ROW_HEIGHT = 29; const HEADER_HEIGHT = 26; @@ -322,6 +325,8 @@ export interface IDataSet { export interface IGridTableOptions { actionOrientation: ActionsOrientation; + inMemoryDataProcessing: boolean; + inMemoryDataCountThreshold?: number; } export abstract class GridTableBase extends Disposable implements IView { @@ -331,7 +336,8 @@ export abstract class GridTableBase extends Disposable implements IView { private selectionModel = new CellSelectionModel(); private styles: ITableStyles; private currentHeight: number; - private dataProvider: AsyncDataProvider; + private dataProvider: HybridDataProvider; + private filterPlugin: HeaderFilter; private columns: Slick.Column[]; @@ -369,13 +375,17 @@ export abstract class GridTableBase extends Disposable implements IView { constructor( state: GridTableState, protected _resultSet: ResultSetSummary, - private readonly options: IGridTableOptions = { actionOrientation: ActionsOrientation.VERTICAL }, + private readonly options: IGridTableOptions = { + inMemoryDataProcessing: false, + actionOrientation: ActionsOrientation.VERTICAL + }, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IUntitledTextEditorService private readonly untitledEditorService: IUntitledTextEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IQueryModelService private readonly queryModelService: IQueryModelService + @IQueryModelService private readonly queryModelService: IQueryModelService, + @IThemeService private readonly themeService: IThemeService ) { super(); let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid'); @@ -405,7 +415,7 @@ export abstract class GridTableBase extends Disposable implements IView { return this._resultSet; } - public onDidInsert() { + public async onDidInsert() { if (!this.table) { this.build(); } @@ -421,7 +431,7 @@ export abstract class GridTableBase extends Disposable implements IView { }); this.dataProvider.dataRows = collection; this.table.updateRowCount(); - this.setupState(); + await this.setupState(); } public onDidRemove() { @@ -476,7 +486,15 @@ export abstract class GridTableBase extends Disposable implements IView { forceFitColumns: false, defaultColumnWidth: 120 }; - this.dataProvider = new AsyncDataProvider(collection); + this.dataProvider = new HybridDataProvider(collection, + (offset, count) => { return this.loadData(offset, count); }, + undefined, + undefined, + (data: ICellValue) => { return data?.displayValue; }, + { + inMemoryDataProcessing: this.options.inMemoryDataProcessing, + inMemoryDataCountThreshold: this.options.inMemoryDataCountThreshold + }); this.table = this._register(new Table(this.tableContainer, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions)); this.table.setTableTitle(localize('resultsGrid', "Results grid")); this.table.setSelectionModel(this.selectionModel); @@ -485,11 +503,33 @@ export abstract class GridTableBase extends Disposable implements IView { this.table.registerPlugin(copyHandler); this.table.registerPlugin(this.rowNumberColumn); this.table.registerPlugin(new AdditionalKeyBindings()); + this._register(this.dataProvider.onFilterStateChange(() => { this.layout(); })); this._register(this.table.onContextMenu(this.contextMenu, this)); this._register(this.table.onClick(this.onTableClick, this)); //This listener is used for correcting auto-scroling when clicking on the header for reszing. this._register(this.table.onHeaderClick(this.onHeaderClick, this)); - + this._register(this.dataProvider.onFilterStateChange(() => { + const columns = this.table.columns as FilterableColumn[]; + this.state.columnFilters = columns.filter((column) => column.filterValues?.length > 0).map(column => { + return { + filterValues: column.filterValues, + field: column.field + }; + }); + this.table.rerenderGrid(); + })); + this._register(this.dataProvider.onSortComplete((args: Slick.OnSortEventArgs) => { + this.state.sortState = { + field: args.sortCol.field, + sortAsc: args.sortAsc + }; + this.table.rerenderGrid(); + })); + if (this.configurationService.getValue('workbench')['enablePreviewFeatures']) { + this.filterPlugin = new HeaderFilter(); + attachButtonStyler(this.filterPlugin, this.themeService); + this.table.registerPlugin(this.filterPlugin); + } if (this.styles) { this.table.style(this.styles); } @@ -558,7 +598,7 @@ export abstract class GridTableBase extends Disposable implements IView { } } - private setupState() { + private async setupState() { // change actionbar on maximize change this._register(this.state.onMaximizedChange(this.rebuildActionBar, this)); @@ -578,6 +618,25 @@ export abstract class GridTableBase extends Disposable implements IView { if (savedSelection) { this.selectionModel.setSelectedRanges(savedSelection); } + + if (this.state.sortState) { + await this.dataProvider.sort({ + multiColumnSort: false, + grid: this.table.grid, + sortAsc: this.state.sortState.sortAsc, + sortCol: this.columns.find((column) => column.field === this.state.sortState.field) + }); + } + + if (this.state.columnFilters) { + this.columns.forEach(column => { + const idx = this.state.columnFilters.findIndex(filter => filter.field === column.field); + if (idx !== -1) { + (>column).filterValues = this.state.columnFilters[idx].filterValues; + } + }); + await this.dataProvider.filter(this.columns); + } } public get state(): GridTableState { @@ -627,6 +686,9 @@ export abstract class GridTableBase extends Disposable implements IView { public updateResult(resultSet: ResultSetSummary) { this._resultSet = resultSet; if (this.table && this.visible) { + if (this.configurationService.getValue('workbench')['enablePreviewFeatures'] && this.options.inMemoryDataProcessing && this.options.inMemoryDataCountThreshold < resultSet.rowCount) { + this.filterPlugin.enabled = false; + } this.dataProvider.length = resultSet.rowCount; this.table.updateRowCount(); } @@ -787,9 +849,14 @@ class GridTable extends GridTableBase { @IEditorService editorService: IEditorService, @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, @IConfigurationService configurationService: IConfigurationService, - @IQueryModelService queryModelService: IQueryModelService + @IQueryModelService queryModelService: IQueryModelService, + @IThemeService themeService: IThemeService ) { - super(state, resultSet, undefined, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService, queryModelService); + super(state, resultSet, { + actionOrientation: ActionsOrientation.VERTICAL, + inMemoryDataProcessing: true, + inMemoryDataCountThreshold: configurationService.getValue('queryEditor').results.inMemoryDataProcessingThreshold, + }, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService, queryModelService, themeService); this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, this._runner, resultSet.batchId, resultSet.id); } diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 73e0fc3bc4..72cb4f42f3 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -391,6 +391,11 @@ const queryEditorConfiguration: IConfigurationNode = { 'description': localize('queryEditor.results.optimizedTable', "(Experimental) Use a optimized table in the results out. Some functionality might be missing and in the works."), 'default': false }, + 'queryEditor.results.inMemoryDataProcessingThreshold': { + 'type': 'number', + 'default': 2000, + 'description': localize('queryEditor.inMemoryDataProcessingThreshold', "Controls the max number of rows allowed to do filtering and sorting in memory. If the number is exceeded, sorting and filtering will be disabled.") + }, 'queryEditor.messages.showBatchTime': { 'type': 'boolean', 'description': localize('queryEditor.messages.showBatchTime', "Should execution time be shown for individual batches"), diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts index 1e1c1a856a..60e5b0327f 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts @@ -11,10 +11,10 @@ import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectio import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { HyperlinkCellValue, isHyperlinkCellValue, TextCellValue } from 'sql/base/browser/ui/table/formatters'; -import { HeaderFilter, CommandEventArgs, IExtendedColumn } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; +import { HeaderFilter, CommandEventArgs } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; import { Disposable } from 'vs/base/common/lifecycle'; import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; -import { ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; +import { FilterableColumn, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { isString } from 'vs/base/common/types'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -127,7 +127,7 @@ export class ResourceViewerTable extends Disposable { const columns = this._resourceViewerTable.grid.getColumns(); let value = true; for (let i = 0; i < columns.length; i++) { - const col: IExtendedColumn = columns[i]; + const col: FilterableColumn = columns[i]; if (!col.field) { continue; } diff --git a/src/sql/workbench/services/insights/browser/insightsDialogView.ts b/src/sql/workbench/services/insights/browser/insightsDialogView.ts index f000b063f2..c037b8bfe3 100644 --- a/src/sql/workbench/services/insights/browser/insightsDialogView.ts +++ b/src/sql/workbench/services/insights/browser/insightsDialogView.ts @@ -17,7 +17,6 @@ import { Table } from 'sql/base/browser/ui/table/table'; import { CopyInsightDialogSelectionAction } from 'sql/workbench/services/insights/browser/insightDialogActions'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; -import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as DOM from 'vs/base/browser/dom'; @@ -50,6 +49,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInsightsConfigDetails } from 'sql/platform/extensions/common/extensions'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; const labelDisplay = nls.localize("insights.item", "Item"); const valueDisplay = nls.localize("insights.value", "Value"); diff --git a/src/sql/workbench/services/query/common/gridDataProvider.ts b/src/sql/workbench/services/query/common/gridDataProvider.ts index 8984e48a23..2f3b220b83 100644 --- a/src/sql/workbench/services/query/common/gridDataProvider.ts +++ b/src/sql/workbench/services/query/common/gridDataProvider.ts @@ -6,6 +6,7 @@ import * as types from 'vs/base/common/types'; import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; import { ResultSetSubset } from 'sql/workbench/services/query/common/query'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; export interface IGridDataProvider { @@ -19,11 +20,10 @@ export interface IGridDataProvider { /** * Sends a copy request to copy data to the clipboard * @param selection The selection range to copy - * @param batchId The batch id of the result to copy from - * @param resultId The result id of the result to copy from * @param includeHeaders [Optional]: Should column headers be included in the copy selection + * @param tableView [Optional]: The data view associated with the table component */ - copyResults(selection: Slick.Range[], includeHeaders?: boolean): Promise; + copyResults(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise; /** * Gets the EOL terminator to use for this data type. @@ -42,7 +42,7 @@ export interface IGridDataProvider { } -export async function getResultsString(provider: IGridDataProvider, selection: Slick.Range[], includeHeaders?: boolean): Promise { +export async function getResultsString(provider: IGridDataProvider, selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { let headers: Map = new Map(); // Maps a column index -> header let rows: Map> = new Map(); // Maps row index -> column index -> actual row value const eol = provider.getEolString(); @@ -52,8 +52,14 @@ export async function getResultsString(provider: IGridDataProvider, selection: S return async (): Promise => { let startCol = range.fromCell; let startRow = range.fromRow; - - const result = await provider.getRowData(range.fromRow, range.toRow - range.fromRow + 1); + let result; + if (tableView && tableView.isDataInMemory) { + // If the data is sorted/filtered in memory, we need to get the data that is currently being displayed + const tableData = await tableView.getRangeAsync(range.fromRow, range.toRow - range.fromRow + 1); + result = tableData.map(item => Object.keys(item).map(key => item[key])); + } else { + result = (await provider.getRowData(range.fromRow, range.toRow - range.fromRow + 1)).rows; + } // If there was a previous selection separate it with a line break. Currently // when there are multiple selections they are never on the same line let columnHeaders = provider.getColumnHeaders(range); @@ -65,8 +71,8 @@ export async function getResultsString(provider: IGridDataProvider, selection: S } } // Iterate over the rows to paste into the copy string - for (let rowIndex: number = 0; rowIndex < result.rows.length; rowIndex++) { - let row = result.rows[rowIndex]; + for (let rowIndex: number = 0; rowIndex < result.length; rowIndex++) { + let row = result[rowIndex]; let cellObjects = row.slice(range.fromCell, (range.toCell + 1)); // Remove newlines if requested let cells = provider.shouldRemoveNewLines() diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index b4555ba0bb..7f464b6cac 100644 --- a/src/sql/workbench/services/query/common/queryRunner.ts +++ b/src/sql/workbench/services/query/common/queryRunner.ts @@ -26,6 +26,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IRange, Range } from 'vs/editor/common/core/range'; import { BatchSummary, IQueryMessage, ResultSetSummary, QueryExecuteSubsetParams, CompleteBatchSummary, IResultMessage, ResultSetSubset, BatchStartSummary } from './query'; import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; +import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; /* * Query Runner class which handles running a query, reports the results to the content manager, @@ -501,18 +502,19 @@ export class QueryGridDataProvider implements IGridDataProvider { return this.queryRunner.getQueryRows(rowStart, numberOfRows, this.batchId, this.resultSetId); } - copyResults(selection: Slick.Range[], includeHeaders?: boolean): Promise { - return this.copyResultsAsync(selection, includeHeaders); + copyResults(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { + return this.copyResultsAsync(selection, includeHeaders, tableView); } - private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise { + private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { try { - let results = await getResultsString(this, selection, includeHeaders); + const results = await getResultsString(this, selection, includeHeaders, tableView); await this._clipboardService.writeText(results); } catch (error) { this._notificationService.error(nls.localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error))); } } + getEolString(): string { return getEolString(this._textResourcePropertiesService, this.queryRunner.uri); }