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:
Chris LaFreniere
2019-01-30 16:56:14 -08:00
committed by GitHub
parent 0e6f2eb1cd
commit 0dab7f02ed
5 changed files with 243 additions and 24 deletions

View File

@@ -80,6 +80,16 @@ export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
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.
*/
@@ -90,5 +100,6 @@ export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFacto
svgRendererFactory,
imageRendererFactory,
javaScriptRendererFactory,
textRendererFactory
textRendererFactory,
dataResourceRendererFactory
];

View File

@@ -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.
*

View 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;
}
}

View File

@@ -7,6 +7,7 @@
import * as renderers from './renderers';
import { IRenderMime } from './common/renderMimeInterfaces';
import { ReadonlyJSONObject } from '../models/jsonext';
import * as tableRenderers from 'sql/parts/notebook/outputs/tableRenderers';
/**
* A common base class for mime renderers.
@@ -345,4 +346,33 @@ export class RenderedJavaScript extends RenderedCommon {
source: 'JavaScript output is disabled in Notebooks'
});
}
}
/**
* 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])
});
}
}

View File

@@ -17,6 +17,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { escape } from 'sql/base/common/strings';
export const sqlKernel: string = localize('sqlKernel', 'SQL');
export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error");
@@ -299,28 +300,7 @@ export class SQLFuture extends Disposable implements FutureInternal {
setIOPubHandler(handler: nb.MessageHandler<nb.IIOPubMessage>): void {
this._register(this._queryRunner.onBatchEnd(batch => {
this._queryRunner.getQueryRows(0, batch.resultSetSummaries[0].rowCount, 0, 0).then(d => {
let data: SQLData = {
columns: batch.resultSetSummaries[0].columnInfo.map(c => c.columnName),
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
};
let table: HTMLTableElement = document.createElement('table');
table.createTHead();
table.createTBody();
let hrow = <HTMLTableRowElement>table.insertRow();
// headers
for (let column of data.columns) {
let cell = hrow.insertCell();
cell.innerHTML = column;
}
for (let row in data.rows) {
let hrow = <HTMLTableRowElement>table.insertRow();
for (let column in data.columns) {
let cell = hrow.insertCell();
cell.innerHTML = data.rows[row][column];
}
}
let tableHtml = '<table>' + table.innerHTML + '</table>';
let columns = batch.resultSetSummaries[0].columnInfo;
let msg: nb.IIOPubMessage = {
channel: 'iopub',
@@ -333,7 +313,7 @@ export class SQLFuture extends Disposable implements FutureInternal {
output_type: 'execute_result',
metadata: {},
execution_count: 0,
data: { 'text/html': tableHtml },
data: { 'application/vnd.dataresource+json': this.convertToDataResource(columns, d), 'text/html': this.convertToHtmlTable(columns, d) }
},
metadata: undefined,
parent_header: undefined
@@ -348,4 +328,63 @@ export class SQLFuture extends Disposable implements FutureInternal {
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 = {
columns: columns.map(c => escape(c.columnName)),
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
};
let table: HTMLTableElement = document.createElement('table');
table.createTHead();
table.createTBody();
let hrow = <HTMLTableRowElement>table.insertRow();
// headers
for (let column of data.columns) {
let cell = hrow.insertCell();
cell.innerHTML = column;
}
for (let row in data.rows) {
let hrow = <HTMLTableRowElement>table.insertRow();
for (let column in data.columns) {
let cell = hrow.insertCell();
cell.innerHTML = escape(data.rows[row][column]);
}
}
let tableHtml = '<table>' + table.innerHTML + '</table>';
return tableHtml;
}
}
export interface IDataResource {
schema: IDataResourceFields;
data: any[];
}
export interface IDataResourceFields {
fields: IDataResourceSchema[];
}
export interface IDataResourceSchema {
name: string;
type?: string;
}