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

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