mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Notebooks: Grid Support (#3832)
* First grid support in notebooks * still trying to get nteract ipynb to display grid correctly * works opening with existing 'application/vnd.dataresource+json' table * fixing merge issue due to core folder structure changing a bit * PR feedback, fix for XSS
This commit is contained in:
@@ -80,6 +80,16 @@ export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
|
|||||||
createRenderer: options => new widgets.RenderedJavaScript(options)
|
createRenderer: options => new widgets.RenderedJavaScript(options)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
|
||||||
|
safe: true,
|
||||||
|
mimeTypes: [
|
||||||
|
'application/vnd.dataresource+json',
|
||||||
|
'application/vnd.dataresource'
|
||||||
|
],
|
||||||
|
defaultRank: 40,
|
||||||
|
createRenderer: options => new widgets.RenderedDataResource(options)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The standard factories provided by the rendermime package.
|
* The standard factories provided by the rendermime package.
|
||||||
*/
|
*/
|
||||||
@@ -90,5 +100,6 @@ export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFacto
|
|||||||
svgRendererFactory,
|
svgRendererFactory,
|
||||||
imageRendererFactory,
|
imageRendererFactory,
|
||||||
javaScriptRendererFactory,
|
javaScriptRendererFactory,
|
||||||
textRendererFactory
|
textRendererFactory,
|
||||||
|
dataResourceRendererFactory
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -295,6 +295,26 @@ export namespace renderLatex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace for the `renderDataResource` function statics.
|
||||||
|
*/
|
||||||
|
export namespace renderDataResource {
|
||||||
|
/**
|
||||||
|
* The options for the `renderDataResource` function.
|
||||||
|
*/
|
||||||
|
export interface IRenderOptions {
|
||||||
|
/**
|
||||||
|
* The host node for the rendered LaTeX.
|
||||||
|
*/
|
||||||
|
host: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DataResource source to render.
|
||||||
|
*/
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render SVG into a host node.
|
* Render SVG into a host node.
|
||||||
*
|
*
|
||||||
|
|||||||
119
src/sql/parts/notebook/outputs/tableRenderers.ts
Normal file
119
src/sql/parts/notebook/outputs/tableRenderers.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/*-----------------------------------------------------------------------------
|
||||||
|
| Copyright (c) Jupyter Development Team.
|
||||||
|
| Distributed under the terms of the Modified BSD License.
|
||||||
|
|----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||||
|
import { Table } from 'sql/base/browser/ui/table/table';
|
||||||
|
import { textFormatter } from 'sql/parts/grid/services/sharedServices';
|
||||||
|
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||||
|
import { IDataResource } from 'sql/workbench/services/notebook/common/sqlSessionManager';
|
||||||
|
import { escape } from 'sql/base/common/strings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render DataResource as a grid into a host node.
|
||||||
|
*
|
||||||
|
* @params options - The options for rendering.
|
||||||
|
*
|
||||||
|
* @returns A promise which resolves when rendering is complete.
|
||||||
|
*/
|
||||||
|
export function renderDataResource(
|
||||||
|
options: renderDataResource.IRenderOptions
|
||||||
|
): Promise<void> {
|
||||||
|
// Unpack the options.
|
||||||
|
let { host, source } = options;
|
||||||
|
let sourceObject: IDataResource = JSON.parse(source);
|
||||||
|
|
||||||
|
// Before doing anything, avoid re-rendering the table multiple
|
||||||
|
// times (as can be the case when going untrusted -> trusted)
|
||||||
|
while (host.firstChild) {
|
||||||
|
host.removeChild(host.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create the table container
|
||||||
|
let tableContainer = document.createElement('div');
|
||||||
|
tableContainer.className = 'notebook-cellTable';
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 29;
|
||||||
|
const BOTTOM_PADDING_AND_SCROLLBAR = 14;
|
||||||
|
let tableResultsData = new TableDataView();
|
||||||
|
let columns: string[] = sourceObject.schema.fields.map(val => val.name);
|
||||||
|
// Table object requires passed in columns to be of datatype Slick.Column
|
||||||
|
let columnsTransformed = transformColumns(columns);
|
||||||
|
|
||||||
|
// In order to show row numbers, we need to put the row number column
|
||||||
|
// ahead of all of the other columns, and register the plugin below
|
||||||
|
let rowNumberColumn = new RowNumberColumn({ numberOfRows: source.length });
|
||||||
|
columnsTransformed.unshift(rowNumberColumn.getColumnDefinition());
|
||||||
|
|
||||||
|
let transformedData = transformData(sourceObject.data, columns);
|
||||||
|
tableResultsData.push(transformedData);
|
||||||
|
|
||||||
|
let detailTable = new Table(tableContainer, {
|
||||||
|
dataProvider: tableResultsData, columns: columnsTransformed
|
||||||
|
}, {
|
||||||
|
rowHeight: ROW_HEIGHT,
|
||||||
|
forceFitColumns: false,
|
||||||
|
defaultColumnWidth: 120
|
||||||
|
});
|
||||||
|
detailTable.registerPlugin(rowNumberColumn);
|
||||||
|
|
||||||
|
// Need to include column headers and scrollbar, so that's why 1 needs to be added
|
||||||
|
let rowsHeight = (detailTable.grid.getDataLength() + 1) * ROW_HEIGHT + BOTTOM_PADDING_AND_SCROLLBAR;
|
||||||
|
|
||||||
|
// Set the height dynamically if the grid's height is < 500px high; otherwise, set height to 500px
|
||||||
|
tableContainer.style.height = rowsHeight >= 500 ? '500px' : rowsHeight.toString() + 'px';
|
||||||
|
|
||||||
|
host.appendChild(tableContainer);
|
||||||
|
detailTable.resizeCanvas();
|
||||||
|
|
||||||
|
// Return the rendered promise.
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: string[]): { [key: string]: string }[] {
|
||||||
|
return rows.map(row => {
|
||||||
|
let dataWithSchema = {};
|
||||||
|
Object.keys(row).forEach((val, index) => {
|
||||||
|
let displayValue = String(Object.values(row)[index]);
|
||||||
|
dataWithSchema[columns[index]] = {
|
||||||
|
displayValue: displayValue,
|
||||||
|
ariaLabel: escape(displayValue),
|
||||||
|
isNull: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return dataWithSchema;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||||
|
return columns.map(col => {
|
||||||
|
return <Slick.Column<any>>{
|
||||||
|
name: col,
|
||||||
|
id: col,
|
||||||
|
field: col,
|
||||||
|
formatter: textFormatter
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace for the `renderDataResource` function statics.
|
||||||
|
*/
|
||||||
|
export namespace renderDataResource {
|
||||||
|
/**
|
||||||
|
* The options for the `renderDataResource` function.
|
||||||
|
*/
|
||||||
|
export interface IRenderOptions {
|
||||||
|
/**
|
||||||
|
* The host node for the rendered LaTeX.
|
||||||
|
*/
|
||||||
|
host: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DataResource source to render.
|
||||||
|
*/
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import * as renderers from './renderers';
|
import * as renderers from './renderers';
|
||||||
import { IRenderMime } from './common/renderMimeInterfaces';
|
import { IRenderMime } from './common/renderMimeInterfaces';
|
||||||
import { ReadonlyJSONObject } from '../models/jsonext';
|
import { ReadonlyJSONObject } from '../models/jsonext';
|
||||||
|
import * as tableRenderers from 'sql/parts/notebook/outputs/tableRenderers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A common base class for mime renderers.
|
* A common base class for mime renderers.
|
||||||
@@ -346,3 +347,32 @@ export class RenderedJavaScript extends RenderedCommon {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A widget for displaying Data Resource schemas and data.
|
||||||
|
*/
|
||||||
|
export class RenderedDataResource extends RenderedCommon {
|
||||||
|
/**
|
||||||
|
* Construct a new rendered data resource widget.
|
||||||
|
*
|
||||||
|
* @param options - The options for initializing the widget.
|
||||||
|
*/
|
||||||
|
constructor(options: IRenderMime.IRendererOptions) {
|
||||||
|
super(options);
|
||||||
|
this.addClass('jp-RenderedDataResource');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a mime model.
|
||||||
|
*
|
||||||
|
* @param model - The mime model to render.
|
||||||
|
*
|
||||||
|
* @returns A promise which resolves when rendering is complete.
|
||||||
|
*/
|
||||||
|
render(model: IRenderMime.IMimeModel): Promise<void> {
|
||||||
|
return tableRenderers.renderDataResource({
|
||||||
|
host: this.node,
|
||||||
|
source: JSON.stringify(model.data[this.mimeType])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
|||||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||||
|
import { escape } from 'sql/base/common/strings';
|
||||||
|
|
||||||
export const sqlKernel: string = localize('sqlKernel', 'SQL');
|
export const sqlKernel: string = localize('sqlKernel', 'SQL');
|
||||||
export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error");
|
export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error");
|
||||||
@@ -299,8 +300,57 @@ export class SQLFuture extends Disposable implements FutureInternal {
|
|||||||
setIOPubHandler(handler: nb.MessageHandler<nb.IIOPubMessage>): void {
|
setIOPubHandler(handler: nb.MessageHandler<nb.IIOPubMessage>): void {
|
||||||
this._register(this._queryRunner.onBatchEnd(batch => {
|
this._register(this._queryRunner.onBatchEnd(batch => {
|
||||||
this._queryRunner.getQueryRows(0, batch.resultSetSummaries[0].rowCount, 0, 0).then(d => {
|
this._queryRunner.getQueryRows(0, batch.resultSetSummaries[0].rowCount, 0, 0).then(d => {
|
||||||
|
let columns = batch.resultSetSummaries[0].columnInfo;
|
||||||
|
|
||||||
|
let msg: nb.IIOPubMessage = {
|
||||||
|
channel: 'iopub',
|
||||||
|
type: 'iopub',
|
||||||
|
header: <nb.IHeader>{
|
||||||
|
msg_id: undefined,
|
||||||
|
msg_type: 'execute_result'
|
||||||
|
},
|
||||||
|
content: <nb.IExecuteResult>{
|
||||||
|
output_type: 'execute_result',
|
||||||
|
metadata: {},
|
||||||
|
execution_count: 0,
|
||||||
|
data: { 'application/vnd.dataresource+json': this.convertToDataResource(columns, d), 'text/html': this.convertToHtmlTable(columns, d) }
|
||||||
|
},
|
||||||
|
metadata: undefined,
|
||||||
|
parent_header: undefined
|
||||||
|
};
|
||||||
|
handler.handle(msg);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable<boolean>): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable<boolean>): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToDataResource(columns: IDbColumn[], d: QueryExecuteSubsetResult): IDataResource {
|
||||||
|
let columnsResources: IDataResourceSchema[] = [];
|
||||||
|
columns.forEach(column => {
|
||||||
|
columnsResources.push({name: escape(column.columnName)});
|
||||||
|
});
|
||||||
|
let columnsFields: IDataResourceFields = { fields: undefined };
|
||||||
|
columnsFields.fields = columnsResources;
|
||||||
|
return {
|
||||||
|
schema: columnsFields,
|
||||||
|
data: d.resultSubset.rows.map(row => {
|
||||||
|
let rowObject: { [key: string]: any; } = {};
|
||||||
|
row.forEach((val, index) => {
|
||||||
|
rowObject[index] = val.displayValue;
|
||||||
|
});
|
||||||
|
return rowObject;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToHtmlTable(columns: IDbColumn[], d: QueryExecuteSubsetResult): string {
|
||||||
let data: SQLData = {
|
let data: SQLData = {
|
||||||
columns: batch.resultSetSummaries[0].columnInfo.map(c => c.columnName),
|
columns: columns.map(c => escape(c.columnName)),
|
||||||
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
|
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
|
||||||
};
|
};
|
||||||
let table: HTMLTableElement = document.createElement('table');
|
let table: HTMLTableElement = document.createElement('table');
|
||||||
@@ -317,35 +367,24 @@ export class SQLFuture extends Disposable implements FutureInternal {
|
|||||||
let hrow = <HTMLTableRowElement>table.insertRow();
|
let hrow = <HTMLTableRowElement>table.insertRow();
|
||||||
for (let column in data.columns) {
|
for (let column in data.columns) {
|
||||||
let cell = hrow.insertCell();
|
let cell = hrow.insertCell();
|
||||||
cell.innerHTML = data.rows[row][column];
|
cell.innerHTML = escape(data.rows[row][column]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tableHtml = '<table>' + table.innerHTML + '</table>';
|
let tableHtml = '<table>' + table.innerHTML + '</table>';
|
||||||
|
return tableHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let msg: nb.IIOPubMessage = {
|
export interface IDataResource {
|
||||||
channel: 'iopub',
|
schema: IDataResourceFields;
|
||||||
type: 'iopub',
|
data: any[];
|
||||||
header: <nb.IHeader>{
|
|
||||||
msg_id: undefined,
|
|
||||||
msg_type: 'execute_result'
|
|
||||||
},
|
|
||||||
content: <nb.IExecuteResult>{
|
|
||||||
output_type: 'execute_result',
|
|
||||||
metadata: {},
|
|
||||||
execution_count: 0,
|
|
||||||
data: { 'text/html': tableHtml },
|
|
||||||
},
|
|
||||||
metadata: undefined,
|
|
||||||
parent_header: undefined
|
|
||||||
};
|
|
||||||
handler.handle(msg);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
registerMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable<boolean>): void {
|
|
||||||
// no-op
|
export interface IDataResourceFields {
|
||||||
}
|
fields: IDataResourceSchema[];
|
||||||
removeMessageHook(hook: (msg: nb.IIOPubMessage) => boolean | Thenable<boolean>): void {
|
|
||||||
// no-op
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDataResourceSchema {
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user