Save charting state in SQL notebooks. (#9789)

This commit is contained in:
Cory Rivera
2020-03-30 16:48:28 -07:00
committed by GitHub
parent bb135d6c67
commit d363ea33b5
10 changed files with 123 additions and 53 deletions

3
src/sql/azdata.d.ts vendored
View File

@@ -4678,6 +4678,9 @@ declare module 'azdata' {
export interface ICellOutput { export interface ICellOutput {
output_type: OutputTypeName; output_type: OutputTypeName;
metadata?: {
azdata_chartOptions?: any;
}
} }
/** /**

View File

@@ -663,6 +663,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
return NotebookChangeKind.ContentUpdated; return NotebookChangeKind.ContentUpdated;
case NotebookChangeType.KernelChanged: case NotebookChangeType.KernelChanged:
case NotebookChangeType.TrustChanged: case NotebookChangeType.TrustChanged:
case NotebookChangeType.CellMetadataUpdated:
return NotebookChangeKind.MetadataUpdated; return NotebookChangeKind.MetadataUpdated;
case NotebookChangeType.Saved: case NotebookChangeType.Saved:
return NotebookChangeKind.Save; return NotebookChangeKind.Save;

View File

@@ -29,6 +29,7 @@ import * as nls from 'vs/nls';
import { find } from 'vs/base/common/arrays'; import { find } from 'vs/base/common/arrays';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { DbCellValue } from 'azdata'; import { DbCellValue } from 'azdata';
import { Event, Emitter } from 'vs/base/common/event';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution); const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
@@ -61,11 +62,10 @@ export class ChartView extends Disposable implements IPanelView {
private _state: ChartState; private _state: ChartState;
private options: IInsightOptions = { private _options: IInsightOptions = {
type: ChartType.Bar type: ChartType.Bar
}; };
/** parent container */ /** parent container */
private container: HTMLElement; private container: HTMLElement;
/** container for the options controls */ /** container for the options controls */
@@ -82,6 +82,9 @@ export class ChartView extends Disposable implements IPanelView {
private optionDisposables: IDisposable[] = []; private optionDisposables: IDisposable[] = [];
private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {}; private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {};
private readonly _onOptionsChange: Emitter<IInsightOptions> = this._register(new Emitter<IInsightOptions>());
public readonly onOptionsChange: Event<IInsightOptions> = this._onOptionsChange.event;
constructor( constructor(
private readonly _renderOptionsInline: boolean, private readonly _renderOptionsInline: boolean,
@IContextViewService private _contextViewService: IContextViewService, @IContextViewService private _contextViewService: IContextViewService,
@@ -111,7 +114,7 @@ export class ChartView extends Disposable implements IPanelView {
} }
const self = this; const self = this;
this.options = new Proxy(this.options, { this._options = new Proxy(this._options, {
get: function (target, key) { get: function (target, key) {
return target[key]; return target[key];
}, },
@@ -128,12 +131,13 @@ export class ChartView extends Disposable implements IPanelView {
} }
if (change) { if (change) {
self.taskbar.context = <IChartActionContext>{ options: self.options, insight: self.insight ? self.insight.insight : undefined }; self.taskbar.context = <IChartActionContext>{ options: self._options, insight: self.insight ? self.insight.insight : undefined };
if (key === 'type') { if (key === 'type') {
self.buildOptions(); self.buildOptions();
} else { } else {
self.verifyOptions(); self.verifyOptions();
} }
self._onOptionsChange.fire(self._options);
} }
return true; return true;
@@ -176,7 +180,7 @@ export class ChartView extends Disposable implements IPanelView {
if (this._renderOptionsInline) { if (this._renderOptionsInline) {
this.chartingContainer.appendChild(this.optionsControl); 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); container.appendChild(this.container);
@@ -259,11 +263,11 @@ export class ChartView extends Disposable implements IPanelView {
DOM.clearNode(this.typeControls); DOM.clearNode(this.typeControls);
this.updateActionbar(); this.updateActionbar();
ChartOptions[this.options.type].map(o => { this.getChartTypeOptions().map(o => {
this.createOption(o, this.typeControls); this.createOption(o, this.typeControls);
}); });
if (this.insight) { if (this.insight) {
this.insight.options = this.options; this.insight.options = this._options;
} }
this.verifyOptions(); this.verifyOptions();
} }
@@ -272,9 +276,9 @@ export class ChartView extends Disposable implements IPanelView {
this.updateActionbar(); this.updateActionbar();
for (let key in this.optionMap) { for (let key in this.optionMap) {
if (this.optionMap.hasOwnProperty(key)) { 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 && option.if) {
if (option.if(this.options)) { if (option.if(this._options)) {
DOM.show(this.optionMap[key].element); DOM.show(this.optionMap[key].element);
} else { } else {
DOM.hide(this.optionMap[key].element); 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() { private updateActionbar() {
let actions: ITaskbarContent[]; let actions: ITaskbarContent[];
if (this.insight && this.insight.isCopyable) { 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 = [ actions = [
{ action: this._createInsightAction }, { action: this._createInsightAction },
{ action: this._copyAction }, { action: this._copyAction },
@@ -318,10 +330,10 @@ export class ChartView extends Disposable implements IPanelView {
ariaLabel: option.label, ariaLabel: option.label,
checked: value, checked: value,
onChange: () => { onChange: () => {
if (this.options[option.configEntry] !== checkbox.checked) { if (this._options[option.configEntry] !== checkbox.checked) {
this.options[option.configEntry] = checkbox.checked; this._options[option.configEntry] = checkbox.checked;
if (this.insight) { 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.select(option.options.indexOf(value));
dropdown.render(optionInput); dropdown.render(optionInput);
dropdown.onDidSelect(e => { dropdown.onDidSelect(e => {
if (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]; this._options[option.configEntry] = option.options[e.index];
if (this.insight) { 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.setAriaLabel(option.label);
input.value = value || ''; input.value = value || '';
input.onDidChange(e => { input.onDidChange(e => {
if (this.options[option.configEntry] !== e) { if (this._options[option.configEntry] !== e) {
this.options[option.configEntry] = e; this._options[option.configEntry] = e;
if (this.insight) { 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.setAriaLabel(option.label);
numberInput.value = value || ''; numberInput.value = value || '';
numberInput.onDidChange(e => { numberInput.onDidChange(e => {
if (this.options[option.configEntry] !== Number(e)) { if (this._options[option.configEntry] !== Number(e)) {
this.options[option.configEntry] = Number(e); this._options[option.configEntry] = Number(e);
if (this.insight) { 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.setAriaLabel(option.label);
dateInput.value = value || ''; dateInput.value = value || '';
dateInput.onDidChange(e => { dateInput.onDidChange(e => {
if (this.options[option.configEntry] !== e) { if (this._options[option.configEntry] !== e) {
this.options[option.configEntry] = e; this._options[option.configEntry] = e;
if (this.insight) { 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 }; this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc };
container.appendChild(optionContainer); container.appendChild(optionContainer);
this.options[option.configEntry] = value; this._options[option.configEntry] = value;
} }
public set state(val: ChartState) { public set state(val: ChartState) {
this._state = val; this._state = val;
if (this.state.options) { this.options = this._state.options;
for (let key in this.state.options) { if (this._state.dataId) {
if (this.state.options.hasOwnProperty(key) && this.optionMap[key]) { this.chart(this._state.dataId);
this.options[key] = this.state.options[key];
this.optionMap[key].set(this.state.options[key]);
}
}
}
if (this.state.dataId) {
this.chart(this.state.dataId);
} }
} }
public get state(): ChartState { public get state(): ChartState {
return this._state; 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]);
}
}
}
}
} }

View File

@@ -172,6 +172,7 @@ export class OutputComponent extends CellView implements OnInit, AfterViewInit {
this._componentInstance = componentRef.instance; this._componentInstance = componentRef.instance;
this._componentInstance.mimeType = mimeType; this._componentInstance.mimeType = mimeType;
this._componentInstance.cellModel = this.cellModel; this._componentInstance.cellModel = this.cellModel;
this._componentInstance.cellOutput = this.cellOutput;
this._componentInstance.bundleOptions = options; this._componentInstance.bundleOptions = options;
this._changeref.detectChanges(); this._changeref.detectChanges();
let el = <HTMLElement>componentRef.location.nativeElement; let el = <HTMLElement>componentRef.location.nativeElement;

View File

@@ -37,6 +37,8 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo
import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView'; import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView';
import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { ToggleableAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; 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({ @Component({
selector: GridOutputComponent.SELECTOR, selector: GridOutputComponent.SELECTOR,
@@ -49,6 +51,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo
private _initialized: boolean = false; private _initialized: boolean = false;
private _cellModel: ICellModel; private _cellModel: ICellModel;
private _cellOutput: azdata.nb.ICellOutput;
private _bundleOptions: MimeModel.IOptions; private _bundleOptions: MimeModel.IOptions;
private _table: DataResourceTable; 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() { ngOnInit() {
this.renderGrid(); this.renderGrid();
} }
@@ -90,7 +101,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo
if (!this._table) { if (!this._table) {
let source = <IDataResource><any>this._bundleOptions.data[this.mimeType]; let source = <IDataResource><any>this._bundleOptions.data[this.mimeType];
let state = new GridTableState(0, 0); 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 = <HTMLElement>this.output.nativeElement; let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.appendChild(this._table.element); outputElement.appendChild(this._table.element);
this._register(attachTableStyler(this._table, this.themeService)); this._register(attachTableStyler(this._table, this.themeService));
@@ -116,7 +127,8 @@ class DataResourceTable extends GridTableBase<any> {
private _chartContainer: HTMLElement; private _chartContainer: HTMLElement;
constructor(source: IDataResource, constructor(source: IDataResource,
public readonly documentUri: string, private cellModel: ICellModel,
private cellOutput: azdata.nb.ICellOutput,
state: GridTableState, state: GridTableState,
@IContextMenuService contextMenuService: IContextMenuService, @IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService protected instantiationService: IInstantiationService, @IInstantiationService protected instantiationService: IInstantiationService,
@@ -125,14 +137,28 @@ class DataResourceTable extends GridTableBase<any> {
@IConfigurationService configurationService: IConfigurationService @IConfigurationService configurationService: IConfigurationService
) { ) {
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); 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); 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; return this._gridDataProvider;
} }
public get chartDisplayed(): boolean {
return this.cellOutput.metadata.azdata_chartOptions !== undefined;
}
protected getCurrentActions(): IAction[] { protected getCurrentActions(): IAction[] {
return this.getContextActions(); return this.getContextActions();
} }
@@ -158,9 +184,15 @@ class DataResourceTable extends GridTableBase<any> {
if (!this._chartContainer) { if (!this._chartContainer) {
this._chartContainer = document.createElement('div'); this._chartContainer = document.createElement('div');
this._chartContainer.style.display = 'none';
this._chartContainer.style.width = '100%'; 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.element.appendChild(this._chartContainer);
this._chart.render(this._chartContainer); this._chart.render(this._chartContainer);
} }
@@ -170,14 +202,26 @@ class DataResourceTable extends GridTableBase<any> {
if (this.tableContainer.style.display !== 'none') { if (this.tableContainer.style.display !== 'none') {
this.tableContainer.style.display = 'none'; this.tableContainer.style.display = 'none';
this._chartContainer.style.display = 'inline-block'; this._chartContainer.style.display = 'inline-block';
this.setChartOptions(this._chart.options);
} else { } else {
this.tableContainer.style.display = 'inline-block';
this._chartContainer.style.display = 'none'; this._chartContainer.style.display = 'none';
this.tableContainer.style.display = 'inline-block';
this.setChartOptions(undefined);
} }
this.layout();
} }
public get chart(): ChartView { public updateChartData(rowCount: number, columnCount: number, gridDataProvider: IGridDataProvider): void {
return this._chart; 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, toggleOnClass: NotebookChartAction.SHOWTABLE_ICON,
toggleOffLabel: NotebookChartAction.SHOWCHART_LABEL, toggleOffLabel: NotebookChartAction.SHOWCHART_LABEL,
toggleOffClass: NotebookChartAction.SHOWCHART_ICON, toggleOffClass: NotebookChartAction.SHOWCHART_ICON,
isOn: false isOn: resourceTable.chartDisplayed
}); });
} }
@@ -407,12 +451,8 @@ export class NotebookChartAction extends ToggleableAction {
this.toggle(!this.state.isOn); this.toggle(!this.state.isOn);
if (this.state.isOn) { if (this.state.isOn) {
let rowCount = context.table.getData().getLength(); let rowCount = context.table.getData().getLength();
let range = new Slick.Range(0, 0, rowCount - 1, context.table.columns.length - 1); let columnCount = context.table.columns.length;
let columns = context.gridDataProvider.getColumnHeaders(range); this.resourceTable.updateChartData(rowCount, columnCount, context.gridDataProvider);
context.gridDataProvider.getRowData(0, rowCount).then(result => {
this.resourceTable.chart.setData(result.resultSubset.rows, columns);
});
} }
return true; return true;
} }

View File

@@ -10,6 +10,7 @@ import { MimeModel } from 'sql/workbench/services/notebook/browser/outputs/mimem
import * as types from 'vs/base/common/types'; import * as types from 'vs/base/common/types';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { values } from 'vs/base/common/collections'; import { values } from 'vs/base/common/collections';
import { nb } from 'azdata';
export type FactoryIdentifier = string; export type FactoryIdentifier = string;
@@ -21,6 +22,7 @@ export interface IMimeComponent {
bundleOptions: MimeModel.IOptions; bundleOptions: MimeModel.IOptions;
mimeType: string; mimeType: string;
cellModel?: ICellModel; cellModel?: ICellModel;
cellOutput?: nb.ICellOutput;
layout(): void; layout(): void;
} }

View File

@@ -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) { if (this._options && this._options.notebook) {
this._options.notebook.onCellChange(this, change); this._options.notebook.onCellChange(this, change);
} }

View File

@@ -487,6 +487,7 @@ export interface ICellModel {
readonly onCellModeChanged: Event<boolean>; readonly onCellModeChanged: Event<boolean>;
modelContentChangedEvent: IModelContentChangedEvent; modelContentChangedEvent: IModelContentChangedEvent;
isEditMode: boolean; isEditMode: boolean;
sendChangeToNotebook(change: NotebookChangeType): void;
} }
export interface IModelFactory { export interface IModelFactory {

View File

@@ -989,6 +989,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
case NotebookChangeType.CellOutputUpdated: case NotebookChangeType.CellOutputUpdated:
case NotebookChangeType.CellSourceUpdated: case NotebookChangeType.CellSourceUpdated:
case NotebookChangeType.CellInputVisibilityChanged: case NotebookChangeType.CellInputVisibilityChanged:
case NotebookChangeType.CellMetadataUpdated:
changeInfo.isDirty = true; changeInfo.isDirty = true;
changeInfo.modelContentChangedEvent = cell.modelContentChangedEvent; changeInfo.modelContentChangedEvent = cell.modelContentChangedEvent;
break; break;

View File

@@ -45,5 +45,6 @@ export enum NotebookChangeType {
Saved, Saved,
CellExecuted, CellExecuted,
CellInputVisibilityChanged, CellInputVisibilityChanged,
CellOutputCleared CellOutputCleared,
CellMetadataUpdated
} }