diff --git a/src/sql/workbench/contrib/query/browser/queryResultsView.ts b/src/sql/workbench/contrib/query/browser/queryResultsView.ts index d89aedff57..5d7acbb02d 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsView.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsView.ts @@ -300,8 +300,8 @@ export class QueryResultsView extends Disposable { this.dynamicModelViewTabs.forEach(t => t.clear()); this.resultsTab.view.state = this.input.state.gridPanelState; - this.qpTab.view.state = this.input.state.queryPlanState; - this.topOperationsTab.view.state = this.input.state.topOperationsState; + this.qpTab.view.setState(this.input.state.queryPlanState); + this.topOperationsTab.view.setState(this.input.state.topOperationsState); this.chartTab.view.state = this.input.state.chartState; this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => { dynamicTab.captureState(this.input.state.dynamicModelViewTabsState); diff --git a/src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts b/src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts index c575fe87fc..3cff2ee841 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ class RunTimeInformation { - runtimePerThreads: RuntimePerThread[]; + constructor(public runtimePerThreads?: RuntimePerThread[]) { } + public get actualRows(): number { let total = 0; if (this.runtimePerThreads) { this.runtimePerThreads.forEach(element => { - total += element.actualRow; + total += element.actualRow ?? 0; }); } @@ -20,26 +21,26 @@ class RunTimeInformation { let total = 0; if (this.runtimePerThreads) { this.runtimePerThreads.forEach(element => { - total += element.actualExecutions; + total += element.actualExecutions ?? 0; }); } return total; } } -class RuntimePerThread { - threadId: number; - actualRow: number; - actualExecutionMode: string; - actualExecutions: number; +interface RuntimePerThread { + threadId?: number; + actualRow?: number; + actualExecutionMode?: string; + actualExecutions?: number; } class IndexObject { - database: string; - schema: string; - table: string; - index: string; - indexKind: string; + public database?: string; + public schema?: string; + public table?: string; + public index?: string; + public indexKind?: string; public get title() { let title: string = ''; @@ -54,22 +55,23 @@ class IndexObject { } class PlanNode { - root: PlanNode; - subtreeCost: number; - private childrenNodes: PlanNode[]; - parent: PlanNode; - physicalOp: string; - logicalOp: string; - id: number; - estimateRows: string; - estimateIo: string; - estimateCpu: string; - parallel: boolean; - partitioned: boolean; - estimateRewinds: string; - estimateRebinds: string; - runtimeInfo: RunTimeInformation; - indexObject: IndexObject; + private childrenNodes: PlanNode[] = []; + + public root?: PlanNode; + public subtreeCost?: number; + public parent?: PlanNode; + public physicalOp?: string; + public logicalOp?: string; + public id?: number; + public estimateRows?: string; + public estimateIo?: string; + public estimateCpu?: string; + public parallel?: boolean; + public partitioned?: boolean; + public estimateRewinds?: string; + public estimateRebinds?: string; + public runtimeInfo?: RunTimeInformation; + public indexObject?: IndexObject; public addChildren(children: PlanNode[]): void { if (children) { @@ -81,10 +83,10 @@ class PlanNode { } public get totalSubTreeCost(): number { - let total = this.subtreeCost; + let total = this.subtreeCost ?? 0; if (total === 0) { this.children.forEach(element => { - total += element.subtreeCost; + total += element.subtreeCost ?? 0; }); } return total; @@ -95,17 +97,17 @@ class PlanNode { } public get cost(): number { - let total = this.subtreeCost; + let total = this.subtreeCost ?? 0; if (this.children && total !== 0) { this.children.forEach(element => { - total -= element.subtreeCost; + total -= element.subtreeCost ?? 0; }); } return total; } public get relativeCost(): number { - let overallCost = this.root.totalSubTreeCost; + let overallCost = this.root?.totalSubTreeCost ?? 0; return overallCost > 0 ? this.cost / overallCost : 0; } @@ -125,7 +127,7 @@ class PlanNode { public get title(): string { if (this.physicalOp === this.logicalOp) { - return this.physicalOp; + return this.physicalOp ?? ''; } else { return `${this.physicalOp}(${this.logicalOp})`; } @@ -141,10 +143,10 @@ class PlanNode { } export class PlanXmlParser { - parser: DOMParser = new DOMParser(); - doc: Document; - planXml: string; - root: PlanNode; + public parser: DOMParser = new DOMParser(); + public doc?: Document; + public planXml?: string; + public root?: PlanNode; constructor(planXml: string) { @@ -179,7 +181,9 @@ export class PlanXmlParser { public get toTreeViewList(): PlanNode[] { let operations: PlanNode[] = []; - operations = this.addOperationsToList(operations, this.root.children); + if (this.root) { + operations = this.addOperationsToList(operations, this.root.children); + } return operations; } @@ -192,7 +196,7 @@ export class PlanXmlParser { return list; } - private findChildren(element: Element, elementName: string, untilNode: string = undefined): Element[] { + private findChildren(element: Element, elementName: string, untilNode: string | undefined = undefined): Element[] | undefined { let elements: Element[] = []; if (element === undefined) { return undefined; @@ -251,10 +255,10 @@ export class PlanXmlParser { private convertToPlanNode(element: Element): PlanNode { let planNode = new PlanNode(); - planNode.id = this.findAttribute(element.attributes, 'NodeId'); + planNode.id = Number(this.findAttribute(element.attributes, 'NodeId')); planNode.logicalOp = this.findAttribute(element.attributes, 'LogicalOp'); planNode.physicalOp = this.findAttribute(element.attributes, 'PhysicalOp'); - planNode.subtreeCost = +this.findAttribute(element.attributes, 'EstimatedTotalSubtreeCost'); + planNode.subtreeCost = Number(this.findAttribute(element.attributes, 'EstimatedTotalSubtreeCost')); planNode.estimateRows = this.findAttribute(element.attributes, 'EstimateRows'); planNode.estimateCpu = this.findAttribute(element.attributes, 'EstimateCPU'); planNode.estimateIo = this.findAttribute(element.attributes, 'EstimateIO'); @@ -266,12 +270,12 @@ export class PlanXmlParser { } private convertToRuntimeInfo(element: Element): RuntimePerThread { - let runtimeNode = new RuntimePerThread(); - runtimeNode.actualExecutionMode = this.findAttribute(element.attributes, 'ActualExecutionMode'); - runtimeNode.actualExecutions = +this.findAttribute(element.attributes, 'ActualExecutions'); - runtimeNode.actualRow = +this.findAttribute(element.attributes, 'ActualRows'); - runtimeNode.threadId = +this.findAttribute(element.attributes, 'Thread'); - return runtimeNode; + return { + actualExecutionMode: this.findAttribute(element.attributes, 'ActualExecutionMode'), + actualExecutions: Number(this.findAttribute(element.attributes, 'ActualExecutions')), + actualRow: Number(this.findAttribute(element.attributes, 'ActualRows')), + threadId: Number(this.findAttribute(element.attributes, 'Thread')) + }; } private convertToObject(element: Element): IndexObject { @@ -284,12 +288,13 @@ export class PlanXmlParser { return objectNode; } - private findAttribute(attributes: NamedNodeMap, attName: string): any { + private findAttribute(attributes: NamedNodeMap, attName: string): string | undefined { for (let index = 0; index < attributes.length; index++) { let attribute = attributes[index]; if (attribute.name === attName) { return attribute.value; } } + return undefined; } } diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.component.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.component.ts deleted file mode 100644 index 5000c1d90b..0000000000 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./media/qp'; - -import { ElementRef, Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import * as QP from 'html-query-plan'; - -import { IQueryPlanParams, IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; - -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService'; -import * as colors from 'vs/platform/theme/common/colorRegistry'; - -export const QUERYPLAN_SELECTOR: string = 'queryplan-component'; - -@Component({ - selector: QUERYPLAN_SELECTOR, - template: ` -
-
- ` -}) -export class QueryPlanComponent implements OnDestroy, OnInit { - - private _planXml: string; - private _disposables: Array = []; - @ViewChild('container', { read: ElementRef }) _container: ElementRef; - - constructor( - @Inject(IBootstrapParams) private _params: IQueryPlanParams - ) { } - - ngOnDestroy() { - dispose(this._disposables); - } - - ngOnInit() { - if (this._params) { - this.planXml = this._params.planXml; - } - this._disposables.push(registerThemingParticipant(this._updateTheme)); - } - - public set planXml(val: string) { - this._planXml = val; - if (this._planXml) { - QP.showPlan(this._container.nativeElement, this._planXml, { - jsTooltips: false - }); - } - } - - private _updateTheme(theme: IColorTheme, collector: ICssStyleCollector) { - let backgroundColor = theme.getColor(colors.editorBackground); - let foregroundColor = theme.getColor(colors.editorForeground); - - if (backgroundColor) { - collector.addRule(`div.qp-node, .qp-tt, .qp-root { background-color: ${backgroundColor} }`); - } - - if (foregroundColor) { - collector.addRule(`.qp-root { color: ${foregroundColor} }`); - } - } -} diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.module.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.module.ts deleted file mode 100644 index cd9100834d..0000000000 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.module.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NgModule, Inject, forwardRef, ApplicationRef, ComponentFactoryResolver, Type } from '@angular/core'; -import { APP_BASE_HREF, CommonModule } from '@angular/common'; -import { BrowserModule } from '@angular/platform-browser'; - -import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService'; -import { QueryPlanComponent } from 'sql/workbench/contrib/queryPlan/browser/queryPlan.component'; - -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; - -// Connection Dashboard main angular module -export const QueryPlanModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type => { - - @NgModule({ - declarations: [ - QueryPlanComponent - ], - entryComponents: [QueryPlanComponent], - imports: [ - CommonModule, - BrowserModule - ], - providers: [ - { provide: APP_BASE_HREF, useValue: '/' }, - { 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) { - const factory = this._resolver.resolveComponentFactory(QueryPlanComponent); - (factory).factory.selector = this.selector; - appRef.bootstrap(factory); - } - } - - return ModuleClass; -}; diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts index e923849e67..baca734e92 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts @@ -31,10 +31,10 @@ export class QueryPlanTab implements IPanelTab { } export class QueryPlanView implements IPanelView { - private qp: QueryPlan; - private xml: string; + private qp?: QueryPlan; + private xml?: string; private container = document.createElement('div'); - private _state: QueryPlanState; + private _state?: QueryPlanState; public render(container: HTMLElement): void { container.appendChild(this.container); @@ -50,7 +50,6 @@ export class QueryPlanView implements IPanelView { dispose() { this.container.remove(); this.qp = undefined; - this.container = undefined; } public layout(dimension: Dimension): void { @@ -79,38 +78,38 @@ export class QueryPlanView implements IPanelView { } } - public set state(val: QueryPlanState) { + public setState(val: QueryPlanState) { this._state = val; - if (this.state.xml) { - this.showPlan(this.state.xml); + if (this._state.xml) { + this.showPlan(this._state.xml); } } - public get state(): QueryPlanState { + public get state(): QueryPlanState | undefined { return this._state; } } export class QueryPlan { - private _xml: string; + private _xml?: string; constructor(private container: HTMLElement) { } - public set xml(xml: string) { + public set xml(xml: string | undefined) { this._xml = xml; clearNode(this.container); if (this.xml) { - QP.showPlan(this.container, this._xml, { + QP.showPlan(this.container, this.xml, { jsTooltips: false }); - (this.container.querySelectorAll('div.qp-tt')).forEach(toolTip => { + this.container.querySelectorAll('div.qp-tt').forEach(toolTip => { toolTip.classList.add('monaco-editor'); toolTip.classList.add('monaco-editor-hover'); }); } } - public get xml(): string { + public get xml(): string | undefined { return this._xml; } } diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts index 29dc3724c6..6a8c6dfd25 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts @@ -8,23 +8,20 @@ import { EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { QueryPlanInput } from 'sql/workbench/contrib/queryPlan/common/queryPlanInput'; -import { QueryPlanModule } from 'sql/workbench/contrib/queryPlan/browser/queryPlan.module'; -import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService'; -import { IQueryPlanParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; -import { QUERYPLAN_SELECTOR } from 'sql/workbench/contrib/queryPlan/browser/queryPlan.component'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { QueryPlanView } from 'sql/workbench/contrib/queryPlan/browser/queryPlan'; export class QueryPlanEditor extends BaseEditor { public static ID: string = 'workbench.editor.queryplan'; + private view = this._register(new QueryPlanView()); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IInstantiationService private instantiationService: IInstantiationService, @IStorageService storageService: IStorageService ) { super(QueryPlanEditor.ID, telemetryService, themeService, storageService); @@ -45,12 +42,14 @@ export class QueryPlanEditor extends BaseEditor { //(plus it's probable that we won't be using this control in the future anyways if development) //continues on the Query plan feature parent.style.background = '#fff'; + this.view.render(parent); } /** * Sets focus on this editor. Specifically, it sets the focus on the hosted text editor. */ public focus(): void { + this.view.focus(); } /** @@ -58,6 +57,7 @@ export class QueryPlanEditor extends BaseEditor { * To be called when the container of this editor changes size. */ public layout(dimension: DOM.Dimension): void { + this.view.layout(dimension); } public async setInput(input: QueryPlanInput, options: EditorOptions): Promise { @@ -65,49 +65,10 @@ export class QueryPlanEditor extends BaseEditor { return Promise.resolve(undefined); } await input.resolve(); - if (!input.hasInitialized) { - this.bootstrapAngular(input); - } - this.revealElementWithTagName(input.uniqueSelector, this.getContainer()); - return super.setInput(input, options, CancellationToken.None); - } + await super.setInput(input, options, CancellationToken.None); - /** - * Reveal the child element with the given tagName and hide all other elements. - */ - private revealElementWithTagName(tagName: string, parent: HTMLElement): void { - let elementToReveal: HTMLElement; - - for (let i = 0; i < parent.children.length; i++) { - let child: HTMLElement = parent.children[i]; - if (child.tagName && child.tagName.toLowerCase() === tagName && !elementToReveal) { - elementToReveal = child; - } else { - child.style.display = 'none'; - } - } - - if (elementToReveal) { - elementToReveal.style.display = ''; - } - } - - /** - * Load the angular components and record for this input that we have done so - */ - private bootstrapAngular(input: QueryPlanInput): void { - // Get the bootstrap params and perform the bootstrap - let params: IQueryPlanParams = { - planXml: input.planXml - }; - - let uniqueSelector = this.instantiationService.invokeFunction(bootstrapAngular, - QueryPlanModule, - this.getContainer(), - QUERYPLAN_SELECTOR, - params); - input.setUniqueSelector(uniqueSelector); + this.view.showPlan(input.planXml!); } public dispose(): void { diff --git a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts index d94d30dac3..e402b20f0d 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts @@ -50,7 +50,7 @@ export class TopOperationsTab extends Disposable implements IPanelTab { } export class TopOperationsView extends Disposable implements IPanelView { - private _state: TopOperationsState; + private _state?: TopOperationsState; private table: Table; private container = document.createElement('div'); private dataView = new TableDataView(); @@ -86,19 +86,19 @@ export class TopOperationsView extends Disposable implements IPanelView { } public showPlan(xml: string) { - this.state.xml = xml; + this.state!.xml = xml; this.dataView.clear(); let parser = new PlanXmlParser(xml); let operations = parser.topOperations; let data = operations.map(i => { return { operation: i.title, - object: i.indexObject.title, + object: i.indexObject?.title, estCost: i.estimatedOperatorCost, estSubtreeCost: i.subtreeCost, - actualRows: i.runtimeInfo.actualRows, + actualRows: i.runtimeInfo?.actualRows, estRows: i.estimateRows, - actualExecutions: i.runtimeInfo.actualExecutions, + actualExecutions: i.runtimeInfo?.actualExecutions, estCPUCost: i.estimateCpu, estIOCost: i.estimateIo, parallel: i.parallel, @@ -112,14 +112,14 @@ export class TopOperationsView extends Disposable implements IPanelView { this.dataView.push(data); } - public set state(val: TopOperationsState) { + public setState(val: TopOperationsState) { this._state = val; - if (this.state.xml) { - this.showPlan(this.state.xml); + if (this._state.xml) { + this.showPlan(this._state.xml); } } - public get state(): TopOperationsState { + public get state(): TopOperationsState | undefined { return this._state; } } diff --git a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts index de12d64993..e26cea556f 100644 --- a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts +++ b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts @@ -4,24 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { EditorInput, EditorModel, IEditorInput } from 'vs/workbench/common/editor'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILanguageAssociation } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class QueryPlanConverter implements ILanguageAssociation { static readonly languages = ['sqlplan']; - constructor(@IInstantiationService private instantiationService: IInstantiationService) { } + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IEditorService private readonly editorService: IEditorService + ) { } - convertInput(activeEditor: IEditorInput): QueryPlanInput { - return this.instantiationService.createInstance(QueryPlanInput, activeEditor.resource); + convertInput(activeEditor: IEditorInput): QueryPlanInput | undefined { + if (activeEditor.resource) { + return this.instantiationService.createInstance(QueryPlanInput, activeEditor.resource); + } + return undefined; } createBase(activeEditor: QueryPlanInput): IEditorInput { - return undefined; + return this.editorService.createEditorInput({ resource: activeEditor.resource }); } } @@ -30,8 +36,7 @@ export class QueryPlanInput extends EditorInput { public static ID: string = 'workbench.editorinputs.queryplan'; public static SCHEMA: string = 'queryplan'; - private _uniqueSelector: string; - private _xml: string; + private _xml?: string; constructor( private _uri: URI, @@ -40,10 +45,6 @@ export class QueryPlanInput extends EditorInput { super(); } - public setUniqueSelector(uniqueSelector: string): void { - this._uniqueSelector = uniqueSelector; - } - public getTypeId(): string { return UntitledTextEditorInput.ID; } @@ -52,7 +53,7 @@ export class QueryPlanInput extends EditorInput { return 'Query Plan'; } - public get planXml(): string { + public get planXml(): string | undefined { return this._xml; } @@ -64,24 +65,11 @@ export class QueryPlanInput extends EditorInput { return false; } - public getConnectionProfile(): IConnectionProfile { - //return this._connection.connectionProfile; - return undefined; - } - - public async resolve(refresh?: boolean): Promise { + public async resolve(refresh?: boolean): Promise { if (!this._xml) { this._xml = (await this.fileService.readFile(this._uri)).value.toString(); } - return undefined; - } - - public get hasInitialized(): boolean { - return !!this._uniqueSelector; - } - - public get uniqueSelector(): string { - return this._uniqueSelector; + return null; } get resource(): URI | undefined { diff --git a/src/tsconfig.vscode.json b/src/tsconfig.vscode.json index 6ee5e2955b..8debee8742 100644 --- a/src/tsconfig.vscode.json +++ b/src/tsconfig.vscode.json @@ -51,7 +51,6 @@ "./sql/workbench/contrib/profiler/**/*.ts", // 204 errors "./sql/workbench/contrib/query/**/*.ts", // 3342 errors "./sql/workbench/contrib/queryHistory/**/*.ts", // 432 errors - "./sql/workbench/contrib/queryPlan/**/*.ts", // 52 errors "./sql/workbench/contrib/restore/**/*.ts", // 142 errors "./sql/workbench/contrib/scripting/**/*.ts", // 280 errors "./sql/workbench/contrib/tasks/**/*.ts", // 100 errors