/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/gridPanel'; import { ITableStyles, ITableMouseEvent, FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; import { attachTableFilterStyler, attachTableStyler } from 'sql/platform/theme/common/styler'; import QueryRunner, { QueryGridDataProvider } from 'sql/workbench/services/query/common/queryRunner'; 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'; import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction, VisualizerDataAction, CopyHeadersAction } from 'sql/workbench/contrib/query/browser/actions'; import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin'; import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin'; import { escape } from 'sql/base/common/strings'; import { hyperLinkFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters'; import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Emitter, Event } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; import { generateUuid } from 'vs/base/common/uuid'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { isInDOM, Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IAction, Separator } from 'vs/base/common/actions'; import { ILogService } from 'vs/platform/log/common/log'; import { localize } from 'vs/nls'; import { IGridDataProvider } from 'sql/workbench/services/query/common/gridDataProvider'; import { CancellationToken } from 'vs/base/common/cancellation'; import { GridPanelState, GridTableState } from 'sql/workbench/common/editor/query/gridTableState'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; import { Progress } from 'vs/platform/progress/common/progress'; import { ScrollableView, IView } from 'sql/base/browser/ui/scrollableView/scrollableView'; 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 { FilterButtonWidth, HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; import { HybridDataProvider } from 'sql/base/browser/ui/table/hybridDataProvider'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { alert, status } from 'vs/base/browser/ui/aria/aria'; import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/browser/format'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; const ROW_HEIGHT = 29; const HEADER_HEIGHT = 26; const MIN_GRID_HEIGHT_ROWS = 8; const ESTIMATED_SCROLL_BAR_HEIGHT = 15; const BOTTOM_PADDING = 15; const ACTIONBAR_WIDTH = 36; // minimum height needed to show the full actionbar const ACTIONBAR_HEIGHT = 120; // this handles min size if rows is greater than the min grid visible rows const MIN_GRID_HEIGHT = (MIN_GRID_HEIGHT_ROWS * ROW_HEIGHT) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT; // The regex to check whether a string is a valid JSON string. It is used to determine: // 1. whether the cell should be rendered as a hyperlink. // 2. when user clicks a cell, whether the cell content should be displayed in a new text editor as json. // Based on the requirements, the solution doesn't need to be very accurate, a simple regex is enough since it is more // performant than trying to parse the string to object. // Regex explaination: after removing the trailing whitespaces and line breaks, the string must start with '[' (to support arrays) // or '{', and there must be a '}' or ']' to close it. const IsJsonRegex = /^\s*[\{|\[][\S\s]*[\}\]]\s*$/g; export class GridPanel extends Disposable { private container = document.createElement('div'); private scrollableView: ScrollableView; private tables: Array> = []; private tableDisposable = this._register(new DisposableStore()); private queryRunnerDisposables = this._register(new DisposableStore()); private runner: QueryRunner; private maximizedGrid: GridTable; private _state: GridPanelState | undefined; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @IThemeService private readonly themeService: IThemeService, ) { super(); this.scrollableView = new ScrollableView(this.container); this.scrollableView.onDidScroll(e => { if (this.state && this.scrollableView.length !== 0) { this.state.scrollPosition = e.scrollTop; } }); } public render(container: HTMLElement): void { this.container.style.width = '100%'; this.container.style.height = '100%'; container.appendChild(this.container); } public layout(size: Dimension): void { this.scrollableView.layout(size.height, size.width); } public focus(): void { // will need to add logic to save the focused grid and focus that this.tables[0].focus(); } public set queryRunner(runner: QueryRunner) { this.queryRunnerDisposables.clear(); this.reset(); this.runner = runner; this.queryRunnerDisposables.add(this.runner.onResultSet(this.onResultSet, this)); this.queryRunnerDisposables.add(this.runner.onResultSetUpdate(this.updateResultSet, this)); this.queryRunnerDisposables.add(this.runner.onQueryStart(() => { status(localize('query.QueryExecutionStarted', "Query execution started.")); if (this.state) { this.state.tableStates = []; } this.reset(); })); this.queryRunnerDisposables.add(this.runner.onQueryEnd(() => { status(localize('query.QueryExecutionEnded', "Query execution completed.")); })); this.queryRunnerDisposables.add(this.runner.onMessage((messages) => { if (messages?.find(m => m.isError)) { alert(localize('query.QueryErrorOccured', "Error occured while executing the query.")); } })); this.addResultSet(this.runner.batchSets.reduce((p, e) => { if (this.configurationService.getValue('queryEditor').results.streaming) { p = p.concat(e.resultSetSummaries ?? []); } else { p = p.concat(e.resultSetSummaries?.filter(c => c.complete) ?? []); } return p; }, [])); if (this.state && this.state.scrollPosition) { this.scrollableView.setScrollTop(this.state.scrollPosition); } } public resetScrollPosition(): void { this.scrollableView.setScrollTop(this.state.scrollPosition); } private onResultSet(resultSet: ResultSetSummary | ResultSetSummary[]) { let resultsToAdd: ResultSetSummary[]; if (!Array.isArray(resultSet)) { resultsToAdd = [resultSet]; } else { resultsToAdd = resultSet.splice(0); } const sizeChanges = () => { this.tables.map(t => { t.state.canBeMaximized = this.tables.length > 1; }); if (this.state && this.state.scrollPosition) { this.scrollableView.setScrollTop(this.state.scrollPosition); } }; if (this.configurationService.getValue('queryEditor').results.streaming) { this.addResultSet(resultsToAdd); sizeChanges(); } else { resultsToAdd = resultsToAdd.filter(e => e.complete); if (resultsToAdd.length > 0) { this.addResultSet(resultsToAdd); } sizeChanges(); } } private updateResultSet(resultSet: ResultSetSummary | ResultSetSummary[]) { let resultsToUpdate: ResultSetSummary[]; if (!Array.isArray(resultSet)) { resultsToUpdate = [resultSet]; } else { resultsToUpdate = resultSet.splice(0); } const sizeChanges = () => { if (this.state && this.state.scrollPosition) { this.scrollableView.setScrollTop(this.state.scrollPosition); } }; if (this.configurationService.getValue('queryEditor').results.streaming) { for (let set of resultsToUpdate) { let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id); if (table) { table.updateResult(set); } else { this.logService.warn('Got result set update request for non-existant table'); } } sizeChanges(); } else { resultsToUpdate = resultsToUpdate.filter(e => e.complete); if (resultsToUpdate.length > 0) { this.addResultSet(resultsToUpdate); } sizeChanges(); } } private addResultSet(resultSet: ResultSetSummary[]) { const tables: Array> = []; for (const set of resultSet) { // ensure we aren't adding a resultSet that is already visible if (this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id)) { continue; } let tableState: GridTableState; if (this.state) { tableState = this.state.tableStates.find(e => e.batchId === set.batchId && e.resultId === set.id); } if (!tableState) { tableState = new GridTableState(set.id, set.batchId); if (this.state) { this.state.tableStates.push(tableState); } } const table = this.instantiationService.createInstance(GridTable, this.runner, set, tableState); this.tableDisposable.add(tableState.onMaximizedChange(e => { if (e) { this.maximizeTable(table.id); } else { this.minimizeTables(); } })); this.tableDisposable.add(attachTableStyler(table, this.themeService)); tables.push(table); } this.tables = this.tables.concat(tables); // turn-off special-case process when only a single table is being displayed if (this.tables.length > 1) { for (let i = 0; i < this.tables.length; ++i) { this.tables[i].isOnlyTable = false; } } if (isUndefinedOrNull(this.maximizedGrid)) { this.scrollableView.addViews(tables); } } public clear() { this.reset(); this.state = undefined; } private reset() { this.scrollableView.clear(); dispose(this.tables); this.tableDisposable.clear(); this.tables = []; this.maximizedGrid = undefined; } private maximizeTable(tableid: string): void { if (!this.tables.find(t => t.id === tableid)) { return; } for (let i = this.tables.length - 1; i >= 0; i--) { if (this.tables[i].id === tableid) { const selectedTable = this.tables[i]; selectedTable.state.maximized = true; this.maximizedGrid = selectedTable; this.scrollableView.clear(); this.scrollableView.addViews([selectedTable]); break; } } } private minimizeTables(): void { if (this.maximizedGrid) { this.maximizedGrid.state.maximized = false; this.maximizedGrid = undefined; this.scrollableView.clear(); this.scrollableView.addViews(this.tables); } } public set state(val: GridPanelState) { this._state = val; if (this.state) { this.tables.map(t => { let state = this.state.tableStates.find(s => s.batchId === t.resultSet.batchId && s.resultId === t.resultSet.id); if (!state) { this.state.tableStates.push(t.state); } if (state) { t.state = state; } }); } } public get state() { return this._state; } public override dispose() { dispose(this.tables); this.tables = undefined; super.dispose(); } } export interface IDataSet { rowCount: number; columnInfo: IColumn[]; } export interface IGridTableOptions { actionOrientation: ActionsOrientation; showActionBar?: boolean; inMemoryDataProcessing: boolean; inMemoryDataCountThreshold?: number; } const defaultGridTableOptions: IGridTableOptions = { showActionBar: true, inMemoryDataProcessing: false, actionOrientation: ActionsOrientation.VERTICAL }; export abstract class GridTableBase extends Disposable implements IView { private table: Table; private actionBar: ActionBar; private container = document.createElement('div'); private selectionModel = new CellSelectionModel({ hasRowSelector: true }); private styles: ITableStyles; private currentHeight: number; private dataProvider: HybridDataProvider; private filterPlugin: HeaderFilter; private isDisposed: boolean = false; private columns: Slick.Column[]; private rowNumberColumn: RowNumberColumn; private _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; public id = generateUuid(); readonly element: HTMLElement = this.container; protected tableContainer: HTMLElement; private _state: GridTableState; private visible = false; private rowHeight: number; public isOnlyTable: boolean = true; public providerId: string; // this handles if the row count is small, like 4-5 rows protected get maxSize(): number { return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT; } public focus(): void { if (!this.table.activeCell) { this.table.setActiveCell(0, 1); this.selectionModel.setSelectedRanges([new Slick.Range(0, 1)]); } this.table.focus(); } constructor( state: GridTableState, protected _resultSet: ResultSetSummary, private readonly options: IGridTableOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IUntitledTextEditorService private readonly untitledEditorService: IUntitledTextEditorService, @IConfigurationService protected readonly configurationService: IConfigurationService, @IQueryModelService private readonly queryModelService: IQueryModelService, @IThemeService private readonly themeService: IThemeService, @IContextViewService private readonly contextViewService: IContextViewService, @INotificationService private readonly notificationService: INotificationService, @IExecutionPlanService private readonly executionPlanService: IExecutionPlanService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); this.options = { ...defaultGridTableOptions, ...options }; let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid'); this.rowHeight = config && config.rowHeight ? config.rowHeight : ROW_HEIGHT; this.state = state; this.container.style.width = '100%'; this.container.style.height = '100%'; this.columns = this.resultSet.columnInfo.map((c, i) => { return >{ id: i.toString(), name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan' ? localize('xmlShowplan', "XML Showplan") : escape(c.columnName), field: i.toString(), formatter: c.isXml ? hyperLinkFormatter : queryResultTextFormatter, width: this.state.columnSizes && this.state.columnSizes[i] ? this.state.columnSizes[i] : undefined }; }); } abstract get gridDataProvider(): IGridDataProvider; public get resultSet(): ResultSetSummary { return this._resultSet; } public async onDidInsert() { if (this.isDisposed) { return; } if (!this.table) { this.build(); } this.visible = true; let collection = new VirtualizedCollection( 50, index => this.placeholdGenerator(index), this.resultSet.rowCount, (offset, count) => this.loadData(offset, count) ); collection.setCollectionChangedCallback((startIndex, count) => { this.renderGridDataRowsRange(startIndex, count); }); this.dataProvider.dataRows = collection; this.setFilterState(); this.table.updateRowCount(); await this.setupState(); } public onDidRemove() { this.visible = false; let collection = new VirtualizedCollection( 50, index => this.placeholdGenerator(index), 0, () => Promise.resolve([]) ); this.dataProvider.dataRows = collection; this.table.updateRowCount(); } // actionsOrientation controls the orientation (horizontal or vertical) of the actionBar private build(): void { let actionBarContainer = document.createElement('div'); // Create a horizontal actionbar if orientation passed in is HORIZONTAL if (this.options.actionOrientation === ActionsOrientation.HORIZONTAL) { actionBarContainer.className = 'grid-panel action-bar horizontal'; this.container.appendChild(actionBarContainer); } this.tableContainer = document.createElement('div'); this.tableContainer.className = 'grid-panel'; this.tableContainer.style.display = 'inline-block'; this.tableContainer.style.width = `calc(100% - ${ACTIONBAR_WIDTH}px)`; this.container.appendChild(this.tableContainer); let collection = new VirtualizedCollection( 50, index => this.placeholdGenerator(index), 0, () => Promise.resolve([]) ); collection.setCollectionChangedCallback((startIndex, count) => { this.renderGridDataRowsRange(startIndex, count); }); this.rowNumberColumn = new RowNumberColumn({ autoCellSelection: false }); this.columns.unshift(this.rowNumberColumn.getColumnDefinition()); let tableOptions: Slick.GridOptions = { rowHeight: this.rowHeight, showRowNumber: true, forceFitColumns: false, defaultColumnWidth: 120 }; this.dataProvider = new HybridDataProvider(collection, (offset, count) => { return this.loadData(offset, count); }, undefined, undefined, (data: ICellValue) => { if (!data || data.isNull) { return undefined; } // If the string only contains whitespaces, it will be treated as empty string to make the filtering easier. // Note: this is the display string and does not impact the export/copy features. return data.displayValue.trim() === '' ? '' : data.displayValue; }, { inMemoryDataProcessing: this.options.inMemoryDataProcessing, inMemoryDataCountThreshold: this.options.inMemoryDataCountThreshold }); this.table = this._register(new Table(this.tableContainer, this.accessibilityService, this.quickInputService, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions)); this.table.setTableTitle(localize('resultsGrid', "Results grid")); this.table.setSelectionModel(this.selectionModel); this.table.registerPlugin(new MouseWheelSupport()); const autoSizeOnRender: boolean = !this.state.columnSizes && this.configurationService.getValue('resultsGrid.autoSizeColumns'); this.table.registerPlugin(new AutoColumnSize({ autoSizeOnRender: autoSizeOnRender, maxWidth: this.configurationService.getValue('resultsGrid.maxColumnWidth'), extraColumnHeaderWidth: FilterButtonWidth })); 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._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(); })); this.filterPlugin = new HeaderFilter(this.contextViewService, this.notificationService, { disabledFilterMessage: localize('resultsGrid.maxRowCountExceeded', "Max row count for filtering/sorting has been exceeded. To update it, navigate to User Settings and change the setting: 'queryEditor.results.inMemoryDataProcessingThreshold'"), refreshColumns: !autoSizeOnRender // The auto size columns plugin refreshes the columns so we don't need to refresh twice if both plugins are on. }); this._register(attachTableFilterStyler(this.filterPlugin, this.themeService)); this.table.registerPlugin(this.filterPlugin); if (this.styles) { this.table.style(this.styles); } // If the actionsOrientation passed in is "VERTICAL" (or no actionsOrientation is passed in at all), create a vertical actionBar if (this.options.actionOrientation === ActionsOrientation.VERTICAL) { actionBarContainer.className = 'grid-panel action-bar vertical'; actionBarContainer.style.width = ACTIONBAR_WIDTH + 'px'; this.container.appendChild(actionBarContainer); } let context: IGridActionContext = { gridDataProvider: this.gridDataProvider, table: this.table, tableState: this.state, batchId: this.resultSet.batchId, resultId: this.resultSet.id }; this.actionBar = new ActionBar(actionBarContainer, { orientation: this.options.actionOrientation, context: context }); // update context before we run an action this.selectionModel.onSelectedRangesChanged.subscribe(e => { this.actionBar.context = this.generateContext(); }); this.rebuildActionBar(); this.selectionModel.onSelectedRangesChanged.subscribe(async e => { if (this.state) { this.state.selection = this.selectionModel.getSelectedRanges(); } await this.notifyTableSelectionChanged(); }); this.table.grid.onScroll.subscribe((e, data) => { if (!this.visible) { // If the grid is not set up yet it can get scroll events resetting the top to 0px, // so ignore those events return; } if (this.state && isInDOM(this.container)) { this.state.scrollPositionY = data.scrollTop; this.state.scrollPositionX = data.scrollLeft; } }); // we need to remove the first column since this is the row number this.table.onColumnResize(() => { let columnSizes = this.table.grid.getColumns().slice(1).map(v => v.width); this.state.columnSizes = columnSizes; }); this.table.grid.onActiveCellChanged.subscribe(e => { if (this.state) { this.state.activeCell = this.table.grid.getActiveCell(); } }); // Add implementation for the copy action to respect the user's global copy command keybinding. // 1 is a priority number that is slightly larger than the basic handler's priority 0 to make sure our implementation // is executed. this._register(CopyAction.addImplementation(1, 'query-result-grid', accessor => { const selectedRanges = this.table.getSelectedRanges(); // Only do copy if the grid is the current active grid. if (this.container.contains(document.activeElement) && selectedRanges && selectedRanges.length !== 0) { this.instantiationService.createInstance(CopyResultAction, CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false).run(this.generateContext()); return true; } return false; })); } private restoreScrollState() { if (this.state.scrollPositionX || this.state.scrollPositionY) { this.table.grid.scrollTo(this.state.scrollPositionY); this.table.grid.getContainerNode().children[3].scrollLeft = this.state.scrollPositionX; } } private async setupState() { // change actionbar on maximize change this._register(this.state.onMaximizedChange(this.rebuildActionBar, this)); this._register(this.state.onCanBeMaximizedChange(this.rebuildActionBar, this)); this.restoreScrollState(); this.rebuildActionBar(); // Setting the active cell resets the selection so save it here let savedSelection = this.state.selection; if (this.state.activeCell) { this.table.setActiveCell(this.state.activeCell.row, this.state.activeCell.cell); } if (savedSelection) { this.selectionModel.setSelectedRanges(savedSelection); } if (this.state.sortState) { const sortAsc = this.state.sortState.sortAsc; const sortCol = this.columns.find((column) => column.field === this.state.sortState.field); this.table.grid.setSortColumn(sortCol.id, sortAsc); await this.dataProvider.sort({ multiColumnSort: false, grid: this.table.grid, sortAsc: sortAsc, sortCol: sortCol }); } 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 { return this._state; } public set state(val: GridTableState) { this._state = val; } private async getRowData(start: number, length: number): Promise { let subset; if (this.dataProvider.isDataInMemory) { // handle the scenario when the data is sorted/filtered, // we need to use the data that is being displayed const data = await this.dataProvider.getRangeAsync(start, length); subset = data.map(item => Object.keys(item).map(key => item[key])); } else { subset = (await this.gridDataProvider.getRowData(start, length)).rows; } return subset; } private async notifyTableSelectionChanged() { const selectedCells = []; for (const range of this.state.selection) { const subset = await this.getRowData(range.fromRow, range.toRow - range.fromRow + 1); subset.forEach(row => { // start with range.fromCell -1 because we have row number column which is not available in the actual data for (let i = range.fromCell - 1; i < range.toCell; i++) { selectedCells.push(row[i]); } }); } this.queryModelService.notifyCellSelectionChanged(selectedCells); } private async onTableClick(event: ITableMouseEvent) { // account for not having the number column const column = this.resultSet.columnInfo[event.cell.cell - 1]; // handle if a showplan link was clicked if (column) { const subset = await this.getRowData(event.cell.row, 1); const value = subset[0][event.cell.cell - 1]; const isJson = isJsonCell(value); if (column.isXml || isJson) { const result = await this.executionPlanService.isExecutionPlan(this.providerId, value.displayValue); if (result.isExecutionPlan) { const executionPlanGraphInfo = { graphFileContent: value.displayValue, graphFileType: result.queryExecutionPlanFileExtension }; const executionPlanInput = this.instantiationService.createInstance(ExecutionPlanInput, undefined, executionPlanGraphInfo); await this.editorService.openEditor(executionPlanInput); } else { const content = value.displayValue; const input = this.untitledEditorService.create({ languageId: column.isXml ? 'xml' : 'json', initialValue: content }); await input.resolve(); await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None); input.setDirty(false); await this.editorService.openEditor(input); } } } } public updateResult(resultSet: ResultSetSummary) { this._resultSet = resultSet; if (this.table && this.visible) { this.dataProvider.length = resultSet.rowCount; this.setFilterState(); this.table.updateRowCount(); } this._onDidChange.fire(undefined); } private setFilterState(): void { const rowCount = this.table.getData().getLength(); this.filterPlugin.enabled = this.options.inMemoryDataProcessing && (this.options.inMemoryDataCountThreshold === undefined || this.options.inMemoryDataCountThreshold >= rowCount); } private generateContext(cell?: Slick.Cell): IGridActionContext { const selection = this.selectionModel.getSelectedRanges(); return { cell, selection, gridDataProvider: this.gridDataProvider, table: this.table, tableState: this.state, selectionModel: this.selectionModel }; } private rebuildActionBar() { let actions = this.getCurrentActions(); this.actionBar.clear(); if (this.options.showActionBar) { this.actionBar.push(actions, { icon: true, label: false }); } } public get showActionBar(): boolean { return this.options.showActionBar; } public set showActionBar(v: boolean) { if (this.options.showActionBar !== v) { this.options.showActionBar = v; this.rebuildActionBar(); } } protected abstract getCurrentActions(): IAction[]; protected abstract getContextActions(): IAction[]; // The actionsOrientation passed in controls the actionBar orientation public layout(size?: number): void { if (!size) { size = this.currentHeight; } else { this.currentHeight = size; } // Table is always called with Orientation as VERTICAL this.table?.layout(size, Orientation.VERTICAL); } public get minimumSize(): number { // clamp between ensuring we can show the actionbar, while also making sure we don't take too much space // if there is only one table then allow a minimum size of ROW_HEIGHT return this.isOnlyTable ? ROW_HEIGHT : Math.max(Math.min(this.maxSize, MIN_GRID_HEIGHT), ACTIONBAR_HEIGHT + BOTTOM_PADDING); } public get maximumSize(): number { return Math.max(this.maxSize, ACTIONBAR_HEIGHT + BOTTOM_PADDING); } private loadData(offset: number, count: number): Thenable { return this.gridDataProvider.getRowData(offset, count).then(response => { if (!response) { return []; } return response.rows.map(r => { let dataWithSchema = {}; // skip the first column since its a number column for (let i = 1; i < this.columns.length; i++) { dataWithSchema[this.columns[i].field] = { displayValue: r[i - 1].displayValue, ariaLabel: escape(r[i - 1].displayValue), isNull: r[i - 1].isNull, invariantCultureDisplayValue: r[i - 1].invariantCultureDisplayValue }; } return dataWithSchema as T; }); }); } private contextMenu(e: ITableMouseEvent): void { const { cell } = e; this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => { let actions: IAction[] = [ new SelectAllGridAction(), new Separator() ]; let contributedActions: IAction[] = this.getContextActions(); if (contributedActions && contributedActions.length > 0) { actions.push(...contributedActions); actions.push(new Separator()); } actions.push( this.instantiationService.createInstance(CopyResultAction, CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false), this.instantiationService.createInstance(CopyResultAction, CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true), this.instantiationService.createInstance(CopyHeadersAction) ); if (this.state.canBeMaximized) { if (this.state.maximized) { actions.splice(1, 0, new RestoreTableAction()); } else { actions.splice(1, 0, new MaximizeTableAction()); } } return actions; }, getActionsContext: () => { return this.generateContext(cell); } }); } private placeholdGenerator(index: number): any { return {}; } private renderGridDataRowsRange(startIndex: number, count: number): void { this.invalidateRange(startIndex, startIndex + count); } private invalidateRange(start: number, end: number): void { let refreshedRows = range(start, end); if (this.table) { this.table.invalidateRows(refreshedRows, true); } } public style(styles: ITableStyles) { if (this.table) { this.table.style(styles); } else { this.styles = styles; } } public override dispose() { this.isDisposed = true; this.container.remove(); if (this.table) { this.table.dispose(); } if (this.actionBar) { this.actionBar.dispose(); } super.dispose(); } } class GridTable extends GridTableBase { private _gridDataProvider: IGridDataProvider; constructor( private _runner: QueryRunner, resultSet: ResultSetSummary, state: GridTableState, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private contextKeyService: IContextKeyService, @IEditorService editorService: IEditorService, @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, @IConfigurationService configurationService: IConfigurationService, @IQueryModelService queryModelService: IQueryModelService, @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService, @INotificationService notificationService: INotificationService, @IExecutionPlanService executionPlanService: IExecutionPlanService, @IAccessibilityService accessibilityService: IAccessibilityService, @IQuickInputService quickInputService: IQuickInputService ) { super(state, resultSet, { actionOrientation: ActionsOrientation.VERTICAL, inMemoryDataProcessing: true, showActionBar: true, inMemoryDataCountThreshold: configurationService.getValue('queryEditor').results.inMemoryDataProcessingThreshold, }, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService, queryModelService, themeService, contextViewService, notificationService, executionPlanService, accessibilityService, quickInputService); this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, this._runner, resultSet.batchId, resultSet.id); this.providerId = this._runner.getProviderId(); } get gridDataProvider(): IGridDataProvider { return this._gridDataProvider; } protected getCurrentActions(): IAction[] { let actions = []; if (this.state.canBeMaximized) { if (this.state.maximized) { actions.splice(1, 0, new RestoreTableAction()); } else { actions.splice(1, 0, new MaximizeTableAction()); } } actions.push( this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveResultAction.SAVEMARKDOWN_ICON, SaveFormat.MARKDOWN), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), this.instantiationService.createInstance(ChartDataAction) ); if (this.contextKeyService.getContextKeyValue('showVisualizer')) { actions.push(this.instantiationService.createInstance(VisualizerDataAction, this._runner)); } return actions; } protected getContextActions(): IAction[] { return [ this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveResultAction.SAVEMARKDOWN_ICON, SaveFormat.MARKDOWN), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), ]; } } function isJsonCell(value: ICellValue): boolean { return !!(value && !value.isNull && value.displayValue?.match(IsJsonRegex)); } function queryResultTextFormatter(row: number | undefined, cell: any | undefined, value: ICellValue, columnDef: any | undefined, dataContext: any | undefined): string { if (isJsonCell(value)) { return hyperLinkFormatter(row, cell, value, columnDef, dataContext); } else { return textFormatter(row, cell, value, columnDef, dataContext); } }