diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 182345b498..2e35d1dc27 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -4678,6 +4678,9 @@ declare module 'azdata' { export interface ICellOutput { output_type: OutputTypeName; + metadata?: { + azdata_chartOptions?: any; + } } /** diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 7bc0930193..0e1684fb59 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -663,6 +663,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements return NotebookChangeKind.ContentUpdated; case NotebookChangeType.KernelChanged: case NotebookChangeType.TrustChanged: + case NotebookChangeType.CellMetadataUpdated: return NotebookChangeKind.MetadataUpdated; case NotebookChangeType.Saved: return NotebookChangeKind.Save; diff --git a/src/sql/workbench/contrib/charts/browser/chartView.ts b/src/sql/workbench/contrib/charts/browser/chartView.ts index 44d08986bf..73bb15da4f 100644 --- a/src/sql/workbench/contrib/charts/browser/chartView.ts +++ b/src/sql/workbench/contrib/charts/browser/chartView.ts @@ -29,6 +29,7 @@ import * as nls from 'vs/nls'; import { find } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { DbCellValue } from 'azdata'; +import { Event, Emitter } from 'vs/base/common/event'; const insightRegistry = Registry.as(Extensions.InsightContribution); @@ -61,11 +62,10 @@ export class ChartView extends Disposable implements IPanelView { private _state: ChartState; - private options: IInsightOptions = { + private _options: IInsightOptions = { type: ChartType.Bar }; - /** parent container */ private container: HTMLElement; /** container for the options controls */ @@ -82,6 +82,9 @@ export class ChartView extends Disposable implements IPanelView { private optionDisposables: IDisposable[] = []; private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {}; + private readonly _onOptionsChange: Emitter = this._register(new Emitter()); + public readonly onOptionsChange: Event = this._onOptionsChange.event; + constructor( private readonly _renderOptionsInline: boolean, @IContextViewService private _contextViewService: IContextViewService, @@ -111,7 +114,7 @@ export class ChartView extends Disposable implements IPanelView { } const self = this; - this.options = new Proxy(this.options, { + this._options = new Proxy(this._options, { get: function (target, key) { return target[key]; }, @@ -128,12 +131,13 @@ export class ChartView extends Disposable implements IPanelView { } if (change) { - self.taskbar.context = { options: self.options, insight: self.insight ? self.insight.insight : undefined }; + self.taskbar.context = { options: self._options, insight: self.insight ? self.insight.insight : undefined }; if (key === 'type') { self.buildOptions(); } else { self.verifyOptions(); } + self._onOptionsChange.fire(self._options); } return true; @@ -176,7 +180,7 @@ export class ChartView extends Disposable implements IPanelView { if (this._renderOptionsInline) { this.chartingContainer.appendChild(this.optionsControl); } - this.insight = new Insight(this.insightContainer, this.options, this._instantiationService); + this.insight = new Insight(this.insightContainer, this._options, this._instantiationService); } container.appendChild(this.container); @@ -259,11 +263,11 @@ export class ChartView extends Disposable implements IPanelView { DOM.clearNode(this.typeControls); this.updateActionbar(); - ChartOptions[this.options.type].map(o => { + this.getChartTypeOptions().map(o => { this.createOption(o, this.typeControls); }); if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } this.verifyOptions(); } @@ -272,9 +276,9 @@ export class ChartView extends Disposable implements IPanelView { this.updateActionbar(); for (let key in this.optionMap) { if (this.optionMap.hasOwnProperty(key)) { - let option = find(ChartOptions[this.options.type], e => e.configEntry === key); + let option = find(this.getChartTypeOptions(), e => e.configEntry === key); if (option && option.if) { - if (option.if(this.options)) { + if (option.if(this._options)) { DOM.show(this.optionMap[key].element); } else { DOM.hide(this.optionMap[key].element); @@ -284,10 +288,18 @@ export class ChartView extends Disposable implements IPanelView { } } + private getChartTypeOptions(): IChartOption[] { + let options = ChartOptions[this._options.type]; + if (!options) { + throw new Error(nls.localize('charting.unsupportedType', "Chart type '{0}' is not supported.", this._options.type)); + } + return options; + } + private updateActionbar() { let actions: ITaskbarContent[]; if (this.insight && this.insight.isCopyable) { - this.taskbar.context = { insight: this.insight.insight, options: this.options }; + this.taskbar.context = { insight: this.insight.insight, options: this._options }; actions = [ { action: this._createInsightAction }, { action: this._copyAction }, @@ -318,10 +330,10 @@ export class ChartView extends Disposable implements IPanelView { ariaLabel: option.label, checked: value, onChange: () => { - if (this.options[option.configEntry] !== checkbox.checked) { - this.options[option.configEntry] = checkbox.checked; + if (this._options[option.configEntry] !== checkbox.checked) { + this._options[option.configEntry] = checkbox.checked; if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } } } @@ -337,10 +349,10 @@ export class ChartView extends Disposable implements IPanelView { dropdown.select(option.options.indexOf(value)); dropdown.render(optionInput); dropdown.onDidSelect(e => { - if (this.options[option.configEntry] !== option.options[e.index]) { - this.options[option.configEntry] = option.options[e.index]; + if (this._options[option.configEntry] !== option.options[e.index]) { + this._options[option.configEntry] = option.options[e.index]; if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } } }); @@ -356,10 +368,10 @@ export class ChartView extends Disposable implements IPanelView { input.setAriaLabel(option.label); input.value = value || ''; input.onDidChange(e => { - if (this.options[option.configEntry] !== e) { - this.options[option.configEntry] = e; + if (this._options[option.configEntry] !== e) { + this._options[option.configEntry] = e; if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } } }); @@ -375,10 +387,10 @@ export class ChartView extends Disposable implements IPanelView { numberInput.setAriaLabel(option.label); numberInput.value = value || ''; numberInput.onDidChange(e => { - if (this.options[option.configEntry] !== Number(e)) { - this.options[option.configEntry] = Number(e); + if (this._options[option.configEntry] !== Number(e)) { + this._options[option.configEntry] = Number(e); if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } } }); @@ -394,10 +406,10 @@ export class ChartView extends Disposable implements IPanelView { dateInput.setAriaLabel(option.label); dateInput.value = value || ''; dateInput.onDidChange(e => { - if (this.options[option.configEntry] !== e) { - this.options[option.configEntry] = e; + if (this._options[option.configEntry] !== e) { + this._options[option.configEntry] = e; if (this.insight) { - this.insight.options = this.options; + this.insight.options = this._options; } } }); @@ -411,25 +423,33 @@ export class ChartView extends Disposable implements IPanelView { } this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc }; container.appendChild(optionContainer); - this.options[option.configEntry] = value; + this._options[option.configEntry] = value; } public set state(val: ChartState) { this._state = val; - if (this.state.options) { - for (let key in this.state.options) { - if (this.state.options.hasOwnProperty(key) && this.optionMap[key]) { - this.options[key] = this.state.options[key]; - this.optionMap[key].set(this.state.options[key]); - } - } - } - if (this.state.dataId) { - this.chart(this.state.dataId); + this.options = this._state.options; + if (this._state.dataId) { + this.chart(this._state.dataId); } } public get state(): ChartState { return this._state; } + + public get options(): IInsightOptions { + return this._options; + } + + public set options(newOptions: IInsightOptions) { + if (newOptions) { + for (let key in newOptions) { + if (newOptions.hasOwnProperty(key) && this.optionMap[key]) { + this._options[key] = newOptions[key]; + this.optionMap[key].set(newOptions[key]); + } + } + } + } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts index 6a67b6a0ca..29a2128324 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts @@ -172,6 +172,7 @@ export class OutputComponent extends CellView implements OnInit, AfterViewInit { this._componentInstance = componentRef.instance; this._componentInstance.mimeType = mimeType; this._componentInstance.cellModel = this.cellModel; + this._componentInstance.cellOutput = this.cellOutput; this._componentInstance.bundleOptions = options; this._changeref.detectChanges(); let el = componentRef.location.nativeElement; diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index 286c510baf..c45797740a 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -37,6 +37,8 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { ToggleableAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; +import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState'; +import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts'; @Component({ selector: GridOutputComponent.SELECTOR, @@ -49,6 +51,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo private _initialized: boolean = false; private _cellModel: ICellModel; + private _cellOutput: azdata.nb.ICellOutput; private _bundleOptions: MimeModel.IOptions; private _table: DataResourceTable; @@ -79,6 +82,14 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo } } + get cellOutput(): azdata.nb.ICellOutput { + return this._cellOutput; + } + + @Input() set cellOutput(value: azdata.nb.ICellOutput) { + this._cellOutput = value; + } + ngOnInit() { this.renderGrid(); } @@ -90,7 +101,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo if (!this._table) { let source = this._bundleOptions.data[this.mimeType]; let state = new GridTableState(0, 0); - this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel.notebookModel.notebookUri.toString(), state); + this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel, this.cellOutput, state); let outputElement = this.output.nativeElement; outputElement.appendChild(this._table.element); this._register(attachTableStyler(this._table, this.themeService)); @@ -116,7 +127,8 @@ class DataResourceTable extends GridTableBase { private _chartContainer: HTMLElement; constructor(source: IDataResource, - public readonly documentUri: string, + private cellModel: ICellModel, + private cellOutput: azdata.nb.ICellOutput, state: GridTableState, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, @@ -125,14 +137,28 @@ class DataResourceTable extends GridTableBase { @IConfigurationService configurationService: IConfigurationService ) { super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); - this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, this.documentUri); + this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, this.cellModel.notebookModel.notebookUri.toString()); this._chart = this.instantiationService.createInstance(ChartView, false); + + if (!this.cellOutput.metadata) { + this.cellOutput.metadata = {}; + } else if (this.cellOutput.metadata.azdata_chartOptions) { + this._chart.options = this.cellOutput.metadata.azdata_chartOptions as IInsightOptions; + this.updateChartData(this.resultSet.rowCount, this.resultSet.columnInfo.length, this.gridDataProvider); + } + this._chart.onOptionsChange(options => { + this.setChartOptions(options); + }); } - get gridDataProvider(): IGridDataProvider { + public get gridDataProvider(): IGridDataProvider { return this._gridDataProvider; } + public get chartDisplayed(): boolean { + return this.cellOutput.metadata.azdata_chartOptions !== undefined; + } + protected getCurrentActions(): IAction[] { return this.getContextActions(); } @@ -158,9 +184,15 @@ class DataResourceTable extends GridTableBase { if (!this._chartContainer) { this._chartContainer = document.createElement('div'); - this._chartContainer.style.display = 'none'; this._chartContainer.style.width = '100%'; + if (this.cellOutput.metadata.azdata_chartOptions) { + this.tableContainer.style.display = 'none'; + this._chartContainer.style.display = 'inline-block'; + } else { + this._chartContainer.style.display = 'none'; + } + this.element.appendChild(this._chartContainer); this._chart.render(this._chartContainer); } @@ -170,14 +202,26 @@ class DataResourceTable extends GridTableBase { if (this.tableContainer.style.display !== 'none') { this.tableContainer.style.display = 'none'; this._chartContainer.style.display = 'inline-block'; + this.setChartOptions(this._chart.options); } else { - this.tableContainer.style.display = 'inline-block'; this._chartContainer.style.display = 'none'; + this.tableContainer.style.display = 'inline-block'; + this.setChartOptions(undefined); } + this.layout(); } - public get chart(): ChartView { - return this._chart; + public updateChartData(rowCount: number, columnCount: number, gridDataProvider: IGridDataProvider): void { + gridDataProvider.getRowData(0, rowCount).then(result => { + let range = new Slick.Range(0, 0, rowCount - 1, columnCount - 1); + let columns = gridDataProvider.getColumnHeaders(range); + this._chart.setData(result.resultSubset.rows, columns); + }); + } + + private setChartOptions(options: IInsightOptions | undefined) { + this.cellOutput.metadata.azdata_chartOptions = options; + this.cellModel.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated); } } @@ -398,7 +442,7 @@ export class NotebookChartAction extends ToggleableAction { toggleOnClass: NotebookChartAction.SHOWTABLE_ICON, toggleOffLabel: NotebookChartAction.SHOWCHART_LABEL, toggleOffClass: NotebookChartAction.SHOWCHART_ICON, - isOn: false + isOn: resourceTable.chartDisplayed }); } @@ -407,12 +451,8 @@ export class NotebookChartAction extends ToggleableAction { this.toggle(!this.state.isOn); if (this.state.isOn) { let rowCount = context.table.getData().getLength(); - let range = new Slick.Range(0, 0, rowCount - 1, context.table.columns.length - 1); - let columns = context.gridDataProvider.getColumnHeaders(range); - - context.gridDataProvider.getRowData(0, rowCount).then(result => { - this.resourceTable.chart.setData(result.resultSubset.rows, columns); - }); + let columnCount = context.table.columns.length; + this.resourceTable.updateChartData(rowCount, columnCount, context.gridDataProvider); } return true; } diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/mimeRegistry.ts b/src/sql/workbench/contrib/notebook/browser/outputs/mimeRegistry.ts index f631fc650b..2f235c961e 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/mimeRegistry.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/mimeRegistry.ts @@ -10,6 +10,7 @@ import { MimeModel } from 'sql/workbench/services/notebook/browser/outputs/mimem import * as types from 'vs/base/common/types'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { values } from 'vs/base/common/collections'; +import { nb } from 'azdata'; export type FactoryIdentifier = string; @@ -21,6 +22,7 @@ export interface IMimeComponent { bundleOptions: MimeModel.IOptions; mimeType: string; cellModel?: ICellModel; + cellOutput?: nb.ICellOutput; layout(): void; } diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 44fc2e4ffa..5e69c20b28 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -445,7 +445,7 @@ export class CellModel implements ICellModel { } } - private sendChangeToNotebook(change: NotebookChangeType): void { + public sendChangeToNotebook(change: NotebookChangeType): void { if (this._options && this._options.notebook) { this._options.notebook.onCellChange(this, change); } diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index f9b7ce76dd..2e2869f22b 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -487,6 +487,7 @@ export interface ICellModel { readonly onCellModeChanged: Event; modelContentChangedEvent: IModelContentChangedEvent; isEditMode: boolean; + sendChangeToNotebook(change: NotebookChangeType): void; } export interface IModelFactory { diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index 33848c0031..cbfb07f560 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -989,6 +989,7 @@ export class NotebookModel extends Disposable implements INotebookModel { case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellSourceUpdated: case NotebookChangeType.CellInputVisibilityChanged: + case NotebookChangeType.CellMetadataUpdated: changeInfo.isDirty = true; changeInfo.modelContentChangedEvent = cell.modelContentChangedEvent; break; diff --git a/src/sql/workbench/services/notebook/common/contracts.ts b/src/sql/workbench/services/notebook/common/contracts.ts index f3d0ad6b96..4c9cf66717 100644 --- a/src/sql/workbench/services/notebook/common/contracts.ts +++ b/src/sql/workbench/services/notebook/common/contracts.ts @@ -45,5 +45,6 @@ export enum NotebookChangeType { Saved, CellExecuted, CellInputVisibilityChanged, - CellOutputCleared + CellOutputCleared, + CellMetadataUpdated }