From 21016c1a5329bca3d47b061c2245e1bf0d1943de Mon Sep 17 00:00:00 2001
From: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Date: Thu, 7 Sep 2023 13:13:08 -0700
Subject: [PATCH] add button to open query store report in new tab (#24303)
* add button to open query store report in new tab
* addressing comments
---
.../images/dark/multiple-windows.svg | 3 ++
.../images/light/multiple-windows.svg | 3 ++
.../query-store/src/common/constants.ts | 5 +++
.../query-store/src/common/iconHelper.ts | 40 +++++++++++++++++++
extensions/query-store/src/common/promise.ts | 25 ++++++++++++
extensions/query-store/src/extension.ts | 12 +++++-
.../src/reports/baseQueryStoreReport.ts | 33 ++++++++++++---
.../src/reports/overallResourceConsumption.ts | 5 +--
.../src/reports/queryStoreDashboard.ts | 31 +++++++++-----
.../reports/topResourceConsumingQueries.ts | 5 +--
10 files changed, 139 insertions(+), 23 deletions(-)
create mode 100644 extensions/query-store/images/dark/multiple-windows.svg
create mode 100644 extensions/query-store/images/light/multiple-windows.svg
create mode 100644 extensions/query-store/src/common/iconHelper.ts
create mode 100644 extensions/query-store/src/common/promise.ts
diff --git a/extensions/query-store/images/dark/multiple-windows.svg b/extensions/query-store/images/dark/multiple-windows.svg
new file mode 100644
index 0000000000..f667cc0580
--- /dev/null
+++ b/extensions/query-store/images/dark/multiple-windows.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/query-store/images/light/multiple-windows.svg b/extensions/query-store/images/light/multiple-windows.svg
new file mode 100644
index 0000000000..2859a0af88
--- /dev/null
+++ b/extensions/query-store/images/light/multiple-windows.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/query-store/src/common/constants.ts b/extensions/query-store/src/common/constants.ts
index 0b1a9db6a9..97b618193a 100644
--- a/extensions/query-store/src/common/constants.ts
+++ b/extensions/query-store/src/common/constants.ts
@@ -9,6 +9,10 @@ const localize = nls.loadMessageBundle();
export function queryStoreDashboardTitle(databaseName: string): string { return localize('queryStoreDashboardTitle', "Query Store - {0}", databaseName); }
+// report dashboard tab ids
+export const overallResourceConsumptionTabId = 'OverallResourceConsumptionTab';
+export const topResourceConsumingQueriesTabId = 'TopResourceConsumingQueriesTab';
+
export const overallResourceConsumption = localize('overallResourceConsumption', "Overall Resource Consumption");
export const duration = localize('duration', "Duration");
export const executionCount = localize('executionCount', "Execution Count");
@@ -23,6 +27,7 @@ export function plan(queryId: string): string { return localize('plan', "Plan {0
export function topResourceConsumingQueriesToolbarLabel(databaseName: string): string { return localize('topResourceConsumingQueriesToolbarLabel', "Top 25 resource consumers for database {0}", databaseName); }
export const configure = localize('configure', "Configure");
+export const openInNewTab = localize('openInNewTab', "Open In New Tab");
export const okButtonText = localize('okButtonText', "Ok");
export const cancelButtonText = localize('cancelButtonText', "Cancel");
export const applyButtonText = localize('applyButtonText', "Apply");
diff --git a/extensions/query-store/src/common/iconHelper.ts b/extensions/query-store/src/common/iconHelper.ts
new file mode 100644
index 0000000000..e2622592b4
--- /dev/null
+++ b/extensions/query-store/src/common/iconHelper.ts
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as vscode from 'vscode';
+
+export interface IconPath {
+ dark: string;
+ light: string;
+}
+
+export class IconPathHelper {
+ private static extensionContext: vscode.ExtensionContext;
+ public static multipleWindows: IconPath;
+ public static gear: IconPath;
+
+ public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
+ IconPathHelper.extensionContext = extensionContext;
+
+ IconPathHelper.gear = IconPathHelper.makeIcon('gear');
+ IconPathHelper.multipleWindows = IconPathHelper.makeIcon('multiple-windows');
+ }
+
+ private static makeIcon(name: string, sameIcon: boolean = false) {
+ const folder = 'images';
+
+ if (sameIcon) {
+ return {
+ dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`),
+ light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`)
+ };
+ } else {
+ return {
+ dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/dark/${name}.svg`),
+ light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/light/${name}.svg`)
+ };
+ }
+ }
+}
diff --git a/extensions/query-store/src/common/promise.ts b/extensions/query-store/src/common/promise.ts
new file mode 100644
index 0000000000..ab8e6c3284
--- /dev/null
+++ b/extensions/query-store/src/common/promise.ts
@@ -0,0 +1,25 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Deferred promise
+ */
+export class Deferred {
+ promise: Promise;
+ resolve!: (value: T | PromiseLike) => void;
+ reject!: (reason?: any) => void;
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+
+ then(onFulfilled?: (value: T) => TResult | Thenable, onRejected?: (reason: any) => TResult | Thenable): Thenable;
+ then(onFulfilled?: (value: T) => TResult | Thenable, onRejected?: (reason: any) => void): Thenable;
+ then(onFulfilled?: (value: T) => TResult | Thenable, onRejected?: (reason: any) => TResult | Thenable): Thenable {
+ return this.promise.then(onFulfilled, onRejected);
+ }
+}
diff --git a/extensions/query-store/src/extension.ts b/extensions/query-store/src/extension.ts
index 77c6e16835..2f9fd0bc4d 100644
--- a/extensions/query-store/src/extension.ts
+++ b/extensions/query-store/src/extension.ts
@@ -5,12 +5,22 @@
import * as vscode from 'vscode';
import { QueryStoreDashboard } from './reports/queryStoreDashboard';
+import { IconPathHelper } from './common/iconHelper';
export async function activate(context: vscode.ExtensionContext): Promise {
// TODO: get db name
// TODO: add OE entry point with condition for command to only be visible for db's with Query Store enabled (or consider always showing and having a way to enable when dashboard is opened?)
// TODO: remove entry point from command palette - keeping for now to speed up testing so a connection doesn't need to be made to launch the dashboard
- context.subscriptions.push(vscode.commands.registerCommand('queryStore.openQueryStoreDashboard', async () => { await new QueryStoreDashboard('AdventureWorks', context).open() }));
+ context.subscriptions.push(vscode.commands.registerCommand('queryStore.openQueryStoreDashboard', async (targetTab?: string) => {
+ IconPathHelper.setExtensionContext(context);
+
+ const dashboard = new QueryStoreDashboard('AdventureWorks')
+ await dashboard.open();
+
+ if (targetTab) {
+ dashboard.selectTab(targetTab);
+ }
+ }));
}
export function deactivate(): void {
diff --git a/extensions/query-store/src/reports/baseQueryStoreReport.ts b/extensions/query-store/src/reports/baseQueryStoreReport.ts
index 69d7b3cfe5..ccc479633a 100644
--- a/extensions/query-store/src/reports/baseQueryStoreReport.ts
+++ b/extensions/query-store/src/reports/baseQueryStoreReport.ts
@@ -5,17 +5,23 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
-import * as path from 'path';
import * as utils from '../common/utils';
import * as constants from '../common/constants';
import { ConfigureDialog } from '../settings/configureDialog';
+import { IconPathHelper } from '../common/iconHelper';
export abstract class BaseQueryStoreReport {
protected flexModel?: azdata.FlexContainer;
protected configureDialog?: ConfigureDialog;
protected configureButton?: azdata.ButtonComponent;
- constructor(private reportTitle: string, protected resizeable: boolean, private extensionContext: vscode.ExtensionContext) { }
+ /**
+ * Constructor
+ * @param reportTitle Title of report shown in toolbar
+ * @param reportId Id of tab used in query store dashboard
+ * @param resizeable Whether or not the sections of the report are resizeable
+ */
+ constructor(private reportTitle: string, private reportId: string, protected resizeable: boolean) { }
public get ReportContent(): azdata.FlexContainer | undefined {
return this.flexModel;
@@ -101,13 +107,25 @@ export abstract class BaseQueryStoreReport {
CSSStyles: { 'margin-top': '5px', 'margin-bottom': '5px', 'margin-right': '15px' }
}).component();
+ // Open in New Tab button
+ const openInNewTabButton = view.modelBuilder.button().withProps({
+ label: constants.openInNewTab,
+ title: constants.openInNewTab,
+ iconPath: IconPathHelper.multipleWindows
+ }).component();
+ openInNewTabButton.enabled = true;
+
+ openInNewTabButton.onDidClick(async () => {
+ await vscode.commands.executeCommand('queryStore.openQueryStoreDashboard', this.reportId);
+ });
+
+ await openInNewTabButton.updateCssStyles({ 'margin-top': '5px' });
+
+ // Configure button
this.configureButton = view.modelBuilder.button().withProps({
label: constants.configure,
title: constants.configure,
- iconPath: {
- light: path.join(this.extensionContext.extensionPath, 'images', 'light', 'gear.svg'),
- dark: path.join(this.extensionContext.extensionPath, 'images', 'dark', 'gear.svg')
- }
+ iconPath: IconPathHelper.gear
}).component();
this.configureButton.enabled = true;
@@ -127,6 +145,9 @@ export abstract class BaseQueryStoreReport {
component: timePeriod,
toolbarSeparatorAfter: true
},
+ {
+ component: openInNewTabButton
+ },
{
component: this.configureButton
}
diff --git a/extensions/query-store/src/reports/overallResourceConsumption.ts b/extensions/query-store/src/reports/overallResourceConsumption.ts
index f32446b989..53af7ae31c 100644
--- a/extensions/query-store/src/reports/overallResourceConsumption.ts
+++ b/extensions/query-store/src/reports/overallResourceConsumption.ts
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
-import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { BaseQueryStoreReport } from './baseQueryStoreReport';
import { QueryStoreView } from './queryStoreView';
@@ -18,8 +17,8 @@ export class OverallResourceConsumption extends BaseQueryStoreReport {
private cpuTime: QueryStoreView;
private logicalReads: QueryStoreView;
- constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
- super(constants.overallResourceConsumptionToolbarLabel(databaseName), /*resizeable*/ false, extensionContext);
+ constructor(databaseName: string) {
+ super(constants.overallResourceConsumptionToolbarLabel(databaseName), constants.overallResourceConsumptionTabId,/*resizeable*/ false);
this.duration = new QueryStoreView(constants.duration, 'chartreuse');
this.executionCount = new QueryStoreView(constants.executionCount, 'coral');
this.cpuTime = new QueryStoreView(constants.cpuTime, 'darkturquoise');
diff --git a/extensions/query-store/src/reports/queryStoreDashboard.ts b/extensions/query-store/src/reports/queryStoreDashboard.ts
index 9e66044af2..6827ea918e 100644
--- a/extensions/query-store/src/reports/queryStoreDashboard.ts
+++ b/extensions/query-store/src/reports/queryStoreDashboard.ts
@@ -3,46 +3,57 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as constants from '../common/constants';
import { TopResourceConsumingQueries } from './topResourceConsumingQueries';
import { OverallResourceConsumption } from './overallResourceConsumption';
+import { Deferred } from '../common/promise';
export class QueryStoreDashboard {
- constructor(private dbName: string, private extensionContext: vscode.ExtensionContext) { }
+ private dashboard?: azdata.window.ModelViewDashboard;
+ private initDashboardComplete: Deferred = new Deferred();
+
+ constructor(private dbName: string) { }
/**
* Creates and opens the report
*/
public async open(): Promise {
// TODO: update title based on selected tab to have the current selected report in editor tab title
- const dashboard = azdata.window.createModelViewDashboard(constants.queryStoreDashboardTitle(this.dbName));
- dashboard.registerTabs(async (view: azdata.ModelView) => {
- const topResourceConsumingQueriesReport = new TopResourceConsumingQueries(this.extensionContext, this.dbName);
- const overallResourceConsumptionReport = new OverallResourceConsumption(this.extensionContext, this.dbName);
+ this.dashboard = azdata.window.createModelViewDashboard(constants.queryStoreDashboardTitle(this.dbName));
+ this.dashboard.registerTabs(async (view: azdata.ModelView) => {
+ const topResourceConsumingQueriesReport = new TopResourceConsumingQueries(this.dbName);
+ const overallResourceConsumptionReport = new OverallResourceConsumption(this.dbName);
await Promise.all([topResourceConsumingQueriesReport.createReport(view), overallResourceConsumptionReport.createReport(view)]);
const topResourceConsumingQueriesTab: azdata.DashboardTab = {
- id: 'TopResourceConsumingQueriesTab',
+ id: constants.topResourceConsumingQueriesTabId,
content: topResourceConsumingQueriesReport.ReportContent!,
title: constants.topResourceConsumingQueries
};
const overallResourceConsumptionTab: azdata.DashboardTab = {
- id: 'OverallResourceConsumptionTab',
+ id: constants.overallResourceConsumptionTabId,
content: overallResourceConsumptionReport.ReportContent!,
title: constants.overallResourceConsumption
};
+ this.initDashboardComplete?.resolve();
+
return [
overallResourceConsumptionTab,
topResourceConsumingQueriesTab
];
});
- await dashboard.open();
+ await this.dashboard.open();
+ await this.initDashboardComplete;
+ }
+
+ public selectTab(selectedTab: string): void {
+ // TODO: fix flashing - currently starts with the first tab selected, then switches to the other tab. Ideally would be able to set the
+ // selected tab when registering the tabs
+ this.dashboard?.selectTab(selectedTab);
}
}
-
diff --git a/extensions/query-store/src/reports/topResourceConsumingQueries.ts b/extensions/query-store/src/reports/topResourceConsumingQueries.ts
index 95c2479f3c..87bd20b2ae 100644
--- a/extensions/query-store/src/reports/topResourceConsumingQueries.ts
+++ b/extensions/query-store/src/reports/topResourceConsumingQueries.ts
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
-import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { BaseQueryStoreReport } from './baseQueryStoreReport';
import { QueryStoreView } from './queryStoreView';
@@ -16,8 +15,8 @@ export class TopResourceConsumingQueries extends BaseQueryStoreReport {
private planSummary: QueryStoreView;
private plan: QueryStoreView;
- constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
- super(constants.topResourceConsumingQueriesToolbarLabel(databaseName), /*resizeable*/ true, extensionContext);
+ constructor(databaseName: string) {
+ super(constants.topResourceConsumingQueriesToolbarLabel(databaseName), constants.topResourceConsumingQueriesTabId,/*resizeable*/ true);
this.queries = new QueryStoreView(constants.queries, 'chartreuse');
this.planSummary = new QueryStoreView(constants.planSummary('x'), 'coral'); // TODO: replace 'x' with actual query id
this.plan = new QueryStoreView(constants.plan('x'), 'darkturquoise');