diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 5de4c3c172..5d44721d8c 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -3576,6 +3576,31 @@ declare module 'azdata' { * Namespace for interacting with query editor */ export namespace queryeditor { + export type QueryEvent = + | 'queryStart' + | 'queryStop' + | 'executionPlan'; + + export interface QueryEventListener { + onQueryEvent(type: QueryEvent, document: queryeditor.QueryDocument, args: any); + } + + // new extensibility interfaces + export interface QueryDocument { + providerId: string; + + uri: string; + + // get the document's execution options + getOptions(): Map; + + // set the document's execution options + setOptions(options: Map): void; + + // tab content is build using the modelview UI builder APIs + // probably should rename DialogTab class since it is useful outside dialogs + createQueryTab(tab: window.DialogTab): void; + } /** * Make connection for the query editor @@ -3588,7 +3613,14 @@ declare module 'azdata' { * Run query if it is a query editor and it is already opened. * @param {string} fileUri file URI for the query editor */ - export function runQuery(fileUri: string): void; + export function runQuery(fileUri: string, options?: Map): void; + + /** + * Register a query event listener + */ + export function registerQueryEventListener(listener: queryeditor.QueryEventListener): void; + + export function getQueryDocument(fileUri: string): queryeditor.QueryDocument } /** diff --git a/src/sql/parts/query/editor/queryEditor.ts b/src/sql/parts/query/editor/queryEditor.ts index 91386b4736..21524855fd 100644 --- a/src/sql/parts/query/editor/queryEditor.ts +++ b/src/sql/parts/query/editor/queryEditor.ts @@ -968,4 +968,8 @@ export class QueryEditor extends BaseEditor { public get changeConnectionAction(): ConnectDatabaseAction { return this._changeConnectionAction; } + + public registerQueryModelViewTab(title: string, componentId: string): void { + this._resultsEditor.registerQueryModelViewTab(title, componentId); + } } diff --git a/src/sql/parts/query/editor/queryResultsEditor.ts b/src/sql/parts/query/editor/queryResultsEditor.ts index cc1b127e08..b90fb2e9ef 100644 --- a/src/sql/parts/query/editor/queryResultsEditor.ts +++ b/src/sql/parts/query/editor/queryResultsEditor.ts @@ -165,4 +165,8 @@ export class QueryResultsEditor extends BaseEditor { public showQueryPlan(xml: string) { this.resultsView.showPlan(xml); } + + public registerQueryModelViewTab(title: string, componentId: string): void { + this.resultsView.registerQueryModelViewTab(title, componentId); + } } diff --git a/src/sql/parts/query/editor/queryResultsView.ts b/src/sql/parts/query/editor/queryResultsView.ts index 04538ace20..ee3851f3fd 100644 --- a/src/sql/parts/query/editor/queryResultsView.ts +++ b/src/sql/parts/query/editor/queryResultsView.ts @@ -13,6 +13,7 @@ import { GridPanel } from './gridPanel'; import { ChartTab } from './charting/chartTab'; import { QueryPlanTab } from 'sql/parts/queryPlan/queryPlan'; import { TopOperationsTab } from 'sql/parts/queryPlan/topOperations'; +import { QueryModelViewTab } from 'sql/parts/query/modelViewTab/queryModelViewTab'; import * as nls from 'vs/nls'; import { PanelViewlet } from 'vs/workbench/browser/parts/views/panelViewlet'; @@ -175,12 +176,13 @@ export class QueryResultsView extends Disposable { private chartTab: ChartTab; private qpTab: QueryPlanTab; private topOperationsTab: TopOperationsTab; + private dynamicModelViewTabs: QueryModelViewTab[] = []; private runnerDisposables: IDisposable[]; constructor( container: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private instantiationService: IInstantiationService, @IQueryModelService private queryModelService: IQueryModelService ) { super(); @@ -207,6 +209,7 @@ export class QueryResultsView extends Disposable { this.runnerDisposables.push(runner.onQueryStart(e => { this.hideChart(); this.hidePlan(); + this.hideDynamicViewModelTabs(); this.input.state.visibleTabs = new Set(); this.input.state.activeTab = this.resultsTab.identifier; })); @@ -225,6 +228,23 @@ export class QueryResultsView extends Disposable { this._panelView.pushTab(this.topOperationsTab); } } + + // restore query model view tabs + this.input.state.visibleTabs.forEach(tabId => { + if (tabId.startsWith('querymodelview;')) { + // tab id format is 'tab type;title;model view id' + let parts = tabId.split(';'); + if (parts.length === 3) { + let tab = this._register(new QueryModelViewTab(parts[1], this.instantiationService)); + tab.view._componentId = parts[2]; + this.dynamicModelViewTabs.push(tab); + if (!this._panelView.contains(tab)) { + this._panelView.pushTab(tab); + } + } + } + }); + this.runnerDisposables.push(runner.onQueryEnd(() => { if (runner.isQueryPlan) { runner.planXml.then(e => { @@ -311,10 +331,35 @@ export class QueryResultsView extends Disposable { if (this._panelView.contains(this.qpTab)) { this._panelView.removeTab(this.qpTab.identifier); } + + if (this._panelView.contains(this.topOperationsTab)) { + this._panelView.removeTab(this.topOperationsTab.identifier); + } + } + + public hideDynamicViewModelTabs() { + this.dynamicModelViewTabs.forEach(tab => { + if (this._panelView.contains(tab)) { + this._panelView.removeTab(tab.identifier); + } + }); + + this.dynamicModelViewTabs = []; } public dispose() { dispose(this.runnerDisposables); super.dispose(); } + + public registerQueryModelViewTab(title: string, componentId: string): void { + let tab = this._register(new QueryModelViewTab(title, this.instantiationService)); + tab.view._componentId = componentId; + this.dynamicModelViewTabs.push(tab); + + this.input.state.visibleTabs.add('querymodelview;' + title + ';' + componentId); + if (!this._panelView.contains(tab)) { + this._panelView.pushTab(tab); + } + } } diff --git a/src/sql/parts/query/modelViewTab/media/dialogModal.css b/src/sql/parts/query/modelViewTab/media/dialogModal.css new file mode 100644 index 0000000000..d0954903a7 --- /dev/null +++ b/src/sql/parts/query/modelViewTab/media/dialogModal.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.dialogModal-body { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + min-width: 500px; + min-height: 600px; +} + +.modal.wide .dialogModal-body { + min-width: 800px; +} + +.dialog-message-and-page-container { + display: flex; + flex-direction: column; + flex: 1 1; + overflow: hidden; +} + +.dialogModal-page-container { + flex: 1 1; + overflow: hidden; +} + +.dialogModal-pane { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: scroll; +} + +.dialogModal-hidden { + display: none; +} + +.footer-button.dialogModal-hidden { + margin: 0; +} + +.footer-button .validating { + background-size: 15px; + background-repeat: no-repeat; + background-position: center; +} + +.vs .footer-button .validating { + background-image: url("loading.svg"); +} + +.vs-dark .footer-button .validating, +.hc-black .footer-button .validating { + background-image: url("loading_inverse.svg"); +} + +.dialogModal-wizardHeader { + padding: 10px 30px; +} + +.dialogModal-wizardHeader h1 { + margin-top: 10px; + margin-bottom: 3px; + font-size: 1.5em; + font-weight: lighter; +} + +.dialogContainer { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +} diff --git a/src/sql/parts/query/modelViewTab/media/loading.svg b/src/sql/parts/query/modelViewTab/media/loading.svg new file mode 100644 index 0000000000..e762f06d5e --- /dev/null +++ b/src/sql/parts/query/modelViewTab/media/loading.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/sql/parts/query/modelViewTab/media/loading_inverse.svg b/src/sql/parts/query/modelViewTab/media/loading_inverse.svg new file mode 100644 index 0000000000..c3633c0dda --- /dev/null +++ b/src/sql/parts/query/modelViewTab/media/loading_inverse.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/sql/parts/query/modelViewTab/media/wizardNavigation.css b/src/sql/parts/query/modelViewTab/media/wizardNavigation.css new file mode 100644 index 0000000000..b1d782d622 --- /dev/null +++ b/src/sql/parts/query/modelViewTab/media/wizardNavigation.css @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.wizardNavigation-container { + display: flex; + flex-direction: column; + width: 80px; + height: 100%; +} + +.hc-black .wizardNavigation-container { + border-right-color: #2b56f2; + border-right-style: solid; + border-right-width: 1px; + background-color: unset; +} + +.wizardNavigation-pageNumber { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-height: 100px; +} + +.wizardNavigation-pageNumber a { + text-decoration: none; +} + +.wizardNavigation-dot { + height: 30px; + width: 30px; + background-color: rgb(200, 200, 200); + color: white; + border-radius: 50%; + border-style: none; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; +} + +.hc-black .wizardNavigation-dot { + flex-grow: 1; + background-color: unset; + border-style: solid; + border-width: 1px; + border-color: white; +} + +.wizardNavigation-connector { + width: 3px; + display: inline-block; + flex-grow: 1; + background-color: rgb(200, 200, 200); +} + +.hc-black .wizardNavigation-connector { + display: none; +} + +.wizardNavigation-connector.active, +.wizardNavigation-dot.active { + background-color: rgb(9, 109, 201); +} + +.hc-black .wizardNavigation-dot.active { + border-color: #2b56f2; + background-color: unset; + border-style: solid; +} + +.hc-black .wizardNavigation-dot.active:hover, +.hc-black .wizardNavigation-dot.active.currentPage:hover { + border-color: #F38518; + border-style: dashed; +} + +.wizardNavigation-dot.active.currentPage { + border-style: double; +} + +.hc-black .wizardNavigation-dot.active.currentPage { + border-style: solid; + border-color: #F38518; +} + +.wizardNavigation-connector.invisible { + visibility: hidden; +} \ No newline at end of file diff --git a/src/sql/parts/query/modelViewTab/queryModelViewTab.module.ts b/src/sql/parts/query/modelViewTab/queryModelViewTab.module.ts new file mode 100644 index 0000000000..7b351a05d4 --- /dev/null +++ b/src/sql/parts/query/modelViewTab/queryModelViewTab.module.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. +*--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/dialogModal'; + +import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule, APP_BASE_HREF } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { WizardNavigation } from 'sql/platform/dialog/wizardNavigation.component'; +import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; +import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component'; +import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component'; +import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive'; +import { IBootstrapParams, ISelector, providerIterator } from 'sql/services/bootstrap/bootstrapService'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component'; +import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component'; +import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component'; + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { QueryModelViewTabContainer } from 'sql/parts/query/modelViewTab/queryModelViewTabContainer.component'; + +export const QueryModelViewTabModule = (params, selector: string, instantiationService: IInstantiationService): any => { + + /* Model-backed components */ + let extensionComponents = Registry.as(Extensions.ComponentContribution).getAllCtors(); + + @NgModule({ + declarations: [ + Checkbox, + SelectBox, + EditableDropDown, + InputBox, + QueryModelViewTabContainer, + WizardNavigation, + ModelViewContent, + ModelComponentWrapper, + ComponentHostDirective, + ...extensionComponents + ], + entryComponents: [QueryModelViewTabContainer, WizardNavigation, ...extensionComponents], + imports: [ + FormsModule, + CommonModule, + BrowserModule + ], + providers: [ + { provide: APP_BASE_HREF, useValue: '/' }, + CommonServiceInterface, + { provide: IBootstrapParams, useValue: params }, + { provide: ISelector, useValue: selector }, + ...providerIterator(instantiationService) + ] + }) + class ModuleClass { + + constructor( + @Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver, + @Inject(ISelector) private selector: string + ) { + } + + ngDoBootstrap(appRef: ApplicationRef) { + let componentClass = QueryModelViewTabContainer; + const factoryWrapper: any = this._resolver.resolveComponentFactory(componentClass); + factoryWrapper.factory.selector = this.selector; + appRef.bootstrap(factoryWrapper); + } + } + + return ModuleClass; +}; diff --git a/src/sql/parts/query/modelViewTab/queryModelViewTab.ts b/src/sql/parts/query/modelViewTab/queryModelViewTab.ts new file mode 100644 index 0000000000..abfaf653c9 --- /dev/null +++ b/src/sql/parts/query/modelViewTab/queryModelViewTab.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Dimension } from 'vs/base/browser/dom'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService'; +import { QueryModelViewTabModule } from 'sql/parts/query/modelViewTab/queryModelViewTab.module'; + +export class QueryModelViewTab implements IPanelTab { + public identifier = 'QueryModelViewTab_'; + public readonly view: QueryModelViewTabView; + + constructor(public title: string, @IInstantiationService instantiationService: IInstantiationService) { + this.identifier += title; + this.view = instantiationService.createInstance(QueryModelViewTabView); + } + + public dispose() { + dispose(this.view); + } + + public clear() { + this.view.clear(); + } +} + +export class QueryModelViewTabView implements IPanelView { + + public _componentId: string; + private _isInitialized: boolean = false; + + private _selector: string; + + constructor( + @IInstantiationService private _instantiationService: IInstantiationService) { + } + + public render(container: HTMLElement): void { + this.bootstrapAngular(container); + } + + dispose() { + } + + public clear() { + } + + public layout(dimension: Dimension): void { + } + + /** + * Load the angular components and record for this input that we have done so + */ + private bootstrapAngular(container: HTMLElement): string { + let uniqueSelector = bootstrapAngular(this._instantiationService, + QueryModelViewTabModule, + container, + 'querytab-modelview-container', + { modelViewId: this._componentId }); + return uniqueSelector; + } +} \ No newline at end of file diff --git a/src/sql/parts/query/modelViewTab/queryModelViewTabContainer.component.ts b/src/sql/parts/query/modelViewTab/queryModelViewTabContainer.component.ts new file mode 100644 index 0000000000..58f65e4c83 --- /dev/null +++ b/src/sql/parts/query/modelViewTab/queryModelViewTabContainer.component.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/dialogModal'; +import { Component, ViewChild, Inject, forwardRef, ElementRef, AfterViewInit } from '@angular/core'; +import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component'; +import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; +import { DialogPane } from 'sql/platform/dialog/dialogPane'; +import { ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { Event, Emitter } from 'vs/base/common/event'; + +export interface LayoutRequestParams { + modelViewId?: string; + alwaysRefresh?: boolean; +} +export interface DialogComponentParams extends IBootstrapParams { + modelViewId: string; + validityChangedCallback: (valid: boolean) => void; + onLayoutRequested: Event; + dialogPane: DialogPane; +} + +@Component({ + selector: 'querytab-modelview-container', + providers: [], + template: ` + + + ` +}) +export class QueryModelViewTabContainer implements AfterViewInit { + private _onResize = new Emitter(); + public readonly onResize: Event = this._onResize.event; + private _dialogPane: DialogPane; + + public modelViewId: string; + @ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent; + constructor( + @Inject(forwardRef(() => ElementRef)) private _el: ElementRef, + @Inject(IBootstrapParams) private _params: DialogComponentParams) { + this.modelViewId = this._params.modelViewId; + } + + ngAfterViewInit(): void { + this._modelViewContent.onEvent(event => { + if (event.isRootComponent && event.eventType === ComponentEventType.validityChanged) { + this._params.validityChangedCallback(event.args); + } + }); + let element = this._el.nativeElement; + element.style.height = '100%'; + element.style.width = '100%'; + } + + public layout(): void { + this._modelViewContent.layout(); + } +} diff --git a/src/sql/platform/query/common/queryModel.ts b/src/sql/platform/query/common/queryModel.ts index 28de7886e7..35e8ef81fd 100644 --- a/src/sql/platform/query/common/queryModel.ts +++ b/src/sql/platform/query/common/queryModel.ts @@ -16,7 +16,8 @@ import { EditSubsetResult, EditCreateRowResult, EditRevertCellResult, - ExecutionPlanOptions + ExecutionPlanOptions, + queryeditor } from 'azdata'; import { QueryInfo } from 'sql/platform/query/common/queryModelService'; @@ -24,6 +25,18 @@ export const SERVICE_ID = 'queryModelService'; export const IQueryModelService = createDecorator(SERVICE_ID); +export interface IQueryPlanInfo { + providerId: string; + fileUri: string; + planXml: string; +} + +export interface IQueryEvent { + type: queryeditor.QueryEvent; + uri: string; + params?: any; +} + /** * Interface for the logic of handling running queries and grid interactions for all URIs. */ @@ -56,7 +69,7 @@ export interface IQueryModelService { onRunQueryStart: Event; onRunQueryComplete: Event; - + onQueryEvent: Event; // Edit Data Functions initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): void; diff --git a/src/sql/platform/query/common/queryModelService.ts b/src/sql/platform/query/common/queryModelService.ts index 40c4673dc8..bf623767cf 100644 --- a/src/sql/platform/query/common/queryModelService.ts +++ b/src/sql/platform/query/common/queryModelService.ts @@ -9,7 +9,7 @@ import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents'; import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants'; import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner'; import { DataService } from 'sql/parts/grid/services/dataService'; -import { IQueryModelService } from 'sql/platform/query/common/queryModel'; +import { IQueryModelService, IQueryPlanInfo, IQueryEvent } from 'sql/platform/query/common/queryModel'; import { QueryInput } from 'sql/parts/query/common/queryInput'; import { QueryStatusbarItem } from 'sql/parts/query/execution/queryStatus'; import { SqlFlavorStatusbarItem } from 'sql/parts/query/common/flavorStatus'; @@ -68,11 +68,13 @@ export class QueryModelService implements IQueryModelService { private _queryInfoMap: Map; private _onRunQueryStart: Emitter; private _onRunQueryComplete: Emitter; + private _onQueryEvent: Emitter; private _onEditSessionReady: Emitter; // EVENTS ///////////////////////////////////////////////////////////// public get onRunQueryStart(): Event { return this._onRunQueryStart.event; } public get onRunQueryComplete(): Event { return this._onRunQueryComplete.event; } + public get onQueryEvent(): Event { return this._onQueryEvent.event; } public get onEditSessionReady(): Event { return this._onEditSessionReady.event; } // CONSTRUCTOR ///////////////////////////////////////////////////////// @@ -83,6 +85,7 @@ export class QueryModelService implements IQueryModelService { this._queryInfoMap = new Map(); this._onRunQueryStart = new Emitter(); this._onRunQueryComplete = new Emitter(); + this._onQueryEvent = new Emitter(); this._onEditSessionReady = new Emitter(); // Register Statusbar items @@ -308,13 +311,40 @@ export class QueryModelService implements IQueryModelService { }); queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => { this._onRunQueryComplete.fire(uri); + + // fire extensibility API event + let event: IQueryEvent = { + type: 'queryStop', + uri: uri + }; + this._onQueryEvent.fire(event); + + // fire UI event this._fireQueryEvent(uri, 'complete', totalMilliseconds); }); queryRunner.addListener(QREvents.START, () => { this._onRunQueryStart.fire(uri); + + // fire extensibility API event + let event: IQueryEvent = { + type: 'queryStart', + uri: uri + }; + this._onQueryEvent.fire(event); + this._fireQueryEvent(uri, 'start'); }); + queryRunner.addListener(QREvents.QUERY_PLAN_AVAILABLE, (planInfo) => { + // fire extensibility API event + let event: IQueryEvent = { + type: 'executionPlan', + uri: planInfo.fileUri, + params: planInfo + }; + this._onQueryEvent.fire(event); + }); + info.queryRunner = queryRunner; info.dataService = this._instantiationService.createInstance(DataService, uri); this._queryInfoMap.set(uri, info); @@ -422,10 +452,26 @@ export class QueryModelService implements IQueryModelService { }); queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => { this._onRunQueryComplete.fire(ownerUri); + // fire extensibility API event + let event: IQueryEvent = { + type: 'queryStop', + uri: ownerUri + }; + this._onQueryEvent.fire(event); + + // fire UI event this._fireQueryEvent(ownerUri, 'complete', totalMilliseconds); }); queryRunner.addListener(QREvents.START, () => { this._onRunQueryStart.fire(ownerUri); + // fire extensibility API event + let event: IQueryEvent = { + type: 'queryStart', + uri: ownerUri + }; + this._onQueryEvent.fire(event); + + // fire UI event this._fireQueryEvent(ownerUri, 'start'); }); queryRunner.addListener(QREvents.EDIT_SESSION_READY, e => { diff --git a/src/sql/platform/query/common/queryRunner.ts b/src/sql/platform/query/common/queryRunner.ts index 9986f1e24e..23c1021e7f 100644 --- a/src/sql/platform/query/common/queryRunner.ts +++ b/src/sql/platform/query/common/queryRunner.ts @@ -25,6 +25,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResultSerializer } from 'sql/platform/node/resultSerializer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQueryPlanInfo } from 'sql/platform/query/common/queryModel'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { URI } from 'vs/base/common/uri'; @@ -41,7 +42,8 @@ export const enum EventType { BATCH_START = 'batchStart', BATCH_COMPLETE = 'batchComplete', RESULT_SET = 'resultSet', - EDIT_SESSION_READY = 'editSessionReady' + EDIT_SESSION_READY = 'editSessionReady', + QUERY_PLAN_AVAILABLE = 'queryPlanAvailable' } export interface IEventType { @@ -52,6 +54,7 @@ export interface IEventType { batchComplete: azdata.BatchSummary; resultSet: azdata.ResultSetSummary; editSessionReady: IEditSessionReadyEvent; + queryPlanAvailable: IQueryPlanInfo; } export interface IGridMessage extends azdata.IResultMessage { @@ -363,7 +366,11 @@ export default class QueryRunner extends Disposable { // check if this result has show plan, this needs work, it won't work for any other provider let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); if (hasShowPlan) { - this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue)); + this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => { + if (e.resultSubset.rows) { + this._planXml.resolve(e.resultSubset.rows[0][0].displayValue); + } + }); } } // we will just ignore the set if we already have it @@ -387,7 +394,20 @@ export default class QueryRunner extends Disposable { // check if this result has show plan, this needs work, it won't work for any other provider let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); if (hasShowPlan) { - this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue)); + this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => { + if (e.resultSubset.rows) { + let planXmlString = e.resultSubset.rows[0][0].displayValue; + this._planXml.resolve(e.resultSubset.rows[0][0].displayValue); + // fire query plan available event if execution is completed + if (result.resultSetSummary.complete) { + this._eventEmitter.emit(EventType.QUERY_PLAN_AVAILABLE, { + providerId: 'MSSQL', + fileUri: result.ownerUri, + planXml: planXmlString + }); + } + } + }); } } if (batchSet) { diff --git a/src/sql/workbench/api/node/extHostQueryEditor.ts b/src/sql/workbench/api/node/extHostQueryEditor.ts index a73b30edb3..54235e2a8c 100644 --- a/src/sql/workbench/api/node/extHostQueryEditor.ts +++ b/src/sql/workbench/api/node/extHostQueryEditor.ts @@ -7,11 +7,35 @@ import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostQueryEditorShape, SqlMainContext, MainThreadQueryEditorShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import * as azdata from 'azdata'; -import * as vscode from 'vscode'; +import { IQueryEvent } from 'sql/platform/query/common/queryModel'; + +class ExtHostQueryDocument implements azdata.queryeditor.QueryDocument { + constructor( + public providerId: string, + public uri: string, + private _proxy: MainThreadQueryEditorShape) { + } + + // get the document's execution options + getOptions(): Map { + return undefined; + } + + // set the document's execution optionsß + setOptions(options: Map): void { + + } + + createQueryTab(tab: azdata.window.DialogTab): void { + this._proxy.$createQueryTab(this.uri, tab.title, tab.content); + } +} export class ExtHostQueryEditor implements ExtHostQueryEditorShape { private _proxy: MainThreadQueryEditorShape; + private _nextListenerHandle: number = 0; + private _queryListeners = new Map(); constructor( mainContext: IMainContext @@ -26,4 +50,17 @@ export class ExtHostQueryEditor implements ExtHostQueryEditorShape { public $runQuery(fileUri: string): void { return this._proxy.$runQuery(fileUri); } + + public $registerQueryInfoListener(providerId: string, listener: azdata.queryeditor.QueryEventListener): void { + this._queryListeners[this._nextListenerHandle] = listener; + this._proxy.$registerQueryInfoListener(this._nextListenerHandle, providerId); + this._nextListenerHandle++; + } + + public $onQueryEvent(handle: number, fileUri:string, event: IQueryEvent): void { + let listener: azdata.queryeditor.QueryEventListener = this._queryListeners[handle]; + if (listener) { + listener.onQueryEvent(event.type, new ExtHostQueryDocument('MSSQL', fileUri, this._proxy), event.params.planXml); + } + } } diff --git a/src/sql/workbench/api/node/mainThreadQueryEditor.ts b/src/sql/workbench/api/node/mainThreadQueryEditor.ts index 3444d91858..b39376f316 100644 --- a/src/sql/workbench/api/node/mainThreadQueryEditor.ts +++ b/src/sql/workbench/api/node/mainThreadQueryEditor.ts @@ -12,6 +12,7 @@ import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/q import { QueryEditor } from 'sql/parts/query/editor/queryEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IQueryModelService, IQueryEvent } from 'sql/platform/query/common/queryModel'; @extHostNamedCustomer(SqlMainContext.MainThreadQueryEditor) export class MainThreadQueryEditor implements MainThreadQueryEditorShape { @@ -23,6 +24,7 @@ export class MainThreadQueryEditor implements MainThreadQueryEditorShape { extHostContext: IExtHostContext, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IQueryEditorService private _queryEditorService: IQueryEditorService, + @IQueryModelService private _queryModelService: IQueryModelService, @IEditorService private _editorService: IEditorService ) { if (extHostContext) { @@ -75,4 +77,24 @@ export class MainThreadQueryEditor implements MainThreadQueryEditorShape { } } } + + public $registerQueryInfoListener(handle: number, providerId: string): void { + this._toDispose.push(this._queryModelService.onQueryEvent(event => { + this._proxy.$onQueryEvent(handle, event.uri, event); + })); + } + + public $createQueryTab(fileUri: string, title: string, componentId: string): void { + let editors = this._editorService.visibleControls.filter(resource => { + return !!resource && resource.input.getResource().toString() === fileUri; + }); + + let editor = editors && editors.length > 0 ? editors[0] : undefined; + if (editor) { + let queryEditor = editor as QueryEditor; + if (queryEditor) { + queryEditor.registerQueryModelViewTab(title, componentId); + } + } + } } diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 468f968f06..5035a6fdaa 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -438,13 +438,20 @@ export function createApiFactory( // namespace: queryeditor const queryEditor: typeof azdata.queryeditor = { - connect(fileUri: string, connectionId: string): Thenable { return extHostQueryEditor.$connect(fileUri, connectionId); }, - runQuery(fileUri: string): void { + runQuery(fileUri: string, options?: Map): void { extHostQueryEditor.$runQuery(fileUri); + }, + + registerQueryEventListener(listener: azdata.queryeditor.QueryEventListener): void { + extHostQueryEditor.$registerQueryInfoListener('MSSQL', listener); + }, + + getQueryDocument(fileUri: string): azdata.queryeditor.QueryDocument { + return undefined; } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 2595092b42..d8e474e562 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -24,6 +24,7 @@ import { import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { IUndoStopOptions } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IQueryEvent } from 'sql/platform/query/common/queryModel'; export abstract class ExtHostAccountManagementShape { $autoOAuthCancelled(handle: number): Thenable { throw ni(); } @@ -757,11 +758,14 @@ export interface MainThreadModelViewDialogShape extends IDisposable { $setDirty(handle: number, isDirty: boolean): void; } export interface ExtHostQueryEditorShape { + $onQueryEvent(handle: number, fileUri:string, event: IQueryEvent): void; } export interface MainThreadQueryEditorShape extends IDisposable { $connect(fileUri: string, connectionId: string): Thenable; $runQuery(fileUri: string): void; + $createQueryTab(fileUri: string, title: string, content: string): void; + $registerQueryInfoListener(handle: number, providerId: string): void; } export interface ExtHostNotebookShape { @@ -870,4 +874,4 @@ export interface ExtHostExtensionManagementShape { export interface MainThreadExtensionManagementShape extends IDisposable { $install(vsixPath: string): Thenable; -} +} \ No newline at end of file