mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Share Notebook grid rendering with Query editor (#6241)
This is a staged refactor to use the exact same grid logic in the Notebook and query editors, including context menu support, font settings, and sizing logic. The goal long term is: - As the core Query grid is updated, Notebook can benefit from the changes - As we add in support for contributions like new buttons & actions working on the grid, can share the logic - Ideally if and when we refactor things like the action bar for grid results, we can apply in both places though this is TBD. Fixes a number of issues: - Fixes #5755 Grids don't respond to font settings. @anthonydresser can we remove setting from each query results editor and just use Notebook Styles since these are global (not scoped) settings? - Fixes #5501 Copy from grid settings. - Fixes #4938 SQL Notebook result sets are missing the actions provide for SQL File results sets. this now has the core ability to solve this, and separate work items for specific asks (serialization, charting) are tracked. Currently hidden: - Save as... options in context menu - All right toolbar actions (save as, chart). Remaining issues to address in future commits: - Need to implement support for serialization (#5137). - Need to add charting support - Need to solve the layout of buttons on the right hand side when a small number of columns are output. It doesn't look right that buttons are so far away from the results - Will work with UX on this. For now, mitigating this by hiding all buttons, but will need to solve in the future - Would like to make buttons contributable via extension, but need to refactor similar to ObjectExplorer context menu so that we can serialize context menu options across to extension host while still having internal actions with full support
This commit is contained in:
@@ -9,7 +9,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||
@@ -17,16 +16,18 @@ import { QueryEditor } from './queryEditor';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
||||
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export interface IGridActionContext {
|
||||
cell: { row: number; cell: number; };
|
||||
selection: Slick.Range[];
|
||||
runner: QueryRunner;
|
||||
gridDataProvider: IGridDataProvider;
|
||||
table: Table<any>;
|
||||
tableState: GridTableState;
|
||||
cell?: { row: number; cell: number; };
|
||||
selection?: Slick.Range[];
|
||||
selectionModel?: CellSelectionModel<any>;
|
||||
batchId: number;
|
||||
resultId: number;
|
||||
table: Table<any>;
|
||||
selectionModel: CellSelectionModel<any>;
|
||||
tableState: GridTableState;
|
||||
}
|
||||
|
||||
export interface IMessagesActionContext {
|
||||
@@ -64,19 +65,17 @@ export class SaveResultAction extends Action {
|
||||
label: string,
|
||||
icon: string,
|
||||
private format: SaveFormat,
|
||||
private accountForNumberColumn = true
|
||||
@INotificationService private notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, icon);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
if (this.accountForNumberColumn) {
|
||||
context.runner.serializeResults(context.batchId, context.resultId, this.format,
|
||||
mapForNumberColumn(context.selection));
|
||||
} else {
|
||||
context.runner.serializeResults(context.batchId, context.resultId, this.format, context.selection);
|
||||
public async run(context: IGridActionContext): Promise<boolean> {
|
||||
if (!context.gridDataProvider.canSerialize) {
|
||||
this.notificationService.warn(localize('saveToFileNotSupported', "Save to file is not supported by the backing data source"));
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
await context.gridDataProvider.serializeResults(this.format, mapForNumberColumn(context.selection));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +97,11 @@ export class CopyResultAction extends Action {
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
if (this.accountForNumberColumn) {
|
||||
context.runner.copyResults(
|
||||
context.gridDataProvider.copyResults(
|
||||
mapForNumberColumn(context.selection),
|
||||
context.batchId, context.resultId, this.copyHeader);
|
||||
this.copyHeader);
|
||||
} else {
|
||||
context.runner.copyResults(context.selection, context.batchId, context.resultId, this.copyHeader);
|
||||
context.gridDataProvider.copyResults(context.selection, this.copyHeader);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class BareResultsGridInfo extends BareFontInfo {
|
||||
}
|
||||
}
|
||||
|
||||
function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string {
|
||||
export function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string {
|
||||
let content = '';
|
||||
if (info.fontFamily) {
|
||||
content += `font-family: ${info.fontFamily};`;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import QueryRunner, { QueryGridDataProvider } from 'sql/platform/query/common/queryRunner';
|
||||
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
|
||||
@@ -39,6 +39,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
||||
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
@@ -362,7 +364,12 @@ export class GridPanel {
|
||||
}
|
||||
}
|
||||
|
||||
class GridTable<T> extends Disposable implements IView {
|
||||
export interface IDataSet {
|
||||
rowCount: number;
|
||||
columnInfo: azdata.IDbColumn[];
|
||||
}
|
||||
|
||||
export abstract class GridTableBase<T> extends Disposable implements IView {
|
||||
private table: Table<T>;
|
||||
private actionBar: ActionBar;
|
||||
private container = document.createElement('div');
|
||||
@@ -390,24 +397,19 @@ class GridTable<T> extends Disposable implements IView {
|
||||
|
||||
public isOnlyTable: boolean = true;
|
||||
|
||||
public get resultSet(): azdata.ResultSetSummary {
|
||||
return this._resultSet;
|
||||
}
|
||||
|
||||
// this handles if the row count is small, like 4-5 rows
|
||||
private get maxSize(): number {
|
||||
protected get maxSize(): number {
|
||||
return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private runner: QueryRunner,
|
||||
private _resultSet: azdata.ResultSetSummary,
|
||||
state: GridTableState,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
protected _resultSet: azdata.ResultSetSummary,
|
||||
protected contextMenuService: IContextMenuService,
|
||||
protected instantiationService: IInstantiationService,
|
||||
protected editorService: IEditorService,
|
||||
protected untitledEditorService: IUntitledEditorService,
|
||||
protected configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid');
|
||||
@@ -423,7 +425,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
return <Slick.Column<T>>{
|
||||
id: i.toString(),
|
||||
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
||||
? 'XML Showplan'
|
||||
? localize('xmlShowplan', "XML Showplan")
|
||||
: escape(c.columnName),
|
||||
field: i.toString(),
|
||||
formatter: isLinked ? hyperLinkFormatter : textFormatter,
|
||||
@@ -432,6 +434,12 @@ class GridTable<T> extends Disposable implements IView {
|
||||
});
|
||||
}
|
||||
|
||||
abstract get gridDataProvider(): IGridDataProvider;
|
||||
|
||||
public get resultSet(): azdata.ResultSetSummary {
|
||||
return this._resultSet;
|
||||
}
|
||||
|
||||
public onAdd() {
|
||||
this.visible = true;
|
||||
let collection = new VirtualizedCollection(
|
||||
@@ -505,28 +513,27 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.table.style(this.styles);
|
||||
}
|
||||
|
||||
let actions = this.getCurrentActions();
|
||||
|
||||
let actionBarContainer = document.createElement('div');
|
||||
actionBarContainer.style.width = ACTIONBAR_WIDTH + 'px';
|
||||
actionBarContainer.style.display = 'inline-block';
|
||||
actionBarContainer.style.height = '100%';
|
||||
actionBarContainer.style.verticalAlign = 'top';
|
||||
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: ActionsOrientation.VERTICAL, context: {
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
table: this.table,
|
||||
tableState: this.state
|
||||
}
|
||||
orientation: ActionsOrientation.VERTICAL, context: context
|
||||
});
|
||||
// update context before we run an action
|
||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||
this.actionBar.context = this.generateContext();
|
||||
});
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
this.rebuildActionBar();
|
||||
|
||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||
if (this.state) {
|
||||
@@ -605,7 +612,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
let column = this.resultSet.columnInfo[event.cell.cell - 1];
|
||||
// handle if a showplan link was clicked
|
||||
if (column && (column.isXml || column.isJson)) {
|
||||
this.runner.getQueryRows(event.cell.row, 1, this.resultSet.batchId, this.resultSet.id).then(async d => {
|
||||
this.gridDataProvider.getRowData(event.cell.row, 1).then(async d => {
|
||||
let value = d.resultSubset.rows[0][event.cell.cell - 1];
|
||||
let content = value.displayValue;
|
||||
|
||||
@@ -631,9 +638,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
return <IGridActionContext>{
|
||||
cell,
|
||||
selection,
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
gridDataProvider: this.gridDataProvider,
|
||||
table: this.table,
|
||||
tableState: this.state,
|
||||
selectionModel: this.selectionModel
|
||||
@@ -646,28 +651,9 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
}
|
||||
|
||||
private getCurrentActions(): IAction[] {
|
||||
protected abstract 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(
|
||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||
new SaveResultAction(SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
this.instantiationService.createInstance(ChartDataAction)
|
||||
);
|
||||
|
||||
return actions;
|
||||
}
|
||||
protected abstract getContextActions(): IAction[];
|
||||
|
||||
public layout(size?: number): void {
|
||||
if (!this.table) {
|
||||
@@ -692,7 +678,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
}
|
||||
|
||||
private loadData(offset: number, count: number): Thenable<T[]> {
|
||||
return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => {
|
||||
return this.gridDataProvider.getRowData(offset, count).then(response => {
|
||||
if (!response.resultSubset) {
|
||||
return [];
|
||||
}
|
||||
@@ -716,17 +702,19 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
let actions = [
|
||||
let actions: IAction[] = [
|
||||
new SelectAllGridAction(),
|
||||
new Separator(),
|
||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||
new SaveResultAction(SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
new Separator(),
|
||||
new Separator()
|
||||
];
|
||||
let contributedActions: IAction[] = this.getContextActions();
|
||||
if (contributedActions && contributedActions.length > 0) {
|
||||
actions.push(...contributedActions);
|
||||
actions.push(new Separator());
|
||||
}
|
||||
actions.push(
|
||||
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false),
|
||||
new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true)
|
||||
];
|
||||
);
|
||||
|
||||
if (this.state.canBeMaximized) {
|
||||
if (this.state.maximized) {
|
||||
@@ -787,3 +775,56 @@ class GridTable<T> extends Disposable implements IView {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class GridTable<T> extends GridTableBase<T> {
|
||||
private _gridDataProvider: IGridDataProvider;
|
||||
constructor(
|
||||
runner: QueryRunner,
|
||||
resultSet: azdata.ResultSetSummary,
|
||||
state: GridTableState,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(state, resultSet, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||
this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, runner, resultSet.batchId, resultSet.id);
|
||||
}
|
||||
|
||||
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.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
this.instantiationService.createInstance(ChartDataAction)
|
||||
);
|
||||
|
||||
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.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user