diff --git a/extensions/mssql/resources/folder.svg b/extensions/mssql/resources/folder.svg new file mode 100644 index 0000000000..64cbba1769 --- /dev/null +++ b/extensions/mssql/resources/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/mssql/src/iconHelper.ts b/extensions/mssql/src/iconHelper.ts index 36b896c5e6..d85d043ad2 100644 --- a/extensions/mssql/src/iconHelper.ts +++ b/extensions/mssql/src/iconHelper.ts @@ -16,6 +16,7 @@ export class IconPathHelper { public static delete: IconPath; public static user: IconPath; public static group: IconPath; + public static folder: IconPath; public static setExtensionContext(extensionContext: vscode.ExtensionContext) { IconPathHelper.extensionContext = extensionContext; @@ -31,5 +32,9 @@ export class IconPathHelper { dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/group_inverse.svg'), light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/group.svg') }; + IconPathHelper.folder = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/folder.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/folder.svg') + }; } } diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index 0bb351fb55..dc600ea0a1 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -32,6 +32,7 @@ export const ViewGeneralServerPropertiesDocUrl = 'https://learn.microsoft.com/sq export const ViewMemoryServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-memory-page'; export const ViewProcessorsServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-processors-page'; export const ViewSecurityServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-security-page'; +export const ViewDatabaseSettingsPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-database-settings-page'; export const DetachDatabaseDocUrl = 'https://go.microsoft.com/fwlink/?linkid=2240322'; export const AttachDatabaseDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/attach-a-database#to-attach-a-database'; export const DatabaseGeneralPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-general-page'; diff --git a/extensions/mssql/src/objectManagement/interfaces.ts b/extensions/mssql/src/objectManagement/interfaces.ts index a515d5c1ac..04c8141fe9 100644 --- a/extensions/mssql/src/objectManagement/interfaces.ts +++ b/extensions/mssql/src/objectManagement/interfaces.ts @@ -541,6 +541,11 @@ export interface Server extends ObjectManagement.SqlObject { numaNodes: NumaNode[]; authenticationMode: ServerLoginMode; loginAuditing: AuditLevel; + checkCompressBackup: boolean; + checkBackupChecksum: boolean; + dataLocation: string; + logLocation: string; + backupLocation: string; } /** diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 619e93cb5b..2bbc9b4362 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -51,6 +51,8 @@ export const AddFileAriaLabel = localize('objectManagement.addFileText', "Add da export const RemoveFileAriaLabel = localize('objectManagement.removeFileText', "Remove database file"); export const CreateObjectLabel = localize('objectManagement.createObjectLabel', "Create"); export const ApplyUpdatesLabel = localize('objectManagement.applyUpdatesLabel', "Apply"); +export const allFiles = localize('objectManagement.allFiles', "All Files"); +export const labelSelectFolder = localize('objectManagement.labelSelectFolder', "Select Folder"); export const DataFileLabel = localize('objectManagement.dataFileLabel', "Data"); export const LogFileLabel = localize('objectManagement.logFileLabel', "Log"); @@ -318,6 +320,14 @@ export const failedLoginsOnlyText = localize('objectManagement.failedLoginsOnlyT export const successfulLoginsOnlyText = localize('objectManagement.successfulLoginsOnlyText', "Successful logins only"); export const bothFailedAndSuccessfulLoginsText = localize('objectManagement.bothFailedAndSuccessfulLoginsText', "Both failed and successful logins"); export const needToRestartServer = localize('objectManagement.needToRestartServer', "Changes require server restart in order to be effective"); +export const logLocationText = localize('objectManagement.logLocationText', "Log"); +export const dataLocationText = localize('objectManagement.dataLocationText', "Data"); +export const backupLocationText = localize('objectManagement.backupLocationText', "Backup"); +export const defaultLocationsLabel = localize('objectManagement.defaultLocationsLabel', "Database default locations"); +export const databaseSettingsText = localize('objectManagement.databaseSettings', "Database Settings"); +export const compressBackupText = localize('objectManagement.compressBackupText', "Compress Backup"); +export const backupChecksumText = localize('objectManagement.backupChecksumText', "Backup checksum"); +export const backupAndRestoreText = localize('objectManagement.backupAndRestoreText', "Backup and Restore"); //Database properties Dialog export const LastDatabaseBackupText = localize('objectManagement.lastDatabaseBackup', "Last Database Backup"); diff --git a/extensions/mssql/src/objectManagement/ui/serverPropertiesDialog.ts b/extensions/mssql/src/objectManagement/ui/serverPropertiesDialog.ts index 3863688eef..462d558333 100644 --- a/extensions/mssql/src/objectManagement/ui/serverPropertiesDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/serverPropertiesDialog.ts @@ -8,7 +8,7 @@ import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './obj import { DefaultColumnCheckboxWidth } from '../../ui/dialogBase'; import { IObjectManagementService } from 'mssql'; import * as localizedConstants from '../localizedConstants'; -import { ViewGeneralServerPropertiesDocUrl, ViewMemoryServerPropertiesDocUrl, ViewProcessorsServerPropertiesDocUrl, ViewSecurityServerPropertiesDocUrl } from '../constants'; +import * as constants from '../constants'; import { Server, ServerViewInfo, NumaNode, AffinityType, ServerLoginMode, AuditLevel } from '../interfaces'; export class ServerPropertiesDialog extends ObjectManagementDialogBase { @@ -60,6 +60,15 @@ export class ServerPropertiesDialog extends ObjectManagementDialogBase { + this.objectInfo.checkCompressBackup = newValue; + }, this.objectInfo.checkCompressBackup); + + this.backupChecksumCheckbox = this.createCheckbox(localizedConstants.backupChecksumText, async (newValue) => { + this.objectInfo.checkBackupChecksum = newValue; + }, this.objectInfo.checkBackupChecksum); + + const checkBoxContainer = this.createGroup(localizedConstants.backupAndRestoreText, [this.compressBackupCheckbox, this.backupChecksumCheckbox], false); + + this.dataLocationInput = this.createInputBox(async (newValue) => { + this.objectInfo.dataLocation = newValue; + }, dataLocationInputboxProps); + const dataLocationButton = this.createBrowseButton(async () => { + const newPath = await this.selectFolder(this.objectInfo.dataLocation); + this.dataLocationInput.value = newPath; + this.objectInfo.dataLocation = newPath; + }, isEnabled); + const dataLocationInputContainer = this.createLabelInputContainer(localizedConstants.dataLocationText, [this.dataLocationInput, dataLocationButton]) + + this.logLocationInput = this.createInputBox(async (newValue) => { + this.objectInfo.logLocation = newValue; + }, logLocationInputboxProps); + const logLocationButton = this.createBrowseButton(async () => { + const newPath = await this.selectFolder(this.objectInfo.logLocation); + this.logLocationInput.value = newPath; + this.objectInfo.logLocation = newPath; + }, isEnabled); + const logLocationInputContainer = this.createLabelInputContainer(localizedConstants.logLocationText, [this.logLocationInput, logLocationButton]) + + this.backupLocationInput = this.createInputBox(async (newValue) => { + this.objectInfo.backupLocation = newValue; + }, backupLocationInputboxProps); + const backupLocationButton = this.createBrowseButton(async () => { + const newPath = await this.selectFolder(this.objectInfo.backupLocation); + this.backupLocationInput.value = newPath; + this.objectInfo.backupLocation = newPath; + }, isEnabled); + const backupLocationInputContainer = this.createLabelInputContainer(localizedConstants.backupLocationText, [this.backupLocationInput, backupLocationButton]) + + const defaultLocationsContainer = this.createGroup(localizedConstants.defaultLocationsLabel, [ + dataLocationInputContainer, + logLocationInputContainer, + backupLocationInputContainer + ], false); + + this.databaseSettingsSection = this.createGroup('', [ + checkBoxContainer, + defaultLocationsContainer + ], false); + + this.databaseSettingsTab = this.createTab(this.databaseSettingsTabId, localizedConstants.databaseSettingsText, this.databaseSettingsSection); + } + + public async selectFolder(location: string): Promise { + const allFilesFilter = localizedConstants.allFiles; + let filter: any = {}; + filter[allFilesFilter] = '*'; + let uris = await vscode.window.showOpenDialog({ + filters: filter, + canSelectFiles: false, + canSelectMany: false, + canSelectFolders: true, + defaultUri: vscode.Uri.file(location), + openLabel: localizedConstants.labelSelectFolder + }); + if (uris && uris.length > 0) { + return uris[0].fsPath; + } + return undefined; + } } diff --git a/extensions/mssql/src/ui/dialogBase.ts b/extensions/mssql/src/ui/dialogBase.ts index f7605d445c..9372152fb6 100644 --- a/extensions/mssql/src/ui/dialogBase.ts +++ b/extensions/mssql/src/ui/dialogBase.ts @@ -7,6 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { EOL } from 'os'; import * as uiLoc from '../ui/localizedConstants'; +import { IconPathHelper } from '../iconHelper'; export const DefaultLabelWidth = 150; export const DefaultInputWidth = 300; @@ -130,10 +131,16 @@ export abstract class DialogBase { return errors.length === 0; } - protected createLabelInputContainer(label: string, component: azdata.Component, required: boolean = false): azdata.FlexContainer { - const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: required, CSSStyles: { 'padding-right': '10px' } }).component(); - const container = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'horizontal', flexWrap: 'nowrap', alignItems: 'center' }).withItems([labelComponent], { flex: '0 0 auto' }).component(); - container.addItem(component, { flex: '1 1 auto' }); + protected createLabelInputContainer(label: string, component: azdata.Component | azdata.Component[], required: boolean = false): azdata.FlexContainer { + let container: azdata.FlexContainer = undefined; + if (Array.isArray(component)) { + const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth - 40, value: label, requiredIndicator: required, CSSStyles: { 'padding-right': '10px' } }).component(); + container = this.modelView.modelBuilder.flexContainer().withItems([labelComponent, ...component], { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + } else { + const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: required, CSSStyles: { 'padding-right': '10px' } }).component(); + container = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'horizontal', flexWrap: 'nowrap', alignItems: 'center' }).withItems([labelComponent], { flex: '0 0 auto' }).component(); + container.addItem(component, { flex: '1 1 auto' }); + } return container; } @@ -371,6 +378,24 @@ export abstract class DialogBase { }).withItems(items, { flex: '0 0 auto' }).component(); } + protected createHorizontalContainer(header: string, items: azdata.Component[]): azdata.FlexContainer { + return this.modelView.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + } + + protected createBrowseButton(handler: () => Promise, enabled: boolean = true): azdata.ButtonComponent { + const button = this.dialogObject.modelView.modelBuilder.button().withProps({ + ariaLabel: 'browse', + iconPath: IconPathHelper.folder, + width: '18px', + height: '20px', + enabled: enabled + }).component(); + this.disposables.push(button.onDidClick(async () => { + await handler(); + })); + return button; + } + protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise, enabled: boolean = true): azdata.RadioButtonComponent { const radio = this.modelView.modelBuilder.radioButton().withProps({ label: label,