Files
azuredatastudio/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts
nasc17 e7fb44b3a2 Connect to SQL from Postgres Parameters Page (#13744)
* Addition: properties page with link to dashboard

* Include new page

* Initial Parameter page start

* Include new changes from merged PRs

* Including new constants

* Git errors

* Add parameter commands and help

* Reset command

* Added chart

* git fix

* Fixed string issues

* connectSqlDialog is an abstract class. Separated out Miaa and Postgress connection

* Initial start to adding connect to sql for postgres instance

* Simplified classes extending ConnectToSqlDialog, added get providerName, and function to create error message

* Miaa models provides dialog title

* Updated failed message parameters

* completionPromise.reject

* Fixed connect to MSSql

* Messy dialog showing from button

* removed this._completionPromise.reject

* Cleaning up code

* Set connectSqlDialog to be an abstract class. Separated out Miaa and Postgres.  (#13532)

* connectSqlDialog is an abstract class. Separated out Miaa and Postgress connection

* Simplified classes extending ConnectToSqlDialog, added get providerName, and function to create error message

* Miaa models provides dialog title

* Updated failed message parameters

* completionPromise.reject

* Fixed connect to MSSql

* removed this._completionPromise.reject

* Connect button clean up

* Format

* Format doc

* Fixed compile errors

* Cleaning up page

* Clean up

* clean up refresh

* Format doc

* Removed ellipse

* Cleaning up problems

* Updating localized constants

* Missing username update

* Get connection profile added to Resource model, abstract method created for calling connection dialog

* Added createConnectionProfile

* took out import

* Pulled in new changes, fixed usercancellederror

* Getting engine settings

* Git errors

* Corrected names of icons and constants, Fixed Miaa dialog title

* Changed gear svg, made postgres extension a loc constant, fixed formatting

* Fixed controller model name

* Put connection profile and id in resource model, changed back controller model in base class
2021-01-07 19:42:48 -08:00

507 lines
17 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
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';
export class PostgresParametersPage extends DashboardPage {
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 connectToServerButton?: azdata.ButtonComponent;
private engineSettings = `'`;
private engineSettingUpdates?: Map<string, string>;
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
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())));
}
protected get title(): string {
return loc.nodeParameters;
}
protected get id(): string {
return 'postgres-node-parameters';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.gear;
}
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.nodeParameters,
CSSStyles: { ...cssStyles.title }
}).component());
const info = this.modelView.modelBuilder.text().withProps({
value: loc.nodeParametersDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const link = 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' } });
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
this.parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: 'Parameter Name',
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: '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'
}
},
{
displayName: 'Description',
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: '50%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow,
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
'max-width': '0'
}
},
{
displayName: 'Reset To Default',
valueType: azdata.DeclarativeDataType.component,
isReadOnly: false,
width: '10%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: [
this.parameterComponents('TEST NAME', 'string'),
this.parameterComponents('TEST NAME 2', 'real'),
this.createParametersTable()]
}).component();
this._parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
this.selectComponent();
content.addItem(this.parameterContainer);
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProps({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
}).component();
this.disposables.push(
this.saveButton.onDidClick(async () => {
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
//Edit multiple
// azdata arc postgres server edit -n <server group name> -e '<parameter name>=<parameter value>, <parameter name>=<parameter value>,...'
try {
this.engineSettingUpdates!.forEach((value: string) => {
this.engineSettings += value + ', ';
});
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, { engineSettings: this.engineSettings + `'` });
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton!.enabled = true;
throw err;
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.engineSettings = `'`;
this.engineSettingUpdates!.clear();
this.discardButton!.enabled = false;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
}
}));
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProps({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
}).component();
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton!.enabled = false;
try {
// TODO
// this.parametersTable.data = [];
this.engineSettingUpdates!.clear();
} catch (error) {
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.saveButton!.enabled = false;
}
}));
// Reset
this.resetButton = this.modelView.modelBuilder.button().withProps({
label: loc.resetAllToDefault,
iconPath: IconPathHelper.reset,
enabled: true
}).component();
this.disposables.push(
this.resetButton.onDidClick(async () => {
this.resetButton!.enabled = false;
this.discardButton!.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
//all
// azdata arc postgres server edit -n <server group name> -e '' -re
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, { engineSettings: `'' -re` });
} 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;
throw err;
}
await this._postgresModel.refresh();
}
);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: this.saveButton },
{ component: this.discardButton },
{ component: this.resetButton }
]).component();
}
private initializeConnectButton() {
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
label: loc.connectToServer,
enabled: false,
CSSStyles: { 'max-width': '125px' }
}).component();
this.disposables.push(
this.connectToServerButton!.onDidClick(async () => {
this.connectToServerButton!.enabled = false;
if (!vscode.extensions.getExtension(loc.postgresExtension)) {
const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no);
if (response !== loc.yes) {
this.connectToServerButton!.enabled = true;
return;
}
await vscode.commands.executeCommand('workbench.extensions.installExtension', 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.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
},
(_progress, _token) => {
return this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, { engineSettings: name + '=' });
}
);
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() {
if (!this._postgresModel.engineSettingsLastUpdated) {
this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.connectToPostgresDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
this.parameterContainer!.addItem(this._parametersTableLoading!);
} else {
this.parameterContainer!.addItem(this.parametersTable!);
}
}
private handleEngineSettingsUpdated(): void {
//TODO
}
private handleServiceUpdated() {
// TODO
if (this._postgresModel.configLastUpdated) {
this.connectToServerButton!.enabled = true;
this._parametersTableLoading!.loading = false;
}
}
}