mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Introducing Visualizer to SQL Query Editor (#6422)
* extension recommendation on application launch * Introducing Visualizer (SandDance) to the SQL Query Editor. (#6347) * Created Visualizer icon in the results grid. Utilized a context key so that the icon only shows if Visualizer extensions (currently, just SandDance) is installed. Visualizer icon open up SandDance in a top-level document. * When the user clicks on Charts, visualizer recommendation popup appears. User can click on "Install Extensions" to download the visualizer extensions. * Enabled SQL Query Editor to pass query data to SandDance extension. * Introducing Visualizer (SandDance) to the SQL Query Editor. (#6347) * Created Visualizer icon in the results grid. Utilized a context key so that the icon only shows if Visualizer extensions (currently, just SandDance) is installed. Visualizer icon open up SandDance in a top-level document. * When the user clicks on Charts, visualizer recommendation popup appears. User can click on "Install Extensions" to download the visualizer extensions. * Enabled SQL Query Editor to pass query data to SandDance extension. * Cleaned code; made changes according to PR comments * removed the test service for extensions gallary * Cleaned up code according to PR changes * unid changes to build/azure-piplines * Removed all the build/azure-pipelines changes * removed changes on media/language.svg * refactored extension recommendation system to allow it to be generic * updated extensionsViews to support generic extension query search; added localized constants for visualizer extensions * Made syntax and error message changes acccording to PR comments. * Updated recommendation message according to scenario type
This commit is contained in:
committed by
Rachel Kim
parent
720a7fbfa2
commit
2c8a22bb0d
@@ -54,6 +54,11 @@
|
|||||||
"IDERA.sqldm-performance-insights",
|
"IDERA.sqldm-performance-insights",
|
||||||
"SentryOne.plan-explorer"
|
"SentryOne.plan-explorer"
|
||||||
],
|
],
|
||||||
|
"recommendedExtensionsByScenario": {
|
||||||
|
"visualizerExtensions": [
|
||||||
|
"msrvida.azdata-sanddance"
|
||||||
|
]
|
||||||
|
},
|
||||||
"extensionsGallery": {
|
"extensionsGallery": {
|
||||||
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
|
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/sql/azdata.proposed.d.ts
vendored
12
src/sql/azdata.proposed.d.ts
vendored
@@ -3974,10 +3974,18 @@ declare module 'azdata' {
|
|||||||
export type QueryEvent =
|
export type QueryEvent =
|
||||||
| 'queryStart'
|
| 'queryStart'
|
||||||
| 'queryStop'
|
| 'queryStop'
|
||||||
| 'executionPlan';
|
| 'executionPlan'
|
||||||
|
| 'visualize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* args for each event type
|
||||||
|
* queryStart: undefined
|
||||||
|
* queryStop: undefined
|
||||||
|
* executionPlan: string
|
||||||
|
* visualize: ResultSetSummary
|
||||||
|
*/
|
||||||
export interface QueryEventListener {
|
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
|
// new extensibility interfaces
|
||||||
|
|||||||
@@ -122,6 +122,10 @@
|
|||||||
background-image: url("viewChart.svg");
|
background-image: url("viewChart.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vs .icon.viewVisualizer {
|
||||||
|
background-image: url("viewVisualizer.svg");
|
||||||
|
}
|
||||||
|
|
||||||
/* headers */
|
/* headers */
|
||||||
.vs .resultsMessageHeader {
|
.vs .resultsMessageHeader {
|
||||||
background: var(--color-bg-header);
|
background: var(--color-bg-header);
|
||||||
@@ -260,6 +264,11 @@
|
|||||||
background-image: url("viewChart_inverse.svg");
|
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 {
|
.grid-panel .action-label.icon {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
|
|||||||
1
src/sql/base/browser/ui/table/media/viewVisualizer.svg
Normal file
1
src/sql/base/browser/ui/table/media/viewVisualizer.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:#f6f6f6;}.cls-1{fill-opacity:0;}.cls-3{fill:#424242;}</style></defs><title>viewVisualizer</title><path id="canvas" class="cls-1" d="M17.11,16.33H-1V-1.75H17.11Z"/><path id="canvas-2" class="cls-1" d="M10.89,16H0V5.13H10.89Z"/><path id="outline-2" class="cls-2" d="M.68,9.21V8.68h0L0,8.08V6.48L1.28,5.13H2l2.2,2.14L2.8,8.74l-.07-.07v.54h2V8.52H6.81V5.81l2.7,0v6.81h0L10.77,14v.73L9.64,16H7.85l-.58-.56.08-.08H.68Z"/><path id="iconBg" class="cls-3" d="M4.76,12.6H3.41V9.89H4.76ZM6.81,9.21H5.44V12.6H6.81Zm2-2.72H7.49V12.6H8.84Zm-.11,6.3-.47.48L9,14H2.05V7l.75.75.48-.49L1.71,5.73.15,7.29l.49.47.71-.71v7.63H9l-.75.76.48.47,1.62-1.57Z"/><path id="iconBg-2" class="cls-3" d="M16,.51V14.07H10.77V12.94h4.08v-9H1.28V5.32H.15V.51Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 857 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:#f6f6f6;}.cls-1{fill-opacity:0;}.cls-3{fill:#c5c5c5;}</style></defs><title>viewVisualizer</title><path id="canvas-2" class="cls-1" d="M10.89,16H0V5.13H10.89Z"/><path id="outline-2" class="cls-2" d="M.68,9.21V8.68h0L0,8.08V6.48L1.28,5.13H2l2.2,2.14L2.8,8.74l-.07-.07v.54h2V8.52H6.81V5.81l2.7,0v6.81h0L10.77,14v.73L9.64,16H7.85l-.58-.56.08-.08H.68Z"/><path id="iconBg" class="cls-3" d="M4.76,12.6H3.41V9.89H4.76ZM6.81,9.21H5.44V12.6H6.81Zm2-2.72H7.49V12.6H8.84Zm-.11,6.3-.47.48L9,14H2.05V7l.75.75.48-.49L1.71,5.73.15,7.29l.49.47.71-.71v7.63H9l-.75.76.48.47,1.62-1.57Z"/><path id="iconBg-2" class="cls-3" d="M16,.51V14.07H10.77V12.94h4.08v-9H1.28V5.32H.15V.51Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 791 B |
@@ -310,6 +310,15 @@ export class QueryModelService implements IQueryModelService {
|
|||||||
this._onQueryEvent.fire(event);
|
this._onQueryEvent.fire(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryRunner.onVisualize(resultSetInfo => {
|
||||||
|
let event: IQueryEvent = {
|
||||||
|
type: 'visualize',
|
||||||
|
uri: uri,
|
||||||
|
params: resultSetInfo
|
||||||
|
};
|
||||||
|
this._onQueryEvent.fire(event);
|
||||||
|
});
|
||||||
|
|
||||||
info.queryRunner = queryRunner;
|
info.queryRunner = queryRunner;
|
||||||
info.dataService = this._instantiationService.createInstance(DataService, uri);
|
info.dataService = this._instantiationService.createInstance(DataService, uri);
|
||||||
this._queryInfoMap.set(uri, info);
|
this._queryInfoMap.set(uri, info);
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export default class QueryRunner extends Disposable {
|
|||||||
private _onQueryPlanAvailable = this._register(new Emitter<IQueryPlanInfo>());
|
private _onQueryPlanAvailable = this._register(new Emitter<IQueryPlanInfo>());
|
||||||
public readonly onQueryPlanAvailable = this._onQueryPlanAvailable.event;
|
public readonly onQueryPlanAvailable = this._onQueryPlanAvailable.event;
|
||||||
|
|
||||||
|
private _onVisualize = this._register(new Emitter<azdata.ResultSetSummary>());
|
||||||
|
public readonly onVisualize = this._onVisualize.event;
|
||||||
|
|
||||||
private _queryStartTime: Date;
|
private _queryStartTime: Date;
|
||||||
public get queryStartTime(): Date {
|
public get queryStartTime(): Date {
|
||||||
return this._queryStartTime;
|
return this._queryStartTime;
|
||||||
@@ -579,6 +582,17 @@ export default class QueryRunner extends Disposable {
|
|||||||
public getGridDataProvider(batchId: number, resultSetId: number): IGridDataProvider {
|
public getGridDataProvider(batchId: number, resultSetId: number): IGridDataProvider {
|
||||||
return this.instantiationService.createInstance(QueryGridDataProvider, this, batchId, resultSetId);
|
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 {
|
export class QueryGridDataProvider implements IGridDataProvider {
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export class ExtHostQueryEditor implements ExtHostQueryEditorShape {
|
|||||||
public $onQueryEvent(handle: number, fileUri: string, event: IQueryEvent): void {
|
public $onQueryEvent(handle: number, fileUri: string, event: IQueryEvent): void {
|
||||||
let listener: azdata.queryeditor.QueryEventListener = this._queryListeners[handle];
|
let listener: azdata.queryeditor.QueryEventListener = this._queryListeners[handle];
|
||||||
if (listener) {
|
if (listener) {
|
||||||
let planXml = event.params ? event.params.planXml : undefined;
|
let params = event.params && event.params.planXml ? event.params.planXml : event.params;
|
||||||
listener.onQueryEvent(event.type, new ExtHostQueryDocument(mssqlProviderName, fileUri, this._proxy), planXml);
|
listener.onQueryEvent(event.type, new ExtHostQueryDocument(mssqlProviderName, fileUri, this._proxy), params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
src/sql/workbench/contrib/extensions/constants.ts
Normal file
6
src/sql/workbench/contrib/extensions/constants.ts
Normal file
@@ -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';
|
||||||
79
src/sql/workbench/contrib/extensions/extensionsActions.ts
Normal file
79
src/sql/workbench/contrib/extensions/extensionsActions.ts
Normal file
@@ -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<void> {
|
||||||
|
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<any> {
|
||||||
|
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<any>[] = [];
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,5 +17,6 @@ export const SaveAsJSON = 'SaveAsJSON';
|
|||||||
export const SaveAsExcel = 'SaveAsExcel';
|
export const SaveAsExcel = 'SaveAsExcel';
|
||||||
export const SaveAsXML = 'SaveAsXML';
|
export const SaveAsXML = 'SaveAsXML';
|
||||||
export const ViewAsChart = 'ViewAsChart';
|
export const ViewAsChart = 'ViewAsChart';
|
||||||
|
export const ViewAsVisualizer = 'ViewAsVisualizer';
|
||||||
export const GoToNextQueryOutputTab = 'GoToNextQueryOutputTab';
|
export const GoToNextQueryOutputTab = 'GoToNextQueryOutputTab';
|
||||||
export const GoToNextGrid = 'GoToNextGrid';
|
export const GoToNextGrid = 'GoToNextGrid';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const TOGGLERESULTS_ID = 'grid.toggleResultPane';
|
|||||||
export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
|
export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
|
||||||
export const GOTONEXTQUERYOUTPUTTAB_ID = 'query.goToNextQueryOutputTab';
|
export const GOTONEXTQUERYOUTPUTTAB_ID = 'query.goToNextQueryOutputTab';
|
||||||
export const GRID_VIEWASCHART_ID = 'grid.viewAsChart';
|
export const GRID_VIEWASCHART_ID = 'grid.viewAsChart';
|
||||||
|
export const GRID_VIEWASVISUALIZER_ID = 'grid.viewAsVisualizer';
|
||||||
export const GRID_GOTONEXTGRID_ID = 'grid.goToNextGrid';
|
export const GRID_GOTONEXTGRID_ID = 'grid.goToNextGrid';
|
||||||
|
|
||||||
export class GridActionProvider {
|
export class GridActionProvider {
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ export const viewAsChart = (accessor: ServicesAccessor) => {
|
|||||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsChart);
|
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsChart);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewAsVisualizer = (accessor: ServicesAccessor) => {
|
||||||
|
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsVisualizer);
|
||||||
|
};
|
||||||
|
|
||||||
export const goToNextGrid = (accessor: ServicesAccessor) => {
|
export const goToNextGrid = (accessor: ServicesAccessor) => {
|
||||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.GoToNextGrid);
|
runActionOnActiveResultsEditor(accessor, GridContentEvents.GoToNextGrid);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ export abstract class GridParentComponent {
|
|||||||
case GridContentEvents.ViewAsChart:
|
case GridContentEvents.ViewAsChart:
|
||||||
self.showChartForGrid(self.activeGrid);
|
self.showChartForGrid(self.activeGrid);
|
||||||
break;
|
break;
|
||||||
|
case GridContentEvents.ViewAsVisualizer:
|
||||||
|
self.showVisualizerForGrid(self.activeGrid);
|
||||||
|
break;
|
||||||
case GridContentEvents.GoToNextGrid:
|
case GridContentEvents.GoToNextGrid:
|
||||||
self.goToNextGrid();
|
self.goToNextGrid();
|
||||||
break;
|
break;
|
||||||
@@ -277,6 +280,9 @@ export abstract class GridParentComponent {
|
|||||||
protected showChartForGrid(index: number) {
|
protected showChartForGrid(index: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected showVisualizerForGrid(index: number) {
|
||||||
|
}
|
||||||
|
|
||||||
protected goToNextGrid() {
|
protected goToNextGrid() {
|
||||||
if (this.renderedDataSets.length > 0) {
|
if (this.renderedDataSets.length > 0) {
|
||||||
let next = this.activeGrid + 1;
|
let next = this.activeGrid + 1;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
|
|||||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
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 { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||||
import { Table } from 'sql/base/browser/ui/table/table';
|
import { Table } from 'sql/base/browser/ui/table/table';
|
||||||
import { QueryEditor } from './queryEditor';
|
import { QueryEditor } from './queryEditor';
|
||||||
@@ -17,7 +17,10 @@ import { isWindows } from 'vs/base/common/platform';
|
|||||||
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
||||||
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
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 { GridTableState } from 'sql/workbench/parts/query/common/gridPanelState';
|
||||||
|
import * as Constants from 'sql/workbench/contrib/extensions/constants';
|
||||||
|
|
||||||
export interface IGridActionContext {
|
export interface IGridActionContext {
|
||||||
gridDataProvider: IGridDataProvider;
|
gridDataProvider: IGridDataProvider;
|
||||||
@@ -199,13 +202,38 @@ export class ChartDataAction extends Action {
|
|||||||
public static LABEL = localize('chart', "Chart");
|
public static LABEL = localize('chart', "Chart");
|
||||||
public static ICON = 'viewChart';
|
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);
|
super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON);
|
||||||
}
|
}
|
||||||
|
|
||||||
public run(context: IGridActionContext): Promise<boolean> {
|
public run(context: IGridActionContext): Promise<boolean> {
|
||||||
const activeEditor = this.editorService.activeControl as QueryEditor;
|
const activeEditor = this.editorService.activeControl as QueryEditor;
|
||||||
|
if (product.quality !== 'stable') {
|
||||||
|
this.extensionTipsService.promptRecommendedExtensionsByScenario(Constants.visualizerExtensions);
|
||||||
|
}
|
||||||
activeEditor.chart({ batchId: context.batchId, resultId: context.resultId });
|
activeEditor.chart({ batchId: context.batchId, resultId: context.resultId });
|
||||||
return Promise.resolve(true);
|
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<boolean> {
|
||||||
|
this.runner.notifyVisualizeRequested(context.batchId, context.resultId);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
|
||||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
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 { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||||
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||||
import { escape } from 'sql/base/common/strings';
|
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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
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 { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||||
@@ -723,17 +724,18 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
|
|||||||
class GridTable<T> extends GridTableBase<T> {
|
class GridTable<T> extends GridTableBase<T> {
|
||||||
private _gridDataProvider: IGridDataProvider;
|
private _gridDataProvider: IGridDataProvider;
|
||||||
constructor(
|
constructor(
|
||||||
runner: QueryRunner,
|
private _runner: QueryRunner,
|
||||||
resultSet: azdata.ResultSetSummary,
|
resultSet: azdata.ResultSetSummary,
|
||||||
state: GridTableState,
|
state: GridTableState,
|
||||||
@IContextMenuService contextMenuService: IContextMenuService,
|
@IContextMenuService contextMenuService: IContextMenuService,
|
||||||
@IInstantiationService instantiationService: IInstantiationService,
|
@IInstantiationService instantiationService: IInstantiationService,
|
||||||
|
@IContextKeyService private contextKeyService: IContextKeyService,
|
||||||
@IEditorService editorService: IEditorService,
|
@IEditorService editorService: IEditorService,
|
||||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||||
@IConfigurationService configurationService: IConfigurationService
|
@IConfigurationService configurationService: IConfigurationService
|
||||||
) {
|
) {
|
||||||
super(state, resultSet, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
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 {
|
get gridDataProvider(): IGridDataProvider {
|
||||||
@@ -760,6 +762,10 @@ class GridTable<T> extends GridTableBase<T> {
|
|||||||
this.instantiationService.createInstance(ChartDataAction)
|
this.instantiationService.createInstance(ChartDataAction)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.contextKeyService.getContextKeyValue('showVisualizer')) {
|
||||||
|
actions.push(this.instantiationService.createInstance(VisualizerDataAction, this._runner));
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
|
|||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
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 Severity from 'vs/base/common/severity';
|
||||||
import { append, $ } from 'vs/base/browser/dom';
|
import { append, $ } from 'vs/base/browser/dom';
|
||||||
|
|
||||||
@@ -107,7 +108,8 @@ export class RunQueryAction extends QueryTaskbarAction {
|
|||||||
constructor(
|
constructor(
|
||||||
editor: QueryEditor,
|
editor: QueryEditor,
|
||||||
@IQueryModelService protected readonly queryModelService: IQueryModelService,
|
@IQueryModelService protected readonly queryModelService: IQueryModelService,
|
||||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||||
|
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService
|
||||||
) {
|
) {
|
||||||
super(connectionManagementService, editor, RunQueryAction.ID, RunQueryAction.EnabledClass);
|
super(connectionManagementService, editor, RunQueryAction.ID, RunQueryAction.EnabledClass);
|
||||||
this.label = nls.localize('runQueryLabel', "Run");
|
this.label = nls.localize('runQueryLabel', "Run");
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ class ResultsView extends Disposable implements IPanelView {
|
|||||||
this.gridPanel.state = val;
|
this.gridPanel.state = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResultsTab implements IPanelTab {
|
class ResultsTab implements IPanelTab {
|
||||||
public readonly title = nls.localize('resultsTabTitle', "Results");
|
public readonly title = nls.localize('resultsTabTitle', "Results");
|
||||||
public readonly identifier = 'resultsTab';
|
public readonly identifier = 'resultsTab';
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const saveJSONLabel = localize('saveJSONLabel', "Save as JSON");
|
|||||||
export const saveExcelLabel = localize('saveExcelLabel', "Save as Excel");
|
export const saveExcelLabel = localize('saveExcelLabel', "Save as Excel");
|
||||||
export const saveXMLLabel = localize('saveXMLLabel', "Save as XML");
|
export const saveXMLLabel = localize('saveXMLLabel', "Save as XML");
|
||||||
export const viewChartLabel = localize('viewChartLabel', "View as Chart");
|
export const viewChartLabel = localize('viewChartLabel', "View as Chart");
|
||||||
|
export const viewVisualizerLabel = localize('viewVisualizerLabel', "Visualize");
|
||||||
|
|
||||||
export const resultPaneLabel = localize('resultPaneLabel', "Results");
|
export const resultPaneLabel = localize('resultPaneLabel', "Results");
|
||||||
export const executeQueryLabel = localize('executeQueryLabel', "Executing query ");
|
export const executeQueryLabel = localize('executeQueryLabel', "Executing query ");
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ suite('SQL QueryAction Tests', () => {
|
|||||||
|
|
||||||
test('setClass sets child CSS class correctly', (done) => {
|
test('setClass sets child CSS class correctly', (done) => {
|
||||||
// If I create a RunQueryAction
|
// 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
|
// "class should automatically get set to include the base class and the RunQueryAction class
|
||||||
let className = RunQueryAction.EnabledClass;
|
let className = RunQueryAction.EnabledClass;
|
||||||
@@ -93,7 +93,7 @@ suite('SQL QueryAction Tests', () => {
|
|||||||
editor.setup(x => x.input).returns(() => testQueryInput.object);
|
editor.setup(x => x.input).returns(() => testQueryInput.object);
|
||||||
|
|
||||||
// If I create a QueryTaskbarAction and I pass a non-connected editor to _getConnectedQueryEditorUri
|
// 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);
|
let connected: boolean = queryAction.isConnected(editor.object);
|
||||||
|
|
||||||
// I should get an unconnected state
|
// 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
|
// 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;
|
isConnected = false;
|
||||||
calledRunQueryOnInput = false;
|
calledRunQueryOnInput = false;
|
||||||
queryAction.run();
|
queryAction.run();
|
||||||
@@ -195,7 +195,7 @@ suite('SQL QueryAction Tests', () => {
|
|||||||
let queryModelService = TypeMoq.Mock.ofType(QueryModelService, TypeMoq.MockBehavior.Loose);
|
let queryModelService = TypeMoq.Mock.ofType(QueryModelService, TypeMoq.MockBehavior.Loose);
|
||||||
|
|
||||||
// If I call run on RunQueryAction when I have a non empty selection
|
// 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;
|
isSelectionEmpty = false;
|
||||||
queryAction.run();
|
queryAction.run();
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ suite('SQL QueryAction Tests', () => {
|
|||||||
/// End Setup Test ///
|
/// End Setup Test ///
|
||||||
|
|
||||||
////// If I call run on RunQueryAction while disconnected and with an undefined selection
|
////// 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;
|
isConnected = false;
|
||||||
selectionToReturnInGetSelection = undefined;
|
selectionToReturnInGetSelection = undefined;
|
||||||
queryAction.run();
|
queryAction.run();
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ suite('SQL QueryEditor Tests', () => {
|
|||||||
return new Promise((resolve) => resolve(mockEditor));
|
return new Promise((resolve) => resolve(mockEditor));
|
||||||
});
|
});
|
||||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((input) => {
|
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
|
// 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) => {
|
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
|
// Default
|
||||||
return new RunQueryAction(undefined, undefined, undefined);
|
return new RunQueryAction(undefined, undefined, undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock EditorDescriptorService to give us a mock editor description
|
// 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) => {
|
queryActionInstantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((input) => {
|
||||||
// Default
|
// Default
|
||||||
return new RunQueryAction(undefined, undefined, undefined);
|
return new RunQueryAction(undefined, undefined, undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup hook to capture calls to create the listDatabase action
|
// Setup hook to capture calls to create the listDatabase action
|
||||||
@@ -280,7 +280,7 @@ suite('SQL QueryEditor Tests', () => {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
// Default
|
// 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);
|
let fileInput = new UntitledEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService.object, undefined, undefined);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export interface IProductConfiguration {
|
|||||||
};
|
};
|
||||||
extensionTips: { [id: string]: string; };
|
extensionTips: { [id: string]: string; };
|
||||||
recommendedExtensions: string[]; // {{SQL CARBON EDIT}}
|
recommendedExtensions: string[]; // {{SQL CARBON EDIT}}
|
||||||
|
recommendedExtensionsByScenario: string[]; // {{SQL CARBON EDIT}}
|
||||||
extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; };
|
extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; };
|
||||||
readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; };
|
readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; };
|
||||||
readonly extensionKeywords: { [extension: string]: readonly string[]; };
|
readonly extensionKeywords: { [extension: string]: readonly string[]; };
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ export class SimpleExtensionTipsService implements IExtensionTipsService {
|
|||||||
getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } {
|
getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } {
|
||||||
return { global: [], workspace: [] };
|
return { global: [], workspace: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// {{SQL CARBON EDIT}}
|
||||||
|
getRecommendedExtensionsByScenario(scenarioType: string): Promise<IExtensionRecommendation[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
promptRecommendedExtensionsByScenario(scenarioType: string): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// {{SQL CARBON EDIT}} - End
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSingleton(IExtensionTipsService, SimpleExtensionTipsService, true);
|
registerSingleton(IExtensionTipsService, SimpleExtensionTipsService, true);
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async
|
|||||||
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||||
import { IProductService } from 'vs/platform/product/common/product';
|
import { IProductService } from 'vs/platform/product/common/product';
|
||||||
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
|
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 {
|
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
|
||||||
|
|
||||||
@@ -424,6 +427,19 @@ export class ExtensionsListView extends ViewletPanel {
|
|||||||
options.sortBy = SortBy.InstallCount;
|
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)) {
|
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
|
||||||
return this.getWorkspaceRecommendationsModel(query, options, token);
|
return this.getWorkspaceRecommendationsModel(query, options, token);
|
||||||
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
|
} 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<IPagedModel<IExtension>> {
|
||||||
|
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
|
// 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[] {
|
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workpsaceRecommendations: IExtensionRecommendation[]): string[] {
|
||||||
const totalCount = 8;
|
const totalCount = 8;
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ import { ITextModel } from 'vs/editor/common/model';
|
|||||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||||
import product from 'vs/platform/product/node/product';
|
import product from 'vs/platform/product/node/product';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
// {{SQL CARBON EDIT}}
|
||||||
import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
|
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 Severity from 'vs/base/common/severity';
|
||||||
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||||
import { IFileService } from 'vs/platform/files/common/files';
|
import { IFileService } from 'vs/platform/files/common/files';
|
||||||
@@ -1148,4 +1152,79 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||||||
private isExtensionAllowedToBeRecommended(id: string): boolean {
|
private isExtensionAllowedToBeRecommended(id: string): boolean {
|
||||||
return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
|
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<void>(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<IExtensionRecommendation[]> {
|
||||||
|
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 => (<IExtensionRecommendation>{ extensionId, sources: ['application'] })));
|
||||||
|
}
|
||||||
|
// {{SQL CARBON EDIT}} - End
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ export interface IExtensionTipsService {
|
|||||||
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void;
|
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void;
|
||||||
getAllIgnoredRecommendations(): { global: string[], workspace: string[] };
|
getAllIgnoredRecommendations(): { global: string[], workspace: string[] };
|
||||||
onRecommendationChange: Event<RecommendationChangeNotification>;
|
onRecommendationChange: Event<RecommendationChangeNotification>;
|
||||||
|
// {{SQL CARBON EDIT}}
|
||||||
|
getRecommendedExtensionsByScenario(scenarioType: string): Promise<IExtensionRecommendation[]>;
|
||||||
|
promptRecommendedExtensionsByScenario(scenarioType: string): void;
|
||||||
|
// {{SQL CARBON EDIT}} - End
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ExtensionRecommendationReason {
|
export const enum ExtensionRecommendationReason {
|
||||||
|
|||||||
Reference in New Issue
Block a user