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
This commit is contained in:
nasc17
2021-01-07 19:42:48 -08:00
committed by GitHub
parent 6c2e713a92
commit e7fb44b3a2
14 changed files with 832 additions and 105 deletions

View File

@@ -0,0 +1,10 @@
<svg id="a2f0dd32-c564-48d6-97d7-86323bfba35b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<defs>
<linearGradient id="b863127b-2eb8-42a1-a46b-989a6a8d258c" x1="9" y1="18" x2="9" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#32bedd" />
<stop offset="0.576" stop-color="#32ceef" />
<stop offset="1" stop-color="#32d4f5" />
</linearGradient>
</defs>
<path d="M18,9.972V7.92l-.288-.108-2.2-.72-.576-1.4,1.116-2.376-1.44-1.44-.288.144L12.276,3.06l-1.4-.576L9.972,0H7.92L7.812.288l-.72,2.2-1.4.576L3.348,1.944l-1.44,1.44.144.288L3.1,5.724l-.576,1.4L0,8.028V10.08l.288.108,2.2.72.576,1.4L1.944,14.688l1.44,1.44.288-.144L5.724,14.94l1.4.576L8.028,18H10.08l.108-.288.72-2.2,1.4-.576,2.376,1.116,1.44-1.44-.144-.288L14.94,12.276l.576-1.4ZM9,12.95A3.95,3.95,0,1,1,12.95,9,3.947,3.947,0,0,1,9,12.95Z" fill="url(#b863127b-2eb8-42a1-a46b-989a6a8d258c)" />
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 5H2V0H3V3.3L3.8 2.4L4.6 1.7L5.4 1L6.3 0.5C6.59362 0.297586 6.94345 0.192635 7.3 0.2L8.5 0L9.9 0.2L11.3 0.8L12.4 1.6C12.7233 1.95924 12.9927 2.36344 13.2 2.8C13.459 3.20359 13.6609 3.64107 13.8 4.1C13.9226 4.55696 13.9897 5.027 14 5.5C13.9637 6.21504 13.8291 6.92168 13.6 7.6C13.2924 8.22655 12.8874 8.80035 12.4 9.3L5.9 15.9L5.1 15.1L11.7 8.6C12.0904 8.19804 12.3964 7.72203 12.6 7.2C12.8748 6.67624 13.0125 6.09136 13 5.5C13.0218 4.90769 12.8836 4.32046 12.6 3.8C12.4219 3.23995 12.072 2.75016 11.6 2.4C11.2498 1.928 10.76 1.57815 10.2 1.4C9.67954 1.11642 9.09231 0.978247 8.5 1C7.9834 0.981133 7.4696 1.08389 7 1.3L5.8 2L4.8 2.9L3.7 4H7V5Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

View File

@@ -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'),
};
}
}

View File

@@ -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); }

View File

@@ -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<azdataExt.SqlMiShowResult | undefined>();
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
@@ -38,8 +33,8 @@ export class MiaaModel extends ResourceModel {
private _refreshPromise: Deferred<void> | 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 = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -124,47 +119,41 @@ export class MiaaModel extends ResourceModel {
}
private async getDatabases(): Promise<void> {
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<azdata.MetadataProvider>(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 = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
} else {
this._databases = (<string[]>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<azdata.MetadataProvider>(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 = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
} else {
this._databases = (<string[]>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<void> {
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<void> {
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<void> {
protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
this._miaaInfo.userName = connectionProfile.userName;

View File

@@ -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<azdataExt.PostgresServerShowResult>();
public readonly _onEngineSettingsUpdated = new vscode.EventEmitter<EngineSettingsModel[]>();
public onConfigUpdated = this._onConfigUpdated.event;
public onEngineSettingsUpdated = this._onEngineSettingsUpdated.event;
public configLastUpdated?: Date;
public engineSettingsLastUpdated?: Date;
private _refreshPromise?: Deferred<void>;
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 = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -103,4 +121,84 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise = undefined;
}
}
public async getEngineSettings(): Promise<void> {
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<azdata.QueryProvider>(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<void> {
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<void> {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
this._pgInfo.userName = connectionProfile.userName;
await this._treeDataProvider.saveControllers();
}
}

View File

@@ -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<Registration>();
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<void> {
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<void>;
protected abstract createConnectionProfile(): azdata.IConnectionProfile;
protected abstract promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise<void>;
protected abstract updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void>;
}

View File

@@ -23,6 +23,10 @@ declare module 'arc' {
userName?: string
};
export type PGResourceInfo = ResourceInfo & {
userName?: string
};
export type ResourceInfo = {
name: string,
resourceType: ResourceType | string,

View File

@@ -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<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;
}
}
}

View File

@@ -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 ?? ''),

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<azdata.IConnectionProfile | undefined>();
protected _completionPromise = new Deferred<azdata.IConnectionProfile | undefined>();
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);
}

View File

@@ -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: