From b8d0e2a9e3415eb7f261b8e4b59d219327efb82c Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Fri, 4 Sep 2020 19:10:26 -0700 Subject: [PATCH] Cleanup and fixes for resource viewer and filter plugin (#12154) * Cleanup and fixes for resource viewer and filter plugin * fix strict nulls --- src/sql/azdata.proposed.d.ts | 43 ++++++++- src/sql/base/browser/ui/table/formatters.ts | 4 + .../ui/table/plugins/headerFilter.plugin.ts | 5 +- .../resourceViewer/resourceViewerInput.ts | 84 +++++++++--------- .../resourceViewer/resourceViewerState.ts | 34 -------- .../browser/media/resourceViewerTable.css | 9 ++ .../browser/resourceViewer.contribution.ts | 10 ++- .../browser/resourceViewerEditor.ts | 31 ++----- .../browser/resourceViewerTable.ts | 87 ++++++++++++------- .../browser/dataGridProviderUtils.ts | 18 ++++ 10 files changed, 191 insertions(+), 134 deletions(-) delete mode 100644 src/sql/workbench/common/editor/resourceViewer/resourceViewerState.ts create mode 100644 src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerTable.css create mode 100644 src/sql/workbench/services/dataGridProvider/browser/dataGridProviderUtils.ts diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 672fb262a9..0685d42265 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -106,22 +106,55 @@ declare module 'azdata' { DataGridProvider = 'DataGridProvider' } + export type DataGridColumnType = 'hyperlink' | 'text' | 'image'; /** * A column in a data grid */ export interface DataGridColumn { /** * The text to display on the column heading. - **/ + */ name: string; + /** * The property name in the DataGridItem - **/ + */ field: string; + /** * A unique identifier for the column within the grid. */ id: string; + + /** + * The type of column this is. This is used to determine how to render the contents. + */ + type: DataGridColumnType; + + /** + * Whether this column is sortable. + */ + sortable?: boolean; + + /** + * Whether this column is filterable + */ + filterable?: boolean; + + /** + * If false, column can no longer be resized. + */ + resizable?: boolean; + + /** + * If set to a non-empty string, a tooltip will appear on hover containing the string. + */ + tooltip?: string; + + /** + * Width of the column in pixels. + */ + width?: number } /** @@ -132,10 +165,14 @@ declare module 'azdata' { * A unique identifier for this item */ id: string; + /** + * The optional icon to display for this item + */ + iconPath?: string; /** * The other properties that will be displayed in the grid */ - [key: string]: string; + [key: string]: any; } /** diff --git a/src/sql/base/browser/ui/table/formatters.ts b/src/sql/base/browser/ui/table/formatters.ts index 9bbf83c9be..7015d29bf6 100644 --- a/src/sql/base/browser/ui/table/formatters.ts +++ b/src/sql/base/browser/ui/table/formatters.ts @@ -68,6 +68,10 @@ export function textFormatter(row: number | undefined, cell: any | undefined, va return `${valueToDisplay}`; } +export function imageFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string { + return ``; +} + /** * Provide slick grid cell with encoded ariaLabel and plain text. * text will be escaped by the textFormatter and ariaLabel will be consumed by slickgrid directly. 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 aa4e24fb01..ff0f58fda0 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -10,7 +10,7 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; -interface IExtendedColumn extends Slick.Column { +export interface IExtendedColumn extends Slick.Column { filterValues?: Array; } @@ -84,6 +84,9 @@ export class HeaderFilter { if (column.id === '_detail_selector') { return; } + if ((column).filterable === false) { + return; + } const $el = jQuery('
') .addClass('slick-header-menubutton') .data('column', column); diff --git a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts index be41c5fd90..acd1b117ca 100644 --- a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts +++ b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts @@ -7,38 +7,32 @@ import * as nls from 'vs/nls'; import { EditorInput } from 'vs/workbench/common/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { ResourceViewerState } from 'sql/workbench/common/editor/resourceViewer/resourceViewerState'; -import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; +import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { getDataGridFormatter } from 'sql/workbench/services/dataGridProvider/browser/dataGridProviderUtils'; export interface ColumnDefinition extends Slick.Column { name: string; + filterable?: boolean; } export class ResourceViewerInput extends EditorInput { public static ID: string = 'workbench.editorInput.resourceViewerInput'; - private _data: TableDataView; - private _columns: string[] = []; - private _state: ResourceViewerState; + private _data: Slick.SlickData[] = []; + private _columns: ColumnDefinition[] = []; private _onColumnsChanged = new Emitter[]>(); public onColumnsChanged: Event[]> = this._onColumnsChanged.event; - constructor() { - super(); - this._state = new ResourceViewerState(); - let searchFn = (val: { [x: string]: string }, exp: string): Array => { - let ret = new Array(); - for (let i = 0; i < this._columns.length; i++) { - let colVal = val[this._columns[i]]; - if (colVal && colVal.toLocaleLowerCase().indexOf(exp.toLocaleLowerCase()) > -1) { - ret.push(i); - } - } - return ret; - }; + private _onDataChanged = new Emitter(); + public onDataChanged: Event = this._onDataChanged.event; + + constructor(private _providerId: string, @IDataGridProviderService private _dataGridProvider: IDataGridProviderService) { + super(); + this.fetchColumns(); + this.fetchItems(); - this._data = new TableDataView(undefined, searchFn, undefined, undefined); } public getTypeId(): string { @@ -49,39 +43,49 @@ export class ResourceViewerInput extends EditorInput { return nls.localize('resourceViewerInput.resourceViewer', "Resource Viewer"); } - public get data(): TableDataView { + public get data(): Slick.SlickData[] { return this._data; } - public get columnDefinitions(): ColumnDefinition[] { - if (this._columns) { - return this._columns.map(i => { - return { - id: i, - field: i, - name: i, - sortable: true - }; - }); - } else { - return []; - } - } - - public set columns(columns: Array) { + public set columns(columns: ColumnDefinition[]) { this._columns = columns; - this._onColumnsChanged.fire(this.columnDefinitions); + this._onColumnsChanged.fire(this._columns); } - public get state(): ResourceViewerState { - return this._state; + public get columns(): ColumnDefinition[] { + return this._columns; } isDirty(): boolean { - return false; // TODO chgagnon implement + return false; } public get resource(): URI | undefined { return undefined; } + + private fetchColumns(): void { + this._dataGridProvider.getDataGridColumns(this._providerId).then(columns => { + this.columns = columns.map(col => { + return { + name: col.name, + field: col.field, + id: col.id, + formatter: getDataGridFormatter(col.type), + sortable: col.sortable ?? true, + filterable: col.filterable ?? true, + resizable: col.resizable ?? true, + tooltip: col.tooltip, + width: col.width + }; + }); + }).catch(err => onUnexpectedError(err)); + } + + private fetchItems(): void { + this._dataGridProvider.getDataGridItems(this._providerId).then(items => { + this._data = items; + this._onDataChanged.fire(); + }).catch(err => onUnexpectedError(err)); + } } diff --git a/src/sql/workbench/common/editor/resourceViewer/resourceViewerState.ts b/src/sql/workbench/common/editor/resourceViewer/resourceViewerState.ts deleted file mode 100644 index 8f2dba9e37..0000000000 --- a/src/sql/workbench/common/editor/resourceViewer/resourceViewerState.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; - -export interface IResourceViewerStateChangedEvent { - -} - -export interface INewResourceViewerState { - // TODO - chgagnon implement state -} - -export class ResourceViewerState implements IDisposable { - - private readonly _onResourceViewerStateChange = new Emitter(); - public readonly onResourceViewerStateChange = this._onResourceViewerStateChange.event; - - public dispose(): void { - } - - public change(newState: INewResourceViewerState): void { - let changeEvent: IResourceViewerStateChangedEvent = { - }; - let somethingChanged = false; - - if (somethingChanged) { - this._onResourceViewerStateChange.fire(changeEvent); - } - } -} diff --git a/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerTable.css b/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerTable.css new file mode 100644 index 0000000000..7ac236d6a3 --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerTable.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.resource-viewer-table .slick-cell { + border-right: none; +} + diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts index f8ef9a858a..467e8cfb04 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts @@ -11,14 +11,18 @@ import { ResourceViewerInput } from 'sql/workbench/browser/editor/resourceViewer import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { isString } from 'vs/base/common/types'; CommandsRegistry.registerCommand({ - id: 'resourceViewer.newResourceViewer', - handler: (accessor: ServicesAccessor, ...args: any[]): void => { + id: 'resourceViewer.openResourceViewer', + handler: async (accessor: ServicesAccessor, ...args: any[]): Promise => { const instantiationService: IInstantiationService = accessor.get(IInstantiationService); const editorService: IEditorService = accessor.get(IEditorService); + if (!isString(args[0])) { + throw new Error('First argument must be the ProviderId'); + } - const resourceViewerInput = instantiationService.createInstance(ResourceViewerInput); + const resourceViewerInput = instantiationService.createInstance(ResourceViewerInput, args[0]); editorService.openEditor(resourceViewerInput, { pinned: true }, ACTIVE_GROUP); } }); diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts index f727f67c83..00990e85c6 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts @@ -12,7 +12,6 @@ import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IResourceViewerStateChangedEvent } from 'sql/workbench/common/editor/resourceViewer/resourceViewerState'; import { ResourceViewerInput } from 'sql/workbench/browser/editor/resourceViewer/resourceViewerInput'; import { ResourceViewerTable } from 'sql/workbench/contrib/resourceViewer/browser/resourceViewerTable'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -39,22 +38,22 @@ export class ResourceViewerEditor extends EditorPane { this._container.className = 'resource-viewer'; parent.appendChild(this._container); - this._createHeader(); - - let tableContainer = this.createResourceViewerTable(); + const header = this.createHeader(); + const tableContainer = this.createResourceViewerTable(); + this._container.appendChild(header); this._container.appendChild(tableContainer); } - private _createHeader(): void { + private createHeader(): HTMLElement { const header = document.createElement('div'); header.className = 'resource-viewer-header'; - this._container.appendChild(header); this._actionBar = this._register(new Taskbar(header)); this._actionBar.setContent([ // TODO - chgagnon add actions ]); + return header; } private createResourceViewerTable(): HTMLElement { @@ -78,33 +77,19 @@ export class ResourceViewerEditor extends EditorPane { this._inputDisposables.clear(); this._resourceViewerTable.data = input.data; + this._resourceViewerTable.columns = input.columns; this._inputDisposables.add(input.onColumnsChanged(columns => { this._resourceViewerTable.columns = columns; })); - - this._inputDisposables.add(input.data.onRowCountChange(() => { - this._resourceViewerTable.updateRowCount(); - })); - - this._inputDisposables.add(input.data.onFilterStateChange(() => { - this._resourceViewerTable.invalidateAllRows(); - this._resourceViewerTable.updateRowCount(); + this._inputDisposables.add(input.onDataChanged(() => { + this._resourceViewerTable.data = input.data; })); this._actionBar.context = input; - this._inputDisposables.add(input.state.onResourceViewerStateChange(e => this.onStateChange(e))); - this.onStateChange({ - }); - this._resourceViewerTable.focus(); } - - private onStateChange(e: IResourceViewerStateChangedEvent): void { - - } - public layout(dimension: DOM.Dimension): void { this._container.style.width = dimension.width + 'px'; this._container.style.height = dimension.height + 'px'; diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts index d0e76e3f9a..f153d40ae8 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts @@ -3,65 +3,92 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/resourceViewerTable'; import { Table } from 'sql/base/browser/ui/table/table'; -import { attachTableStyler } from 'sql/platform/theme/common/styler'; +import { attachTableStyler, attachButtonStyler } from 'sql/platform/theme/common/styler'; import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { Dimension } from 'vs/base/browser/dom'; -import { textFormatter, slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters'; -import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; +import { slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters'; +import { HeaderFilter, CommandEventArgs, IExtendedColumn } 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'; export class ResourceViewerTable extends Disposable { private _resourceViewerTable!: Table; - private _data: TableDataView | undefined; + private _dataView: TableDataView; constructor(parent: HTMLElement, @IWorkbenchThemeService private _themeService: IWorkbenchThemeService) { super(); + let filterFn = (data: Array): Array => { + return data.filter(item => this.filter(item)); + }; + + this._dataView = new TableDataView(undefined, undefined, undefined, filterFn); this._resourceViewerTable = this._register(new Table(parent, { sorter: (args) => { - this._data?.sort(args); + this._dataView.sort(args); } }, { dataItemColumnValueExtractor: slickGridDataItemColumnValueExtractor })); this._resourceViewerTable.setSelectionModel(new RowSelectionModel()); - attachTableStyler(this._resourceViewerTable, this._themeService); + let filterPlugin = new HeaderFilter(); + this._register(attachButtonStyler(filterPlugin, this._themeService)); + this._register(attachTableStyler(this._resourceViewerTable, this._themeService)); + filterPlugin.onFilterApplied.subscribe(() => { + this._dataView.filter(); + this._resourceViewerTable.grid.render(); + this._resourceViewerTable.grid.resetActiveCell(); + this._resourceViewerTable.grid.resizeCanvas(); + }); + filterPlugin.onCommand.subscribe((e, args: CommandEventArgs) => { + // Convert filter command to SlickGrid sort args + this._dataView.sort({ + grid: args.grid, + multiColumnSort: false, + sortCol: args.column, + sortAsc: args.command === 'sort-asc' + }); + this._resourceViewerTable.grid.invalidate(); + this._resourceViewerTable.grid.render(); + }); + this._resourceViewerTable.registerPlugin(filterPlugin); } - public set data(data: TableDataView) { - this._data = data; - this._resourceViewerTable.setData(data); + public set data(data: Slick.SlickData[]) { + this._dataView.clear(); + this._dataView.push(data); + this._resourceViewerTable.grid.setData(this._dataView, true); + this._resourceViewerTable.grid.render(); } public set columns(columns: Slick.Column[]) { - this._resourceViewerTable.columns = columns.map(column => { - column.formatter = textFormatter; - return column; - }); - this._resourceViewerTable.autosizeColumns(); - } - - public updateRowCount(): void { - this._resourceViewerTable.updateRowCount(); - } - - public invalidateAllRows(): void { - this._resourceViewerTable.grid.invalidateAllRows(); - } - - public autosizeColumns(): void { - this._resourceViewerTable.autosizeColumns(); + this._resourceViewerTable.columns = columns; } public focus(): void { this._resourceViewerTable.focus(); } - public layout(dimension: Dimension): void { - this._resourceViewerTable.layout(dimension); - this._resourceViewerTable.autosizeColumns(); + private filter(item: Slick.SlickData) { + const columns = this._resourceViewerTable.grid.getColumns(); + let value = true; + for (let i = 0; i < columns.length; i++) { + const col: IExtendedColumn = columns[i]; + if (!col.field) { + continue; + } + let filterValues = col.filterValues; + if (filterValues && filterValues.length > 0) { + if (item._parent) { + value = value && !!filterValues.find(x => x === item._parent[col.field!]); + } else { + value = value && !!filterValues.find(x => x === item[col.field!]); + } + } + } + return value; } } diff --git a/src/sql/workbench/services/dataGridProvider/browser/dataGridProviderUtils.ts b/src/sql/workbench/services/dataGridProvider/browser/dataGridProviderUtils.ts new file mode 100644 index 0000000000..664bd9735c --- /dev/null +++ b/src/sql/workbench/services/dataGridProvider/browser/dataGridProviderUtils.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { textFormatter, hyperLinkFormatter, imageFormatter } from 'sql/base/browser/ui/table/formatters'; + +export function getDataGridFormatter(formatterType: azdata.DataGridColumnType): Slick.Formatter { + switch (formatterType) { + case 'text': + return textFormatter; + case 'hyperlink': + return hyperLinkFormatter; + case 'image': + return imageFormatter; + } +}