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:
Rebecca Runxin Wang
2019-07-29 13:54:32 -07:00
committed by Rachel Kim
parent 720a7fbfa2
commit 2c8a22bb0d
28 changed files with 337 additions and 22 deletions

View File

@@ -34,4 +34,4 @@ export class ChartTab implements IPanelTab {
public clear() {
this.view.clear();
}
}
}

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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<boolean> {
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<boolean> {
this.runner.notifyVisualizeRequested(context.batchId, context.resultId);
return Promise.resolve(true);
}
}

View File

@@ -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<T> extends Disposable implements IView {
class GridTable<T> extends GridTableBase<T> {
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<T> extends GridTableBase<T> {
this.instantiationService.createInstance(ChartDataAction)
);
if (this.contextKeyService.getContextKeyValue('showVisualizer')) {
actions.push(this.instantiationService.createInstance(VisualizerDataAction, this._runner));
}
return actions;
}

View File

@@ -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");

View File

@@ -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';

View File

@@ -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 ");

View File

@@ -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();

View File

@@ -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);