From c695c5f941ed672e6b74351a793eced5c41f745a Mon Sep 17 00:00:00 2001
From: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com>
Date: Thu, 31 Aug 2023 14:26:08 -0700
Subject: [PATCH] Add database settings tab (#24260)
---
extensions/mssql/resources/folder.svg | 3 +
extensions/mssql/src/iconHelper.ts | 5 +
.../mssql/src/objectManagement/constants.ts | 1 +
.../mssql/src/objectManagement/interfaces.ts | 5 +
.../objectManagement/localizedConstants.ts | 10 ++
.../ui/serverPropertiesDialog.ts | 120 +++++++++++++++++-
extensions/mssql/src/ui/dialogBase.ts | 33 ++++-
7 files changed, 167 insertions(+), 10 deletions(-)
create mode 100644 extensions/mssql/resources/folder.svg
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,