mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
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
This commit is contained in:
23
extensions/query-store/src/common/constants.ts
Normal file
23
extensions/query-store/src/common/constants.ts
Normal file
@@ -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");
|
||||
78
extensions/query-store/src/common/utils.ts
Normal file
78
extensions/query-store/src/common/utils.ts
Normal file
@@ -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<azdata.FlexContainer> {
|
||||
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<azdata.FlexContainer> {
|
||||
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 = <azdata.SplitViewContainer>view.modelBuilder.splitViewContainer().component();
|
||||
splitview.addItem(topComponent);
|
||||
splitview.addItem(bottomComponent);
|
||||
|
||||
splitview.setLayout({
|
||||
orientation: 'vertical',
|
||||
splitViewHeight: splitViewHeight
|
||||
});
|
||||
|
||||
return splitview;
|
||||
}
|
||||
@@ -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<void> {
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||
// 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 {
|
||||
|
||||
146
extensions/query-store/src/reports/baseQueryStoreReport.ts
Normal file
146
extensions/query-store/src/reports/baseQueryStoreReport.ts
Normal file
@@ -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<void> {
|
||||
this.editor.registerContent(async (view) => {
|
||||
this.flexModel = <azdata.FlexContainer>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<azdata.FlexContainer | azdata.SplitViewContainer> {
|
||||
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<azdata.ToolbarContainer> {
|
||||
const toolBar = <azdata.ToolbarBuilder>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<azdata.FlexContainer[]>;
|
||||
}
|
||||
|
||||
@@ -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<azdata.FlexContainer[]> {
|
||||
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];
|
||||
}
|
||||
}
|
||||
37
extensions/query-store/src/reports/queryStoreView.ts
Normal file
37
extensions/query-store/src/reports/queryStoreView.ts
Normal file
@@ -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<azdata.FlexContainer> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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<azdata.FlexContainer[]> {
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user