diff --git a/extensions/arc/images/gear.svg b/extensions/arc/images/gear.svg new file mode 100644 index 0000000000..34cae96d48 --- /dev/null +++ b/extensions/arc/images/gear.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/arc/images/reset.svg b/extensions/arc/images/reset.svg new file mode 100644 index 0000000000..fdaa1833c4 --- /dev/null +++ b/extensions/arc/images/reset.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts index c6d3e8ce3f..e03893b249 100644 --- a/extensions/arc/src/constants.ts +++ b/extensions/arc/src/constants.ts @@ -34,6 +34,7 @@ export class IconPathHelper { public static properties: IconPath; public static networking: IconPath; public static refresh: IconPath; + public static reset: IconPath; public static support: IconPath; public static wrench: IconPath; public static miaa: IconPath; @@ -44,6 +45,7 @@ export class IconPathHelper { public static discard: IconPath; public static fail: IconPath; public static information: IconPath; + public static gear: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.context = context; @@ -95,6 +97,10 @@ export class IconPathHelper { light: context.asAbsolutePath('images/refresh.svg'), dark: context.asAbsolutePath('images/refresh.svg') }; + IconPathHelper.reset = { + light: context.asAbsolutePath('images/reset.svg'), + dark: context.asAbsolutePath('images/reset.svg') + }; IconPathHelper.support = { light: context.asAbsolutePath('images/support.svg'), dark: context.asAbsolutePath('images/support.svg') @@ -135,6 +141,10 @@ export class IconPathHelper { light: context.asAbsolutePath('images/information.svg'), dark: context.asAbsolutePath('images/information.svg'), }; + IconPathHelper.gear = { + light: context.asAbsolutePath('images/gear.svg'), + dark: context.asAbsolutePath('images/gear.svg'), + }; } } diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index c23e084447..53f85b3d48 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -23,6 +23,7 @@ export const properties = localize('arc.properties', "Properties"); export const settings = localize('arc.settings', "Settings"); export const security = localize('arc.security', "Security"); export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage"); +export const nodeParameters = localize('arc.nodeParameters', "Node Parameters"); export const compute = localize('arc.compute', "Compute"); export const backup = localize('arc.backup', "Backup"); export const newSupportRequest = localize('arc.newSupportRequest', "New support request"); @@ -68,6 +69,8 @@ export const workerNodesInformation = localize('arc.workerNodeInformation', "In export const vCores = localize('arc.vCores', "vCores"); export const ram = localize('arc.ram', "RAM"); export const refresh = localize('arc.refresh', "Refresh"); +export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to default"); +export const resetToDefault = localize('arc.resetToDefault', "Reset to default"); export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot"); export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal."); export const running = localize('arc.running', "Running"); @@ -79,8 +82,10 @@ export const indirect = localize('arc.indirect', "Indirect"); export const loading = localize('arc.loading', "Loading..."); export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials"); export const noInstancesAvailable = localize('arc.noInstancesAvailable', "No instances available"); +export const connectToServer = localize('arc.connecToServer', "Connect to Server"); export const connectToController = localize('arc.connectToController', "Connect to Existing Controller"); -export function connectToSql(name: string): string { return localize('arc.connectToSql', "Connect to SQL managed instance - Azure Arc ({0})", name); } +export function connectToMSSql(name: string): string { return localize('arc.connectToMSSql', "Connect to SQL managed instance - Azure Arc ({0})", name); } +export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); } export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller"); export const controllerUrl = localize('arc.controllerUrl', "Controller URL"); export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint"); @@ -88,12 +93,16 @@ export const controllerName = localize('arc.controllerName', "Name"); export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path"); export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context"); export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc"); +export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL"); +export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL"); export const username = localize('arc.username', "Username"); export const password = localize('arc.password', "Password"); export const rememberPassword = localize('arc.rememberPassword', "Remember Password"); export const connect = localize('arc.connect', "Connect"); export const cancel = localize('arc.cancel', "Cancel"); export const ok = localize('arc.ok', "Ok"); +export const on = localize('arc.on', "On"); +export const off = localize('arc.off', "Off"); export const notConfigured = localize('arc.notConfigured', "Not Configured"); // Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states @@ -122,6 +131,10 @@ export const databaseName = localize('arc.databaseName', "Database name"); export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password"); export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password"); export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces"); +export const nodeParametersDescription = localize('arc.nodeParametersDescription', " These server parameters of the Coordinator node and the Worker nodes can be set to custom (non-default) values. Search to find parameters."); +export const learnAboutNodeParameters = localize('arc.learnAboutNodeParameters', "Learn more about database engine settings for Azure Arc enabled PostgreSQL Hyperscale"); +export const noNodeParametersFound = localize('arc.noNodeParametersFound', "No worker server parameters found..."); +export const searchToFilter = localize('arc.searchToFilter', "Search to filter items..."); export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory."); export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled"); export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by"); @@ -153,7 +166,11 @@ export const details = localize('arc.details', "Details"); export const lastUpdated = localize('arc.lastUpdated', "Last updated"); export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available."); export const podsReady = localize('arc.podsReady', "pods ready"); +export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed."); +export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql"); +export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); } +export function allowedValue(value: string): string { return localize('arc.allowedValue', "Value is expected to be {0}", value); } export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); } export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); } export function updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); } @@ -177,7 +194,9 @@ export function validationMin(min: number): string { return localize('arc.valida // Errors export const connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information"); +export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings."); export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration."); +export function outOfRange(min: string, max: string): string { return localize('arc.outOfRange', "The number must be in range {0} - {1}", min, max); } export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); } export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); } export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); } @@ -185,11 +204,14 @@ export function instanceUpdateFailed(name: string, error: any): string { return export function pageDiscardFailed(error: any): string { return localize('arc.pageDiscardFailed', "Failed to discard user input. {0}", getErrorMessage(error)); } export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); } export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); } -export function connectToSqlFailed(serverName: string, error: any): string { return localize('arc.connectToSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } +export function connectToMSSqlFailed(serverName: string, error: any): string { return localize('arc.connectToMSSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } +export function connectToPGSqlFailed(serverName: string, error: any): string { return localize('arc.connectToPGSqlFailed', "Could not connect to PostgreSQL Hyperscale - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } +export function missingExtension(extensionName: string): string { return localize('arc.missingExtension', "The {0} extension is required to view engine settings. Do you wish to install it now?", extensionName); } export function fetchConfigFailed(name: string, error: any): string { return localize('arc.fetchConfigFailed', "An unexpected error occurred retrieving the config for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occurred retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); } +export function fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); } export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); } export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); } export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); } diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index 77a796d225..00223afc36 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -9,10 +9,9 @@ import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; import { UserCancelledError } from '../common/api'; import { Deferred } from '../common/promise'; -import { createCredentialId, parseIpAndPort } from '../common/utils'; -import { credentialNamespace } from '../constants'; +import { parseIpAndPort } from '../common/utils'; import * as loc from '../localizedConstants'; -import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog'; +import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; @@ -23,10 +22,6 @@ export class MiaaModel extends ResourceModel { private _config: azdataExt.SqlMiShowResult | undefined; private _databases: DatabaseModel[] = []; - // The saved connection information - private _connectionProfile: azdata.IConnectionProfile | undefined = undefined; - // The ID of the active connection used to query the server - private _activeConnectionId: string | undefined = undefined; private readonly _onConfigUpdated = new vscode.EventEmitter(); private readonly _onDatabasesUpdated = new vscode.EventEmitter(); @@ -38,8 +33,8 @@ export class MiaaModel extends ResourceModel { private _refreshPromise: Deferred | undefined = undefined; - constructor(controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { - super(controllerModel, _miaaInfo, registration); + constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { + super(_controllerModel, _miaaInfo, registration); this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } @@ -124,47 +119,41 @@ export class MiaaModel extends ResourceModel { } private async getDatabases(): Promise { - await this.getConnectionProfile(); - if (this._connectionProfile) { - // We haven't connected yet so do so now and then store the ID for the active connection - if (!this._activeConnectionId) { - const result = await azdata.connection.connect(this._connectionProfile, false, false); - if (!result.connected) { - throw new Error(result.errorMessage); - } - this._activeConnectionId = result.connectionId; - } - - const provider = azdata.dataprotocol.getProvider(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider); - const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId); - const databases = await provider.getDatabases(ownerUri); - if (!databases) { - throw new Error('Could not fetch databases'); - } - if (databases.length > 0 && typeof (databases[0]) === 'object') { - this._databases = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; }); - } else { - this._databases = (databases).map(db => { return { name: db, status: '-' }; }); - } - this.databasesLastUpdated = new Date(); - this._onDatabasesUpdated.fire(this._databases); + if (!this._connectionProfile) { + await this.getConnectionProfile(); } + + // We haven't connected yet so do so now and then store the ID for the active connection + if (!this._activeConnectionId) { + const result = await azdata.connection.connect(this._connectionProfile!, false, false); + if (!result.connected) { + throw new Error(result.errorMessage); + } + this._activeConnectionId = result.connectionId; + } + + const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.MetadataProvider); + const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId); + const databases = await provider.getDatabases(ownerUri); + if (!databases) { + throw new Error('Could not fetch databases'); + } + if (databases.length > 0 && typeof (databases[0]) === 'object') { + this._databases = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; }); + } else { + this._databases = (databases).map(db => { return { name: db, status: '-' }; }); + } + this.databasesLastUpdated = new Date(); + this._onDatabasesUpdated.fire(this._databases); } - /** - * Loads the saved connection profile associated with this model. Will prompt for one if - * we don't have one or can't find it (it was deleted) - */ - private async getConnectionProfile(): Promise { - if (this._connectionProfile) { - return; - } + protected createConnectionProfile(): azdata.IConnectionProfile { const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || ''); - let connectionProfile: azdata.IConnectionProfile | undefined = { + return { serverName: `${ipAndPort.ip},${ipAndPort.port}`, databaseName: '', authenticationType: 'SqlLogin', - providerName: 'MSSQL', + providerName: loc.miaaProviderName, connectionName: '', userName: this._miaaInfo.userName || '', password: '', @@ -175,47 +164,21 @@ export class MiaaModel extends ResourceModel { groupId: undefined, options: {} }; + } - // If we have the ID stored then try to retrieve the password from previous connections - if (this.info.connectionId) { - try { - const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); - const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name)); - if (credentials.password) { - // Try to connect to verify credentials are still valid - connectionProfile.password = credentials.password; - // If we don't have a username for some reason then just continue on and we'll prompt for the username below - if (connectionProfile.userName) { - const result = await azdata.connection.connect(connectionProfile, false, false); - if (!result.connected) { - vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage)); - const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this); - connectToSqlDialog.showDialog(connectionProfile); - connectionProfile = await connectToSqlDialog.waitForClose(); - } - } - } - } catch (err) { - console.warn(`Unexpected error fetching password for MIAA instance ${err}`); - // ignore - something happened fetching the password so just reprompt - } - } + protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise { + const connectToSqlDialog = new ConnectToMiaaSqlDialog(this.controllerModel, this); + connectToSqlDialog.showDialog(loc.connectToMSSql(this.info.name), connectionProfile); + let profileFromDialog = await connectToSqlDialog.waitForClose(); - if (!connectionProfile?.userName || !connectionProfile?.password) { - // Need to prompt user for password since we don't have one stored - const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this); - connectToSqlDialog.showDialog(connectionProfile); - connectionProfile = await connectToSqlDialog.waitForClose(); - } - - if (connectionProfile) { - this.updateConnectionProfile(connectionProfile); + if (profileFromDialog) { + this.updateConnectionProfile(profileFromDialog); } else { throw new UserCancelledError(); } } - private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { + protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { this._connectionProfile = connectionProfile; this.info.connectionId = connectionProfile.id; this._miaaInfo.userName = connectionProfile.userName; diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 429aec686a..0aad439598 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -3,27 +3,45 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceInfo } from 'arc'; +import { PGResourceInfo } from 'arc'; +import * as azdata from 'azdata'; import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; import * as loc from '../localizedConstants'; +import { ConnectToPGSqlDialog } from '../ui/dialogs/connectPGDialog'; +import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; +import { parseIpAndPort } from '../common/utils'; +import { UserCancelledError } from '../common/api'; import { ResourceModel } from './resourceModel'; import { Deferred } from '../common/promise'; -import { parseIpAndPort } from '../common/utils'; + +export type EngineSettingsModel = { + parameterName: string | undefined, + value: string | undefined, + description: string | undefined, + min: string | undefined, + max: string | undefined, + options: string | undefined, + type: string | undefined +}; export class PostgresModel extends ResourceModel { private _config?: azdataExt.PostgresServerShowResult; + public _engineSettings: EngineSettingsModel[] = []; private readonly _azdataApi: azdataExt.IExtension; private readonly _onConfigUpdated = new vscode.EventEmitter(); + public readonly _onEngineSettingsUpdated = new vscode.EventEmitter(); public onConfigUpdated = this._onConfigUpdated.event; + public onEngineSettingsUpdated = this._onEngineSettingsUpdated.event; public configLastUpdated?: Date; + public engineSettingsLastUpdated?: Date; private _refreshPromise?: Deferred; - constructor(controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) { - super(controllerModel, info, registration); + constructor(_controllerModel: ControllerModel, private _pgInfo: PGResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { + super(_controllerModel, _pgInfo, registration); this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } @@ -103,4 +121,84 @@ export class PostgresModel extends ResourceModel { this._refreshPromise = undefined; } } + + public async getEngineSettings(): Promise { + if (!this._connectionProfile) { + await this.getConnectionProfile(); + } + + // We haven't connected yet so do so now and then store the ID for the active connection + if (!this._activeConnectionId) { + const result = await azdata.connection.connect(this._connectionProfile!, false, false); + if (!result.connected) { + throw new Error(result.errorMessage); + } + this._activeConnectionId = result.connectionId; + } + + const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider); + const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId); + + const engineSettings = await provider.runQueryAndReturn(ownerUri, 'select name, setting, short_desc,min_val, max_val, enumvals, vartype from pg_settings'); + if (!engineSettings) { + throw new Error('Could not fetch engine settings'); + } + engineSettings.rows.forEach(row => { + let rowValues = row.map(c => c.displayValue); + let result: EngineSettingsModel = { + parameterName: rowValues.shift(), + value: rowValues.shift(), + description: rowValues.shift(), + min: rowValues.shift(), + max: rowValues.shift(), + options: rowValues.shift(), + type: rowValues.shift() + }; + + this._engineSettings.push(result); + }); + + this.engineSettingsLastUpdated = new Date(); + } + + protected createConnectionProfile(): azdata.IConnectionProfile { + const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || ''); + return { + serverName: `${ipAndPort.ip},${ipAndPort.port}`, + databaseName: '', + authenticationType: 'SqlLogin', + providerName: loc.postgresProviderName, + connectionName: '', + userName: this._pgInfo.userName || '', + password: '', + savePassword: true, + groupFullName: undefined, + saveProfile: true, + id: '', + groupId: undefined, + options: { + host: `${ipAndPort.ip}`, + port: `${ipAndPort.port}`, + } + }; + } + + protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise { + const connectToSqlDialog = new ConnectToPGSqlDialog(this.controllerModel, this); + connectToSqlDialog.showDialog(loc.connectToPGSql(this.info.name), connectionProfile); + let profileFromDialog = await connectToSqlDialog.waitForClose(); + + if (profileFromDialog) { + this.updateConnectionProfile(profileFromDialog); + } else { + throw new UserCancelledError(); + } + } + + protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { + this._connectionProfile = connectionProfile; + this.info.connectionId = connectionProfile.id; + this._pgInfo.userName = connectionProfile.userName; + await this._treeDataProvider.saveControllers(); + } } diff --git a/extensions/arc/src/models/resourceModel.ts b/extensions/arc/src/models/resourceModel.ts index 99760732a4..adb6fc6b8f 100644 --- a/extensions/arc/src/models/resourceModel.ts +++ b/extensions/arc/src/models/resourceModel.ts @@ -4,14 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { ResourceInfo } from 'arc'; +import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { ControllerModel, Registration } from './controllerModel'; +import { createCredentialId } from '../common/utils'; +import { credentialNamespace } from '../constants'; export abstract class ResourceModel { private readonly _onRegistrationUpdated = new vscode.EventEmitter(); public onRegistrationUpdated = this._onRegistrationUpdated.event; + // The saved connection information + protected _connectionProfile: azdata.IConnectionProfile | undefined = undefined; + // The ID of the active connection used to query the server + protected _activeConnectionId: string | undefined = undefined; + constructor(public readonly controllerModel: ControllerModel, public info: ResourceInfo, private _registration: Registration) { } public get registration(): Registration { @@ -23,5 +31,48 @@ export abstract class ResourceModel { this._onRegistrationUpdated.fire(this._registration); } + /** + * Loads the saved connection profile associated with this model. Will prompt for one if + * we don't have one or can't find it (it was deleted) + */ + protected async getConnectionProfile(): Promise { + let connectionProfile: azdata.IConnectionProfile | undefined = this.createConnectionProfile(); + + // If we have the ID stored then try to retrieve the password from previous connections + if (this.info.connectionId) { + try { + const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); + const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name)); + if (credentials.password) { + // Try to connect to verify credentials are still valid + connectionProfile.password = credentials.password; + // If we don't have a username for some reason then just continue on and we'll prompt for the username below + if (connectionProfile.userName) { + const result = await azdata.connection.connect(connectionProfile, false, false); + if (!result.connected) { + await this.promptForConnection(connectionProfile); + } else { + this.updateConnectionProfile(connectionProfile); + } + } + } + } catch (err) { + console.warn(`Unexpected error fetching password for instance ${err}`); + // ignore - something happened fetching the password so just reprompt + } + } + + if (!connectionProfile?.userName || !connectionProfile?.password) { + // Need to prompt user for password since we don't have one stored + await this.promptForConnection(connectionProfile); + } + } + public abstract refresh(): Promise; + + protected abstract createConnectionProfile(): azdata.IConnectionProfile; + + protected abstract promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise; + + protected abstract updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise; } diff --git a/extensions/arc/src/typings/arc.d.ts b/extensions/arc/src/typings/arc.d.ts index 090990d39c..ab0fc0f694 100644 --- a/extensions/arc/src/typings/arc.d.ts +++ b/extensions/arc/src/typings/arc.d.ts @@ -23,6 +23,10 @@ declare module 'arc' { userName?: string }; + export type PGResourceInfo = ResourceInfo & { + userName?: string + }; + export type ResourceInfo = { name: string, resourceType: ResourceType | string, diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts new file mode 100644 index 0000000000..f9957deeb7 --- /dev/null +++ b/extensions/arc/src/ui/dashboards/postgres/postgresParametersPage.ts @@ -0,0 +1,506 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as azdataExt from 'azdata-ext'; +import * as loc from '../../../localizedConstants'; +import { UserCancelledError } from '../../../common/api'; +import { IconPathHelper, cssStyles } from '../../../constants'; +import { DashboardPage } from '../../components/dashboardPage'; +import { PostgresModel } from '../../../models/postgresModel'; + +export class PostgresParametersPage extends DashboardPage { + private searchBox?: azdata.InputBoxComponent; + private parametersTable!: azdata.DeclarativeTableComponent; + private parameterContainer?: azdata.DivContainer; + private _parametersTableLoading!: azdata.LoadingComponent; + + private discardButton?: azdata.ButtonComponent; + private saveButton?: azdata.ButtonComponent; + private resetButton?: azdata.ButtonComponent; + private connectToServerButton?: azdata.ButtonComponent; + + private engineSettings = `'`; + private engineSettingUpdates?: Map; + + private readonly _azdataApi: azdataExt.IExtension; + + constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { + super(modelView); + this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; + + this.initializeConnectButton(); + this.initializeSearchBox(); + + this.engineSettingUpdates = new Map(); + + this.disposables.push( + this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())), + this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEngineSettingsUpdated()))); + } + + protected get title(): string { + return loc.nodeParameters; + } + + protected get id(): string { + return 'postgres-node-parameters'; + } + + protected get icon(): { dark: string; light: string; } { + return IconPathHelper.gear; + } + + protected get container(): azdata.Component { + const root = this.modelView.modelBuilder.divContainer().component(); + const content = this.modelView.modelBuilder.divContainer().component(); + root.addItem(content, { CSSStyles: { 'margin': '20px' } }); + + content.addItem(this.modelView.modelBuilder.text().withProps({ + value: loc.nodeParameters, + CSSStyles: { ...cssStyles.title } + }).component()); + + const info = this.modelView.modelBuilder.text().withProps({ + value: loc.nodeParametersDescription, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + + const link = this.modelView.modelBuilder.hyperlink().withProps({ + label: loc.learnAboutNodeParameters, + url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale', + }).component(); + + const infoAndLink = this.modelView.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component(); + infoAndLink.addItem(info, { CSSStyles: { 'margin-right': '5px' } }); + infoAndLink.addItem(link); + content.addItem(infoAndLink, { CSSStyles: { 'margin-bottom': '20px' } }); + + content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } }); + + this.parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({ + width: '100%', + columns: [ + { + displayName: 'Parameter Name', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: '20%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, + { + displayName: 'Value', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: false, + width: '20%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: { + ...cssStyles.tableRow, + 'overflow': 'hidden', + 'text-overflow': 'ellipsis', + 'white-space': 'nowrap', + 'max-width': '0' + } + }, + { + displayName: 'Description', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: '50%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: { + ...cssStyles.tableRow, + 'overflow': 'hidden', + 'text-overflow': 'ellipsis', + 'white-space': 'nowrap', + 'max-width': '0' + } + }, + { + displayName: 'Reset To Default', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: false, + width: '10%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + } + ], + data: [ + this.parameterComponents('TEST NAME', 'string'), + this.parameterComponents('TEST NAME 2', 'real'), + this.createParametersTable()] + + }).component(); + + this._parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component(); + + this.parameterContainer = this.modelView.modelBuilder.divContainer().component(); + this.selectComponent(); + + content.addItem(this.parameterContainer); + + this.initialized = true; + + return root; + } + + protected get toolbarContainer(): azdata.ToolbarContainer { + // Save Edits + this.saveButton = this.modelView.modelBuilder.button().withProps({ + label: loc.saveText, + iconPath: IconPathHelper.save, + enabled: false + }).component(); + + this.disposables.push( + this.saveButton.onDidClick(async () => { + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.updatingInstance(this._postgresModel.info.name), + cancellable: false + }, + async (_progress, _token): Promise => { + //Edit multiple + // azdata arc postgres server edit -n -e '=, =,...' + try { + this.engineSettingUpdates!.forEach((value: string) => { + this.engineSettings += value + ', '; + }); + await this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, { engineSettings: this.engineSettings + `'` }); + } catch (err) { + // If an error occurs while editing the instance then re-enable the save button since + // the edit wasn't successfully applied + this.saveButton!.enabled = true; + throw err; + } + await this._postgresModel.refresh(); + } + ); + + vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); + + this.engineSettings = `'`; + this.engineSettingUpdates!.clear(); + this.discardButton!.enabled = false; + + } catch (error) { + vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error)); + } + })); + + // Discard + this.discardButton = this.modelView.modelBuilder.button().withProps({ + label: loc.discardText, + iconPath: IconPathHelper.discard, + enabled: false + }).component(); + + this.disposables.push( + this.discardButton.onDidClick(async () => { + this.discardButton!.enabled = false; + try { + // TODO + // this.parametersTable.data = []; + this.engineSettingUpdates!.clear(); + } catch (error) { + vscode.window.showErrorMessage(loc.pageDiscardFailed(error)); + } finally { + this.saveButton!.enabled = false; + } + })); + + // Reset + this.resetButton = this.modelView.modelBuilder.button().withProps({ + label: loc.resetAllToDefault, + iconPath: IconPathHelper.reset, + enabled: true + }).component(); + + this.disposables.push( + this.resetButton.onDidClick(async () => { + this.resetButton!.enabled = false; + this.discardButton!.enabled = false; + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.updatingInstance(this._postgresModel.info.name), + cancellable: false + }, + async (_progress, _token): Promise => { + //all + // azdata arc postgres server edit -n -e '' -re + try { + await this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, { engineSettings: `'' -re` }); + } catch (err) { + // If an error occurs while resetting the instance then re-enable the reset button since + // the edit wasn't successfully applied + this.resetButton!.enabled = true; + throw err; + } + await this._postgresModel.refresh(); + } + + ); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } + })); + + return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ + { component: this.saveButton }, + { component: this.discardButton }, + { component: this.resetButton } + ]).component(); + } + + private initializeConnectButton() { + + this.connectToServerButton = this.modelView.modelBuilder.button().withProps({ + label: loc.connectToServer, + enabled: false, + CSSStyles: { 'max-width': '125px' } + }).component(); + + this.disposables.push( + this.connectToServerButton!.onDidClick(async () => { + this.connectToServerButton!.enabled = false; + if (!vscode.extensions.getExtension(loc.postgresExtension)) { + const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no); + if (response !== loc.yes) { + this.connectToServerButton!.enabled = true; + return; + } + + await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension); + } + + await this._postgresModel.getEngineSettings().catch(err => { + // If an error occurs show a message so the user knows something failed but still + // fire the event so callers can know to update (e.g. so dashboards don't show the + // loading icon forever) + if (err instanceof UserCancelledError) { + vscode.window.showWarningMessage(loc.pgConnectionRequired); + } else { + vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, err)); + } + this._postgresModel.engineSettingsLastUpdated = new Date(); + this._postgresModel._onEngineSettingsUpdated.fire(this._postgresModel._engineSettings); + this.connectToServerButton!.enabled = true; + throw err; + }); + + this.parameterContainer!.clearItems(); + this.parameterContainer!.addItem(this.parametersTable); + + })); + } + + private initializeSearchBox() { + this.searchBox = this.modelView.modelBuilder.inputBox().withProps({ + readOnly: false, + placeHolder: loc.searchToFilter + }).component(); + + this.disposables.push( + this.searchBox.onTextChanged(() => { + this.filterParameters(); + }) + ); + } + + private filterParameters() { + //TODO + } + + private createParametersTable(): any[] { + /*Define server settings that shouldn't be modified. we block archive_*, restore_*, and synchronous_commit to prevent the user + from messing up our backups. (we rely on synchronous_commit to ensure WAL changes are written immediately.) + we block log_* to protect our logging. we block wal_level because Citus needs a particular wal_Level to rebalance shards + TODO: Review list of blacklisted parameters. wal_level should only be blacklisted if sharding is enabled + To not be modified + "archive_command", "archive_timeout", "log_directory", "log_file_mode", "log_filename", "restore_command", + "shared_preload_libraries", "synchronous_commit", "ssl", "unix_socket_permissions", "wal_level" */ + + // For ev in this._postgresModel._engineSettings + // create row + // return rows + this.parameterComponents('engineSetting', ''); + return []; + } + + private parameterComponents(name: string, type: string): any[] { + let data = []; + + // Set parameter name + const parameterName = this.modelView.modelBuilder.text().withProps({ + value: name, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + data.push(parameterName); + + // Container to hold input component and information bubble + const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component(); + + // Information bubble title to be set depening on type of input + let information = this.modelView.modelBuilder.button().withProps({ + iconPath: IconPathHelper.information, + width: '12px', + height: '12px', + enabled: false + }).component(); + + if (type === 'enum') { + // If type is enum, component should be drop down menu + let valueBox = this.modelView.modelBuilder.dropDown().withProps({ + values: [], //TODO, + value: '', //TODO + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); + + this.disposables.push( + valueBox.onValueChanged(() => { + this.engineSettingUpdates!.set(name, String(valueBox.value)); + + }) + ); + + information.updateProperty('title', loc.allowedValue('enums')); //TODO + } else if (type === 'bool') { + // If type is bool, component should be checkbox to turn on or off + let valueBox = this.modelView.modelBuilder.checkBox().withProps({ + label: loc.on, + checked: true, //TODO + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); + + this.disposables.push( + valueBox.onChanged(() => { + if (valueBox.checked) { + this.engineSettingUpdates!.set(name, loc.on); + } else { + this.engineSettingUpdates!.set(name, loc.off); + } + }) + ); + + information.updateProperty('title', loc.allowedValue('on,off')); //TODO + } else if (type === 'string') { + // If type is string, component should be text inputbox + // How to add validation: .withValidation(component => component.value?.search('[0-9]') == -1) + let valueBox = this.modelView.modelBuilder.inputBox().withProps({ + readOnly: false, + value: '', //TODO + CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '200px' } + }).component(); + valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); + + this.disposables.push( + valueBox.onTextChanged(() => { + this.engineSettingUpdates!.set(name, valueBox.value!); + }) + ); + + information.updateProperty('title', loc.allowedValue(loc.allowedValue('[A-Za-z._]+'))); //TODO + } else { + // If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set. + let valueBox = this.modelView.modelBuilder.inputBox().withProps({ + readOnly: false, + min: 0, //TODO + max: 10000, + validationErrorMessage: loc.outOfRange('min', 'max'), //TODO + inputType: 'number', + value: '0', //TODO + CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '200px' } + }).component(); + valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } }); + + this.disposables.push( + valueBox.onTextChanged(() => { + this.engineSettingUpdates!.set(name, valueBox.value!); + }) + ); + + information.updateProperty('title', loc.allowedValue(loc.rangeSetting('min', 'max'))); //TODO + } + + valueContainer.addItem(information, { CSSStyles: { 'margin-left': '5px', 'margin-bottom': '15px' } }); + data.push(valueContainer); + + const parameterDescription = this.modelView.modelBuilder.text().withProps({ + value: 'TEST DESCRIPTION HERE ...............................ytgbyugvtyvctyrcvytjv ycrtctyv tyfty ftyuvuyvuy', // TODO + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component(); + data.push(parameterDescription); + + // Can reset individual component + const resetParameter = this.modelView.modelBuilder.button().withProps({ + iconPath: IconPathHelper.reset, + title: loc.resetToDefault, + width: '20px', + height: '20px', + enabled: true + }).component(); + data.push(resetParameter); + + // azdata arc postgres server edit -n postgres01 -e shared_buffers= + this.disposables.push( + resetParameter.onDidClick(async () => { + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.updatingInstance(this._postgresModel.info.name), + cancellable: false + }, + (_progress, _token) => { + return this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, { engineSettings: name + '=' }); + } + ); + + vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name)); + + } catch (error) { + vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error)); + } + })); + + return data; + } + + private selectComponent() { + if (!this._postgresModel.engineSettingsLastUpdated) { + this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({ + value: loc.connectToPostgresDescription, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + }).component()); + this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } }); + this.parameterContainer!.addItem(this._parametersTableLoading!); + } else { + this.parameterContainer!.addItem(this.parametersTable!); + } + } + + private handleEngineSettingsUpdated(): void { + //TODO + } + + private handleServiceUpdated() { + // TODO + if (this._postgresModel.configLastUpdated) { + this.connectToServerButton!.enabled = true; + this._parametersTableLoading!.loading = false; + } + } +} diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 03fb1e7858..66c6fb780e 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -7,10 +7,11 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../../constants'; -import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer'; +import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue, LinkKeyValue } from '../../components/keyValueContainer'; import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; +import { ControllerDashboard } from '../controller/controllerDashboard'; export class PostgresPropertiesPage extends DashboardPage { private loading?: azdata.LoadingComponent; @@ -93,12 +94,13 @@ export class PostgresPropertiesPage extends DashboardPage { private getProperties(): KeyValue[] { const endpoint = this._postgresModel.endpoint; const status = this._postgresModel.config?.status; + const controllerDashboard = new ControllerDashboard(this._controllerModel); return [ new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''), new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'), new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown), - // TODO: Make this a LinkKeyValue that opens the controller dashboard + new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? '', () => controllerDashboard.showDashboard()), new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''), new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''), new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''), diff --git a/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts b/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts new file mode 100644 index 0000000000..7a360f6120 --- /dev/null +++ b/extensions/arc/src/ui/dialogs/connectMiaaDialog.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ControllerModel } from '../../models/controllerModel'; +import { MiaaModel } from '../../models/miaaModel'; +import { ConnectToSqlDialog } from './connectSqlDialog'; +import * as loc from '../../localizedConstants'; + +export class ConnectToMiaaSqlDialog extends ConnectToSqlDialog { + + constructor(_controllerModel: ControllerModel, _miaaModel: MiaaModel) { + super(_controllerModel, _miaaModel); + } + + protected get providerName(): string { + return 'MSSQL'; + } + + protected connectionFailedMessage(error: any): string { + return loc.connectToMSSqlFailed(this.serverNameInputBox.value!, error); + } +} diff --git a/extensions/arc/src/ui/dialogs/connectPGDialog.ts b/extensions/arc/src/ui/dialogs/connectPGDialog.ts new file mode 100644 index 0000000000..4fb6effec8 --- /dev/null +++ b/extensions/arc/src/ui/dialogs/connectPGDialog.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ControllerModel } from '../../models/controllerModel'; +import { PostgresModel } from '../../models/postgresModel'; +import { ConnectToSqlDialog } from './connectSqlDialog'; +import * as loc from '../../localizedConstants'; + +export class ConnectToPGSqlDialog extends ConnectToSqlDialog { + + constructor(_controllerModel: ControllerModel, _postgresModel: PostgresModel) { + super(_controllerModel, _postgresModel); + } + + protected get providerName(): string { + return 'PGSQL'; + } + + protected connectionFailedMessage(error: any): string { + return loc.connectToPGSqlFailed(this.serverNameInputBox.value!, error); + } +} diff --git a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts index de9fdd1683..fe7a223642 100644 --- a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts +++ b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts @@ -6,29 +6,30 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { Deferred } from '../../common/promise'; +import * as loc from '../../localizedConstants'; import { createCredentialId } from '../../common/utils'; import { credentialNamespace } from '../../constants'; -import * as loc from '../../localizedConstants'; -import { ControllerModel } from '../../models/controllerModel'; -import { MiaaModel } from '../../models/miaaModel'; import { InitializingComponent } from '../components/initializingComponent'; +import { ResourceModel } from '../../models/resourceModel'; +import { ControllerModel } from '../../models/controllerModel'; -export class ConnectToSqlDialog extends InitializingComponent { - private modelBuilder!: azdata.ModelBuilder; +export abstract class ConnectToSqlDialog extends InitializingComponent { + protected modelBuilder!: azdata.ModelBuilder; - private serverNameInputBox!: azdata.InputBoxComponent; - private usernameInputBox!: azdata.InputBoxComponent; - private passwordInputBox!: azdata.InputBoxComponent; - private rememberPwCheckBox!: azdata.CheckBoxComponent; + protected serverNameInputBox!: azdata.InputBoxComponent; + protected usernameInputBox!: azdata.InputBoxComponent; + protected passwordInputBox!: azdata.InputBoxComponent; + protected rememberPwCheckBox!: azdata.CheckBoxComponent; + private options: { [name: string]: any } = {}; - private _completionPromise = new Deferred(); + protected _completionPromise = new Deferred(); - constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { + constructor(private _controllerModel: ControllerModel, protected _model: ResourceModel) { super(); } - public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog { - const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name)); + public showDialog(dialogTitle: string, connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog { + const dialog = azdata.window.createModelViewDialog(dialogTitle); dialog.cancelButton.onClick(() => this.handleCancel()); dialog.registerContent(async view => { this.modelBuilder = view.modelBuilder; @@ -84,6 +85,7 @@ export class ConnectToSqlDialog extends InitializingComponent { dialog.registerCloseValidator(async () => await this.validate()); dialog.okButton.label = loc.connect; dialog.cancelButton.label = loc.cancel; + this.options = connectionProfile?.options!; azdata.window.openDialog(dialog); return dialog; } @@ -96,7 +98,7 @@ export class ConnectToSqlDialog extends InitializingComponent { serverName: this.serverNameInputBox.value, databaseName: '', authenticationType: 'SqlLogin', - providerName: 'MSSQL', + providerName: this.providerName, connectionName: '', userName: this.usernameInputBox.value, password: this.passwordInputBox.value, @@ -105,26 +107,30 @@ export class ConnectToSqlDialog extends InitializingComponent { saveProfile: true, id: '', groupId: undefined, - options: {} + options: this.options }; const result = await azdata.connection.connect(connectionProfile, false, false); if (result.connected) { connectionProfile.id = result.connectionId; const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); if (connectionProfile.savePassword) { - await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password); + await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name), connectionProfile.password); } else { - await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name)); + await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name)); } this._completionPromise.resolve(connectionProfile); return true; } else { - vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage)); + vscode.window.showErrorMessage(this.connectionFailedMessage(result.errorMessage)); return false; } } + protected abstract get providerName(): string; + + protected abstract connectionFailedMessage(error: any): string; + private handleCancel(): void { this._completionPromise.resolve(undefined); } diff --git a/extensions/arc/src/ui/tree/controllerTreeNode.ts b/extensions/arc/src/ui/tree/controllerTreeNode.ts index 9af1bae180..47f9c11ddd 100644 --- a/extensions/arc/src/ui/tree/controllerTreeNode.ts +++ b/extensions/arc/src/ui/tree/controllerTreeNode.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc'; +import { MiaaResourceInfo, PGResourceInfo, ResourceInfo, ResourceType } from 'arc'; import * as vscode from 'vscode'; import { UserCancelledError } from '../../common/api'; import * as loc from '../../localizedConstants'; @@ -102,7 +102,11 @@ export class ControllerTreeNode extends TreeNode { switch (registration.instanceType) { case ResourceType.postgresInstances: - const postgresModel = new PostgresModel(this.model, resourceInfo, registration); + // Fill in the username too if we already have it + (resourceInfo as PGResourceInfo).userName = (this.model.info.resources.find(info => + info.name === resourceInfo.name && + info.resourceType === resourceInfo.resourceType) as PGResourceInfo)?.userName; + const postgresModel = new PostgresModel(this.model, resourceInfo, registration, this._treeDataProvider); node = new PostgresTreeNode(postgresModel, this.model, this._context); break; case ResourceType.sqlManagedInstances: