From 0dab7f02edd7e5a0efe96003ba43bcc8c549211e Mon Sep 17 00:00:00 2001 From: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com> Date: Wed, 30 Jan 2019 16:56:14 -0800 Subject: [PATCH] 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 --- src/sql/parts/notebook/outputs/factories.ts | 13 +- src/sql/parts/notebook/outputs/renderers.ts | 20 +++ .../parts/notebook/outputs/tableRenderers.ts | 119 ++++++++++++++++++ src/sql/parts/notebook/outputs/widgets.ts | 30 +++++ .../notebook/common/sqlSessionManager.ts | 85 +++++++++---- 5 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 src/sql/parts/notebook/outputs/tableRenderers.ts diff --git a/src/sql/parts/notebook/outputs/factories.ts b/src/sql/parts/notebook/outputs/factories.ts index 145f2357a7..797d96806a 100644 --- a/src/sql/parts/notebook/outputs/factories.ts +++ b/src/sql/parts/notebook/outputs/factories.ts @@ -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 { + // 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[] { + return columns.map(col => { + return >{ + 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; + } +} diff --git a/src/sql/parts/notebook/outputs/widgets.ts b/src/sql/parts/notebook/outputs/widgets.ts index 0d580a8f5a..db71f6e67c 100644 --- a/src/sql/parts/notebook/outputs/widgets.ts +++ b/src/sql/parts/notebook/outputs/widgets.ts @@ -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 { + return tableRenderers.renderDataResource({ + host: this.node, + source: JSON.stringify(model.data[this.mimeType]) + }); + } } \ No newline at end of file diff --git a/src/sql/workbench/services/notebook/common/sqlSessionManager.ts b/src/sql/workbench/services/notebook/common/sqlSessionManager.ts index 2d1ed3efa3..017e631df1 100644 --- a/src/sql/workbench/services/notebook/common/sqlSessionManager.ts +++ b/src/sql/workbench/services/notebook/common/sqlSessionManager.ts @@ -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): 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 = table.insertRow(); - // headers - for (let column of data.columns) { - let cell = hrow.insertCell(); - cell.innerHTML = column; - } - - for (let row in data.rows) { - let hrow = table.insertRow(); - for (let column in data.columns) { - let cell = hrow.insertCell(); - cell.innerHTML = data.rows[row][column]; - } - } - let tableHtml = '' + table.innerHTML + '
'; + 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): 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 = table.insertRow(); + // headers + for (let column of data.columns) { + let cell = hrow.insertCell(); + cell.innerHTML = column; + } + + for (let row in data.rows) { + let hrow = table.insertRow(); + for (let column in data.columns) { + let cell = hrow.insertCell(); + cell.innerHTML = escape(data.rows[row][column]); + } + } + let tableHtml = '' + table.innerHTML + '
'; + return tableHtml; + } +} + +export interface IDataResource { + schema: IDataResourceFields; + data: any[]; +} + +export interface IDataResourceFields { + fields: IDataResourceSchema[]; +} + +export interface IDataResourceSchema { + name: string; + type?: string; } \ No newline at end of file