diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 9608df3d9c..30aa1663c3 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.9.0.32", + "version": "4.9.0.34", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows_64": "win-x64-net7.0.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index c04a1e0ee0..f8774bbf7f 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1699,6 +1699,16 @@ export namespace GetAssociatedFilesRequest { export const type = new RequestType('admin/getassociatedfiles'); } +export namespace PurgeQueryStoreDataRequest { + export const type = new RequestType('objectManagement/purgeQueryStoreData'); +} + +export interface purgeQueryStoreDataRequestParams { + connectionUri: string; + database: string; + objectUrn: string; +} + // ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 4a45981623..2a439c894e 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -1020,6 +1020,13 @@ declare module 'mssql' { * @returns An array of file path strings for each of the associated files. */ getAssociatedFiles(connectionUri: string, primaryFilePath: string): Thenable; + /** + * Clears all query store data from the database + * @param connectionUri The URI of the server connection. + * @param database The target database. + * @param objectUrn SMO Urn of the database to be detached. More information: https://learn.microsoft.com/sql/relational-databases/server-management-objects-smo/overview-smo + */ + purgeQueryStoreData(connectionUri: string, database: string, objectUrn: string): Thenable; } export interface DatabaseFileData { diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index ee73ce8291..b423084652 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -40,6 +40,7 @@ export const DatabaseGeneralPropertiesDocUrl = 'https://learn.microsoft.com/sql/ export const DatabaseOptionsPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-options-page' export const DropDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/drop-database-transact-sql'; export const DatabaseScopedConfigurationPropertiesDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-database-scoped-configuration-transact-sql' +export const QueryStorePropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-query-store-page' export const DatabaseFilesPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-files-page' export const DatabaseFileGroupsPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-filegroups-page' diff --git a/extensions/mssql/src/objectManagement/interfaces.ts b/extensions/mssql/src/objectManagement/interfaces.ts index 4ee4aa8f0c..445699ca6c 100644 --- a/extensions/mssql/src/objectManagement/interfaces.ts +++ b/extensions/mssql/src/objectManagement/interfaces.ts @@ -458,6 +458,7 @@ export interface Database extends ObjectManagement.SqlObject { isFilesTabSupported?: boolean; files?: DatabaseFile[]; filegroups?: FileGroup[]; + queryStoreOptions?: QueryStoreOptions; } export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo { @@ -481,6 +482,32 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo localize('objectManagement.databaseProperties.FilegroupExistsError', "File group '{0}' could not be added to the collection, because it already exists.", name); export const EmptyFilegroupNameError = localize('objectManagement.databaseProperties.emptyFilegroupNameError', "Cannot use empty object names for filegroups."); +export const ActualOperationModeText = localize('objectManagement.databaseProperties.actualOperationModeText', "Operation Mode (Actual)"); +export const RequestedOperationModeText = localize('objectManagement.databaseProperties.requestedOperationModeText', "Operation Mode (Requested)"); +export const DataFlushIntervalInMinutesText = localize('objectManagement.databaseProperties.dataFlushIntervalInMinutesText', "Data Flush Interval (Minutes)"); +export const StatisticsCollectionInterval = localize('objectManagement.databaseProperties.statisticsCollectionInterval', "Statistics Collection Interval"); +export const MaxPlansPerQueryText = localize('objectManagement.databaseProperties.maxPlansPerQueryText', "Max Plans Per Query"); +export const MaxSizeInMbText = localize('objectManagement.databaseProperties.maxSizeInMbText', "Max Size (MB)"); +export const QueryStoreCaptureModeText = localize('objectManagement.databaseProperties.queryStoreCaptureModeText', "Query Store Capture Mode"); +export const SizeBasedCleanupModeText = localize('objectManagement.databaseProperties.sizeBasedCleanupModeText', "Size Based Cleanup Mode"); +export const StateQueryThresholdInDaysText = localize('objectManagement.databaseProperties.stateQueryThresholdInDaysText', "State Query Threshold (Days)"); +export const WaitStatisticsCaptureModeText = localize('objectManagement.databaseProperties.waitStatisticsCaptureModeText', "Wait Statistics Capture Mode"); +export const MonitoringSectionText = localize('objectManagement.databaseProperties.monitoringSectionText', "Monitoring"); +export const QueryStoreRetentionSectionText = localize('objectManagement.databaseProperties.queryStoreRetentionSectionText', "Query Store Retention"); +export const QueryStoreCapturePolicySectionText = localize('objectManagement.databaseProperties.queryStoreCapturePolicySectionText', "Query Store Capture Policy"); +export const QueryStoreCurrentDiskUsageSectionText = localize('objectManagement.databaseProperties.queryStoreCurrentDiskUsageSectionText', "Current Disk Usage"); +export const ExecutionCountText = localize('objectManagement.databaseProperties.executionCountText', "Execution Count"); +export const StaleThresholdText = localize('objectManagement.databaseProperties.staleThresholdText', "Stale Threshold"); +export const TotalCompileCPUTimeInMsText = localize('objectManagement.databaseProperties.totalCompileCPUTimeInMs', "Total Compile CPU Time (ms)"); +export const TotalExecutionCPUTimeInMsText = localize('objectManagement.databaseProperties.totalExecutionCPUTimeInMsText', "Total Execution CPU Time (ms)"); +export const QueryStoreCapturemodeCustomText = localize('objectManagement.databaseProperties.queryStoreCapturemodeCustomText', "Custom"); +export const QueryStoreUsedText = localize('objectManagement.databaseProperties.queryStoreUsedText', "Query Store Used"); +export const QueryStoreAvailableText = localize('objectManagement.databaseProperties.queryStoreAvailableText', "Query Store Available"); +export const PurgeQueryDataButtonText = localize('objectManagement.databaseProperties.purgeQueryDataButtonText', "Purge Query Store Data"); +export const YesText = localize('objectManagement.databaseProperties.yesText', "Yes"); +export const PurgeQueryStoreDataMessage = (databaseName: string) => localize('objectManagement.databaseProperties.purgeQueryStoreDataMessage', "Are you sure you want to purge the Query Store data from '{0}'?", databaseName); // Util functions export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string { diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 16491a3d88..aa1ea77067 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -90,6 +90,11 @@ export class ObjectManagementService extends BaseService implements IObjectManag const params: contracts.GetAssociatedFilesRequestParams = { connectionUri, primaryFilePath }; return this.runWithErrorHandling(contracts.GetAssociatedFilesRequest.type, params); } + + async purgeQueryStoreData(connectionUri: string, database: string, objectUrn: string): Promise { + const params: contracts.purgeQueryStoreDataRequestParams = { connectionUri, database, objectUrn }; + return this.runWithErrorHandling(contracts.PurgeQueryStoreDataRequest.type, params); + } } const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [ @@ -277,6 +282,10 @@ export class TestObjectManagementService implements IObjectManagementService { return this.delayAndResolve([]); } + async purgeQueryStoreData(connectionUri: string, database: string, objectUrn: string): Promise { + return this.delayAndResolve([]); + } + private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] { let items: ObjectManagement.SearchResultItem[] = []; for (let i = 0; i < count; i++) { diff --git a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts index 7353bde5ee..e78bd99d4d 100644 --- a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts @@ -8,17 +8,23 @@ import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './obj import { DefaultInputWidth, DefaultTableWidth, DefaultMinTableRowCount, DefaultMaxTableRowCount, getTableHeight, DialogButton } from '../../ui/dialogBase'; import { IObjectManagementService } from 'mssql'; import * as localizedConstants from '../localizedConstants'; -import { CreateDatabaseDocUrl, DatabaseGeneralPropertiesDocUrl, DatabaseFilesPropertiesDocUrl, DatabaseOptionsPropertiesDocUrl, DatabaseScopedConfigurationPropertiesDocUrl, DatabaseFileGroupsPropertiesDocUrl } from '../constants'; +import { CreateDatabaseDocUrl, DatabaseGeneralPropertiesDocUrl, DatabaseFilesPropertiesDocUrl, DatabaseOptionsPropertiesDocUrl, DatabaseScopedConfigurationPropertiesDocUrl, DatabaseFileGroupsPropertiesDocUrl, QueryStorePropertiesDocUrl } from '../constants'; import { Database, DatabaseFile, DatabaseScopedConfigurationsInfo, DatabaseViewInfo, FileGrowthType, FileGroup, FileGroupType } from '../interfaces'; import { convertNumToTwoDecimalStringInMB } from '../utils'; import { isUndefinedOrNull } from '../../types'; import { deepClone } from '../../util/objects'; import { DatabaseFileDialog } from './databaseFileDialog'; +import * as vscode from 'vscode'; const MAXDOP_Max_Limit = 32767; const PAUSED_RESUMABLE_INDEX_Max_Limit = 71582; const DscTableRowLength = 15; +export const tableHeaderCssStylings = { 'border': 'solid 1px #C8C8C8' }; +export const tableRowCssStylings = { 'border': 'solid 1px #C8C8C8' } +// export const tableHeaderCssStylings = { 'border-top': 'solid 1px #C8C8C8', 'border-bottom': 'none', 'border-left': 'none', 'border-right': 'none' }; +// export const tableRowCssStylings = { 'border-top': 'solid 1px #ccc', 'border-bottom': 'none', 'border-left': 'none', 'border-right': 'none' } + export class DatabaseDialog extends ObjectManagementDialogBase { // Database Properties tabs private generalTab: azdata.Tab; @@ -26,6 +32,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase { if (fileType === localizedConstants.RowsDataFileType) { - let data = this.getTableData(FileGroupType.RowsFileGroup); - await this.setTableData(this.rowsFilegroupsTable, data); + // let data = this.getTableData(FileGroupType.RowsFileGroup); + // await this.setTableData(this.rowsFilegroupsTable, data); } else if (fileType === localizedConstants.FilestreamFileType) { let data = this.getTableData(FileGroupType.FileStreamDataFileGroup); @@ -649,13 +696,13 @@ export class DatabaseDialog extends ObjectManagementDialogBase 0) { - isEnabled = false; - } - } + // else if (table === this.rowsFilegroupsTable && this.rowsFilegroupsTable.selectedRows !== undefined && this.rowsFilegroupsTable.selectedRows.length === 1) { + // const selectedRow = this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRows[0]]; + // // Cannot delete a row file if the fileGroup is Primary. + // if (selectedRow.name === 'PRIMARY' && selectedRow.id > 0) { + // isEnabled = false; + // } + // } return isEnabled; } @@ -713,49 +760,62 @@ export class DatabaseDialog extends ObjectManagementDialogBase { const data = this.getTableData(FileGroupType.RowsFileGroup); - this.rowsFilegroupsTable = this.modelView.modelBuilder.table().withProps({ + this.rowsFilegroupsTable = this.modelView.modelBuilder.declarativeTable().withProps({ columns: [{ - type: azdata.ColumnType.text, - value: localizedConstants.NameText, - width: 120 + valueType: azdata.DeclarativeDataType.string, + width: 120, + isReadOnly: false, + displayName: localizedConstants.NameText, + headerCssStyles: tableHeaderCssStylings, + rowCssStyles: tableRowCssStylings }, { - type: azdata.ColumnType.text, - value: localizedConstants.FilesText, - width: 60 + valueType: azdata.DeclarativeDataType.string, + width: 60, + isReadOnly: true, + displayName: localizedConstants.FilesText, + headerCssStyles: tableHeaderCssStylings, + rowCssStyles: tableRowCssStylings }, { - type: azdata.ColumnType.checkBox, - value: localizedConstants.ReadOnlyText, - width: 80 + valueType: azdata.DeclarativeDataType.boolean, + width: 80, + isReadOnly: false, + displayName: localizedConstants.ReadOnlyText, + headerCssStyles: tableHeaderCssStylings, + rowCssStyles: tableRowCssStylings }, { - type: azdata.ColumnType.checkBox, - value: localizedConstants.DefaultText, - width: 80 + valueType: azdata.DeclarativeDataType.boolean, + width: 80, + isReadOnly: false, + displayName: localizedConstants.DefaultText, + headerCssStyles: tableHeaderCssStylings, + rowCssStyles: tableRowCssStylings }, { - type: azdata.ColumnType.checkBox, - value: localizedConstants.AutogrowAllFilesText, - width: 110 + valueType: azdata.DeclarativeDataType.boolean, + isReadOnly: false, + displayName: localizedConstants.AutogrowAllFilesText, + width: 110, + headerCssStyles: tableHeaderCssStylings, + rowCssStyles: tableRowCssStylings }], data: data, height: getTableHeight(data.length, DefaultMinTableRowCount, DefaultMaxTableRowCount), width: DefaultTableWidth, - forceFitColumns: azdata.ColumnSizingMode.DataFit, CSSStyles: { 'margin-left': '10px' } }).component(); - this.rowsFilegroupNameInput = this.getFilegroupNameInput(this.rowsFilegroupsTable, FileGroupType.RowsFileGroup); const addButtonComponent: DialogButton = { buttonAriaLabel: localizedConstants.AddFilegroupText, - buttonHandler: () => this.onAddDatabaseFileGroupsButtonClicked(this.rowsFilegroupsTable) + buttonHandler: () => this.onAddDatabaseFileGroupsButtonClicked1(this.rowsFilegroupsTable) }; const removeButtonComponent: DialogButton = { buttonAriaLabel: localizedConstants.RemoveButton, - buttonHandler: () => this.onRemoveDatabaseFileGroupsButtonClicked(this.rowsFilegroupsTable) + buttonHandler: () => this.onRemoveDatabaseFileGroupsButtonClicked1(this.rowsFilegroupsTable) }; - const rowsFileGroupButtonContainer = this.addButtonsForTable(this.rowsFilegroupsTable, addButtonComponent, removeButtonComponent); + const rowsFileGroupButtonContainer = this.addButtonsForTable1(this.rowsFilegroupsTable, addButtonComponent, removeButtonComponent); this.disposables.push( - this.rowsFilegroupsTable.onCellAction(async (arg: azdata.ICheckboxCellActionEventArgs) => { + this.rowsFilegroupsTable.onDataChanged(async (arg: azdata.ICheckboxCellActionEventArgs) => { let filegroup = this.rowDataFileGroupsTableRows[arg.row]; // Read-Only column if (arg.column === 2) { @@ -771,14 +831,14 @@ export class DatabaseDialog extends ObjectManagementDialogBase { - if (this.rowsFilegroupsTable.selectedRows.length === 1) { - const fileGroup = this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRows[0]]; + if (this.rowsFilegroupsTable.selectedRow === 1) { + const fileGroup = this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRow]; await this.rowsFilegroupNameInput.updateCssStyles({ 'visibility': fileGroup.id < 0 ? 'visible' : 'hidden' }); this.rowsFilegroupNameInput.value = fileGroup.name; this.onFormFieldChange(); @@ -786,10 +846,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase { + let newData: any[] | undefined; + let newRow: FileGroup = { + id: --this.newFileGroupTemporaryId, + name: '', + type: undefined, + isReadOnly: false, + isDefault: false, + autogrowAllFiles: false + }; + if (table === this.rowsFilegroupsTable) { + newRow.type = FileGroupType.RowsFileGroup; + newRow.isReadOnly = false; + newRow.isDefault = false; + newRow.autogrowAllFiles = false + this.objectInfo.filegroups?.push(newRow); + newData = this.getTableData(FileGroupType.RowsFileGroup); + } + + if (newData !== undefined) { + // Refresh the table with new row data + this.updateFileGroupsOptionsAndTableRows(); + // await this.setTableData(table, newData, DefaultMaxTableRowCount); + + await table.updateProperties({ + data: newData, + height: getTableHeight(newData.length, DefaultMinTableRowCount) + }); + } + } + private async onRemoveDatabaseFileGroupsButtonClicked1(table: azdata.DeclarativeTableComponent): Promise { + if (table === this.rowsFilegroupsTable) { + if (this.rowsFilegroupsTable.selectedRow === 1) { + const removeFilegroupIndex = this.objectInfo.filegroups.indexOf(this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRow]); + this.objectInfo.filegroups?.splice(removeFilegroupIndex, 1); + var newData = this.getTableData(FileGroupType.RowsFileGroup); + await this.rowsFilegroupNameInput.updateCssStyles({ 'visibility': 'hidden' }); + } + } + + // Refresh the individual table rows object and table with updated data + this.updateFileGroupsOptionsAndTableRows(); + await table.updateProperties({ + data: newData, + height: getTableHeight(newData.length, DefaultMinTableRowCount) + }); + } + + /** * Prepares the individual table rows for each filegroup type and list of filegroups options * This will be useful to get the selected row data from the table to get the filegroup property details, helps when have duplicate rows added @@ -1000,15 +1107,16 @@ export class DatabaseDialog extends ObjectManagementDialogBase { - if (table === this.rowsFilegroupsTable) { - if (this.rowsFilegroupsTable.selectedRows.length === 1) { - const removeFilegroupIndex = this.objectInfo.filegroups.indexOf(this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRows[0]]); - this.objectInfo.filegroups?.splice(removeFilegroupIndex, 1); - var newData = this.getTableData(FileGroupType.RowsFileGroup); - await this.rowsFilegroupNameInput.updateCssStyles({ 'visibility': 'hidden' }); - } - } - else if (table === this.filestreamFilegroupsTable) { + // if (table === this.rowsFilegroupsTable) { + // if (this.rowsFilegroupsTable.selectedRows.length === 1) { + // const removeFilegroupIndex = this.objectInfo.filegroups.indexOf(this.rowDataFileGroupsTableRows[this.rowsFilegroupsTable.selectedRows[0]]); + // this.objectInfo.filegroups?.splice(removeFilegroupIndex, 1); + // var newData = this.getTableData(FileGroupType.RowsFileGroup); + // await this.rowsFilegroupNameInput.updateCssStyles({ 'visibility': 'hidden' }); + // } + // } + // else + if (table === this.filestreamFilegroupsTable) { if (this.filestreamFilegroupsTable.selectedRows.length === 1) { const removeFilegroupIndex = this.objectInfo.filegroups.indexOf(this.filestreamDataFileGroupsTableRows[this.filestreamFilegroupsTable.selectedRows[0]]); this.objectInfo.filegroups?.splice(removeFilegroupIndex, 1); @@ -1040,9 +1148,10 @@ export class DatabaseDialog extends ObjectManagementDialogBase { if (table.selectedRows.length === 1) { let fg = null; - if (table === this.rowsFilegroupsTable) { - fg = this.rowDataFileGroupsTableRows[table.selectedRows[0]]; - } else if (table === this.filestreamFilegroupsTable) { + // if (table === this.rowsFilegroupsTable) { + // fg = this.rowDataFileGroupsTableRows[table.selectedRows[0]]; + // } else + if (table === this.filestreamFilegroupsTable) { fg = this.filestreamDataFileGroupsTableRows[table.selectedRows[0]]; } else if (table === this.memoryOptimizedFilegroupsTable) { fg = this.memoryoptimizedFileGroupsTableRows[table.selectedRows[0]]; @@ -1581,6 +1690,245 @@ export class DatabaseDialog extends ObjectManagementDialogBase { }, { + ariaLabel: localizedConstants.ActualOperationModeText, + inputType: 'text', + enabled: false, + value: actualOperationMode + }); + containers.push(this.createLabelInputContainer(localizedConstants.ActualOperationModeText, operationModeActual)); + + // Operation Mode (Requested) + this.requestedOperationMode = this.createDropdown(localizedConstants.RequestedOperationModeText, async (newValue) => { + this.objectInfo.queryStoreOptions.actualMode = newValue as string; + this.areQueryStoreOptionsEnabled = newValue !== this.operationModeOffOption; + await this.toggleQueryStoreOptions(); + }, this.viewInfo.operationModeOptions, String(this.objectInfo.queryStoreOptions.actualMode), true, DefaultInputWidth); + containers.push(this.createLabelInputContainer(localizedConstants.RequestedOperationModeText, this.requestedOperationMode)); + + const generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, containers, true); + this.queryStoreTabSectionsContainer.push(generalSection); + } + + private initializeQueryStoreMonitoringSection(): void { + let containers: azdata.Component[] = []; + // Data Flush Interval (Minutes) + this.dataFlushIntervalInMinutes = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.dataFlushIntervalInMinutes = Number(newValue); + }, { + ariaLabel: localizedConstants.DataFlushIntervalInMinutesText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.dataFlushIntervalInMinutes), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.DataFlushIntervalInMinutesText, this.dataFlushIntervalInMinutes)); + + // Statistics Collection Interval + this.statisticsCollectionInterval = this.createDropdown(localizedConstants.StatisticsCollectionInterval, async (newValue) => { + this.objectInfo.queryStoreOptions.statisticsCollectionInterval = String(newValue); + }, this.viewInfo.statisticsCollectionIntervalOptions, this.objectInfo.queryStoreOptions.statisticsCollectionInterval, this.areQueryStoreOptionsEnabled, DefaultInputWidth); + containers.push(this.createLabelInputContainer(localizedConstants.StatisticsCollectionInterval, this.statisticsCollectionInterval)); + + const monitoringSection = this.createGroup(localizedConstants.MonitoringSectionText, containers, true); + this.queryStoreTabSectionsContainer.push(monitoringSection); + } + + private initializeQueryStoreRetentionSection(): void { + let containers: azdata.Component[] = []; + // Max Plans Per Query + this.maxPlansPerQuery = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.maxPlansPerQuery = Number(newValue); + }, { + ariaLabel: localizedConstants.MaxPlansPerQueryText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.maxPlansPerQuery), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.MaxPlansPerQueryText, this.maxPlansPerQuery)); + + // Max size (MB) + this.maxSizeinMB = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.maxSizeInMB = Number(newValue); + }, { + ariaLabel: localizedConstants.MaxSizeInMbText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.maxSizeInMB), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.MaxSizeInMbText, this.maxSizeinMB)); + + // Query Store Capture Mode + this.queryStoreCaptureMode = this.createDropdown(localizedConstants.QueryStoreCaptureModeText, async (newValue) => { + this.objectInfo.queryStoreOptions.queryStoreCaptureMode = newValue as string; + await this.toggleQueryCapturePolicySection(newValue === localizedConstants.QueryStoreCapturemodeCustomText + && this.requestedOperationMode.value !== this.operationModeOffOption); + }, this.viewInfo.queryStoreCaptureModeOptions, this.objectInfo.queryStoreOptions.queryStoreCaptureMode, this.areQueryStoreOptionsEnabled, DefaultInputWidth); + containers.push(this.createLabelInputContainer(localizedConstants.QueryStoreCaptureModeText, this.queryStoreCaptureMode)); + + // Size Based Cleanup Mode + this.sizeBasedCleanupMode = this.createDropdown(localizedConstants.SizeBasedCleanupModeText, async (newValue) => { + this.objectInfo.queryStoreOptions.sizeBasedCleanupMode = newValue as string; + }, this.viewInfo.sizeBasedCleanupModeOptions, this.objectInfo.queryStoreOptions.sizeBasedCleanupMode, this.areQueryStoreOptionsEnabled, DefaultInputWidth); + containers.push(this.createLabelInputContainer(localizedConstants.SizeBasedCleanupModeText, this.sizeBasedCleanupMode)); + + // State Query Threshold (Days) + this.stateQueryThresholdInDays = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.staleQueryThresholdInDays = Number(newValue); + }, { + ariaLabel: localizedConstants.StateQueryThresholdInDaysText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.staleQueryThresholdInDays), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.StateQueryThresholdInDaysText, this.stateQueryThresholdInDays)); + + // Wait Statistics Capture Mode - supported from 2017 or higher + if (!isUndefinedOrNull(this.objectInfo.queryStoreOptions.waitStatisticsCaptureMode)) { + this.waitStatisticsCaptureMode = this.createCheckbox(localizedConstants.WaitStatisticsCaptureModeText, async (checked) => { + this.objectInfo.queryStoreOptions.waitStatisticsCaptureMode = checked; + }, this.objectInfo.queryStoreOptions.waitStatisticsCaptureMode, this.areQueryStoreOptionsEnabled); + containers.push(this.waitStatisticsCaptureMode); + } + const retentionSection = this.createGroup(localizedConstants.WaitStatisticsCaptureModeText, containers, true); + this.queryStoreTabSectionsContainer.push(retentionSection); + } + + private initializeQueryStoreCapturePolicySection(): void { + let containers: azdata.Component[] = []; + // Execution Count + this.executionCount = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.capturePolicyOptions.executionCount = Number(newValue); + }, { + ariaLabel: localizedConstants.ExecutionCountText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.capturePolicyOptions.executionCount), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.ExecutionCountText, this.executionCount)); + + // Stale Threshold + this.staleThreshold = this.createDropdown(localizedConstants.StaleThresholdText, async (newValue) => { + this.objectInfo.queryStoreOptions.capturePolicyOptions.staleThreshold = newValue as string; + }, this.viewInfo.staleThresholdOptions, this.objectInfo.queryStoreOptions.capturePolicyOptions.staleThreshold, this.areQueryStoreOptionsEnabled, DefaultInputWidth); + containers.push(this.createLabelInputContainer(localizedConstants.StaleThresholdText, this.staleThreshold)); + + // Total Compile CPU Time (ms) + this.totalCompileCPUTimeInMS = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.capturePolicyOptions.totalCompileCPUTimeInMS = Number(newValue); + }, { + ariaLabel: localizedConstants.TotalCompileCPUTimeInMsText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.capturePolicyOptions.totalCompileCPUTimeInMS), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.TotalCompileCPUTimeInMsText, this.totalCompileCPUTimeInMS)); + + // Total Execution CPU Time (ms) + this.totalExecutionCPUTimeInMS = this.createInputBox(async (newValue) => { + this.objectInfo.queryStoreOptions.capturePolicyOptions.totalExecutionCPUTimeInMS = Number(newValue); + }, { + ariaLabel: localizedConstants.TotalExecutionCPUTimeInMsText, + inputType: 'number', + enabled: this.areQueryStoreOptionsEnabled, + value: String(this.objectInfo.queryStoreOptions.capturePolicyOptions.totalExecutionCPUTimeInMS), + min: 0 + }); + containers.push(this.createLabelInputContainer(localizedConstants.TotalExecutionCPUTimeInMsText, this.totalExecutionCPUTimeInMS)); + + const policySection = this.createGroup(localizedConstants.QueryStoreCapturePolicySectionText, containers, true); + this.queryStoreTabSectionsContainer.push(policySection); + } + + private async initializeQueryStoreCurrentDiskStorageSection(): Promise { + let containers: azdata.Component[] = []; + // Database Max size + const databaseName = this.createInputBox(async () => { }, { + ariaLabel: this.objectInfo.name, + inputType: 'text', + enabled: false, + value: localizedConstants.StringValueInMB(String(this.objectInfo.sizeInMb)) + }); + containers.push(this.createLabelInputContainer(this.objectInfo.name, databaseName)); + + // Query Store Used + const queryStoreUsed = this.createInputBox(async () => { }, { + ariaLabel: localizedConstants.QueryStoreUsedText, + inputType: 'text', + enabled: false, + value: localizedConstants.StringValueInMB(String(this.objectInfo.queryStoreOptions.currentStorageSizeInMB)) + }); + containers.push(this.createLabelInputContainer(localizedConstants.QueryStoreUsedText, queryStoreUsed)); + + // Query Store Available + const queryStoreAvailable = this.createInputBox(async () => { }, { + ariaLabel: localizedConstants.QueryStoreAvailableText, + inputType: 'text', + enabled: false, + value: localizedConstants.StringValueInMB(String(this.objectInfo.queryStoreOptions.maxSizeInMB - this.objectInfo.queryStoreOptions.currentStorageSizeInMB)) + }); + containers.push(this.createLabelInputContainer(localizedConstants.QueryStoreAvailableText, queryStoreAvailable)); + + // Prge query data button + this.purgeQueryDataButton = this.createButton(localizedConstants.PurgeQueryDataButtonText, localizedConstants.PurgeQueryDataButtonText, async () => { + await this.purgeQueryStoreDataButtonClick(); + }); + this.purgeQueryDataButton.width = DefaultInputWidth; + await this.purgeQueryDataButton.updateCssStyles({ 'margin': '10px 0px, 0px, 0px' }); + containers.push(this.createLabelInputContainer('', this.purgeQueryDataButton)); + + const diskUsageSection = this.createGroup(localizedConstants.QueryStoreCurrentDiskUsageSectionText, containers, true); + this.queryStoreTabSectionsContainer.push(diskUsageSection); + } + + /** + * Opens confirmation warning for clearing the query store data for the database + */ + private async purgeQueryStoreDataButtonClick(): Promise { + const response = await vscode.window.showWarningMessage(localizedConstants.PurgeQueryStoreDataMessage(this.objectInfo.name), localizedConstants.YesText); + if (response !== localizedConstants.YesText) { + return; + } + + await this.objectManagementService.purgeQueryStoreData(this.options.connectionUri, this.options.database, this.options.objectUrn); + } + + private async toggleQueryStoreOptions(): Promise { + this.dataFlushIntervalInMinutes.enabled + = this.statisticsCollectionInterval.enabled + = this.maxPlansPerQuery.enabled + = this.maxSizeinMB.enabled + = this.queryStoreCaptureMode.enabled + = this.sizeBasedCleanupMode.enabled + = this.stateQueryThresholdInDays.enabled = this.areQueryStoreOptionsEnabled; + if (!isUndefinedOrNull(this.objectInfo.queryStoreOptions.waitStatisticsCaptureMode)) { + this.waitStatisticsCaptureMode.enabled = this.areQueryStoreOptionsEnabled + } + await this.toggleQueryCapturePolicySection(this.areQueryStoreOptionsEnabled); + } + + private async toggleQueryCapturePolicySection(enable: boolean): Promise { + if (!isUndefinedOrNull(this.objectInfo.queryStoreOptions.capturePolicyOptions)) { + this.executionCount.enabled + = this.staleThreshold.enabled + = this.totalCompileCPUTimeInMS.enabled + = this.totalExecutionCPUTimeInMS.enabled = enable; + } + } + //#endregion + private initializeConfigureSLOSection(): azdata.GroupContainer { let containers: azdata.Component[] = []; if (this.viewInfo.azureEditions?.length > 0) { diff --git a/extensions/mssql/src/ui/dialogBase.ts b/extensions/mssql/src/ui/dialogBase.ts index 9372152fb6..03630f79f5 100644 --- a/extensions/mssql/src/ui/dialogBase.ts +++ b/extensions/mssql/src/ui/dialogBase.ts @@ -328,6 +328,51 @@ export abstract class DialogBase { return this.createButtonContainer(buttonComponents) } + protected addButtonsForTable1(table: azdata.DeclarativeTableComponent, addbutton: DialogButton, removeButton: DialogButton, editButton: DialogButton = undefined): azdata.FlexContainer { + let addButtonComponent: azdata.ButtonComponent; + let editButtonComponent: azdata.ButtonComponent; + let removeButtonComponent: azdata.ButtonComponent; + let buttonComponents: azdata.ButtonComponent[] = []; + const updateButtons = (isRemoveEnabled: boolean = undefined) => { + this.onFormFieldChange(); + const tableSelectedRowsLengthCheck = table.selectedRow === 1 && table.selectedRow < table.data.length; + if (editButton !== undefined) { + editButtonComponent.enabled = tableSelectedRowsLengthCheck; + } + removeButtonComponent.enabled = !!isRemoveEnabled && tableSelectedRowsLengthCheck; + } + addButtonComponent = this.createButton(uiLoc.AddText, addbutton.buttonAriaLabel, async () => { + await addbutton.buttonHandler(addButtonComponent); + updateButtons(); + }); + buttonComponents.push(addButtonComponent); + + if (editButton !== undefined) { + editButtonComponent = this.createButton(uiLoc.EditText, editButton.buttonAriaLabel, async () => { + await editButton.buttonHandler(editButtonComponent); + updateButtons(); + }, false); + buttonComponents.push(editButtonComponent); + } + + removeButtonComponent = this.createButton(uiLoc.RemoveText, removeButton.buttonAriaLabel, async () => { + await removeButton.buttonHandler(removeButtonComponent); + if (table.selectedRow === 1) { + // table.selectedRow = [table.data.length - 1]; + } + updateButtons(); + }, false); + buttonComponents.push(removeButtonComponent); + + this.disposables.push(table.onRowSelected(() => { + const isRemoveButtonEnabled = true; + updateButtons(isRemoveButtonEnabled); + })); + + return this.createButtonContainer(buttonComponents) + } + + protected createDropdown(ariaLabel: string, handler: (newValue: string) => Promise, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth, editable?: boolean, strictSelection?: boolean): azdata.DropDownComponent { // Automatically add an empty item to the beginning of the list if the current value is not specified. // This is needed when no meaningful default value can be provided.