diff --git a/src/sql/workbench/contrib/charts/browser/chartView.ts b/src/sql/workbench/contrib/charts/browser/chartView.ts index 8a9f9f4db8..a67365e4ba 100644 --- a/src/sql/workbench/contrib/charts/browser/chartView.ts +++ b/src/sql/workbench/contrib/charts/browser/chartView.ts @@ -28,6 +28,7 @@ import { ChartState } from 'sql/workbench/common/editor/query/chartState'; 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'; const insightRegistry = Registry.as(Extensions.InsightContribution); @@ -198,6 +199,22 @@ export class ChartView extends Disposable implements IPanelView { this.shouldGraph(); } + public setData(rows: DbCellValue[][], columns: string[]): void { + if (!rows) { + this._data = { columns: [], rows: [] }; + this._notificationService.error(nls.localize('charting.failedToGetRows', "Failed to get rows for the dataset to chart.")); + } else { + this._data = { + columns: columns, + rows: rows.map(r => r.map(c => c.displayValue)) + }; + } + + if (this.insight) { + this.insight.data = this._data; + } + } + private shouldGraph() { // Check if we have the necessary information if (this._currentData && this._queryRunner) { @@ -207,18 +224,9 @@ export class ChartView extends Disposable implements IPanelView { let summary = batch.resultSetSummaries[this._currentData.resultId]; if (summary) { this._queryRunner.getQueryRows(0, summary.rowCount, this._currentData.batchId, this._currentData.resultId).then(d => { - if (!d.resultSubset.rows) { // be defensive against this - this._data = { columns: [], rows: [] }; - this._notificationService.error(nls.localize('charting.failedToGetRows', "Failed to get rows for the dataset to chart.")); - } else { - this._data = { - columns: summary.columnInfo.map(c => c.columnName), - rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue)) - }; - } - if (this.insight) { - this.insight.data = this._data; - } + let rows = d.resultSubset.rows; + let columns = summary.columnInfo.map(c => c.columnName); + this.setData(rows, columns); }); } } diff --git a/src/sql/workbench/contrib/charts/browser/media/chartView.css b/src/sql/workbench/contrib/charts/browser/media/chartView.css index 56c6e01b62..26f21b895d 100644 --- a/src/sql/workbench/contrib/charts/browser/media/chartView.css +++ b/src/sql/workbench/contrib/charts/browser/media/chartView.css @@ -8,6 +8,7 @@ width: 100%; display: flex; flex-direction: column; + overflow: scroll; } .actionbar-container { @@ -25,6 +26,6 @@ } .options-container { - width: 250px; + min-width: 250px; padding-right: 10px; } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts index ae305e27d3..681d037b8a 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts @@ -247,16 +247,6 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf `); } - // Styling for all links in notebooks - const linkForeground = theme.getColor(textLinkForeground); - if (linkForeground) { - collector.addRule(` - .notebookEditor .scrollable a { - color: ${linkForeground}; - } - `); - } - // Styling for tables in notebooks const borderColor = theme.getColor(SIDE_BAR_BACKGROUND); if (borderColor) { @@ -269,6 +259,7 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf // Styling for markdown cells & links in notebooks. // This matches the values used by default in all web views + const linkForeground = theme.getColor(textLinkForeground); if (linkForeground) { collector.addRule(` .notebookEditor a:link { 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 92a8bd79b0..b6e501bbf7 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -28,16 +28,19 @@ import { GridTableState } from 'sql/workbench/common/editor/query/gridPanelState import { GridTableBase } from 'sql/workbench/contrib/query/browser/gridPanel'; import { getErrorMessage } from 'vs/base/common/errors'; import { ISerializationService, SerializeDataParams } from 'sql/platform/serialization/common/serializationService'; -import { SaveResultAction } from 'sql/workbench/contrib/query/browser/actions'; +import { SaveResultAction, IGridActionContext } from 'sql/workbench/contrib/query/browser/actions'; import { ResultSerializer, SaveResultsResponse, SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { values } from 'vs/base/common/collections'; import { assign } from 'vs/base/common/objects'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +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'; @Component({ selector: GridOutputComponent.SELECTOR, - template: `
` + template: `
` }) export class GridOutputComponent extends AngularDisposable implements IMimeComponent, OnInit { public static readonly SELECTOR: string = 'grid-output'; @@ -48,7 +51,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo private _cellModel: ICellModel; private _bundleOptions: MimeModel.IOptions; private _table: DataResourceTable; - private _hover: boolean; + constructor( @Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(IThemeService) private readonly themeService: IThemeService @@ -76,14 +79,6 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo } } - @Input() set hover(value: boolean) { - // only reaction on hover changes - if (this._hover !== value) { - this.toggleActionbar(value); - this._hover = value; - } - } - ngOnInit() { this.renderGrid(); } @@ -100,8 +95,7 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo outputElement.appendChild(this._table.element); this._register(attachTableStyler(this._table, this.themeService)); this.layout(); - // By default, do not show the actions - this.toggleActionbar(false); + this._table.onAdd(); this._initialized = true; } @@ -113,36 +107,26 @@ export class GridOutputComponent extends AngularDisposable implements IMimeCompo this._table.layout(maxSize, undefined, ActionsOrientation.HORIZONTAL); } } - - private toggleActionbar(visible: boolean) { - let outputElement = this.output.nativeElement; - let actionsContainers: HTMLElement[] = Array.prototype.slice.call(outputElement.getElementsByClassName('actions-container')); - if (actionsContainers && actionsContainers.length) { - if (visible) { - actionsContainers.forEach(container => container.style.visibility = 'visible'); - } else { - actionsContainers.forEach(container => container.style.visibility = 'hidden'); - } - } - } } class DataResourceTable extends GridTableBase { private _gridDataProvider: IGridDataProvider; + private _chart: ChartView; + private _chartContainer: HTMLElement; constructor(source: IDataResource, - documentUri: string, + public readonly documentUri: string, state: GridTableState, @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService editorService: IEditorService, @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, - @IConfigurationService configurationService: IConfigurationService, - @ISerializationService private _serializationService: ISerializationService + @IConfigurationService configurationService: IConfigurationService ) { super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); - this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri); + this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, this.documentUri); + this._chart = this.instantiationService.createInstance(ChartView); } get gridDataProvider(): IGridDataProvider { @@ -154,14 +138,12 @@ class DataResourceTable extends GridTableBase { } protected getContextActions(): IAction[] { - if (!this._serializationService.hasProvider()) { - return []; - } return [ this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), + this.instantiationService.createInstance(NotebookChartAction, this) ]; } @@ -170,6 +152,33 @@ class DataResourceTable extends GridTableBase { // When we add this back in, we should update this calculation return Math.max(this.maxSize, /* ACTIONBAR_HEIGHT + BOTTOM_PADDING */ 0); } + + public layout(size?: number, orientation?: Orientation, actionsOrientation?: ActionsOrientation): void { + super.layout(size, orientation, actionsOrientation); + + if (!this._chartContainer) { + this._chartContainer = document.createElement('div'); + this._chartContainer.style.display = 'none'; + this._chartContainer.style.width = '100%'; + + this.element.appendChild(this._chartContainer); + this._chart.render(this._chartContainer); + } + } + + public toggleChartVisibility(): void { + if (this.tableContainer.style.display !== 'none') { + this.tableContainer.style.display = 'none'; + this._chartContainer.style.display = 'inline-block'; + } else { + this.tableContainer.style.display = 'inline-block'; + this._chartContainer.style.display = 'none'; + } + } + + public get chart(): ChartView { + return this._chart; + } } class DataResourceDataProvider implements IGridDataProvider { @@ -252,7 +261,6 @@ class DataResourceDataProvider implements IGridDataProvider { return this._serializationService.hasProvider(); } - serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable { let serializer = this._instantiationService.createInstance(ResultSerializer); return serializer.handleSerialization(this.documentUri, format, (filePath) => this.doSerialize(serializer, filePath, format, selection)); @@ -375,3 +383,37 @@ class SimpleDbColumn implements azdata.IDbColumn { udtAssemblyQualifiedName: string; dataTypeName: string; } + +export class NotebookChartAction extends ToggleableAction { + public static ID = 'notebook.showChart'; + public static SHOWCHART_LABEL = localize('notebook.showChart', "Show chart"); + public static SHOWCHART_ICON = 'viewChart'; + + public static HIDECHART_LABEL = localize('notebook.hideChart', "Hide chart"); + public static HIDECHART_ICON = 'close'; + + constructor(private resourceTable: DataResourceTable) { + super(NotebookChartAction.ID, { + toggleOnLabel: NotebookChartAction.HIDECHART_LABEL, + toggleOnClass: NotebookChartAction.HIDECHART_ICON, + toggleOffLabel: NotebookChartAction.SHOWCHART_LABEL, + toggleOffClass: NotebookChartAction.SHOWCHART_ICON, + isOn: false + }); + } + + public async run(context: IGridActionContext): Promise { + this.resourceTable.toggleChartVisibility(); + 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); + }); + } + return true; + } +} diff --git a/src/sql/workbench/contrib/query/browser/actions.ts b/src/sql/workbench/contrib/query/browser/actions.ts index a1fc492869..bc29052b11 100644 --- a/src/sql/workbench/contrib/query/browser/actions.ts +++ b/src/sql/workbench/contrib/query/browser/actions.ts @@ -78,6 +78,7 @@ export class SaveResultAction extends Action { public async run(context: IGridActionContext): Promise { if (!context.gridDataProvider.canSerialize) { this.notificationService.warn(localize('saveToFileNotSupported', "Save to file is not supported by the backing data source")); + return false; } try { await context.gridDataProvider.serializeResults(this.format, mapForNumberColumn(context.selection)); diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index d75c87a591..0cef189662 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -336,6 +336,7 @@ export abstract class GridTableBase extends Disposable implements IView { public id = generateUuid(); readonly element: HTMLElement = this.container; + protected tableContainer: HTMLElement; private _state: GridTableState; @@ -366,7 +367,6 @@ export abstract class GridTableBase extends Disposable implements IView { this.state = state; this.container.style.width = '100%'; this.container.style.height = '100%'; - this.container.className = 'grid-panel'; this.columns = this.resultSet.columnInfo.map((c, i) => { let isLinked = c.isXml || c.isJson; @@ -435,11 +435,12 @@ export abstract class GridTableBase extends Disposable implements IView { this.container.appendChild(actionBarContainer); } - let tableContainer = document.createElement('div'); - tableContainer.style.display = 'inline-block'; - tableContainer.style.width = `calc(100% - ${ACTIONBAR_WIDTH}px)`; + this.tableContainer = document.createElement('div'); + this.tableContainer.className = 'grid-panel'; + this.tableContainer.style.display = 'inline-block'; + this.tableContainer.style.width = `calc(100% - ${ACTIONBAR_WIDTH}px)`; - this.container.appendChild(tableContainer); + this.container.appendChild(this.tableContainer); let collection = new VirtualizedCollection( 50, @@ -463,7 +464,7 @@ export abstract class GridTableBase extends Disposable implements IView { defaultColumnWidth: 120 }; this.dataProvider = new AsyncDataProvider(collection); - this.table = this._register(new Table(tableContainer, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions)); + this.table = this._register(new Table(this.tableContainer, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions)); this.table.setTableTitle(localize('resultsGrid', "Results grid")); this.table.setSelectionModel(this.selectionModel); this.table.registerPlugin(new MouseWheelSupport());