From 220b2b4ac47f7c5b653349845e896cf557b8842a Mon Sep 17 00:00:00 2001
From: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Date: Thu, 29 Jun 2023 13:24:22 -1000
Subject: [PATCH] Initial changes for reusable QDS report (#23527)
* initial changes for making a QDS report with placeholders
* Add icon for configure button
* Add another example report to show layout
* move files
* add placeholder names to components and cleanup toolbar
* cleanup
* switch to createViews() instead of createTop and BottomSections()
* add QueryStoreView class for the different components in a report
* cleanup
* add more comments
* fix yarn not running for query store extension folder
* add missing break
* change one more view to container
---
build/npm/dirs.js | 1 +
extensions/query-store/images/dark/gear.svg | 3 +
extensions/query-store/images/light/gear.svg | 3 +
extensions/query-store/package.json | 13 +-
extensions/query-store/package.nls.json | 5 +-
.../query-store/src/common/constants.ts | 23 +++
extensions/query-store/src/common/utils.ts | 78 ++++++++++
extensions/query-store/src/extension.ts | 7 +-
.../src/reports/baseQueryStoreReport.ts | 146 ++++++++++++++++++
.../src/reports/overallResourceConsumption.ts | 35 +++++
.../query-store/src/reports/queryStoreView.ts | 37 +++++
.../reports/topResourceConsumingQueries.ts | 31 ++++
12 files changed, 379 insertions(+), 3 deletions(-)
create mode 100644 extensions/query-store/images/dark/gear.svg
create mode 100644 extensions/query-store/images/light/gear.svg
create mode 100644 extensions/query-store/src/common/constants.ts
create mode 100644 extensions/query-store/src/common/utils.ts
create mode 100644 extensions/query-store/src/reports/baseQueryStoreReport.ts
create mode 100644 extensions/query-store/src/reports/overallResourceConsumption.ts
create mode 100644 extensions/query-store/src/reports/queryStoreView.ts
create mode 100644 extensions/query-store/src/reports/topResourceConsumingQueries.ts
diff --git a/build/npm/dirs.js b/build/npm/dirs.js
index b519b8f96c..a7eb2bd8dd 100644
--- a/build/npm/dirs.js
+++ b/build/npm/dirs.js
@@ -44,6 +44,7 @@ const dirs = [
'extensions/notebook-renderers',
'extensions/profiler',
'extensions/query-history',
+ 'extensions/query-store',
'extensions/resource-deployment',
'extensions/schema-compare',
'extensions/search-result',
diff --git a/extensions/query-store/images/dark/gear.svg b/extensions/query-store/images/dark/gear.svg
new file mode 100644
index 0000000000..0a8f58d309
--- /dev/null
+++ b/extensions/query-store/images/dark/gear.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/query-store/images/light/gear.svg b/extensions/query-store/images/light/gear.svg
new file mode 100644
index 0000000000..0040bfcff1
--- /dev/null
+++ b/extensions/query-store/images/light/gear.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/query-store/package.json b/extensions/query-store/package.json
index dd505e9d18..33415cc6e8 100644
--- a/extensions/query-store/package.json
+++ b/extensions/query-store/package.json
@@ -13,7 +13,8 @@
"icon": "images/extension.png",
"aiKey": "29a207bb14f84905966a8f22524cb730-25407f35-11b6-4d4e-8114-ab9e843cb52f-7380",
"activationEvents": [
- "*"
+ "onCommand:queryStore.topResourceConsumingQueriesOpen",
+ "onCommand:queryStore.overallResourceConsumptionOpen"
],
"main": "./out/extension",
"repository": {
@@ -31,6 +32,16 @@
],
"contributes": {
"commands": [
+ {
+ "command": "queryStore.topResourceConsumingQueriesOpen",
+ "title": "%queryStore.topResourceConsumingQueriesOpen%",
+ "category": "%queryStore.category%"
+ },
+ {
+ "command": "queryStore.overallResourceConsumptionOpen",
+ "title": "%queryStore.overallResourceConsumptionOpen%",
+ "category": "%queryStore.category%"
+ }
],
"menus": {
"objectExplorer/item/context": [
diff --git a/extensions/query-store/package.nls.json b/extensions/query-store/package.nls.json
index 82e3c1e362..c51a47d357 100644
--- a/extensions/query-store/package.nls.json
+++ b/extensions/query-store/package.nls.json
@@ -1,4 +1,7 @@
{
"queryStore.displayName": "Query Store",
- "queryStore.description": "Query Store extension for Azure Data Studio."
+ "queryStore.description": "Query Store extension for Azure Data Studio.",
+ "queryStore.category": "Query Store",
+ "queryStore.topResourceConsumingQueriesOpen": "Open Top Resource Consuming Queries",
+ "queryStore.overallResourceConsumptionOpen": "Open Overall Resource Consumption"
}
diff --git a/extensions/query-store/src/common/constants.ts b/extensions/query-store/src/common/constants.ts
new file mode 100644
index 0000000000..d519f6d5a0
--- /dev/null
+++ b/extensions/query-store/src/common/constants.ts
@@ -0,0 +1,23 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as nls from 'vscode-nls';
+
+const localize = nls.loadMessageBundle();
+
+export const overallResourceConsumption = localize('overallResourceConsumption', "Overall Resource Consumption");
+export const duration = localize('duration', "Duration");
+export const executionCount = localize('executionCount', "Execution Count");
+export const cpuTime = localize('cpuTime', "CPU Time");
+export const logicalReads = localize('logicalReads', "Logical Reads");
+export function overallResourceConsumptionToolbarLabel(databaseName: string): string { return localize('overallResourceConsumptionToolbarLabel', "Overall resource consumption for database {0}", databaseName); }
+
+export const topResourceConsumingQueries = localize('topResourceConsumingQueries', "Top Resource Consuming Queries");
+export const queries = localize('queries', "Queries");
+export function planSummary(queryId: string): string { return localize('planSummary', "Plan Summary for query {0}", queryId); }
+export function plan(queryId: string): string { return localize('plan', "Plan {0}", queryId); }
+export function topResourceConsumingQueriesToolbarLabel(databaseName: string): string { return localize('topResourceConsumingQueriesToolbarLabel', "Top 25 resource consumers for database {0}", databaseName); }
+
+export const configure = localize('configure', "Configure");
diff --git a/extensions/query-store/src/common/utils.ts b/extensions/query-store/src/common/utils.ts
new file mode 100644
index 0000000000..94120c6e9b
--- /dev/null
+++ b/extensions/query-store/src/common/utils.ts
@@ -0,0 +1,78 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+
+/**
+ * Creates a flex container with the provided component and sets the background color
+ * TODO: Remove/redo this helper function after chart components are hooked up, when background color is no longer used
+ * @param view
+ * @param component
+ * @param backgroundColor
+ * @returns Flex container with the specified background color containing component
+ */
+export async function createOneComponentFlexContainer(view: azdata.ModelView, component: azdata.Component, backgroundColor: string): Promise {
+ const flexContainer = view.modelBuilder.flexContainer().component();
+
+ await flexContainer.updateCssStyles({ 'background-color': backgroundColor });
+
+ flexContainer.addItem(component);
+
+ flexContainer.setLayout({
+ width: '100%',
+ height: '100%'
+ });
+
+ return flexContainer;
+}
+
+/**
+ * Creates a flex container with two components, either horizontally or vertically based on the passed in flexFlow
+ * @param view
+ * @param firstComponent
+ * @param secondComponent
+ * @param flexFlow row or column
+ * @returns Flex container containing the two components
+ */
+export async function createTwoComponentFlexContainer(view: azdata.ModelView, firstComponent: azdata.Component, secondComponent: azdata.Component, flexFlow: string): Promise {
+ const flexContainer = view.modelBuilder.flexContainer().component();
+
+ if (flexFlow === 'row') {
+ flexContainer.addItems([firstComponent, secondComponent], { CSSStyles: { 'width': '50%' } });
+ } else {
+ flexContainer.addItems([firstComponent, secondComponent], { CSSStyles: { 'height': '50%' } });
+ }
+
+ flexContainer.setLayout({
+ flexFlow: flexFlow,
+ width: '100%',
+ height: '100%'
+ });
+
+ return flexContainer;
+}
+
+/**
+ * Creates a vertical splitview
+ * @param view
+ * @param topComponent
+ * @param bottomComponent
+ * @param splitViewHeight
+ * @returns Vertical SplitViewContainer with the top and bottom components
+ */
+export function createVerticalSplitView(view: azdata.ModelView, topComponent: azdata.Component, bottomComponent: azdata.Component, splitViewHeight: number): azdata.SplitViewContainer {
+ // TODO: figure out why the horizontal spliview isn't working
+
+ const splitview = view.modelBuilder.splitViewContainer().component();
+ splitview.addItem(topComponent);
+ splitview.addItem(bottomComponent);
+
+ splitview.setLayout({
+ orientation: 'vertical',
+ splitViewHeight: splitViewHeight
+ });
+
+ return splitview;
+}
diff --git a/extensions/query-store/src/extension.ts b/extensions/query-store/src/extension.ts
index d09c0499de..7d54b63a08 100644
--- a/extensions/query-store/src/extension.ts
+++ b/extensions/query-store/src/extension.ts
@@ -4,8 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
+import { TopResourceConsumingQueries } from './reports/topResourceConsumingQueries';
+import { OverallResourceConsumption } from './reports/overallResourceConsumption';
-export async function activate(_context: vscode.ExtensionContext): Promise {
+export async function activate(context: vscode.ExtensionContext): Promise {
+ // TODO: get db name
+ context.subscriptions.push(vscode.commands.registerCommand('queryStore.topResourceConsumingQueriesOpen', async () => { await new TopResourceConsumingQueries(context, 'WideWorldImporters').open() }));
+ context.subscriptions.push(vscode.commands.registerCommand('queryStore.overallResourceConsumptionOpen', async () => { await new OverallResourceConsumption(context, 'WideWorldImporters').open() }));
}
export function deactivate(): void {
diff --git a/extensions/query-store/src/reports/baseQueryStoreReport.ts b/extensions/query-store/src/reports/baseQueryStoreReport.ts
new file mode 100644
index 0000000000..8916c62c1a
--- /dev/null
+++ b/extensions/query-store/src/reports/baseQueryStoreReport.ts
@@ -0,0 +1,146 @@
+/*---------------------------------------------------------------------------------------------
+ * 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';
+import * as azdata from 'azdata';
+import * as path from 'path';
+import * as utils from '../common/utils';
+import * as constants from '../common/constants';
+
+export abstract class BaseQueryStoreReport {
+ protected editor: azdata.workspace.ModelViewEditor;
+ protected flexModel?: azdata.FlexContainer;
+
+ constructor(reportName: string, private reportTitle: string, protected resizeable: boolean, private extensionContext: vscode.ExtensionContext) {
+ this.editor = azdata.workspace.createModelViewEditor(reportName, { retainContextWhenHidden: true, supportsSave: false }, reportName);
+ }
+
+ /**
+ * Creates and opens the report
+ */
+ public async open(): Promise {
+ this.editor.registerContent(async (view) => {
+ this.flexModel = view.modelBuilder.flexContainer().component();
+
+ const toolbar = await this.createToolbar(view);
+ this.flexModel.addItem(toolbar, { flex: 'none' });
+
+ const views = await this.createViews(view);
+
+ const mainContainer = await this.createMainContainer(view, views);
+
+ this.flexModel.addItem(mainContainer, { CSSStyles: { 'width': '100%', 'height': '100%' } });
+
+ this.flexModel.setLayout({
+ flexFlow: 'column',
+ height: '100%'
+ });
+
+ await view.initializeModel(this.flexModel);
+ });
+
+ await this.editor.openEditor();
+ }
+
+ /**
+ * Creates the main container containing the different components of the report
+ * @param view
+ * @param containers Array of containers to add to the main container
+ * @returns FlexContainer or SplitViewContainer containing the containers
+ */
+ private async createMainContainer(view: azdata.ModelView, containers: azdata.FlexContainer[]): Promise {
+ let mainContainer;
+
+ switch (containers.length) {
+ case 1: {
+ mainContainer = containers[0];
+ break;
+ }
+ case 2: {
+ // TODO: replace 800 to have the number be based on how big the window is
+ // one container on top, one on the bottom
+ mainContainer = this.resizeable ? utils.createVerticalSplitView(view, containers[0], containers[1], 800) : await utils.createTwoComponentFlexContainer(view, containers[0], containers[1], 'column');
+ break;
+ } case 3: {
+ // 2 containers on top, one on the bottom
+ // TODO: support portrait and landscape view. Right now it's landscape view only
+ mainContainer = this.resizeable ? utils.createVerticalSplitView(view, await utils.createTwoComponentFlexContainer(view, containers[0], containers[1], 'row'), containers[2], 800)
+ : await utils.createTwoComponentFlexContainer(view, await utils.createTwoComponentFlexContainer(view, containers[0], containers[1], 'row'), containers[2], 'column');
+ break;
+ } case 4: {
+ // 2 containers on top, 2 on the bottom
+ mainContainer = this.resizeable ? utils.createVerticalSplitView(view, await utils.createTwoComponentFlexContainer(view, containers[0], containers[1], 'row'), await utils.createTwoComponentFlexContainer(view, containers[2], containers[3], 'row'), 800)
+ : await utils.createTwoComponentFlexContainer(view, await utils.createTwoComponentFlexContainer(view, containers[0], containers[1], 'row'), await utils.createTwoComponentFlexContainer(view, containers[2], containers[3], 'row'), 'column');
+ break;
+ } default: {
+ throw new Error(`{views.length} number of views in a QDS report is not supported`);
+ }
+ }
+
+ return mainContainer
+ }
+
+ /**
+ * Creates the toolbar for the overall report with the report title, time range, and configure button
+ * @param view
+ */
+ protected async createToolbar(view: azdata.ModelView): Promise {
+ const toolBar = view.modelBuilder.toolbarContainer().withProps({
+ CSSStyles: { 'padding': '5px' }
+ });
+
+ const reportTitle = view.modelBuilder.text().withProps({
+ value: this.reportTitle,
+ title: this.reportTitle,
+ CSSStyles: { 'margin-top': '5px', 'margin-bottom': '5px', 'margin-right': '15px' }
+ }).component();
+
+ // TODO: get time from configuration dialog
+ const timePeriod = view.modelBuilder.text().withProps({
+ // placeholder times
+ value: 'Time period: 5/15/2023 11:58 AM - 5/23/2023 11:58 AM',
+ title: 'Time period: 5/15/2023 11:58 AM - 5/23/2023 11:58 AM',
+ CSSStyles: { 'margin-top': '5px', 'margin-bottom': '5px', 'margin-right': '15px' }
+ }).component();
+
+ const 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')
+ }
+ }).component();
+
+ // TODO: enable after the configuration dialog is implemented
+ configureButton.enabled = false;
+
+ configureButton.onDidClick(() => {
+ // TODO: implement configuration dialog
+ console.error('configuration dialog not implemented')
+ });
+
+ await configureButton.updateCssStyles({ 'margin-top': '5px' });
+
+ toolBar.addToolbarItems([
+ {
+ component: reportTitle,
+ toolbarSeparatorAfter: true
+ },
+ {
+ component: timePeriod,
+ toolbarSeparatorAfter: true
+ },
+ {
+ component: configureButton
+ }
+ ]);
+
+ return toolBar.component();
+ }
+
+ protected abstract createViews(_view: azdata.ModelView): Promise;
+}
+
diff --git a/extensions/query-store/src/reports/overallResourceConsumption.ts b/extensions/query-store/src/reports/overallResourceConsumption.ts
new file mode 100644
index 0000000000..9d67ef4c69
--- /dev/null
+++ b/extensions/query-store/src/reports/overallResourceConsumption.ts
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import * as vscode from 'vscode';
+import * as constants from '../common/constants';
+import { BaseQueryStoreReport } from './baseQueryStoreReport';
+import { QueryStoreView } from './queryStoreView';
+
+
+export class OverallResourceConsumption extends BaseQueryStoreReport {
+ private duration: QueryStoreView;
+ private executionCount: QueryStoreView;
+ private cpuTime: QueryStoreView;
+ private logicalReads: QueryStoreView;
+
+ constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
+ super(constants.overallResourceConsumption, constants.overallResourceConsumptionToolbarLabel(databaseName), /*resizeable*/ false, extensionContext);
+ this.duration = new QueryStoreView(constants.duration, 'chartreuse');
+ this.executionCount = new QueryStoreView(constants.executionCount, 'coral');
+ this.cpuTime = new QueryStoreView(constants.cpuTime, 'darkturquoise');
+ this.logicalReads = new QueryStoreView(constants.logicalReads, 'forestgreen');
+ }
+
+ public override async createViews(view: azdata.ModelView): Promise {
+ const durationContainer = await this.duration.createViewContainer(view);
+ const executionCountContainer = await this.executionCount.createViewContainer(view);
+ const cpuTimeContainer = await this.cpuTime.createViewContainer(view);
+ const logicalReadsContainer = await this.logicalReads.createViewContainer(view);
+
+ return [durationContainer, executionCountContainer, cpuTimeContainer, logicalReadsContainer];
+ }
+}
diff --git a/extensions/query-store/src/reports/queryStoreView.ts b/extensions/query-store/src/reports/queryStoreView.ts
new file mode 100644
index 0000000000..66ae39f284
--- /dev/null
+++ b/extensions/query-store/src/reports/queryStoreView.ts
@@ -0,0 +1,37 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import { createOneComponentFlexContainer } from '../common/utils';
+
+/**
+ * Defines a view in a query store report
+ */
+export class QueryStoreView {
+ // TODO: add toolbar support
+ // TODO: add support for toggling between chart and table components (could potentially add a child class to support this).
+ public component?: azdata.Component; // chart, query plan, text (component to display whole query text)
+
+ /**
+ *
+ * @param title Title of view to display
+ * @param backgroundColor TODO: remove this after chart components are supported
+ */
+ constructor(private title: string, private backgroundColor: string) { }
+
+ /**
+ * Creates component in a container with the background color. Eventually will create the component with a toolbar in a flex container
+ * @param view
+ * @returns
+ */
+ public async createViewContainer(view: azdata.ModelView): Promise {
+ // TODO: replace these text components with the actual chart/table/query plan components
+ this.component = view.modelBuilder.text().withProps({
+ value: this.title
+ }).component();
+
+ return await createOneComponentFlexContainer(view, this.component, this.backgroundColor);
+ }
+}
diff --git a/extensions/query-store/src/reports/topResourceConsumingQueries.ts b/extensions/query-store/src/reports/topResourceConsumingQueries.ts
new file mode 100644
index 0000000000..59b17f3c34
--- /dev/null
+++ b/extensions/query-store/src/reports/topResourceConsumingQueries.ts
@@ -0,0 +1,31 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import * as vscode from 'vscode';
+import * as constants from '../common/constants';
+import { BaseQueryStoreReport } from './baseQueryStoreReport';
+import { QueryStoreView } from './queryStoreView';
+
+export class TopResourceConsumingQueries extends BaseQueryStoreReport {
+ private queries: QueryStoreView;
+ private planSummary: QueryStoreView;
+ private plan: QueryStoreView;
+
+ constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
+ super(constants.topResourceConsumingQueries, constants.topResourceConsumingQueriesToolbarLabel(databaseName), /*resizeable*/ true, extensionContext);
+ 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');
+ }
+
+ public override async createViews(view: azdata.ModelView): Promise {
+ const queriesContainer = await this.queries.createViewContainer(view);
+ const planSummaryContainer = await this.planSummary.createViewContainer(view);
+ const planContainer = await this.plan.createViewContainer(view);
+
+ return [queriesContainer, planSummaryContainer, planContainer];
+ }
+}