mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
add button to open query store report in new tab (#24303)
* add button to open query store report in new tab * addressing comments
This commit is contained in:
3
extensions/query-store/images/dark/multiple-windows.svg
Normal file
3
extensions/query-store/images/dark/multiple-windows.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 1.5L6.5 1H14.5L15 1.5V3.5V8.5L14.5 9H12V8H14V4H7V5H6V3.5V1.5ZM7 2V3H14V2H7ZM1.5 7L1 7.5V14.5L1.5 15H9.5L10 14.5V7.5L9.5 7H1.5ZM2 9V8H9V9H2ZM2 10H9V14H2V10Z" fill="#C5C5C5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 329 B |
3
extensions/query-store/images/light/multiple-windows.svg
Normal file
3
extensions/query-store/images/light/multiple-windows.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.00024 1.5L6.50024 1H14.5002L15.0002 1.5V3.5V8.5L14.5002 9H12.0002V8H14.0002V4H7.00024V5H6.00024V3.5V1.5ZM7.00024 2V3H14.0002V2H7.00024ZM1.50024 7L1.00024 7.5V14.5L1.50024 15H9.50024L10.0002 14.5V7.5L9.50024 7H1.50024ZM2.00024 9V8H9.00024V9H2.00024ZM2.00024 10H9.00024V14H2.00024V10Z" fill="#424242"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
@@ -9,6 +9,10 @@ const localize = nls.loadMessageBundle();
|
|||||||
|
|
||||||
export function queryStoreDashboardTitle(databaseName: string): string { return localize('queryStoreDashboardTitle', "Query Store - {0}", databaseName); }
|
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 overallResourceConsumption = localize('overallResourceConsumption', "Overall Resource Consumption");
|
||||||
export const duration = localize('duration', "Duration");
|
export const duration = localize('duration', "Duration");
|
||||||
export const executionCount = localize('executionCount', "Execution Count");
|
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 function topResourceConsumingQueriesToolbarLabel(databaseName: string): string { return localize('topResourceConsumingQueriesToolbarLabel', "Top 25 resource consumers for database {0}", databaseName); }
|
||||||
|
|
||||||
export const configure = localize('configure', "Configure");
|
export const configure = localize('configure', "Configure");
|
||||||
|
export const openInNewTab = localize('openInNewTab', "Open In New Tab");
|
||||||
export const okButtonText = localize('okButtonText', "Ok");
|
export const okButtonText = localize('okButtonText', "Ok");
|
||||||
export const cancelButtonText = localize('cancelButtonText', "Cancel");
|
export const cancelButtonText = localize('cancelButtonText', "Cancel");
|
||||||
export const applyButtonText = localize('applyButtonText', "Apply");
|
export const applyButtonText = localize('applyButtonText', "Apply");
|
||||||
|
|||||||
40
extensions/query-store/src/common/iconHelper.ts
Normal file
40
extensions/query-store/src/common/iconHelper.ts
Normal file
@@ -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`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
extensions/query-store/src/common/promise.ts
Normal file
25
extensions/query-store/src/common/promise.ts
Normal file
@@ -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<T = void> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve!: (value: T | PromiseLike<T>) => void;
|
||||||
|
reject!: (reason?: any) => void;
|
||||||
|
constructor() {
|
||||||
|
this.promise = new Promise<T>((resolve, reject) => {
|
||||||
|
this.resolve = resolve;
|
||||||
|
this.reject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
then<TResult>(onFulfilled?: (value: T) => TResult | Thenable<TResult>, onRejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
|
||||||
|
then<TResult>(onFulfilled?: (value: T) => TResult | Thenable<TResult>, onRejected?: (reason: any) => void): Thenable<TResult>;
|
||||||
|
then<TResult>(onFulfilled?: (value: T) => TResult | Thenable<TResult>, onRejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
|
||||||
|
return this.promise.then(onFulfilled, onRejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,22 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { QueryStoreDashboard } from './reports/queryStoreDashboard';
|
import { QueryStoreDashboard } from './reports/queryStoreDashboard';
|
||||||
|
import { IconPathHelper } from './common/iconHelper';
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||||
// TODO: get db name
|
// 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: 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
|
// 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 {
|
export function deactivate(): void {
|
||||||
|
|||||||
@@ -5,17 +5,23 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as path from 'path';
|
|
||||||
import * as utils from '../common/utils';
|
import * as utils from '../common/utils';
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
import { ConfigureDialog } from '../settings/configureDialog';
|
import { ConfigureDialog } from '../settings/configureDialog';
|
||||||
|
import { IconPathHelper } from '../common/iconHelper';
|
||||||
|
|
||||||
export abstract class BaseQueryStoreReport {
|
export abstract class BaseQueryStoreReport {
|
||||||
protected flexModel?: azdata.FlexContainer;
|
protected flexModel?: azdata.FlexContainer;
|
||||||
protected configureDialog?: ConfigureDialog;
|
protected configureDialog?: ConfigureDialog;
|
||||||
protected configureButton?: azdata.ButtonComponent;
|
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 {
|
public get ReportContent(): azdata.FlexContainer | undefined {
|
||||||
return this.flexModel;
|
return this.flexModel;
|
||||||
@@ -101,13 +107,25 @@ export abstract class BaseQueryStoreReport {
|
|||||||
CSSStyles: { 'margin-top': '5px', 'margin-bottom': '5px', 'margin-right': '15px' }
|
CSSStyles: { 'margin-top': '5px', 'margin-bottom': '5px', 'margin-right': '15px' }
|
||||||
}).component();
|
}).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({
|
this.configureButton = view.modelBuilder.button().withProps({
|
||||||
label: constants.configure,
|
label: constants.configure,
|
||||||
title: constants.configure,
|
title: constants.configure,
|
||||||
iconPath: {
|
iconPath: IconPathHelper.gear
|
||||||
light: path.join(this.extensionContext.extensionPath, 'images', 'light', 'gear.svg'),
|
|
||||||
dark: path.join(this.extensionContext.extensionPath, 'images', 'dark', 'gear.svg')
|
|
||||||
}
|
|
||||||
}).component();
|
}).component();
|
||||||
this.configureButton.enabled = true;
|
this.configureButton.enabled = true;
|
||||||
|
|
||||||
@@ -127,6 +145,9 @@ export abstract class BaseQueryStoreReport {
|
|||||||
component: timePeriod,
|
component: timePeriod,
|
||||||
toolbarSeparatorAfter: true
|
toolbarSeparatorAfter: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: openInNewTabButton
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: this.configureButton
|
component: this.configureButton
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
import { BaseQueryStoreReport } from './baseQueryStoreReport';
|
import { BaseQueryStoreReport } from './baseQueryStoreReport';
|
||||||
import { QueryStoreView } from './queryStoreView';
|
import { QueryStoreView } from './queryStoreView';
|
||||||
@@ -18,8 +17,8 @@ export class OverallResourceConsumption extends BaseQueryStoreReport {
|
|||||||
private cpuTime: QueryStoreView;
|
private cpuTime: QueryStoreView;
|
||||||
private logicalReads: QueryStoreView;
|
private logicalReads: QueryStoreView;
|
||||||
|
|
||||||
constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
|
constructor(databaseName: string) {
|
||||||
super(constants.overallResourceConsumptionToolbarLabel(databaseName), /*resizeable*/ false, extensionContext);
|
super(constants.overallResourceConsumptionToolbarLabel(databaseName), constants.overallResourceConsumptionTabId,/*resizeable*/ false);
|
||||||
this.duration = new QueryStoreView(constants.duration, 'chartreuse');
|
this.duration = new QueryStoreView(constants.duration, 'chartreuse');
|
||||||
this.executionCount = new QueryStoreView(constants.executionCount, 'coral');
|
this.executionCount = new QueryStoreView(constants.executionCount, 'coral');
|
||||||
this.cpuTime = new QueryStoreView(constants.cpuTime, 'darkturquoise');
|
this.cpuTime = new QueryStoreView(constants.cpuTime, 'darkturquoise');
|
||||||
|
|||||||
@@ -3,46 +3,57 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* 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 azdata from 'azdata';
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
import { TopResourceConsumingQueries } from './topResourceConsumingQueries';
|
import { TopResourceConsumingQueries } from './topResourceConsumingQueries';
|
||||||
import { OverallResourceConsumption } from './overallResourceConsumption';
|
import { OverallResourceConsumption } from './overallResourceConsumption';
|
||||||
|
import { Deferred } from '../common/promise';
|
||||||
|
|
||||||
export class QueryStoreDashboard {
|
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
|
* Creates and opens the report
|
||||||
*/
|
*/
|
||||||
public async open(): Promise<void> {
|
public async open(): Promise<void> {
|
||||||
// TODO: update title based on selected tab to have the current selected report in editor tab title
|
// 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));
|
this.dashboard = azdata.window.createModelViewDashboard(constants.queryStoreDashboardTitle(this.dbName));
|
||||||
dashboard.registerTabs(async (view: azdata.ModelView) => {
|
this.dashboard.registerTabs(async (view: azdata.ModelView) => {
|
||||||
const topResourceConsumingQueriesReport = new TopResourceConsumingQueries(this.extensionContext, this.dbName);
|
const topResourceConsumingQueriesReport = new TopResourceConsumingQueries(this.dbName);
|
||||||
const overallResourceConsumptionReport = new OverallResourceConsumption(this.extensionContext, this.dbName);
|
const overallResourceConsumptionReport = new OverallResourceConsumption(this.dbName);
|
||||||
|
|
||||||
await Promise.all([topResourceConsumingQueriesReport.createReport(view), overallResourceConsumptionReport.createReport(view)]);
|
await Promise.all([topResourceConsumingQueriesReport.createReport(view), overallResourceConsumptionReport.createReport(view)]);
|
||||||
|
|
||||||
const topResourceConsumingQueriesTab: azdata.DashboardTab = {
|
const topResourceConsumingQueriesTab: azdata.DashboardTab = {
|
||||||
id: 'TopResourceConsumingQueriesTab',
|
id: constants.topResourceConsumingQueriesTabId,
|
||||||
content: topResourceConsumingQueriesReport.ReportContent!,
|
content: topResourceConsumingQueriesReport.ReportContent!,
|
||||||
title: constants.topResourceConsumingQueries
|
title: constants.topResourceConsumingQueries
|
||||||
};
|
};
|
||||||
|
|
||||||
const overallResourceConsumptionTab: azdata.DashboardTab = {
|
const overallResourceConsumptionTab: azdata.DashboardTab = {
|
||||||
id: 'OverallResourceConsumptionTab',
|
id: constants.overallResourceConsumptionTabId,
|
||||||
content: overallResourceConsumptionReport.ReportContent!,
|
content: overallResourceConsumptionReport.ReportContent!,
|
||||||
title: constants.overallResourceConsumption
|
title: constants.overallResourceConsumption
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.initDashboardComplete?.resolve();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
overallResourceConsumptionTab,
|
overallResourceConsumptionTab,
|
||||||
topResourceConsumingQueriesTab
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
import { BaseQueryStoreReport } from './baseQueryStoreReport';
|
import { BaseQueryStoreReport } from './baseQueryStoreReport';
|
||||||
import { QueryStoreView } from './queryStoreView';
|
import { QueryStoreView } from './queryStoreView';
|
||||||
@@ -16,8 +15,8 @@ export class TopResourceConsumingQueries extends BaseQueryStoreReport {
|
|||||||
private planSummary: QueryStoreView;
|
private planSummary: QueryStoreView;
|
||||||
private plan: QueryStoreView;
|
private plan: QueryStoreView;
|
||||||
|
|
||||||
constructor(extensionContext: vscode.ExtensionContext, databaseName: string) {
|
constructor(databaseName: string) {
|
||||||
super(constants.topResourceConsumingQueriesToolbarLabel(databaseName), /*resizeable*/ true, extensionContext);
|
super(constants.topResourceConsumingQueriesToolbarLabel(databaseName), constants.topResourceConsumingQueriesTabId,/*resizeable*/ true);
|
||||||
this.queries = new QueryStoreView(constants.queries, 'chartreuse');
|
this.queries = new QueryStoreView(constants.queries, 'chartreuse');
|
||||||
this.planSummary = new QueryStoreView(constants.planSummary('x'), 'coral'); // TODO: replace 'x' with actual query id
|
this.planSummary = new QueryStoreView(constants.planSummary('x'), 'coral'); // TODO: replace 'x' with actual query id
|
||||||
this.plan = new QueryStoreView(constants.plan('x'), 'darkturquoise');
|
this.plan = new QueryStoreView(constants.plan('x'), 'darkturquoise');
|
||||||
|
|||||||
Reference in New Issue
Block a user