diff --git a/extensions/arc/images/gear.svg b/extensions/arc/images/gear.svg
new file mode 100644
index 0000000000..34cae96d48
--- /dev/null
+++ b/extensions/arc/images/gear.svg
@@ -0,0 +1,10 @@
+
diff --git a/extensions/arc/images/reset.svg b/extensions/arc/images/reset.svg
new file mode 100644
index 0000000000..fdaa1833c4
--- /dev/null
+++ b/extensions/arc/images/reset.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts
index c6d3e8ce3f..e03893b249 100644
--- a/extensions/arc/src/constants.ts
+++ b/extensions/arc/src/constants.ts
@@ -34,6 +34,7 @@ export class IconPathHelper {
public static properties: IconPath;
public static networking: IconPath;
public static refresh: IconPath;
+ public static reset: IconPath;
public static support: IconPath;
public static wrench: IconPath;
public static miaa: IconPath;
@@ -44,6 +45,7 @@ export class IconPathHelper {
public static discard: IconPath;
public static fail: IconPath;
public static information: IconPath;
+ public static gear: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.context = context;
@@ -95,6 +97,10 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/refresh.svg'),
dark: context.asAbsolutePath('images/refresh.svg')
};
+ IconPathHelper.reset = {
+ light: context.asAbsolutePath('images/reset.svg'),
+ dark: context.asAbsolutePath('images/reset.svg')
+ };
IconPathHelper.support = {
light: context.asAbsolutePath('images/support.svg'),
dark: context.asAbsolutePath('images/support.svg')
@@ -135,6 +141,10 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/information.svg'),
dark: context.asAbsolutePath('images/information.svg'),
};
+ IconPathHelper.gear = {
+ light: context.asAbsolutePath('images/gear.svg'),
+ dark: context.asAbsolutePath('images/gear.svg'),
+ };
}
}
diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts
index c23e084447..53f85b3d48 100644
--- a/extensions/arc/src/localizedConstants.ts
+++ b/extensions/arc/src/localizedConstants.ts
@@ -23,6 +23,7 @@ export const properties = localize('arc.properties', "Properties");
export const settings = localize('arc.settings', "Settings");
export const security = localize('arc.security', "Security");
export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage");
+export const nodeParameters = localize('arc.nodeParameters', "Node Parameters");
export const compute = localize('arc.compute', "Compute");
export const backup = localize('arc.backup', "Backup");
export const newSupportRequest = localize('arc.newSupportRequest', "New support request");
@@ -68,6 +69,8 @@ export const workerNodesInformation = localize('arc.workerNodeInformation', "In
export const vCores = localize('arc.vCores', "vCores");
export const ram = localize('arc.ram', "RAM");
export const refresh = localize('arc.refresh', "Refresh");
+export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to default");
+export const resetToDefault = localize('arc.resetToDefault', "Reset to default");
export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot");
export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal.");
export const running = localize('arc.running', "Running");
@@ -79,8 +82,10 @@ export const indirect = localize('arc.indirect', "Indirect");
export const loading = localize('arc.loading', "Loading...");
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
export const noInstancesAvailable = localize('arc.noInstancesAvailable', "No instances available");
+export const connectToServer = localize('arc.connecToServer', "Connect to Server");
export const connectToController = localize('arc.connectToController', "Connect to Existing Controller");
-export function connectToSql(name: string): string { return localize('arc.connectToSql', "Connect to SQL managed instance - Azure Arc ({0})", name); }
+export function connectToMSSql(name: string): string { return localize('arc.connectToMSSql', "Connect to SQL managed instance - Azure Arc ({0})", name); }
+export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); }
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
@@ -88,12 +93,16 @@ export const controllerName = localize('arc.controllerName', "Name");
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
+export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
+export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL");
export const username = localize('arc.username', "Username");
export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
export const connect = localize('arc.connect', "Connect");
export const cancel = localize('arc.cancel', "Cancel");
export const ok = localize('arc.ok', "Ok");
+export const on = localize('arc.on', "On");
+export const off = localize('arc.off', "Off");
export const notConfigured = localize('arc.notConfigured', "Not Configured");
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
@@ -122,6 +131,10 @@ export const databaseName = localize('arc.databaseName', "Database name");
export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password");
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
+export const nodeParametersDescription = localize('arc.nodeParametersDescription', " These server parameters of the Coordinator node and the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
+export const learnAboutNodeParameters = localize('arc.learnAboutNodeParameters', "Learn more about database engine settings for Azure Arc enabled PostgreSQL Hyperscale");
+export const noNodeParametersFound = localize('arc.noNodeParametersFound', "No worker server parameters found...");
+export const searchToFilter = localize('arc.searchToFilter', "Search to filter items...");
export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory.");
export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by");
@@ -153,7 +166,11 @@ export const details = localize('arc.details', "Details");
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
export const podsReady = localize('arc.podsReady', "pods ready");
+export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed.");
+export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql");
+export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); }
+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 updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); }
@@ -177,7 +194,9 @@ export function validationMin(min: number): string { return localize('arc.valida
// Errors
export const connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information");
+export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings.");
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 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)); }
@@ -185,11 +204,14 @@ export function instanceUpdateFailed(name: string, error: any): string { return
export function pageDiscardFailed(error: any): string { return localize('arc.pageDiscardFailed', "Failed to discard user input. {0}", getErrorMessage(error)); }
export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); }
export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); }
-export function connectToSqlFailed(serverName: string, error: any): string { return localize('arc.connectToSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); }
+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 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)); }
export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); }
+export function fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); }
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts
index 77a796d225..00223afc36 100644
--- a/extensions/arc/src/models/miaaModel.ts
+++ b/extensions/arc/src/models/miaaModel.ts
@@ -9,10 +9,9 @@ import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api';
import { Deferred } from '../common/promise';
-import { createCredentialId, parseIpAndPort } from '../common/utils';
-import { credentialNamespace } from '../constants';
+import { parseIpAndPort } from '../common/utils';
import * as loc from '../localizedConstants';
-import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
+import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerModel, Registration } from './controllerModel';
import { ResourceModel } from './resourceModel';
@@ -23,10 +22,6 @@ export class MiaaModel extends ResourceModel {
private _config: azdataExt.SqlMiShowResult | undefined;
private _databases: DatabaseModel[] = [];
- // The saved connection information
- private _connectionProfile: azdata.IConnectionProfile | undefined = undefined;
- // The ID of the active connection used to query the server
- private _activeConnectionId: string | undefined = undefined;
private readonly _onConfigUpdated = new vscode.EventEmitter();
private readonly _onDatabasesUpdated = new vscode.EventEmitter();
@@ -38,8 +33,8 @@ export class MiaaModel extends ResourceModel {
private _refreshPromise: Deferred | undefined = undefined;
- constructor(controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
- super(controllerModel, _miaaInfo, registration);
+ constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
+ super(_controllerModel, _miaaInfo, registration);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -124,47 +119,41 @@ export class MiaaModel extends ResourceModel {
}
private async getDatabases(): Promise {
- await this.getConnectionProfile();
- if (this._connectionProfile) {
- // We haven't connected yet so do so now and then store the ID for the active connection
- if (!this._activeConnectionId) {
- const result = await azdata.connection.connect(this._connectionProfile, false, false);
- if (!result.connected) {
- throw new Error(result.errorMessage);
- }
- this._activeConnectionId = result.connectionId;
- }
-
- const provider = azdata.dataprotocol.getProvider(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider);
- const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
- const databases = await provider.getDatabases(ownerUri);
- if (!databases) {
- throw new Error('Could not fetch databases');
- }
- if (databases.length > 0 && typeof (databases[0]) === 'object') {
- this._databases = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
- } else {
- this._databases = (databases).map(db => { return { name: db, status: '-' }; });
- }
- this.databasesLastUpdated = new Date();
- this._onDatabasesUpdated.fire(this._databases);
+ if (!this._connectionProfile) {
+ await this.getConnectionProfile();
}
+
+ // We haven't connected yet so do so now and then store the ID for the active connection
+ if (!this._activeConnectionId) {
+ const result = await azdata.connection.connect(this._connectionProfile!, false, false);
+ if (!result.connected) {
+ throw new Error(result.errorMessage);
+ }
+ this._activeConnectionId = result.connectionId;
+ }
+
+ const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.MetadataProvider);
+ const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
+ const databases = await provider.getDatabases(ownerUri);
+ if (!databases) {
+ throw new Error('Could not fetch databases');
+ }
+ if (databases.length > 0 && typeof (databases[0]) === 'object') {
+ this._databases = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
+ } else {
+ this._databases = (databases).map(db => { return { name: db, status: '-' }; });
+ }
+ this.databasesLastUpdated = new Date();
+ this._onDatabasesUpdated.fire(this._databases);
}
- /**
- * Loads the saved connection profile associated with this model. Will prompt for one if
- * we don't have one or can't find it (it was deleted)
- */
- private async getConnectionProfile(): Promise {
- if (this._connectionProfile) {
- return;
- }
+ protected createConnectionProfile(): azdata.IConnectionProfile {
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
- let connectionProfile: azdata.IConnectionProfile | undefined = {
+ return {
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
databaseName: '',
authenticationType: 'SqlLogin',
- providerName: 'MSSQL',
+ providerName: loc.miaaProviderName,
connectionName: '',
userName: this._miaaInfo.userName || '',
password: '',
@@ -175,47 +164,21 @@ export class MiaaModel extends ResourceModel {
groupId: undefined,
options: {}
};
+ }
- // If we have the ID stored then try to retrieve the password from previous connections
- if (this.info.connectionId) {
- try {
- const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
- const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name));
- if (credentials.password) {
- // Try to connect to verify credentials are still valid
- connectionProfile.password = credentials.password;
- // If we don't have a username for some reason then just continue on and we'll prompt for the username below
- if (connectionProfile.userName) {
- const result = await azdata.connection.connect(connectionProfile, false, false);
- if (!result.connected) {
- vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
- const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this);
- connectToSqlDialog.showDialog(connectionProfile);
- connectionProfile = await connectToSqlDialog.waitForClose();
- }
- }
- }
- } catch (err) {
- console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
- // ignore - something happened fetching the password so just reprompt
- }
- }
+ protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise {
+ const connectToSqlDialog = new ConnectToMiaaSqlDialog(this.controllerModel, this);
+ connectToSqlDialog.showDialog(loc.connectToMSSql(this.info.name), connectionProfile);
+ let profileFromDialog = await connectToSqlDialog.waitForClose();
- if (!connectionProfile?.userName || !connectionProfile?.password) {
- // Need to prompt user for password since we don't have one stored
- const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this);
- connectToSqlDialog.showDialog(connectionProfile);
- connectionProfile = await connectToSqlDialog.waitForClose();
- }
-
- if (connectionProfile) {
- this.updateConnectionProfile(connectionProfile);
+ if (profileFromDialog) {
+ this.updateConnectionProfile(profileFromDialog);
} else {
throw new UserCancelledError();
}
}
- private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise {
+ protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
this._miaaInfo.userName = connectionProfile.userName;
diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts
index 429aec686a..0aad439598 100644
--- a/extensions/arc/src/models/postgresModel.ts
+++ b/extensions/arc/src/models/postgresModel.ts
@@ -3,27 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { ResourceInfo } from 'arc';
+import { PGResourceInfo } from 'arc';
+import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
+import { ConnectToPGSqlDialog } from '../ui/dialogs/connectPGDialog';
+import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerModel, Registration } from './controllerModel';
+import { parseIpAndPort } from '../common/utils';
+import { UserCancelledError } from '../common/api';
import { ResourceModel } from './resourceModel';
import { Deferred } from '../common/promise';
-import { parseIpAndPort } from '../common/utils';
+
+export type EngineSettingsModel = {
+ parameterName: string | undefined,
+ value: string | undefined,
+ description: string | undefined,
+ min: string | undefined,
+ max: string | undefined,
+ options: string | undefined,
+ type: string | undefined
+};
export class PostgresModel extends ResourceModel {
private _config?: azdataExt.PostgresServerShowResult;
+ public _engineSettings: EngineSettingsModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
private readonly _onConfigUpdated = new vscode.EventEmitter();
+ public readonly _onEngineSettingsUpdated = new vscode.EventEmitter();
public onConfigUpdated = this._onConfigUpdated.event;
+ public onEngineSettingsUpdated = this._onEngineSettingsUpdated.event;
public configLastUpdated?: Date;
+ public engineSettingsLastUpdated?: Date;
private _refreshPromise?: Deferred;
- constructor(controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
- super(controllerModel, info, registration);
+ constructor(_controllerModel: ControllerModel, private _pgInfo: PGResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
+ super(_controllerModel, _pgInfo, registration);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -103,4 +121,84 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise = undefined;
}
}
+
+ public async getEngineSettings(): Promise {
+ if (!this._connectionProfile) {
+ await this.getConnectionProfile();
+ }
+
+ // We haven't connected yet so do so now and then store the ID for the active connection
+ if (!this._activeConnectionId) {
+ const result = await azdata.connection.connect(this._connectionProfile!, false, false);
+ if (!result.connected) {
+ throw new Error(result.errorMessage);
+ }
+ this._activeConnectionId = result.connectionId;
+ }
+
+ const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider);
+ const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
+
+ const engineSettings = await provider.runQueryAndReturn(ownerUri, 'select name, setting, short_desc,min_val, max_val, enumvals, vartype from pg_settings');
+ if (!engineSettings) {
+ throw new Error('Could not fetch engine settings');
+ }
+ 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()
+ };
+
+ this._engineSettings.push(result);
+ });
+
+ this.engineSettingsLastUpdated = new Date();
+ }
+
+ protected createConnectionProfile(): azdata.IConnectionProfile {
+ const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
+ return {
+ serverName: `${ipAndPort.ip},${ipAndPort.port}`,
+ databaseName: '',
+ authenticationType: 'SqlLogin',
+ providerName: loc.postgresProviderName,
+ connectionName: '',
+ userName: this._pgInfo.userName || '',
+ password: '',
+ savePassword: true,
+ groupFullName: undefined,
+ saveProfile: true,
+ id: '',
+ groupId: undefined,
+ options: {
+ host: `${ipAndPort.ip}`,
+ port: `${ipAndPort.port}`,
+ }
+ };
+ }
+
+ protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise {
+ const connectToSqlDialog = new ConnectToPGSqlDialog(this.controllerModel, this);
+ connectToSqlDialog.showDialog(loc.connectToPGSql(this.info.name), connectionProfile);
+ let profileFromDialog = await connectToSqlDialog.waitForClose();
+
+ if (profileFromDialog) {
+ this.updateConnectionProfile(profileFromDialog);
+ } else {
+ throw new UserCancelledError();
+ }
+ }
+
+ protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise {
+ this._connectionProfile = connectionProfile;
+ this.info.connectionId = connectionProfile.id;
+ this._pgInfo.userName = connectionProfile.userName;
+ await this._treeDataProvider.saveControllers();
+ }
}
diff --git a/extensions/arc/src/models/resourceModel.ts b/extensions/arc/src/models/resourceModel.ts
index 99760732a4..adb6fc6b8f 100644
--- a/extensions/arc/src/models/resourceModel.ts
+++ b/extensions/arc/src/models/resourceModel.ts
@@ -4,14 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { ResourceInfo } from 'arc';
+import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { ControllerModel, Registration } from './controllerModel';
+import { createCredentialId } from '../common/utils';
+import { credentialNamespace } from '../constants';
export abstract class ResourceModel {
private readonly _onRegistrationUpdated = new vscode.EventEmitter();
public onRegistrationUpdated = this._onRegistrationUpdated.event;
+ // The saved connection information
+ protected _connectionProfile: azdata.IConnectionProfile | undefined = undefined;
+ // The ID of the active connection used to query the server
+ protected _activeConnectionId: string | undefined = undefined;
+
constructor(public readonly controllerModel: ControllerModel, public info: ResourceInfo, private _registration: Registration) { }
public get registration(): Registration {
@@ -23,5 +31,48 @@ export abstract class ResourceModel {
this._onRegistrationUpdated.fire(this._registration);
}
+ /**
+ * Loads the saved connection profile associated with this model. Will prompt for one if
+ * we don't have one or can't find it (it was deleted)
+ */
+ protected async getConnectionProfile(): Promise {
+ let connectionProfile: azdata.IConnectionProfile | undefined = this.createConnectionProfile();
+
+ // If we have the ID stored then try to retrieve the password from previous connections
+ if (this.info.connectionId) {
+ try {
+ const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
+ const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name));
+ if (credentials.password) {
+ // Try to connect to verify credentials are still valid
+ connectionProfile.password = credentials.password;
+ // If we don't have a username for some reason then just continue on and we'll prompt for the username below
+ if (connectionProfile.userName) {
+ const result = await azdata.connection.connect(connectionProfile, false, false);
+ if (!result.connected) {
+ await this.promptForConnection(connectionProfile);
+ } else {
+ this.updateConnectionProfile(connectionProfile);
+ }
+ }
+ }
+ } catch (err) {
+ console.warn(`Unexpected error fetching password for instance ${err}`);
+ // ignore - something happened fetching the password so just reprompt
+ }
+ }
+
+ if (!connectionProfile?.userName || !connectionProfile?.password) {
+ // Need to prompt user for password since we don't have one stored
+ await this.promptForConnection(connectionProfile);
+ }
+ }
+
public abstract refresh(): Promise;
+
+ protected abstract createConnectionProfile(): azdata.IConnectionProfile;
+
+ protected abstract promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise;
+
+ protected abstract updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise;
}
diff --git a/extensions/arc/src/typings/arc.d.ts b/extensions/arc/src/typings/arc.d.ts
index 090990d39c..ab0fc0f694 100644
--- a/extensions/arc/src/typings/arc.d.ts
+++ b/extensions/arc/src/typings/arc.d.ts
@@ -23,6 +23,10 @@ declare module 'arc' {
userName?: string
};
+ export type PGResourceInfo = ResourceInfo & {
+ userName?: string
+ };
+
export type ResourceInfo = {
name: string,
resourceType: ResourceType | string,
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts
new file mode 100644
index 0000000000..f9957deeb7
--- /dev/null
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts
@@ -0,0 +1,506 @@
+/*---------------------------------------------------------------------------------------------
+ * 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;
+
+ 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 => {
+ //Edit multiple
+ // azdata arc postgres server edit -n -e '=, =,...'
+ 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 => {
+ //all
+ // azdata arc postgres server edit -n -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;
+ }
+ }
+}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
index 03fb1e7858..66c6fb780e 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
@@ -7,10 +7,11 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
-import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer';
+import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue, LinkKeyValue } from '../../components/keyValueContainer';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
+import { ControllerDashboard } from '../controller/controllerDashboard';
export class PostgresPropertiesPage extends DashboardPage {
private loading?: azdata.LoadingComponent;
@@ -93,12 +94,13 @@ export class PostgresPropertiesPage extends DashboardPage {
private getProperties(): KeyValue[] {
const endpoint = this._postgresModel.endpoint;
const status = this._postgresModel.config?.status;
+ const controllerDashboard = new ControllerDashboard(this._controllerModel);
return [
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
- // TODO: Make this a LinkKeyValue that opens the controller dashboard
+ new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? '', () => controllerDashboard.showDashboard()),
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
diff --git a/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts b/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts
new file mode 100644
index 0000000000..7a360f6120
--- /dev/null
+++ b/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts
@@ -0,0 +1,24 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { ControllerModel } from '../../models/controllerModel';
+import { MiaaModel } from '../../models/miaaModel';
+import { ConnectToSqlDialog } from './connectSqlDialog';
+import * as loc from '../../localizedConstants';
+
+export class ConnectToMiaaSqlDialog extends ConnectToSqlDialog {
+
+ constructor(_controllerModel: ControllerModel, _miaaModel: MiaaModel) {
+ super(_controllerModel, _miaaModel);
+ }
+
+ protected get providerName(): string {
+ return 'MSSQL';
+ }
+
+ protected connectionFailedMessage(error: any): string {
+ return loc.connectToMSSqlFailed(this.serverNameInputBox.value!, error);
+ }
+}
diff --git a/extensions/arc/src/ui/dialogs/connectPGDialog.ts b/extensions/arc/src/ui/dialogs/connectPGDialog.ts
new file mode 100644
index 0000000000..4fb6effec8
--- /dev/null
+++ b/extensions/arc/src/ui/dialogs/connectPGDialog.ts
@@ -0,0 +1,24 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { ControllerModel } from '../../models/controllerModel';
+import { PostgresModel } from '../../models/postgresModel';
+import { ConnectToSqlDialog } from './connectSqlDialog';
+import * as loc from '../../localizedConstants';
+
+export class ConnectToPGSqlDialog extends ConnectToSqlDialog {
+
+ constructor(_controllerModel: ControllerModel, _postgresModel: PostgresModel) {
+ super(_controllerModel, _postgresModel);
+ }
+
+ protected get providerName(): string {
+ return 'PGSQL';
+ }
+
+ protected connectionFailedMessage(error: any): string {
+ return loc.connectToPGSqlFailed(this.serverNameInputBox.value!, error);
+ }
+}
diff --git a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts
index de9fdd1683..fe7a223642 100644
--- a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts
+++ b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts
@@ -6,29 +6,30 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { Deferred } from '../../common/promise';
+import * as loc from '../../localizedConstants';
import { createCredentialId } from '../../common/utils';
import { credentialNamespace } from '../../constants';
-import * as loc from '../../localizedConstants';
-import { ControllerModel } from '../../models/controllerModel';
-import { MiaaModel } from '../../models/miaaModel';
import { InitializingComponent } from '../components/initializingComponent';
+import { ResourceModel } from '../../models/resourceModel';
+import { ControllerModel } from '../../models/controllerModel';
-export class ConnectToSqlDialog extends InitializingComponent {
- private modelBuilder!: azdata.ModelBuilder;
+export abstract class ConnectToSqlDialog extends InitializingComponent {
+ protected modelBuilder!: azdata.ModelBuilder;
- private serverNameInputBox!: azdata.InputBoxComponent;
- private usernameInputBox!: azdata.InputBoxComponent;
- private passwordInputBox!: azdata.InputBoxComponent;
- private rememberPwCheckBox!: azdata.CheckBoxComponent;
+ protected serverNameInputBox!: azdata.InputBoxComponent;
+ protected usernameInputBox!: azdata.InputBoxComponent;
+ protected passwordInputBox!: azdata.InputBoxComponent;
+ protected rememberPwCheckBox!: azdata.CheckBoxComponent;
+ private options: { [name: string]: any } = {};
- private _completionPromise = new Deferred();
+ protected _completionPromise = new Deferred();
- constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
+ constructor(private _controllerModel: ControllerModel, protected _model: ResourceModel) {
super();
}
- public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
- const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name));
+ public showDialog(dialogTitle: string, connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
+ const dialog = azdata.window.createModelViewDialog(dialogTitle);
dialog.cancelButton.onClick(() => this.handleCancel());
dialog.registerContent(async view => {
this.modelBuilder = view.modelBuilder;
@@ -84,6 +85,7 @@ export class ConnectToSqlDialog extends InitializingComponent {
dialog.registerCloseValidator(async () => await this.validate());
dialog.okButton.label = loc.connect;
dialog.cancelButton.label = loc.cancel;
+ this.options = connectionProfile?.options!;
azdata.window.openDialog(dialog);
return dialog;
}
@@ -96,7 +98,7 @@ export class ConnectToSqlDialog extends InitializingComponent {
serverName: this.serverNameInputBox.value,
databaseName: '',
authenticationType: 'SqlLogin',
- providerName: 'MSSQL',
+ providerName: this.providerName,
connectionName: '',
userName: this.usernameInputBox.value,
password: this.passwordInputBox.value,
@@ -105,26 +107,30 @@ export class ConnectToSqlDialog extends InitializingComponent {
saveProfile: true,
id: '',
groupId: undefined,
- options: {}
+ options: this.options
};
const result = await azdata.connection.connect(connectionProfile, false, false);
if (result.connected) {
connectionProfile.id = result.connectionId;
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
if (connectionProfile.savePassword) {
- await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password);
+ await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name), connectionProfile.password);
} else {
- await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name));
+ await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name));
}
this._completionPromise.resolve(connectionProfile);
return true;
}
else {
- vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage));
+ vscode.window.showErrorMessage(this.connectionFailedMessage(result.errorMessage));
return false;
}
}
+ protected abstract get providerName(): string;
+
+ protected abstract connectionFailedMessage(error: any): string;
+
private handleCancel(): void {
this._completionPromise.resolve(undefined);
}
diff --git a/extensions/arc/src/ui/tree/controllerTreeNode.ts b/extensions/arc/src/ui/tree/controllerTreeNode.ts
index 9af1bae180..47f9c11ddd 100644
--- a/extensions/arc/src/ui/tree/controllerTreeNode.ts
+++ b/extensions/arc/src/ui/tree/controllerTreeNode.ts
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
+import { MiaaResourceInfo, PGResourceInfo, ResourceInfo, ResourceType } from 'arc';
import * as vscode from 'vscode';
import { UserCancelledError } from '../../common/api';
import * as loc from '../../localizedConstants';
@@ -102,7 +102,11 @@ export class ControllerTreeNode extends TreeNode {
switch (registration.instanceType) {
case ResourceType.postgresInstances:
- const postgresModel = new PostgresModel(this.model, resourceInfo, registration);
+ // Fill in the username too if we already have it
+ (resourceInfo as PGResourceInfo).userName = (this.model.info.resources.find(info =>
+ info.name === resourceInfo.name &&
+ info.resourceType === resourceInfo.resourceType) as PGResourceInfo)?.userName;
+ const postgresModel = new PostgresModel(this.model, resourceInfo, registration, this._treeDataProvider);
node = new PostgresTreeNode(postgresModel, this.model, this._context);
break;
case ResourceType.sqlManagedInstances: