diff --git a/product.json b/product.json
index a2398faaac..f27e1ab8d9 100644
--- a/product.json
+++ b/product.json
@@ -54,6 +54,11 @@
"IDERA.sqldm-performance-insights",
"SentryOne.plan-explorer"
],
+ "recommendedExtensionsByScenario": {
+ "visualizerExtensions": [
+ "msrvida.azdata-sanddance"
+ ]
+ },
"extensionsGallery": {
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
}
diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts
index 0d13aa9c9c..fbec63c414 100644
--- a/src/sql/azdata.proposed.d.ts
+++ b/src/sql/azdata.proposed.d.ts
@@ -3974,10 +3974,18 @@ declare module 'azdata' {
export type QueryEvent =
| 'queryStart'
| 'queryStop'
- | 'executionPlan';
+ | 'executionPlan'
+ | 'visualize';
+ /**
+ * args for each event type
+ * queryStart: undefined
+ * queryStop: undefined
+ * executionPlan: string
+ * visualize: ResultSetSummary
+ */
export interface QueryEventListener {
- onQueryEvent(type: QueryEvent, document: queryeditor.QueryDocument, args: any): void;
+ onQueryEvent(type: QueryEvent, document: queryeditor.QueryDocument, args: ResultSetSummary | string | undefined): void;
}
// new extensibility interfaces
diff --git a/src/sql/base/browser/ui/table/media/slickColorTheme.css b/src/sql/base/browser/ui/table/media/slickColorTheme.css
index bd92a83354..1b16834fcd 100644
--- a/src/sql/base/browser/ui/table/media/slickColorTheme.css
+++ b/src/sql/base/browser/ui/table/media/slickColorTheme.css
@@ -122,6 +122,10 @@
background-image: url("viewChart.svg");
}
+.vs .icon.viewVisualizer {
+ background-image: url("viewVisualizer.svg");
+}
+
/* headers */
.vs .resultsMessageHeader {
background: var(--color-bg-header);
@@ -260,6 +264,11 @@
background-image: url("viewChart_inverse.svg");
}
+.vs-dark .icon.viewVisualizer,
+.hc-black .icon.viewVisualizer {
+ background-image: url("viewVisualizer_inverse.svg");
+}
+
.grid-panel .action-label.icon {
height: 16px;
min-width: 28px;
diff --git a/src/sql/base/browser/ui/table/media/viewVisualizer.svg b/src/sql/base/browser/ui/table/media/viewVisualizer.svg
new file mode 100644
index 0000000000..9a345e9e77
--- /dev/null
+++ b/src/sql/base/browser/ui/table/media/viewVisualizer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/base/browser/ui/table/media/viewVisualizer_inverse.svg b/src/sql/base/browser/ui/table/media/viewVisualizer_inverse.svg
new file mode 100644
index 0000000000..2f926edff2
--- /dev/null
+++ b/src/sql/base/browser/ui/table/media/viewVisualizer_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/platform/query/common/queryModelService.ts b/src/sql/platform/query/common/queryModelService.ts
index 35ad50a507..a775bcc69f 100644
--- a/src/sql/platform/query/common/queryModelService.ts
+++ b/src/sql/platform/query/common/queryModelService.ts
@@ -310,6 +310,15 @@ export class QueryModelService implements IQueryModelService {
this._onQueryEvent.fire(event);
});
+ queryRunner.onVisualize(resultSetInfo => {
+ let event: IQueryEvent = {
+ type: 'visualize',
+ uri: uri,
+ params: resultSetInfo
+ };
+ this._onQueryEvent.fire(event);
+ });
+
info.queryRunner = queryRunner;
info.dataService = this._instantiationService.createInstance(DataService, uri);
this._queryInfoMap.set(uri, info);
diff --git a/src/sql/platform/query/common/queryRunner.ts b/src/sql/platform/query/common/queryRunner.ts
index 4aa60be6a8..d72713d881 100644
--- a/src/sql/platform/query/common/queryRunner.ts
+++ b/src/sql/platform/query/common/queryRunner.ts
@@ -85,6 +85,9 @@ export default class QueryRunner extends Disposable {
private _onQueryPlanAvailable = this._register(new Emitter());
public readonly onQueryPlanAvailable = this._onQueryPlanAvailable.event;
+ private _onVisualize = this._register(new Emitter());
+ public readonly onVisualize = this._onVisualize.event;
+
private _queryStartTime: Date;
public get queryStartTime(): Date {
return this._queryStartTime;
@@ -579,6 +582,17 @@ export default class QueryRunner extends Disposable {
public getGridDataProvider(batchId: number, resultSetId: number): IGridDataProvider {
return this.instantiationService.createInstance(QueryGridDataProvider, this, batchId, resultSetId);
}
+
+ public notifyVisualizeRequested(batchId: number, resultSetId: number): void {
+ let result: azdata.ResultSetSummary = {
+ batchId: batchId,
+ id: resultSetId,
+ columnInfo: this.batchSets[batchId].resultSetSummaries[resultSetId].columnInfo,
+ complete: true,
+ rowCount: this.batchSets[batchId].resultSetSummaries[resultSetId].rowCount
+ };
+ this._onVisualize.fire(result);
+ }
}
export class QueryGridDataProvider implements IGridDataProvider {
diff --git a/src/sql/workbench/api/common/extHostQueryEditor.ts b/src/sql/workbench/api/common/extHostQueryEditor.ts
index 53df160ed0..ab02d594e4 100644
--- a/src/sql/workbench/api/common/extHostQueryEditor.ts
+++ b/src/sql/workbench/api/common/extHostQueryEditor.ts
@@ -57,8 +57,8 @@ export class ExtHostQueryEditor implements ExtHostQueryEditorShape {
public $onQueryEvent(handle: number, fileUri: string, event: IQueryEvent): void {
let listener: azdata.queryeditor.QueryEventListener = this._queryListeners[handle];
if (listener) {
- let planXml = event.params ? event.params.planXml : undefined;
- listener.onQueryEvent(event.type, new ExtHostQueryDocument(mssqlProviderName, fileUri, this._proxy), planXml);
+ let params = event.params && event.params.planXml ? event.params.planXml : event.params;
+ listener.onQueryEvent(event.type, new ExtHostQueryDocument(mssqlProviderName, fileUri, this._proxy), params);
}
}
diff --git a/src/sql/workbench/contrib/extensions/constants.ts b/src/sql/workbench/contrib/extensions/constants.ts
new file mode 100644
index 0000000000..e28624ffc4
--- /dev/null
+++ b/src/sql/workbench/contrib/extensions/constants.ts
@@ -0,0 +1,6 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export const visualizerExtensions = 'visualizerExtensions';
diff --git a/src/sql/workbench/contrib/extensions/extensionsActions.ts b/src/sql/workbench/contrib/extensions/extensionsActions.ts
new file mode 100644
index 0000000000..33bf428ddc
--- /dev/null
+++ b/src/sql/workbench/contrib/extensions/extensionsActions.ts
@@ -0,0 +1,79 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { localize } from 'vs/nls';
+import { IAction, Action } from 'vs/base/common/actions';
+import { ShowViewletAction } from 'vs/workbench/browser/viewlet';
+import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
+import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewlet, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/workbench/contrib/extensions/common/extensions';
+import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
+import { IExtensionRecommendation, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
+import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
+import { IOpenerService } from 'vs/platform/opener/common/opener';
+import { CancellationToken } from 'vs/base/common/cancellation';
+import { PagedModel } from 'vs/base/common/paging';
+import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
+
+function getScenarioID(scenarioType: string) {
+ return 'workbench.extensions.action.show' + scenarioType;
+}
+
+export class ShowRecommendedExtensionsByScenarioAction extends Action {
+ constructor(
+ private readonly scenarioType: string,
+ @IViewletService private readonly viewletService: IViewletService
+ ) {
+ super(getScenarioID(scenarioType), localize('showRecommendations', "Show Recommendations"), undefined, true);
+ }
+
+ run(): Promise {
+ return this.viewletService.openViewlet(VIEWLET_ID, true)
+ .then(viewlet => viewlet as IExtensionsViewlet)
+ .then(viewlet => {
+ viewlet.search('@' + this.scenarioType);
+ viewlet.focus();
+ });
+ }
+}
+
+export class InstallRecommendedExtensionsByScenarioAction extends Action {
+ private _recommendations: IExtensionRecommendation[] = [];
+ get recommendations(): IExtensionRecommendation[] { return this._recommendations; }
+ set recommendations(recommendations: IExtensionRecommendation[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; }
+
+ constructor(
+ private readonly scenarioType: string,
+ recommendations: IExtensionRecommendation[],
+ @IViewletService private readonly viewletService: IViewletService,
+ @INotificationService private readonly notificationService: INotificationService,
+ @IInstantiationService private readonly instantiationService: IInstantiationService,
+ @IOpenerService private readonly openerService: IOpenerService,
+ @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
+ @IConfigurationService private readonly configurationService: IConfigurationService,
+ @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService
+ ) {
+ super(getScenarioID(scenarioType), localize('Install Extensions', "Install Extensions"), 'extension-action');
+ this.recommendations = recommendations;
+ }
+
+ run(): Promise {
+ if (!this.recommendations.length) { return Promise.resolve(); }
+ return this.viewletService.openViewlet(VIEWLET_ID, true)
+ .then(viewlet => viewlet as IExtensionsViewlet)
+ .then(viewlet => {
+ viewlet.search('@' + this.scenarioType);
+ viewlet.focus();
+ const names = this.recommendations.map(({ extensionId }) => extensionId);
+ return this.extensionWorkbenchService.queryGallery({ names, source: 'install-' + this.scenarioType }, CancellationToken.None).then(pager => {
+ let installPromises: Promise[] = [];
+ let model = new PagedModel(pager);
+ for (let i = 0; i < pager.total; i++) {
+ installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.extensionWorkbenchService.install(e)));
+ }
+ return Promise.all(installPromises);
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/sql/workbench/parts/charts/browser/chartTab.ts b/src/sql/workbench/parts/charts/browser/chartTab.ts
index e5c744a8e7..d36ff495df 100644
--- a/src/sql/workbench/parts/charts/browser/chartTab.ts
+++ b/src/sql/workbench/parts/charts/browser/chartTab.ts
@@ -34,4 +34,4 @@ export class ChartTab implements IPanelTab {
public clear() {
this.view.clear();
}
-}
+}
\ No newline at end of file
diff --git a/src/sql/workbench/parts/grid/common/gridContentEvents.ts b/src/sql/workbench/parts/grid/common/gridContentEvents.ts
index 74aa5e9ada..5d39850433 100644
--- a/src/sql/workbench/parts/grid/common/gridContentEvents.ts
+++ b/src/sql/workbench/parts/grid/common/gridContentEvents.ts
@@ -17,5 +17,6 @@ export const SaveAsJSON = 'SaveAsJSON';
export const SaveAsExcel = 'SaveAsExcel';
export const SaveAsXML = 'SaveAsXML';
export const ViewAsChart = 'ViewAsChart';
+export const ViewAsVisualizer = 'ViewAsVisualizer';
export const GoToNextQueryOutputTab = 'GoToNextQueryOutputTab';
export const GoToNextGrid = 'GoToNextGrid';
diff --git a/src/sql/workbench/parts/grid/views/gridActions.ts b/src/sql/workbench/parts/grid/views/gridActions.ts
index 9b6aa2589c..2bbb770409 100644
--- a/src/sql/workbench/parts/grid/views/gridActions.ts
+++ b/src/sql/workbench/parts/grid/views/gridActions.ts
@@ -22,6 +22,7 @@ export const TOGGLERESULTS_ID = 'grid.toggleResultPane';
export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
export const GOTONEXTQUERYOUTPUTTAB_ID = 'query.goToNextQueryOutputTab';
export const GRID_VIEWASCHART_ID = 'grid.viewAsChart';
+export const GRID_VIEWASVISUALIZER_ID = 'grid.viewAsVisualizer';
export const GRID_GOTONEXTGRID_ID = 'grid.goToNextGrid';
export class GridActionProvider {
diff --git a/src/sql/workbench/parts/grid/views/gridCommands.ts b/src/sql/workbench/parts/grid/views/gridCommands.ts
index 00135a29f7..3eed2c5a7b 100644
--- a/src/sql/workbench/parts/grid/views/gridCommands.ts
+++ b/src/sql/workbench/parts/grid/views/gridCommands.ts
@@ -83,6 +83,10 @@ export const viewAsChart = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsChart);
};
+export const viewAsVisualizer = (accessor: ServicesAccessor) => {
+ runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsVisualizer);
+};
+
export const goToNextGrid = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.GoToNextGrid);
};
diff --git a/src/sql/workbench/parts/grid/views/gridParentComponent.ts b/src/sql/workbench/parts/grid/views/gridParentComponent.ts
index 4c2187e4a5..07d50ca48e 100644
--- a/src/sql/workbench/parts/grid/views/gridParentComponent.ts
+++ b/src/sql/workbench/parts/grid/views/gridParentComponent.ts
@@ -156,6 +156,9 @@ export abstract class GridParentComponent {
case GridContentEvents.ViewAsChart:
self.showChartForGrid(self.activeGrid);
break;
+ case GridContentEvents.ViewAsVisualizer:
+ self.showVisualizerForGrid(self.activeGrid);
+ break;
case GridContentEvents.GoToNextGrid:
self.goToNextGrid();
break;
@@ -277,6 +280,9 @@ export abstract class GridParentComponent {
protected showChartForGrid(index: number) {
}
+ protected showVisualizerForGrid(index: number) {
+ }
+
protected goToNextGrid() {
if (this.renderedDataSets.length > 0) {
let next = this.activeGrid + 1;
diff --git a/src/sql/workbench/parts/query/browser/actions.ts b/src/sql/workbench/parts/query/browser/actions.ts
index e2fe5d0872..5a9c5162c7 100644
--- a/src/sql/workbench/parts/query/browser/actions.ts
+++ b/src/sql/workbench/parts/query/browser/actions.ts
@@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
-
+import { IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
import { Table } from 'sql/base/browser/ui/table/table';
import { QueryEditor } from './queryEditor';
@@ -17,7 +17,10 @@ import { isWindows } from 'vs/base/common/platform';
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
import { INotificationService } from 'vs/platform/notification/common/notification';
+import QueryRunner from 'sql/platform/query/common/queryRunner';
+import product from 'vs/platform/product/node/product';
import { GridTableState } from 'sql/workbench/parts/query/common/gridPanelState';
+import * as Constants from 'sql/workbench/contrib/extensions/constants';
export interface IGridActionContext {
gridDataProvider: IGridDataProvider;
@@ -199,13 +202,38 @@ export class ChartDataAction extends Action {
public static LABEL = localize('chart', "Chart");
public static ICON = 'viewChart';
- constructor(@IEditorService private editorService: IEditorService) {
+ constructor(
+ @IEditorService private editorService: IEditorService,
+ @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService
+ ) {
super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON);
}
public run(context: IGridActionContext): Promise {
const activeEditor = this.editorService.activeControl as QueryEditor;
+ if (product.quality !== 'stable') {
+ this.extensionTipsService.promptRecommendedExtensionsByScenario(Constants.visualizerExtensions);
+ }
activeEditor.chart({ batchId: context.batchId, resultId: context.resultId });
return Promise.resolve(true);
}
}
+
+export class VisualizerDataAction extends Action {
+ public static ID = 'grid.visualizer';
+ public static LABEL = localize("visualizer", "Visualizer");
+ public static ICON = 'viewVisualizer';
+
+ constructor(
+ private runner: QueryRunner,
+ @IEditorService private editorService: IEditorService,
+
+ ) {
+ super(VisualizerDataAction.ID, VisualizerDataAction.LABEL, VisualizerDataAction.ICON);
+ }
+
+ public run(context: IGridActionContext): Promise {
+ this.runner.notifyVisualizeRequested(context.batchId, context.resultId);
+ return Promise.resolve(true);
+ }
+}
diff --git a/src/sql/workbench/parts/query/browser/gridPanel.ts b/src/sql/workbench/parts/query/browser/gridPanel.ts
index 7ffc4ff355..b4dad55043 100644
--- a/src/sql/workbench/parts/query/browser/gridPanel.ts
+++ b/src/sql/workbench/parts/query/browser/gridPanel.ts
@@ -11,7 +11,7 @@ import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitv
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
-import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction } from 'sql/workbench/parts/query/browser/actions';
+import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction, VisualizerDataAction } from 'sql/workbench/parts/query/browser/actions';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
import { escape } from 'sql/base/common/strings';
@@ -24,6 +24,7 @@ import * as azdata from 'azdata';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
+import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Emitter, Event } from 'vs/base/common/event';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { isUndefinedOrNull } from 'vs/base/common/types';
@@ -723,17 +724,18 @@ export abstract class GridTableBase extends Disposable implements IView {
class GridTable extends GridTableBase {
private _gridDataProvider: IGridDataProvider;
constructor(
- runner: QueryRunner,
+ private _runner: QueryRunner,
resultSet: azdata.ResultSetSummary,
state: GridTableState,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
+ @IContextKeyService private contextKeyService: IContextKeyService,
@IEditorService editorService: IEditorService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IConfigurationService configurationService: IConfigurationService
) {
super(state, resultSet, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
- this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, runner, resultSet.batchId, resultSet.id);
+ this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, this._runner, resultSet.batchId, resultSet.id);
}
get gridDataProvider(): IGridDataProvider {
@@ -760,6 +762,10 @@ class GridTable extends GridTableBase {
this.instantiationService.createInstance(ChartDataAction)
);
+ if (this.contextKeyService.getContextKeyValue('showVisualizer')) {
+ actions.push(this.instantiationService.createInstance(VisualizerDataAction, this._runner));
+ }
+
return actions;
}
diff --git a/src/sql/workbench/parts/query/browser/queryActions.ts b/src/sql/workbench/parts/query/browser/queryActions.ts
index 1dcd685e4a..55b91b8164 100644
--- a/src/sql/workbench/parts/query/browser/queryActions.ts
+++ b/src/sql/workbench/parts/query/browser/queryActions.ts
@@ -11,6 +11,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { INotificationService } from 'vs/platform/notification/common/notification';
+import { IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import Severity from 'vs/base/common/severity';
import { append, $ } from 'vs/base/browser/dom';
@@ -107,7 +108,8 @@ export class RunQueryAction extends QueryTaskbarAction {
constructor(
editor: QueryEditor,
@IQueryModelService protected readonly queryModelService: IQueryModelService,
- @IConnectionManagementService connectionManagementService: IConnectionManagementService
+ @IConnectionManagementService connectionManagementService: IConnectionManagementService,
+ @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService
) {
super(connectionManagementService, editor, RunQueryAction.ID, RunQueryAction.EnabledClass);
this.label = nls.localize('runQueryLabel', "Run");
diff --git a/src/sql/workbench/parts/query/browser/queryResultsView.ts b/src/sql/workbench/parts/query/browser/queryResultsView.ts
index efe5e442be..a2b7b2075f 100644
--- a/src/sql/workbench/parts/query/browser/queryResultsView.ts
+++ b/src/sql/workbench/parts/query/browser/queryResultsView.ts
@@ -119,7 +119,6 @@ class ResultsView extends Disposable implements IPanelView {
this.gridPanel.state = val;
}
}
-
class ResultsTab implements IPanelTab {
public readonly title = nls.localize('resultsTabTitle', "Results");
public readonly identifier = 'resultsTab';
diff --git a/src/sql/workbench/parts/query/common/localizedConstants.ts b/src/sql/workbench/parts/query/common/localizedConstants.ts
index c6a9ba432c..aad4974a40 100644
--- a/src/sql/workbench/parts/query/common/localizedConstants.ts
+++ b/src/sql/workbench/parts/query/common/localizedConstants.ts
@@ -26,6 +26,7 @@ export const saveJSONLabel = localize('saveJSONLabel', "Save as JSON");
export const saveExcelLabel = localize('saveExcelLabel', "Save as Excel");
export const saveXMLLabel = localize('saveXMLLabel', "Save as XML");
export const viewChartLabel = localize('viewChartLabel', "View as Chart");
+export const viewVisualizerLabel = localize('viewVisualizerLabel', "Visualize");
export const resultPaneLabel = localize('resultPaneLabel', "Results");
export const executeQueryLabel = localize('executeQueryLabel', "Executing query ");
diff --git a/src/sql/workbench/parts/query/test/browser/queryActions.test.ts b/src/sql/workbench/parts/query/test/browser/queryActions.test.ts
index c7d24f5d5f..da6213401e 100644
--- a/src/sql/workbench/parts/query/test/browser/queryActions.test.ts
+++ b/src/sql/workbench/parts/query/test/browser/queryActions.test.ts
@@ -69,7 +69,7 @@ suite('SQL QueryAction Tests', () => {
test('setClass sets child CSS class correctly', (done) => {
// If I create a RunQueryAction
- let queryAction: QueryTaskbarAction = new RunQueryAction(undefined, undefined, undefined);
+ let queryAction: QueryTaskbarAction = new RunQueryAction(undefined, undefined, undefined, undefined);
// "class should automatically get set to include the base class and the RunQueryAction class
let className = RunQueryAction.EnabledClass;
@@ -93,7 +93,7 @@ suite('SQL QueryAction Tests', () => {
editor.setup(x => x.input).returns(() => testQueryInput.object);
// If I create a QueryTaskbarAction and I pass a non-connected editor to _getConnectedQueryEditorUri
- let queryAction: QueryTaskbarAction = new RunQueryAction(undefined, undefined, connectionManagementService.object);
+ let queryAction: QueryTaskbarAction = new RunQueryAction(undefined, undefined, connectionManagementService.object, undefined);
let connected: boolean = queryAction.isConnected(editor.object);
// I should get an unconnected state
@@ -136,7 +136,7 @@ suite('SQL QueryAction Tests', () => {
});
// If I call run on RunQueryAction when I am not connected
- let queryAction: RunQueryAction = new RunQueryAction(editor.object, queryModelService.object, connectionManagementService.object);
+ let queryAction: RunQueryAction = new RunQueryAction(editor.object, queryModelService.object, connectionManagementService.object, undefined);
isConnected = false;
calledRunQueryOnInput = false;
queryAction.run();
@@ -195,7 +195,7 @@ suite('SQL QueryAction Tests', () => {
let queryModelService = TypeMoq.Mock.ofType(QueryModelService, TypeMoq.MockBehavior.Loose);
// If I call run on RunQueryAction when I have a non empty selection
- let queryAction: RunQueryAction = new RunQueryAction(queryEditor.object, queryModelService.object, connectionManagementService.object);
+ let queryAction: RunQueryAction = new RunQueryAction(queryEditor.object, queryModelService.object, connectionManagementService.object, undefined);
isSelectionEmpty = false;
queryAction.run();
@@ -266,7 +266,7 @@ suite('SQL QueryAction Tests', () => {
/// End Setup Test ///
////// If I call run on RunQueryAction while disconnected and with an undefined selection
- let queryAction: RunQueryAction = new RunQueryAction(queryEditor.object, undefined, connectionManagementService.object);
+ let queryAction: RunQueryAction = new RunQueryAction(queryEditor.object, undefined, connectionManagementService.object, undefined);
isConnected = false;
selectionToReturnInGetSelection = undefined;
queryAction.run();
diff --git a/src/sql/workbench/parts/query/test/browser/queryEditor.test.ts b/src/sql/workbench/parts/query/test/browser/queryEditor.test.ts
index bbe25e9f15..9fbe720fdc 100644
--- a/src/sql/workbench/parts/query/test/browser/queryEditor.test.ts
+++ b/src/sql/workbench/parts/query/test/browser/queryEditor.test.ts
@@ -54,7 +54,7 @@ suite('SQL QueryEditor Tests', () => {
return new Promise((resolve) => resolve(mockEditor));
});
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((input) => {
- return new Promise((resolve) => resolve(new RunQueryAction(undefined, undefined, undefined)));
+ return new Promise((resolve) => resolve(new RunQueryAction(undefined, undefined, undefined, undefined)));
});
// Setup hook to capture calls to create the listDatabase action
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((classDef, editor, action) => {
@@ -64,7 +64,7 @@ suite('SQL QueryEditor Tests', () => {
}
}
// Default
- return new RunQueryAction(undefined, undefined, undefined);
+ return new RunQueryAction(undefined, undefined, undefined, undefined);
});
// Mock EditorDescriptorService to give us a mock editor description
@@ -269,7 +269,7 @@ suite('SQL QueryEditor Tests', () => {
queryActionInstantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((input) => {
// Default
- return new RunQueryAction(undefined, undefined, undefined);
+ return new RunQueryAction(undefined, undefined, undefined, undefined);
});
// Setup hook to capture calls to create the listDatabase action
@@ -280,7 +280,7 @@ suite('SQL QueryEditor Tests', () => {
return item;
}
// Default
- return new RunQueryAction(undefined, undefined, undefined);
+ return new RunQueryAction(undefined, undefined, undefined, undefined);
});
let fileInput = new UntitledEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService.object, undefined, undefined);
diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts
index 4fafc347a2..3d32f3d96d 100644
--- a/src/vs/platform/extensionManagement/common/extensionManagement.ts
+++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts
@@ -211,4 +211,4 @@ export interface IExtensionManagementService {
export const ExtensionsLabel = localize('extensions', "Extensions");
export const ExtensionsChannelId = 'extensions';
-export const PreferencesLabel = localize('preferences', "Preferences");
+export const PreferencesLabel = localize('preferences', "Preferences");
\ No newline at end of file
diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts
index 71e8fc69b8..e8080310f7 100644
--- a/src/vs/platform/product/common/product.ts
+++ b/src/vs/platform/product/common/product.ts
@@ -74,6 +74,7 @@ export interface IProductConfiguration {
};
extensionTips: { [id: string]: string; };
recommendedExtensions: string[]; // {{SQL CARBON EDIT}}
+ recommendedExtensionsByScenario: string[]; // {{SQL CARBON EDIT}}
extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; };
readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; };
readonly extensionKeywords: { [extension: string]: readonly string[]; };
diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts
index c7fc53d235..b791e65a4c 100644
--- a/src/vs/workbench/browser/web.simpleservices.ts
+++ b/src/vs/workbench/browser/web.simpleservices.ts
@@ -71,6 +71,16 @@ export class SimpleExtensionTipsService implements IExtensionTipsService {
getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } {
return { global: [], workspace: [] };
}
+
+ // {{SQL CARBON EDIT}}
+ getRecommendedExtensionsByScenario(scenarioType: string): Promise {
+ return Promise.resolve([]);
+ }
+
+ promptRecommendedExtensionsByScenario(scenarioType: string): void {
+ return;
+ }
+ // {{SQL CARBON EDIT}} - End
}
registerSingleton(IExtensionTipsService, SimpleExtensionTipsService, true);
diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts
index ac6af06cb5..7a632bcff4 100644
--- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts
+++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts
@@ -47,6 +47,9 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IProductService } from 'vs/platform/product/common/product';
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
+// {{SQL CARBON EDIT}}
+import product from 'vs/platform/product/node/product';
+
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
@@ -424,6 +427,19 @@ export class ExtensionsListView extends ViewletPanel {
options.sortBy = SortBy.InstallCount;
}
+ // {{SQL CARBON EDIT}}
+ let promiseRecommendedExtensionsByScenario;
+ Object.keys(product.recommendedExtensionsByScenario).forEach(scenarioType => {
+ let re = new RegExp('@' + scenarioType, 'i');
+ if (re.test(query.value)) {
+ promiseRecommendedExtensionsByScenario = this.getRecommendedExtensionsByScenario(token, scenarioType);
+ }
+ });
+ if (promiseRecommendedExtensionsByScenario) {
+ return promiseRecommendedExtensionsByScenario;
+ }
+ // {{SQL CARBON EDIT}} - End
+
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
return this.getWorkspaceRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
@@ -650,6 +666,31 @@ export class ExtensionsListView extends ViewletPanel {
});
}
+ // {{SQL CARBON EDIT}}
+ private getRecommendedExtensionsByScenario(token: CancellationToken, scenarioType: string): Promise> {
+ if (!scenarioType) {
+ return Promise.reject(new Error(localize('scenarioTypeUndefined', 'The scenario type for extension recommendations must be provided.')));
+ }
+ return this.extensionsWorkbenchService.queryLocal()
+ .then(result => result.filter(e => e.type === ExtensionType.User))
+ .then(local => {
+ return this.tipsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => {
+ const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
+ return this.extensionsWorkbenchService.queryGallery(token).then((pager) => {
+ // filter out installed extensions and the extensions not in the recommended list
+ pager.firstPage = pager.firstPage.filter((p) => {
+ const extensionId = `${p.publisher}.${p.name}`;
+ return installedExtensions.indexOf(extensionId) === -1 && recommmended.findIndex(ext => ext.extensionId === extensionId) !== -1;
+ });
+ pager.total = pager.firstPage.length;
+ pager.pageSize = pager.firstPage.length;
+ return this.getPagedModel(pager);
+ });
+ });
+ });
+ }
+ // {{SQL CARBON EDIT}} - End
+
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workpsaceRecommendations: IExtensionRecommendation[]): string[] {
const totalCount = 8;
diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts
index 2ca6604731..708938b5c9 100644
--- a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts
+++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts
@@ -16,7 +16,11 @@ import { ITextModel } from 'vs/editor/common/model';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import product from 'vs/platform/product/node/product';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+// {{SQL CARBON EDIT}}
import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
+import { ShowRecommendedExtensionsByScenarioAction, InstallRecommendedExtensionsByScenarioAction } from 'sql/workbench/contrib/extensions/extensionsActions';
+import * as Constants from 'sql/workbench/contrib/extensions/constants';
+// {{SQL CARBON EDIT}} - End
import Severity from 'vs/base/common/severity';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
@@ -1148,4 +1152,79 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
private isExtensionAllowedToBeRecommended(id: string): boolean {
return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
}
+
+ // {{SQL CARBON EDIT}}
+ promptRecommendedExtensionsByScenario(scenarioType: string): void {
+ const storageKey = 'extensionAssistant/RecommendationsIgnore/' + scenarioType;
+
+ if (this.storageService.getBoolean(storageKey, StorageScope.GLOBAL, false)) {
+ return;
+ }
+
+ let recommendations: IExtensionRecommendation[];
+ let localExtensions: ILocalExtension[];
+ const getRecommendationPromise = this.getRecommendedExtensionsByScenario(scenarioType).then(recs => { recommendations = recs; });
+ const getLocalExtensionPromise = this.extensionsService.getInstalled(ExtensionType.User).then(local => { localExtensions = local; });
+
+ let recommendationMessage = localize('ExtensionsRecommended', "Azure Data Studio has extension recommendations.");
+ if (scenarioType === Constants.visualizerExtensions) {
+ recommendationMessage = localize('VisualizerExtensionsRecommended', "Azure Data Studio has extension recommendations for data visualization.\nOnce installed, you can select the Visualizer icon to visualize your query results.");
+ }
+ Promise.all([getRecommendationPromise, getLocalExtensionPromise]).then(() => {
+ if (!recommendations.every(rec => { return localExtensions.findIndex(local => local.identifier.id.toLocaleLowerCase() === rec.extensionId.toLocaleLowerCase()) !== -1; })) {
+ return new Promise(c => {
+ this.notificationService.prompt(
+ Severity.Info,
+ recommendationMessage,
+ [{
+ label: localize('installAll', "Install All"),
+ run: () => {
+ this.telemetryService.publicLog(scenarioType + 'Recommendations:popup', { userReaction: 'install' });
+ const installAllAction = this.instantiationService.createInstance(InstallRecommendedExtensionsByScenarioAction, scenarioType, recommendations);
+ installAllAction.run();
+ installAllAction.dispose();
+ }
+ }, {
+ label: localize('showRecommendations', "Show Recommendations"),
+ run: () => {
+ this.telemetryService.publicLog(scenarioType + 'Recommendations:popup', { userReaction: 'show' });
+ const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsByScenarioAction, scenarioType);
+ showAction.run();
+ showAction.dispose();
+ c(undefined);
+ }
+ }, {
+ label: choiceNever,
+ isSecondary: true,
+ run: () => {
+ this.telemetryService.publicLog(scenarioType + 'Recommendations:popup', { userReaction: 'neverShowAgain' });
+ this.storageService.store(storageKey, true, StorageScope.GLOBAL);
+ c(undefined);
+ }
+ }],
+ {
+ sticky: true,
+ onCancel: () => {
+ this.telemetryService.publicLog(scenarioType + 'Recommendations:popup', { userReaction: 'cancelled' });
+ c(undefined);
+ }
+ }
+ );
+ });
+ } else {
+ return Promise.resolve();
+ }
+ });
+ }
+
+ getRecommendedExtensionsByScenario(scenarioType: string): Promise {
+ if (!scenarioType) {
+ return Promise.reject(new Error(localize('scenarioTypeUndefined', 'The scenario type for extension recommendations must be provided.')));
+ }
+
+ return Promise.resolve((product.recommendedExtensionsByScenario[scenarioType] || [])
+ .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId))
+ .map(extensionId => ({ extensionId, sources: ['application'] })));
+ }
+ // {{SQL CARBON EDIT}} - End
}
diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts
index 2383d94fcb..d0bc631b1d 100644
--- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts
+++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts
@@ -106,6 +106,10 @@ export interface IExtensionTipsService {
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void;
getAllIgnoredRecommendations(): { global: string[], workspace: string[] };
onRecommendationChange: Event;
+ // {{SQL CARBON EDIT}}
+ getRecommendedExtensionsByScenario(scenarioType: string): Promise;
+ promptRecommendedExtensionsByScenario(scenarioType: string): void;
+ // {{SQL CARBON EDIT}} - End
}
export const enum ExtensionRecommendationReason {