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:
@@ -54,6 +54,7 @@ export class OutputComponent extends AngularDisposable implements OnInit, AfterV
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
|
||||
this.loadComponent();
|
||||
this.layout();
|
||||
this._initialized = true;
|
||||
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
|
||||
@@ -62,10 +63,6 @@ export class OutputComponent extends AngularDisposable implements OnInit, AfterV
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.updateTheme(this._themeService.getTheme());
|
||||
if (this.componentHost) {
|
||||
this.loadComponent();
|
||||
}
|
||||
this._changeref.detectChanges();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import product from 'vs/platform/product/node/product';
|
||||
import { registerComponentType } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
|
||||
import { MimeRendererComponent as MimeRendererComponent } from 'sql/workbench/parts/notebook/outputs/mimeRenderer.component';
|
||||
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/outputs/markdownOutput.component';
|
||||
import { GridOutputComponent } from 'sql/workbench/parts/notebook/outputs/gridOutput.component';
|
||||
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/outputs/plotlyOutput.component';
|
||||
|
||||
// Model View editor registration
|
||||
@@ -126,16 +127,31 @@ registerComponentType({
|
||||
* A mime renderer component for grid data.
|
||||
* This will be replaced by a dedicated component in the future
|
||||
*/
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
rank: 40,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
if (product.quality !== 'stable') {
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
rank: 40,
|
||||
safe: true,
|
||||
ctor: GridOutputComponent,
|
||||
selector: GridOutputComponent.SELECTOR
|
||||
});
|
||||
} else {
|
||||
// Default to existing grid view until we're sure the new
|
||||
// implementation is fully stable
|
||||
registerComponentType({
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
rank: 40,
|
||||
safe: true,
|
||||
ctor: MimeRendererComponent,
|
||||
selector: MimeRendererComponent.SELECTOR
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A mime renderer component for LaTeX.
|
||||
|
||||
@@ -9,8 +9,12 @@ import { SIDE_BAR_BACKGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, EDITOR_GROUP_H
|
||||
import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IDisposable } from 'vscode-xterm';
|
||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { BareResultsGridInfo, getBareResultsGridInfoStyles } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import * as types from 'vs/base/common/types';
|
||||
|
||||
export function registerNotebookThemes(overrideEditorThemeSetting: boolean): IDisposable {
|
||||
export function registerNotebookThemes(overrideEditorThemeSetting: boolean, configurationService: IConfigurationService): IDisposable {
|
||||
return registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
|
||||
|
||||
let lightBoxShadow = '0px 4px 6px 0px rgba(0, 0, 0, 0.14)';
|
||||
@@ -231,5 +235,23 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean): IDi
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Results grid options. Putting these here since query editor only adds them on query editor load.
|
||||
// We may want to remove from query editor as it can just live here and be loaded once, instead of once
|
||||
// per editor group which is inefficient
|
||||
let rawOptions = BareResultsGridInfo.createFromRawSettings(configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
|
||||
let cssRuleText = '';
|
||||
if (types.isNumber(rawOptions.cellPadding)) {
|
||||
cssRuleText = rawOptions.cellPadding + 'px';
|
||||
} else {
|
||||
cssRuleText = rawOptions.cellPadding.join('px ') + 'px;';
|
||||
}
|
||||
collector.addRule(`.grid-panel .monaco-table .slick-cell {
|
||||
padding: ${cssRuleText}
|
||||
}
|
||||
.grid-panel .monaco-table, .message-tree {
|
||||
${getBareResultsGridInfoStyles(rawOptions)}
|
||||
}`);
|
||||
});
|
||||
}
|
||||
282
src/sql/workbench/parts/notebook/outputs/gridOutput.component.ts
Normal file
282
src/sql/workbench/parts/notebook/outputs/gridOutput.component.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Component, Input, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
|
||||
import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||
import { GridTableBase, GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||
import { IDataResource } from 'sql/workbench/services/notebook/sql/sqlSessionManager';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { getEolString, shouldIncludeHeaders, shouldRemoveNewLines } from 'sql/platform/query/common/queryRunner';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
|
||||
@Component({
|
||||
selector: GridOutputComponent.SELECTOR,
|
||||
template: `<div #output class="notebook-cellTable"></div>`
|
||||
})
|
||||
export class GridOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||
public static readonly SELECTOR: string = 'grid-output';
|
||||
|
||||
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
|
||||
|
||||
private _initialized: boolean = false;
|
||||
private _cellModel: ICellModel;
|
||||
private _bundleOptions: MimeModel.IOptions;
|
||||
private _table: DataResourceTable;
|
||||
constructor(
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IThemeService) private readonly themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
this._bundleOptions = value;
|
||||
if (this._initialized) {
|
||||
this.renderGrid();
|
||||
}
|
||||
}
|
||||
|
||||
@Input() mimeType: string;
|
||||
|
||||
get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this._initialized) {
|
||||
this.renderGrid();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.renderGrid();
|
||||
}
|
||||
|
||||
renderGrid(): void {
|
||||
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
|
||||
return;
|
||||
}
|
||||
if (!this._table) {
|
||||
let source = <IDataResource><any>this._bundleOptions.data[this.mimeType];
|
||||
let state = new GridTableState(0, 0);
|
||||
this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel.notebookModel.notebookUri.toString(), state);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.appendChild(this._table.element);
|
||||
this._register(attachTableStyler(this._table, this.themeService));
|
||||
this.layout();
|
||||
this._table.onAdd();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this._table) {
|
||||
let maxSize = Math.min(this._table.maximumSize, 500);
|
||||
this._table.layout(maxSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataResourceTable extends GridTableBase<any> {
|
||||
|
||||
private _gridDataProvider: IGridDataProvider;
|
||||
|
||||
constructor(source: IDataResource,
|
||||
documentUri: string,
|
||||
state: GridTableState,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||
this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri);
|
||||
}
|
||||
|
||||
get gridDataProvider(): IGridDataProvider {
|
||||
return this._gridDataProvider;
|
||||
}
|
||||
|
||||
protected getCurrentActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected getContextActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public get maximumSize(): number {
|
||||
// Overriding action bar size calculation for now.
|
||||
// When we add this back in, we should update this calculation
|
||||
return Math.max(this.maxSize, /* ACTIONBAR_HEIGHT + BOTTOM_PADDING */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
class DataResourceDataProvider implements IGridDataProvider {
|
||||
private rows: azdata.DbCellValue[][];
|
||||
constructor(source: IDataResource,
|
||||
private resultSet: azdata.ResultSetSummary,
|
||||
private documentUri: string,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
this.transformSource(source);
|
||||
}
|
||||
|
||||
private transformSource(source: IDataResource): void {
|
||||
this.rows = source.data.map(row => {
|
||||
let rowData: azdata.DbCellValue[] = [];
|
||||
Object.keys(row).forEach((val, index) => {
|
||||
let displayValue = String(Object.values(row)[index]);
|
||||
// Since the columns[0] represents the row number, start at 1
|
||||
rowData.push({
|
||||
displayValue: displayValue,
|
||||
isNull: false,
|
||||
invariantCultureDisplayValue: displayValue
|
||||
});
|
||||
});
|
||||
return rowData;
|
||||
});
|
||||
}
|
||||
|
||||
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
|
||||
let rowEnd = rowStart + numberOfRows;
|
||||
if (rowEnd > this.rows.length) {
|
||||
rowEnd = this.rows.length;
|
||||
}
|
||||
let resultSubset: azdata.QueryExecuteSubsetResult = {
|
||||
message: undefined,
|
||||
resultSubset: {
|
||||
rowCount: rowEnd - rowStart,
|
||||
rows: this.rows.slice(rowStart, rowEnd)
|
||||
}
|
||||
};
|
||||
return Promise.resolve(resultSubset);
|
||||
}
|
||||
|
||||
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
|
||||
this.copyResultsAsync(selection, includeHeaders);
|
||||
}
|
||||
|
||||
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||
try {
|
||||
let results = await getResultsString(this, selection, includeHeaders);
|
||||
this._clipboardService.writeText(results);
|
||||
} catch (error) {
|
||||
this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
|
||||
}
|
||||
}
|
||||
|
||||
getEolString(): string {
|
||||
return getEolString(this._textResourcePropertiesService, this.documentUri);
|
||||
}
|
||||
shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||
return shouldIncludeHeaders(includeHeaders, this._configurationService);
|
||||
}
|
||||
shouldRemoveNewLines(): boolean {
|
||||
return shouldRemoveNewLines(this._configurationService);
|
||||
}
|
||||
|
||||
getColumnHeaders(range: Slick.Range): string[] {
|
||||
let headers: string[] = this.resultSet.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
|
||||
return info.columnName;
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
get canSerialize(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
function createResultSet(source: IDataResource): azdata.ResultSetSummary {
|
||||
let columnInfo: azdata.IDbColumn[] = source.schema.fields.map(field => {
|
||||
let column = new SimpleDbColumn(field.name);
|
||||
if (field.type) {
|
||||
switch (field.type) {
|
||||
case 'xml':
|
||||
column.isXml = true;
|
||||
break;
|
||||
case 'json':
|
||||
column.isJson = true;
|
||||
break;
|
||||
default:
|
||||
// Only handling a few cases for now
|
||||
break;
|
||||
}
|
||||
}
|
||||
return column;
|
||||
});
|
||||
let summary: azdata.ResultSetSummary = {
|
||||
batchId: 0,
|
||||
id: 0,
|
||||
complete: true,
|
||||
rowCount: source.data.length,
|
||||
columnInfo: columnInfo
|
||||
};
|
||||
return summary;
|
||||
}
|
||||
|
||||
class SimpleDbColumn implements azdata.IDbColumn {
|
||||
|
||||
constructor(columnName: string) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
allowDBNull?: boolean;
|
||||
baseCatalogName: string;
|
||||
baseColumnName: string;
|
||||
baseSchemaName: string;
|
||||
baseServerName: string;
|
||||
baseTableName: string;
|
||||
columnName: string;
|
||||
columnOrdinal?: number;
|
||||
columnSize?: number;
|
||||
isAliased?: boolean;
|
||||
isAutoIncrement?: boolean;
|
||||
isExpression?: boolean;
|
||||
isHidden?: boolean;
|
||||
isIdentity?: boolean;
|
||||
isKey?: boolean;
|
||||
isBytes?: boolean;
|
||||
isChars?: boolean;
|
||||
isSqlVariant?: boolean;
|
||||
isUdt?: boolean;
|
||||
dataType: string;
|
||||
isXml?: boolean;
|
||||
isJson?: boolean;
|
||||
isLong?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isUnique?: boolean;
|
||||
numericPrecision?: number;
|
||||
numericScale?: number;
|
||||
udtAssemblyQualifiedName: string;
|
||||
dataTypeName: string;
|
||||
}
|
||||
@@ -66,6 +66,9 @@ export class MarkdownOutputComponent extends AngularDisposable implements IMimeC
|
||||
|
||||
@Input() set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this._initialized) {
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
public get isTrusted(): boolean {
|
||||
|
||||
@@ -85,7 +85,7 @@ export function renderDataResource(
|
||||
}
|
||||
|
||||
// SlickGrid requires columns and data to be in a very specific format; this code was adapted from tableInsight.component.ts
|
||||
function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: string]: string }[] {
|
||||
export function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
let dataWithSchema = {};
|
||||
Object.keys(row).forEach((val, index) => {
|
||||
@@ -101,7 +101,7 @@ function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: strin
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
export function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map((col, index) => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
|
||||
Reference in New Issue
Block a user