diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 147532d3f2..b99603386a 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -30,6 +30,8 @@ export const newSupportRequest = localize('arc.newSupportRequest', "New support export const diagnoseAndSolveProblems = localize('arc.diagnoseAndSolveProblems', "Diagnose and solve problems"); export const supportAndTroubleshooting = localize('arc.supportAndTroubleshooting', "Support + troubleshooting"); export const resourceHealth = localize('arc.resourceHealth', "Resource health"); +export const parameterName = localize('arc.parameterName', "Parameter Name"); +export const value = localize('arc.value', "Value"); export const newInstance = localize('arc.createNew', "New Instance"); export const deleteText = localize('arc.delete', "Delete"); @@ -173,6 +175,8 @@ export function rangeSetting(min: string, max: string): string { return localize export function allowedValue(value: string): string { return localize('arc.allowedValue', "Value is expected to be {0}", value); } export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); } export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); } +export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); } +export function extensionInstalled(name: string): string { return localize('arc.extensionInstalled', "Extension '{0}' has been installed.", name); } export function updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); } export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); } export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); } @@ -198,6 +202,7 @@ export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A conn export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration."); export function outOfRange(min: string, max: string): string { return localize('arc.outOfRange', "The number must be in range {0} - {1}", min, max); } export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); } +export function resetFailed(error: any): string { return localize('arc.resetFailed', "Reset failed. {0}", getErrorMessage(error)); } export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); } export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); } export function instanceUpdateFailed(name: string, error: any): string { return localize('arc.instanceUpdateFailed', "Failed to update instance {0}. {1}", name, getErrorMessage(error)); } @@ -207,6 +212,7 @@ export function connectToControllerFailed(url: string, error: any): string { ret export function connectToMSSqlFailed(serverName: string, error: any): string { return localize('arc.connectToMSSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } export function connectToPGSqlFailed(serverName: string, error: any): string { return localize('arc.connectToPGSqlFailed', "Could not connect to PostgreSQL Hyperscale - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } export function missingExtension(extensionName: string): string { return localize('arc.missingExtension', "The {0} extension is required to view engine settings. Do you wish to install it now?", extensionName); } +export function extensionInstallationFailed(extensionName: string): string { return localize('arc.extensionInstallationFailed', "Failed to install extension {0}.", extensionName); } export function fetchConfigFailed(name: string, error: any): string { return localize('arc.fetchConfigFailed', "An unexpected error occurred retrieving the config for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occurred retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); } diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 43ce1fbb20..cb8d61906d 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -144,22 +144,34 @@ export class PostgresModel extends ResourceModel { if (!engineSettings) { throw new Error('Could not fetch engine settings'); } + + const skippedEngineSettings: String[] = [ + 'archive_command', 'archive_timeout', 'log_directory', 'log_file_mode', 'log_filename', 'restore_command', + 'shared_preload_libraries', 'synchronous_commit', 'ssl', 'unix_socket_permissions', 'wal_level' + ]; + + this._engineSettings = []; + engineSettings.rows.forEach(row => { let rowValues = row.map(c => c.displayValue); - let result: EngineSettingsModel = { - parameterName: rowValues.shift(), - value: rowValues.shift(), - description: rowValues.shift(), - min: rowValues.shift(), - max: rowValues.shift(), - options: rowValues.shift(), - type: rowValues.shift() - }; + let name = rowValues.shift(); + if (!skippedEngineSettings.includes(name!)) { + let result: EngineSettingsModel = { + parameterName: name, + value: rowValues.shift(), + description: rowValues.shift(), + min: rowValues.shift(), + max: rowValues.shift(), + options: rowValues.shift(), + type: rowValues.shift() + }; - this._engineSettings.push(result); + this._engineSettings.push(result); + } }); this.engineSettingsLastUpdated = new Date(); + this._onEngineSettingsUpdated.fire(this._engineSettings); } protected createConnectionProfile(): azdata.IConnectionProfile { diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresComputeAndStoragePage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresComputeAndStoragePage.ts index c867dbb5f4..37b80f4826 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresComputeAndStoragePage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresComputeAndStoragePage.ts @@ -341,8 +341,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage { const information = this.modelView.modelBuilder.button().withProperties({ iconPath: IconPathHelper.information, title: loc.workerNodesInformation, - width: '12px', - height: '12px', + width: '15px', + height: '15px', enabled: false }).component(); @@ -434,8 +434,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage { const information = this.modelView.modelBuilder.button().withProperties({ iconPath: IconPathHelper.information, title: loc.postgresConfigurationInformation, - width: '12px', - height: '12px', + width: '15px', + height: '15px', enabled: false }).component(); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index 0a6568ce58..573075d80d 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -34,6 +34,8 @@ export class PostgresDashboard extends Dashboard { const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel); // TODO: Removed properties page while investigating bug where refreshed values don't appear in UI // const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); + // TODO: Removed parameters page while investigating bug where UI freezes dealing with large declarative table of components + // const parametersPage = new PostgresParametersPage(modelView, this._postgresModel); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel); const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts index ecdc2bb314..7c9c227ea5 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts @@ -10,21 +10,28 @@ import * as loc from '../../../localizedConstants'; import { UserCancelledError } from '../../../common/api'; import { IconPathHelper, cssStyles } from '../../../constants'; import { DashboardPage } from '../../components/dashboardPage'; -import { PostgresModel } from '../../../models/postgresModel'; +import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel'; + +export type ParametersModel = { + parameterName: string, + valueContainer: azdata.FlexContainer, + description: string, + resetButton: azdata.ButtonComponent +}; export class PostgresParametersPage extends DashboardPage { - private searchBox?: azdata.InputBoxComponent; + private searchBox!: azdata.InputBoxComponent; private parametersTable!: azdata.DeclarativeTableComponent; private parameterContainer?: azdata.DivContainer; private _parametersTableLoading!: azdata.LoadingComponent; - private discardButton?: azdata.ButtonComponent; - private saveButton?: azdata.ButtonComponent; - private resetButton?: azdata.ButtonComponent; + private discardButton!: azdata.ButtonComponent; + private saveButton!: azdata.ButtonComponent; + private resetAllButton!: azdata.ButtonComponent; private connectToServerButton?: azdata.ButtonComponent; - private engineSettings = `'`; - private engineSettingUpdates?: Map; + private _parameters: ParametersModel[] = []; + private parameterUpdates: Map = new Map(); private readonly _azdataApi: azdataExt.IExtension; @@ -35,11 +42,10 @@ export class PostgresParametersPage extends DashboardPage { this.initializeConnectButton(); this.initializeSearchBox(); - this.engineSettingUpdates = new Map(); - this.disposables.push( this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())), - this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEngineSettingsUpdated()))); + this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable())) + ); } protected get title(): string { @@ -64,20 +70,15 @@ export class PostgresParametersPage extends DashboardPage { CSSStyles: { ...cssStyles.title } }).component()); - const info = this.modelView.modelBuilder.text().withProps({ + content.addItem(this.modelView.modelBuilder.text().withProps({ value: loc.nodeParametersDescription, CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); + }).component()); - const link = this.modelView.modelBuilder.hyperlink().withProps({ + content.addItem(this.modelView.modelBuilder.hyperlink().withProps({ label: loc.learnAboutNodeParameters, - url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale', - }).component(); - - const infoAndLink = this.modelView.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component(); - infoAndLink.addItem(info, { CSSStyles: { 'margin-right': '5px' } }); - infoAndLink.addItem(link); - content.addItem(infoAndLink, { CSSStyles: { 'margin-bottom': '20px' } }); + url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale' + }).component(), { CSSStyles: { 'margin-bottom': '20px' } }); content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } }); @@ -85,43 +86,33 @@ export class PostgresParametersPage extends DashboardPage { width: '100%', columns: [ { - displayName: 'Parameter Name', - valueType: azdata.DeclarativeDataType.component, + displayName: loc.parameterName, + valueType: azdata.DeclarativeDataType.string, isReadOnly: true, width: '20%', headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }, { - displayName: 'Value', + displayName: loc.value, valueType: azdata.DeclarativeDataType.component, isReadOnly: false, width: '20%', headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { - ...cssStyles.tableRow, - 'overflow': 'hidden', - 'text-overflow': 'ellipsis', - 'white-space': 'nowrap', - 'max-width': '0' - } + rowCssStyles: cssStyles.tableRow }, { - displayName: 'Description', - valueType: azdata.DeclarativeDataType.component, + displayName: loc.description, + valueType: azdata.DeclarativeDataType.string, isReadOnly: true, width: '50%', headerCssStyles: cssStyles.tableHeader, rowCssStyles: { - ...cssStyles.tableRow, - 'overflow': 'hidden', - 'text-overflow': 'ellipsis', - 'white-space': 'nowrap', - 'max-width': '0' + ...cssStyles.tableRow } }, { - displayName: 'Reset To Default', + displayName: loc.resetToDefault, valueType: azdata.DeclarativeDataType.component, isReadOnly: false, width: '10%', @@ -129,11 +120,7 @@ export class PostgresParametersPage extends DashboardPage { rowCssStyles: cssStyles.tableRow } ], - data: [ - this.parameterComponents('TEST NAME', 'string'), - this.parameterComponents('TEST NAME 2', 'real'), - this.createParametersTable()] - + data: [] }).component(); this._parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component(); @@ -156,8 +143,10 @@ export class PostgresParametersPage extends DashboardPage { enabled: false }).component(); + let engineSettings: string[] = []; this.disposables.push( this.saveButton.onDidClick(async () => { + this.saveButton!.enabled = false; try { await vscode.window.withProgress( { @@ -166,20 +155,19 @@ export class PostgresParametersPage extends DashboardPage { cancellable: false }, async (_progress, _token): Promise => { - //Edit multiple - // azdata arc postgres server edit -n -e '=, =,...' try { - this.engineSettingUpdates!.forEach((value: string) => { - this.engineSettings += value + ', '; + this.parameterUpdates!.forEach((value, key) => { + engineSettings.push(`${key}="${value}"`); }); const session = await this._postgresModel.controllerModel.acquireAzdataSession(); try { await this._azdataApi.azdata.arc.postgres.server.edit( - this._postgresModel.info.name, { engineSettings: this.engineSettings + `'` }); + this._postgresModel.info.name, + { engineSettings: engineSettings.toString() }, + this._postgresModel.engineVersion); } finally { session.dispose(); } - } catch (err) { // If an error occurs while editing the instance then re-enable the save button since // the edit wasn't successfully applied @@ -192,14 +180,16 @@ export class PostgresParametersPage extends DashboardPage { vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); - this.engineSettings = `'`; - this.engineSettingUpdates!.clear(); + engineSettings = []; + this.parameterUpdates!.clear(); this.discardButton!.enabled = false; + this.resetAllButton!.enabled = true; } catch (error) { vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error)); } - })); + }) + ); // Discard this.discardButton = this.modelView.modelBuilder.button().withProps({ @@ -212,27 +202,27 @@ export class PostgresParametersPage extends DashboardPage { this.discardButton.onDidClick(async () => { this.discardButton!.enabled = false; try { - // TODO - // this.parametersTable.data = []; - this.engineSettingUpdates!.clear(); + this.refreshParametersTable(); } catch (error) { vscode.window.showErrorMessage(loc.pageDiscardFailed(error)); } finally { this.saveButton!.enabled = false; } - })); + }) + ); - // Reset - this.resetButton = this.modelView.modelBuilder.button().withProps({ + // Reset all + this.resetAllButton = this.modelView.modelBuilder.button().withProps({ label: loc.resetAllToDefault, iconPath: IconPathHelper.reset, - enabled: true + enabled: false }).component(); this.disposables.push( - this.resetButton.onDidClick(async () => { - this.resetButton!.enabled = false; + this.resetAllButton.onDidClick(async () => { + this.resetAllButton!.enabled = false; this.discardButton!.enabled = false; + this.saveButton!.enabled = false; try { await vscode.window.withProgress( { @@ -247,11 +237,17 @@ export class PostgresParametersPage extends DashboardPage { try { session = await this._postgresModel.controllerModel.acquireAzdataSession(); await this._azdataApi.azdata.arc.postgres.server.edit( - this._postgresModel.info.name, { engineSettings: `'' -re` }); + this._postgresModel.info.name, + { engineSettings: `''`, replaceEngineSettings: true }, + this._postgresModel.engineVersion); } catch (err) { // If an error occurs while resetting the instance then re-enable the reset button since // the edit wasn't successfully applied - this.resetButton!.enabled = true; + if (this.parameterUpdates.size > 0) { + this.discardButton!.enabled = true; + this.saveButton!.enabled = true; + } + this.resetAllButton!.enabled = true; throw err; } finally { session?.dispose(); @@ -260,20 +256,24 @@ export class PostgresParametersPage extends DashboardPage { } ); + + vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); + this.parameterUpdates!.clear(); + } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); + vscode.window.showErrorMessage(loc.resetFailed(error)); } - })); + }) + ); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: this.saveButton }, { component: this.discardButton }, - { component: this.resetButton } + { component: this.resetAllButton } ]).component(); } - private initializeConnectButton() { - + private initializeConnectButton(): void { this.connectToServerButton = this.modelView.modelBuilder.button().withProps({ label: loc.connectToServer, enabled: false, @@ -290,211 +290,36 @@ export class PostgresParametersPage extends DashboardPage { return; } - await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension); + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.installingExtension(loc.postgresExtension), + cancellable: false + }, + async (_progress, _token): Promise => { + try { + await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension); + } catch (err) { + vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension)); + this.connectToServerButton!.enabled = true; + throw err; + } + } + ); + vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension)); } - await this._postgresModel.getEngineSettings().catch(err => { - // If an error occurs show a message so the user knows something failed but still - // fire the event so callers can know to update (e.g. so dashboards don't show the - // loading icon forever) - if (err instanceof UserCancelledError) { - vscode.window.showWarningMessage(loc.pgConnectionRequired); - } else { - vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, err)); - } - this._postgresModel.engineSettingsLastUpdated = new Date(); - this._postgresModel._onEngineSettingsUpdated.fire(this._postgresModel._engineSettings); - this.connectToServerButton!.enabled = true; - throw err; - }); - + this._parametersTableLoading!.loading = true; + await this.callGetEngineSettings().finally(() => this._parametersTableLoading!.loading = false); + this.searchBox!.enabled = true; + this.resetAllButton!.enabled = true; this.parameterContainer!.clearItems(); this.parameterContainer!.addItem(this.parametersTable); - - })); - } - - private initializeSearchBox() { - this.searchBox = this.modelView.modelBuilder.inputBox().withProps({ - readOnly: false, - placeHolder: loc.searchToFilter - }).component(); - - this.disposables.push( - this.searchBox.onTextChanged(() => { - this.filterParameters(); }) ); } - private filterParameters() { - //TODO - } - - private createParametersTable(): any[] { - /*Define server settings that shouldn't be modified. we block archive_*, restore_*, and synchronous_commit to prevent the user - from messing up our backups. (we rely on synchronous_commit to ensure WAL changes are written immediately.) - we block log_* to protect our logging. we block wal_level because Citus needs a particular wal_Level to rebalance shards - TODO: Review list of blacklisted parameters. wal_level should only be blacklisted if sharding is enabled - To not be modified - "archive_command", "archive_timeout", "log_directory", "log_file_mode", "log_filename", "restore_command", - "shared_preload_libraries", "synchronous_commit", "ssl", "unix_socket_permissions", "wal_level" */ - - // For ev in this._postgresModel._engineSettings - // create row - // return rows - this.parameterComponents('engineSetting', ''); - return []; - } - - private parameterComponents(name: string, type: string): any[] { - let data = []; - - // Set parameter name - const parameterName = this.modelView.modelBuilder.text().withProps({ - value: name, - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - data.push(parameterName); - - // Container to hold input component and information bubble - const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component(); - - // Information bubble title to be set depening on type of input - let information = this.modelView.modelBuilder.button().withProps({ - iconPath: IconPathHelper.information, - width: '12px', - height: '12px', - enabled: false - }).component(); - - if (type === 'enum') { - // If type is enum, component should be drop down menu - let valueBox = this.modelView.modelBuilder.dropDown().withProps({ - values: [], //TODO, - value: '', //TODO - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); - - this.disposables.push( - valueBox.onValueChanged(() => { - this.engineSettingUpdates!.set(name, String(valueBox.value)); - - }) - ); - - information.updateProperty('title', loc.allowedValue('enums')); //TODO - } else if (type === 'bool') { - // If type is bool, component should be checkbox to turn on or off - let valueBox = this.modelView.modelBuilder.checkBox().withProps({ - label: loc.on, - checked: true, //TODO - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); - - this.disposables.push( - valueBox.onChanged(() => { - if (valueBox.checked) { - this.engineSettingUpdates!.set(name, loc.on); - } else { - this.engineSettingUpdates!.set(name, loc.off); - } - }) - ); - - information.updateProperty('title', loc.allowedValue('on,off')); //TODO - } else if (type === 'string') { - // If type is string, component should be text inputbox - // How to add validation: .withValidation(component => component.value?.search('[0-9]') == -1) - let valueBox = this.modelView.modelBuilder.inputBox().withProps({ - readOnly: false, - value: '', //TODO - CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '200px' } - }).component(); - valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); - - this.disposables.push( - valueBox.onTextChanged(() => { - this.engineSettingUpdates!.set(name, valueBox.value!); - }) - ); - - information.updateProperty('title', loc.allowedValue(loc.allowedValue('[A-Za-z._]+'))); //TODO - } else { - // If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set. - let valueBox = this.modelView.modelBuilder.inputBox().withProps({ - readOnly: false, - min: 0, //TODO - max: 10000, - validationErrorMessage: loc.outOfRange('min', 'max'), //TODO - inputType: 'number', - value: '0', //TODO - CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '200px' } - }).component(); - valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); - - this.disposables.push( - valueBox.onTextChanged(() => { - this.engineSettingUpdates!.set(name, valueBox.value!); - }) - ); - - information.updateProperty('title', loc.allowedValue(loc.rangeSetting('min', 'max'))); //TODO - } - - valueContainer.addItem(information, { CSSStyles: { 'margin-left': '5px', 'margin-bottom': '15px' } }); - data.push(valueContainer); - - const parameterDescription = this.modelView.modelBuilder.text().withProps({ - value: 'TEST DESCRIPTION HERE ...............................ytgbyugvtyvctyrcvytjv ycrtctyv tyfty ftyuvuyvuy', // TODO - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - data.push(parameterDescription); - - // Can reset individual component - const resetParameter = this.modelView.modelBuilder.button().withProps({ - iconPath: IconPathHelper.reset, - title: loc.resetToDefault, - width: '20px', - height: '20px', - enabled: true - }).component(); - data.push(resetParameter); - - // azdata arc postgres server edit -n postgres01 -e shared_buffers= - this.disposables.push( - resetParameter.onDidClick(async () => { - try { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: loc.updatingInstance(this._postgresModel.info.name), - cancellable: false - }, - async (_progress, _token) => { - const session = await this._postgresModel.controllerModel.acquireAzdataSession(); - try { - this._azdataApi.azdata.arc.postgres.server.edit( - this._postgresModel.info.name, { engineSettings: name + '=' }); - } finally { - session.dispose(); - } - } - ); - - vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); - - } catch (error) { - vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error)); - } - })); - - return data; - } - - private selectComponent() { + private selectComponent(): void { if (!this._postgresModel.engineSettingsLastUpdated) { this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({ value: loc.connectToPostgresDescription, @@ -503,19 +328,249 @@ export class PostgresParametersPage extends DashboardPage { this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } }); this.parameterContainer!.addItem(this._parametersTableLoading!); } else { + this.searchBox!.enabled = true; + this.resetAllButton!.enabled = true; this.parameterContainer!.addItem(this.parametersTable!); + this.refreshParametersTable(); } } - private handleEngineSettingsUpdated(): void { - //TODO + private async callGetEngineSettings(): Promise { + try { + await this._postgresModel.getEngineSettings(); + } catch (error) { + if (error instanceof UserCancelledError) { + vscode.window.showWarningMessage(loc.pgConnectionRequired); + } else { + vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error)); + } + this.connectToServerButton!.enabled = true; + throw error; + } } - private handleServiceUpdated() { - // TODO - if (this._postgresModel.configLastUpdated) { + private initializeSearchBox(): void { + this.searchBox = this.modelView.modelBuilder.inputBox().withProps({ + readOnly: false, + enabled: false, + placeHolder: loc.searchToFilter + }).component(); + + this.disposables.push( + this.searchBox.onTextChanged(() => { + if (!this.searchBox!.value) { + this.parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]); + } else { + this.filterParameters(this.searchBox!.value); + } + }) + ); + } + + private filterParameters(search: string): void { + this.parametersTable.data = this._parameters + .filter(p => p.parameterName?.search(search) !== -1 || p.description?.search(search) !== -1) + .map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]); + } + + private handleOnTextChanged(component: azdata.InputBoxComponent, currentValue: string | undefined): boolean { + if (!component.valid) { + // If invalid value retun false and enable discard button + this.discardButton!.enabled = true; + return false; + } else if (component.value === currentValue) { + return false; + } else { + /* If a valid value has been entered into the input box, enable save and discard buttons + so that user could choose to either edit instance or clear all inputs + return true */ + this.saveButton!.enabled = true; + this.discardButton!.enabled = true; + return true; + } + } + + private createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel { + + // Container to hold input component and information bubble + const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component(); + + + if (engineSetting.type === 'enum') { + // If type is enum, component should be drop down menu + let options = engineSetting.options?.slice(1, -1).split(','); + let values: string[] = []; + options!.forEach(option => { + values.push(option.slice(option.indexOf('"') + 1, -1)); + }); + + let valueBox = this.modelView.modelBuilder.dropDown().withProps({ + values: values, + value: engineSetting.value, + width: '150px', + CSSStyles: { 'height': '40px' } + }).component(); + valueContainer.addItem(valueBox); + + this.disposables.push( + valueBox.onValueChanged(() => { + if (engineSetting.value !== String(valueBox.value)) { + this.parameterUpdates!.set(engineSetting.parameterName!, String(valueBox.value)); + this.saveButton!.enabled = true; + this.discardButton!.enabled = true; + } else if (this.parameterUpdates!.has(engineSetting.parameterName!)) { + this.parameterUpdates!.delete(engineSetting.parameterName!); + } + }) + ); + } else if (engineSetting.type === 'bool') { + // If type is bool, component should be checkbox to turn on or off + let valueBox = this.modelView.modelBuilder.checkBox().withProps({ + label: loc.on, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + valueContainer.addItem(valueBox); + + if (engineSetting.value === 'on') { + valueBox.checked = true; + } else { + valueBox.checked = false; + } + + this.disposables.push( + valueBox.onChanged(() => { + if (valueBox.checked && engineSetting.value === 'off') { + this.parameterUpdates!.set(engineSetting.parameterName!, loc.on); + this.saveButton!.enabled = true; + this.discardButton!.enabled = true; + } else if (!valueBox.checked && engineSetting.value === 'on') { + this.parameterUpdates!.set(engineSetting.parameterName!, loc.off); + this.saveButton!.enabled = true; + this.discardButton!.enabled = true; + } else if (this.parameterUpdates!.has(engineSetting.parameterName!)) { + this.parameterUpdates!.delete(engineSetting.parameterName!); + } + }) + ); + } else if (engineSetting.type === 'string') { + // If type is string, component should be text inputbox + let valueBox = this.modelView.modelBuilder.inputBox().withProps({ + required: true, + readOnly: false, + value: engineSetting.value, + width: '150px' + }).component(); + valueContainer.addItem(valueBox); + + this.disposables.push( + valueBox.onTextChanged(() => { + if ((this.handleOnTextChanged(valueBox, engineSetting.value))) { + this.parameterUpdates!.set(engineSetting.parameterName!, `"${valueBox.value!}"`); + } else if (this.parameterUpdates!.has(engineSetting.parameterName!)) { + this.parameterUpdates!.delete(engineSetting.parameterName!); + } + }) + ); + } else { + // Child components to be added to container + let components: Array = []; + + // If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set. + let valueBox = this.modelView.modelBuilder.inputBox().withProps({ + required: true, + readOnly: false, + min: parseInt(engineSetting.min!), + max: parseInt(engineSetting.max!), + validationErrorMessage: loc.outOfRange(engineSetting.min!, engineSetting.max!), + inputType: 'number', + value: engineSetting.value, + width: '150px' + }).component(); + components.push(valueBox); + + this.disposables.push( + valueBox.onTextChanged(() => { + if ((this.handleOnTextChanged(valueBox, engineSetting.value))) { + this.parameterUpdates!.set(engineSetting.parameterName!, valueBox.value!); + } else if (this.parameterUpdates!.has(engineSetting.parameterName!)) { + this.parameterUpdates!.delete(engineSetting.parameterName!); + } + }) + ); + + // Information bubble title to show allowed values + let information = this.modelView.modelBuilder.button().withProps({ + iconPath: IconPathHelper.information, + width: '15px', + height: '15px', + enabled: false + }).component(); + + information.updateProperty('title', loc.allowedValue(loc.rangeSetting(engineSetting.min!, engineSetting.max!))); + components.push(information); + valueContainer.addItems(components); + } + + // Can reset individual parameter + const resetParameterButton = this.modelView.modelBuilder.button().withProps({ + iconPath: IconPathHelper.reset, + title: loc.resetToDefault, + width: '20px', + height: '20px', + enabled: true + }).component(); + + this.disposables.push( + resetParameterButton.onDidClick(async () => { + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.updatingInstance(this._postgresModel.info.name), + cancellable: false + }, + async (_progress, _token): Promise => { + const session = await this._postgresModel.controllerModel.acquireAzdataSession(); + try { + await this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, + { engineSettings: engineSetting.parameterName + '=' }, + this._postgresModel.engineVersion); + } finally { + session.dispose(); + } + await this._postgresModel.refresh(); + } + ); + + vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); + } catch (error) { + vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error)); + } + }) + ); + + let parameter: ParametersModel = { + parameterName: engineSetting.parameterName!, + valueContainer: valueContainer, + description: engineSetting.description!, + resetButton: resetParameterButton + }; + + return parameter; + } + + private refreshParametersTable(): void { + this._parameters = this._postgresModel._engineSettings.map(engineSetting => this.createParameterComponents(engineSetting)); + this.parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]); + } + + private async handleServiceUpdated(): Promise { + if (this._postgresModel.configLastUpdated && !this._postgresModel.engineSettingsLastUpdated) { this.connectToServerButton!.enabled = true; this._parametersTableLoading!.loading = false; + } else if (this._postgresModel.engineSettingsLastUpdated) { + await this.callGetEngineSettings(); } } } diff --git a/resources/localization/LCL/fr/cms.xlf.lcl b/resources/localization/LCL/fr/cms.xlf.lcl index 9ead6fb180..4e3795e629 100644 --- a/resources/localization/LCL/fr/cms.xlf.lcl +++ b/resources/localization/LCL/fr/cms.xlf.lcl @@ -1097,4 +1097,4 @@ - \ No newline at end of file +