diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index b8431afa67..6fc2217d6b 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -73,6 +73,7 @@ export enum TelemetryAction { RunQuery = 'RunQuery', RunQueryStatement = 'RunQueryStatement', RunQueryString = 'RunQueryString', + ShowChart = 'ShowChart', StopAgentJob = 'StopAgentJob', WizardPagesNavigation = 'WizardPagesNavigation' } @@ -82,3 +83,7 @@ export enum NbTelemetryAction { RunAll = 'RunNotebook' } +export enum TelemetryPropertyName { + ChartMaxRowCountExceeded = 'chartMaxRowCountExceeded' +} + diff --git a/src/sql/workbench/contrib/charts/browser/chartView.ts b/src/sql/workbench/contrib/charts/browser/chartView.ts index 51fe2d9304..894e6b8878 100644 --- a/src/sql/workbench/contrib/charts/browser/chartView.ts +++ b/src/sql/workbench/contrib/charts/browser/chartView.ts @@ -2,33 +2,34 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import 'vs/css!./media/chartView'; - -import { IPanelView } from 'sql/base/browser/ui/panel/panel'; -import { Insight } from './insight'; -import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; -import { ICellValue, VisualizationOptions } from 'sql/workbench/services/query/common/query'; -import { ChartOptions, IChartOption, ControlType } from './chartOptions'; -import { Extensions, IInsightRegistry, IInsightData } from 'sql/platform/dashboard/browser/insightRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; -import * as DOM from 'vs/base/browser/dom'; -import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { isUndefinedOrNull } from 'vs/base/common/types'; -import { CreateInsightAction, CopyAction, SaveImageAction, IChartActionContext, ConfigureChartAction } from 'sql/workbench/contrib/charts/browser/actions'; -import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar'; import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox'; -import { IInsightOptions, ChartType, InsightType } from 'sql/workbench/contrib/charts/common/interfaces'; +import { IPanelView } from 'sql/base/browser/ui/panel/panel'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; +import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; +import { Extensions, IInsightData, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry'; import { ChartState } from 'sql/workbench/common/editor/query/chartState'; +import { ConfigureChartAction, CopyAction, CreateInsightAction, IChartActionContext, SaveImageAction } from 'sql/workbench/contrib/charts/browser/actions'; +import { getChartMaxRowCount } from 'sql/workbench/contrib/charts/browser/utils'; +import { ChartType, IInsightOptions, InsightType } from 'sql/workbench/contrib/charts/common/interfaces'; +import { ICellValue, VisualizationOptions } from 'sql/workbench/services/query/common/query'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import * as DOM from 'vs/base/browser/dom'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { isUndefinedOrNull } from 'vs/base/common/types'; import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ChartOptions, ControlType, IChartOption } from './chartOptions'; +import { Insight } from './insight'; + const insightRegistry = Registry.as(Extensions.InsightContribution); @@ -89,7 +90,8 @@ export class ChartView extends Disposable implements IPanelView { @IContextViewService private _contextViewService: IContextViewService, @IThemeService private _themeService: IThemeService, @IInstantiationService private _instantiationService: IInstantiationService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { super(); this.taskbarContainer = DOM.$('div.taskbar-container'); @@ -195,7 +197,7 @@ export class ChartView extends Disposable implements IPanelView { public chart(dataId: { batchId: number, resultId: number }) { this.state.dataId = dataId; this._currentData = dataId; - this.shouldGraph(); + this.fetchData(); } layout(dimension: DOM.Dimension): void { @@ -209,7 +211,7 @@ export class ChartView extends Disposable implements IPanelView { public set queryRunner(runner: QueryRunner) { this._queryRunner = runner; - this.shouldGraph(); + this.fetchData(); } public setData(rows: ICellValue[][], columns: string[]): void { @@ -228,7 +230,7 @@ export class ChartView extends Disposable implements IPanelView { } } - private shouldGraph() { + private fetchData(): void { // Check if we have the necessary information if (this._currentData && this._queryRunner) { // check if we are being asked to graph something that is available @@ -236,7 +238,7 @@ export class ChartView extends Disposable implements IPanelView { if (batch) { let summary = batch.resultSetSummaries[this._currentData.resultId]; if (summary) { - this._queryRunner.getQueryRows(0, summary.rowCount, this._currentData.batchId, this._currentData.resultId).then(d => { + this._queryRunner.getQueryRows(0, Math.min(getChartMaxRowCount(this._configurationService), summary.rowCount), this._currentData.batchId, this._currentData.resultId).then(d => { let rows = d.rows; let columns = summary.columnInfo.map(c => c.columnName); this.setData(rows, columns); diff --git a/src/sql/workbench/contrib/charts/browser/charts.contribution.ts b/src/sql/workbench/contrib/charts/browser/charts.contribution.ts new file mode 100644 index 0000000000..4a57471915 --- /dev/null +++ b/src/sql/workbench/contrib/charts/browser/charts.contribution.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Extensions, IConfigurationRegistry, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import * as nls from 'vs/nls'; + +const configurationRegistry = Registry.as(Extensions.Configuration); + +const chartsConfiguration: IConfigurationNode = { + id: 'builtinCharts', + type: 'object', + title: nls.localize('builtinChartsConfigurationTitle', "Built-in Charts"), + properties: { + 'builtinCharts.maxRowCount': { + type: 'number', + default: 300, + description: nls.localize('builtinCharts.maxRowCountDescription', "The maximum number of rows for charts to display. Warning: increasing this may impact performance.") + } + } +}; + +configurationRegistry.registerConfiguration(chartsConfiguration); diff --git a/src/sql/workbench/contrib/charts/browser/interfaces.ts b/src/sql/workbench/contrib/charts/browser/interfaces.ts index 1046c51a9f..9ba076fc5e 100644 --- a/src/sql/workbench/contrib/charts/browser/interfaces.ts +++ b/src/sql/workbench/contrib/charts/browser/interfaces.ts @@ -46,3 +46,7 @@ export interface IInsightCtor { new (container: HTMLElement, options: IInsightOptions, ...services: Services): IInsight; readonly types: Array; } + +export interface IChartsConfiguration { + readonly maxRowCount: number; +} diff --git a/src/sql/workbench/contrib/charts/browser/utils.ts b/src/sql/workbench/contrib/charts/browser/utils.ts new file mode 100644 index 0000000000..a81559c525 --- /dev/null +++ b/src/sql/workbench/contrib/charts/browser/utils.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChartsConfiguration } from 'sql/workbench/contrib/charts/browser/interfaces'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +/** + * Gets the max allowed row count for chart rendering. + */ +export function getChartMaxRowCount(configurationService: IConfigurationService): number { + return configurationService.getValue('builtinCharts').maxRowCount; +} + +/** + * Show a toast notification about the max row count for chart has exceeded. + */ +export function notifyMaxRowCountExceeded(storageService: IStorageService, notificationService: INotificationService, configurationService: IConfigurationService): void { + const storageKey = 'charts/ignoreMaxRowCountExceededNotification'; + if (!storageService.getBoolean(storageKey, StorageScope.GLOBAL, false)) { + notificationService.prompt(Severity.Info, + nls.localize('charts.maxAllowedRowsExceeded', "Maximum row count for built-in charts has been exceeded, only the first {0} rows are used. To configure the value, you can open user settings and search for: 'builtinCharts.maxRowCount'.", getChartMaxRowCount(configurationService)), + [{ + label: nls.localize('charts.neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => { + storageService.store(storageKey, true, StorageScope.GLOBAL, StorageTarget.MACHINE); + } + }]); + } +} diff --git a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts index 993cf07c97..b0852e5e93 100644 --- a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts +++ b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts @@ -11,6 +11,7 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; suite('Chart View', () => { test('initializes without error', () => { @@ -40,6 +41,7 @@ function createChartView(isQueryEditorChart: boolean): ChartView { const themeService = new TestThemeService(); const instantiationService = new TestInstantiationService(); const notificationService = new TestNotificationService(); + const configurationService = new TestConfigurationService(); instantiationService.stub(IThemeService, themeService); - return new ChartView(isQueryEditorChart, contextViewService, themeService, instantiationService, notificationService); + return new ChartView(isQueryEditorChart, contextViewService, themeService, instantiationService, notificationService, configurationService); } 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 51dd9c6d47..3326c204b0 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -44,6 +44,11 @@ import { assign } from 'vs/base/common/objects'; import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell'; import { equals } from 'vs/base/common/arrays'; import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { getChartMaxRowCount, notifyMaxRowCountExceeded } from 'sql/workbench/contrib/charts/browser/utils'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; + @Component({ selector: GridOutputComponent.SELECTOR, template: `
` @@ -284,8 +289,9 @@ class DataResourceTable extends GridTableBase { public updateChartData(rowCount: number, columnCount: number, gridDataProvider: IGridDataProvider): void { if (this.chartDisplayed) { - gridDataProvider.getRowData(0, rowCount).then(result => { - let range = new Slick.Range(0, 0, rowCount - 1, columnCount - 1); + const actualRowCount = Math.min(getChartMaxRowCount(this.configurationService), rowCount); + gridDataProvider.getRowData(0, actualRowCount).then(result => { + let range = new Slick.Range(0, 0, actualRowCount - 1, columnCount - 1); let columns = gridDataProvider.getColumnHeaders(range); this._chart.setData(result.rows, columns); }); @@ -535,7 +541,11 @@ export class NotebookChartAction extends ToggleableAction { public static SHOWTABLE_LABEL = localize('notebook.showTable', "Show table"); public static SHOWTABLE_ICON = 'table'; - constructor(private resourceTable: DataResourceTable) { + constructor(private resourceTable: DataResourceTable, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService, + @INotificationService private readonly notificationService: INotificationService, + @IAdsTelemetryService private readonly adsTelemetryService: IAdsTelemetryService) { super(NotebookChartAction.ID, { toggleOnLabel: NotebookChartAction.SHOWTABLE_LABEL, toggleOnClass: NotebookChartAction.SHOWTABLE_ICON, @@ -549,9 +559,17 @@ export class NotebookChartAction extends ToggleableAction { this.resourceTable.toggleChartVisibility(); this.toggle(!this.state.isOn); if (this.state.isOn) { - let rowCount = context.table.getData().getLength(); - let columnCount = context.table.columns.length; - this.resourceTable.updateChartData(rowCount, columnCount, context.gridDataProvider); + const rowCount = context.table.getData().getLength(); + const columnCount = context.table.columns.length; + const maxRowCount = getChartMaxRowCount(this.configurationService); + const maxRowCountExceeded = rowCount > maxRowCount; + if (maxRowCountExceeded) { + notifyMaxRowCountExceeded(this.storageService, this.notificationService, this.configurationService); + } + this.adsTelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.ShowChart).withAdditionalProperties( + { [TelemetryKeys.TelemetryPropertyName.ChartMaxRowCountExceeded]: maxRowCountExceeded } + ).send(); + this.resourceTable.updateChartData(Math.min(rowCount, maxRowCount), columnCount, context.gridDataProvider); } return true; } diff --git a/src/sql/workbench/contrib/preferences/browser/sqlSettingsLayout.ts b/src/sql/workbench/contrib/preferences/browser/sqlSettingsLayout.ts index bc49afbfc8..e5ff239191 100644 --- a/src/sql/workbench/contrib/preferences/browser/sqlSettingsLayout.ts +++ b/src/sql/workbench/contrib/preferences/browser/sqlSettingsLayout.ts @@ -36,6 +36,11 @@ let sqlTocItems: ITOCEntry[] = [{ id: 'data/profiler', label: localize('profiler', "Profiler"), settings: ['profiler.*'] + }, + { + id: 'data/builtinCharts', + label: localize('builtinCharts', "Built-in Charts"), + settings: ['builtinCharts.*'] } ] }]; diff --git a/src/sql/workbench/contrib/query/browser/actions.ts b/src/sql/workbench/contrib/query/browser/actions.ts index 01e61e8d58..edd1b4031f 100644 --- a/src/sql/workbench/contrib/query/browser/actions.ts +++ b/src/sql/workbench/contrib/query/browser/actions.ts @@ -20,6 +20,9 @@ import { getErrorMessage } from 'vs/base/common/errors'; import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IEncodingSupport } from 'vs/workbench/common/editor'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { getChartMaxRowCount, notifyMaxRowCountExceeded } from 'sql/workbench/contrib/charts/browser/utils'; export interface IGridActionContext { gridDataProvider: IGridDataProvider; @@ -167,7 +170,11 @@ export class ChartDataAction extends Action { constructor( @IEditorService private editorService: IEditorService, - @IExtensionRecommendationsService private readonly extensionTipsService: IExtensionRecommendationsService + @IExtensionRecommendationsService private readonly extensionTipsService: IExtensionRecommendationsService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService, + @INotificationService private readonly notificationService: INotificationService, + @IAdsTelemetryService private readonly adsTelemetryService: IAdsTelemetryService ) { super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON); } @@ -175,7 +182,15 @@ export class ChartDataAction extends Action { public run(context: IGridActionContext): Promise { // show the visualizer extension recommendation notification this.extensionTipsService.promptRecommendedExtensionsByScenario(Constants.visualizerExtensions); - + const maxRowCount = getChartMaxRowCount(this.configurationService); + const rowCount = context.table.getData().getLength(); + const maxRowCountExceeded = rowCount > maxRowCount; + if (maxRowCountExceeded) { + notifyMaxRowCountExceeded(this.storageService, this.notificationService, this.configurationService); + } + this.adsTelemetryService.createActionEvent(TelemetryKeys.TelemetryView.ResultsPanel, TelemetryKeys.TelemetryAction.ShowChart).withAdditionalProperties( + { [TelemetryKeys.TelemetryPropertyName.ChartMaxRowCountExceeded]: maxRowCountExceeded } + ).send(); const activeEditor = this.editorService.activeEditorPane as QueryEditor; activeEditor.chart({ batchId: context.batchId, resultId: context.resultId }); return Promise.resolve(true); diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index 083123f676..baa6d91e49 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -383,7 +383,7 @@ export abstract class GridTableBase extends Disposable implements IView { @IInstantiationService protected readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IUntitledTextEditorService private readonly untitledEditorService: IUntitledTextEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService protected readonly configurationService: IConfigurationService, @IQueryModelService private readonly queryModelService: IQueryModelService, @IThemeService private readonly themeService: IThemeService ) { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 4202a3ea22..c4d8baa048 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -512,4 +512,7 @@ import 'sql/workbench/contrib/extensions/browser/extensions.contribution'; // Azure import 'sql/workbench/contrib/azure/browser/azure.contribution'; +// Charts +import 'sql/workbench/contrib/charts/browser/charts.contribution'; + //#endregion