diff --git a/extensions/arc/images/postgres.svg b/extensions/arc/images/postgres.svg index e247e4e717..8c4cb91c17 100644 --- a/extensions/arc/images/postgres.svg +++ b/extensions/arc/images/postgres.svg @@ -1 +1,21 @@ -Icon-databases-131 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index 19954753a5..a915321cf1 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -107,24 +107,40 @@ export function getDatabaseStateDisplayText(state: string): string { * @param name The name of the resource to delete * @returns Promise resolving to true if the user confirmed the name, false if the input box was closed for any other reason */ -export async function promptForResourceDeletion(namespace: string, name: string): Promise { + +/** + * Opens an input box prompting and validating the user's input. + * @param options Options for the input box + * @param title An optional title for the input box + * @returns Promise resolving to the user's input if it passed validation, + * or undefined if the input box was closed for any other reason + */ +async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise { const inputBox = vscode.window.createInputBox(); - inputBox.title = loc.resourceDeletionWarning(namespace, name); - inputBox.placeholder = name; + inputBox.title = title; + inputBox.prompt = options.prompt; + inputBox.placeholder = options.placeHolder; + inputBox.password = options.password ?? false; + inputBox.value = options.value ?? ''; + inputBox.ignoreFocusOut = options.ignoreFocusOut ?? false; + return new Promise(resolve => { let valueAccepted = false; - inputBox.onDidAccept(() => { - if (inputBox.value === name) { - valueAccepted = true; - inputBox.hide(); - resolve(true); - } else { - inputBox.validationMessage = loc.invalidResourceDeletionName(inputBox.value); + inputBox.onDidAccept(async () => { + if (options.validateInput) { + const errorMessage = await options.validateInput(inputBox.value); + if (errorMessage) { + inputBox.validationMessage = errorMessage; + return; + } } + valueAccepted = true; + inputBox.hide(); + resolve(inputBox.value); }); inputBox.onDidHide(() => { if (!valueAccepted) { - resolve(false); + resolve(undefined); } inputBox.dispose(); }); @@ -135,6 +151,46 @@ export async function promptForResourceDeletion(namespace: string, name: string) }); } +/** + * Opens an input box prompting the user to enter in the name of a resource to delete + * @param namespace The namespace of the resource to delete + * @param name The name of the resource to delete + * @returns Promise resolving to true if the user confirmed the name, false if the input box was closed for any other reason + */ +export async function promptForResourceDeletion(namespace: string, name: string): Promise { + const title = loc.resourceDeletionWarning(namespace, name); + const options: vscode.InputBoxOptions = { + placeHolder: name, + validateInput: input => input !== name ? loc.invalidResourceDeletionName(name) : '' + }; + + return await promptInputBox(title, options) !== undefined; +} + +/** + * Opens an input box prompting the user to enter and confirm a password + * @param validate A function that accepts the password and returns an error message if it's invalid + * @returns Promise resolving to the password if it passed validation, + * or false if the input box was closed for any other reason + */ +export async function promptAndConfirmPassword(validate: (input: string) => string): Promise { + const title = loc.resetPassword; + const options: vscode.InputBoxOptions = { + prompt: loc.enterNewPassword, + password: true, + validateInput: input => validate(input) + }; + + const password = await promptInputBox(title, options); + if (password) { + options.prompt = loc.confirmNewPassword; + options.validateInput = input => input !== password ? loc.thePasswordsDoNotMatch : ''; + return promptInputBox(title, options); + } + + return false; +} + /** * Gets the message to display for a given error object that may be a variety of types. * @param error The error object diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index d994530d6f..19a8ce90fe 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -98,15 +98,19 @@ export const worker = localize('arc.worker', "Worker"); export const monitor = localize('arc.monitor', "Monitor"); export const newDatabase = localize('arc.newDatabase', "New Database"); export const databaseName = localize('arc.databaseName', "Database name"); -export const newPassword = localize('arc.newPassword', "New password"); +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 node = localize('arc.node', "node"); export const nodes = localize('arc.nodes', "nodes"); export const storagePerNode = localize('arc.storagePerNode', "storage per node"); export const arcResources = localize('arc.arcResources', "Azure Arc Resources"); +export const enterANonEmptyPassword = localize('arc.enterANonEmptyPassword', "Enter a non empty password or press escape to exit."); +export const thePasswordsDoNotMatch = localize('arc.thePasswordsDoNotMatch', "The passwords do not match. Confirm the password or press escape to exit."); +export const passwordReset = localize('arc.passwordReset', "Password reset successfully"); +export const passwordResetFailed = localize('arc.passwordResetFailed', "Failed to reset password"); export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); } -export function passwordReset(name: string): string { return localize('arc.passwordReset', "Password reset for service {0}", name); } export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); } export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); } export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); } @@ -127,6 +131,5 @@ export function invalidResourceDeletionName(name: string): string { return local 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 resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(error)); } -export function passwordResetFailed(name: string, error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password for service {0}. {1}", name, 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)); } diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 539470a789..262e4f61bf 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -17,16 +17,12 @@ export enum PodRole { export class PostgresModel { private _databaseRouter: DatabaseRouterApi; private _service?: DuskyObjectModelsDatabaseService; - private _password?: string; private _pods?: V1Pod[]; private readonly _onServiceUpdated = new vscode.EventEmitter(); - private readonly _onPasswordUpdated = new vscode.EventEmitter(); private readonly _onPodsUpdated = new vscode.EventEmitter(); public onServiceUpdated = this._onServiceUpdated.event; - public onPasswordUpdated = this._onPasswordUpdated.event; public onPodsUpdated = this._onPodsUpdated.event; public serviceLastUpdated?: Date; - public passwordLastUpdated?: Date; public podsLastUpdated?: Date; constructor(controllerUrl: string, auth: Authentication, private _namespace: string, private _name: string) { @@ -54,11 +50,6 @@ export class PostgresModel { return this._service; } - /** Returns the service's password */ - public get password(): string | undefined { - return this._password; - } - /** Returns the service's pods */ public get pods(): V1Pod[] | undefined { return this._pods; @@ -72,11 +63,6 @@ export class PostgresModel { this.serviceLastUpdated = new Date(); this._onServiceUpdated.fire(this._service); }), - this._databaseRouter.getDuskyPassword(this._namespace, this._name).then(response => { - this._password = response.body; - this.passwordLastUpdated = new Date(); - this._onPasswordUpdated.fire(this._password!); - }), this._databaseRouter.getDuskyPods(this._namespace, this._name).then(response => { this._pods = response.body; this.podsLastUpdated = new Date(); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 05fa9f0b00..1ca1772a39 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -17,7 +17,6 @@ export class PostgresConnectionStringsPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); - this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); } protected get title(): string { @@ -43,18 +42,19 @@ export class PostgresConnectionStringsPage extends DashboardPage { }).component()); const info = this.modelView.modelBuilder.text().withProperties({ - value: `${loc.selectConnectionString}. `, + value: loc.selectConnectionString, CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); const link = this.modelView.modelBuilder.hyperlink().withProperties({ label: loc.learnAboutPostgresClients, - url: 'http://example.com', // TODO link to documentation + url: 'https://docs.microsoft.com/azure/postgresql/concepts-connection-libraries', }).component(); - content.addItem( - this.modelView.modelBuilder.flexContainer().withItems([info, link]).withLayout({ flexWrap: 'wrap' }).component(), - { CSSStyles: { display: 'inline-flex', 'margin-bottom': '25px' } }); + 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': '25px' } }); this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, []); content.addItem(this.keyValueContainer.container); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index 0bb2b326c1..b3753a485b 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -9,11 +9,8 @@ import * as loc from '../../../localizedConstants'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; import { PostgresOverviewPage } from './postgresOverviewPage'; -import { PostgresComputeStoragePage } from './postgresComputeStoragePage'; import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage'; -import { PostgresBackupPage } from './postgresBackupPage'; import { PostgresPropertiesPage } from './postgresPropertiesPage'; -import { PostgresNetworkingPage } from './postgresNetworkingPage'; import { Dashboard } from '../../components/dashboard'; import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage'; import { PostgresSupportRequestPage } from './postgresSupportRequestPage'; @@ -25,11 +22,8 @@ export class PostgresDashboard extends Dashboard { protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); - const computeStoragePage = new PostgresComputeStoragePage(modelView); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); - const backupPage = new PostgresBackupPage(modelView); const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); - const networkingPage = new PostgresNetworkingPage(modelView); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel); const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel); @@ -38,16 +32,9 @@ export class PostgresDashboard extends Dashboard { { title: loc.settings, tabs: [ - computeStoragePage.tab, connectionStringsPage.tab, - backupPage.tab, propertiesPage.tab ] - }, { - title: loc.security, - tabs: [ - networkingPage.tab - ] }, { title: loc.supportAndTroubleshooting, diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index 120b16f5f0..8137f9cc0d 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -51,7 +51,7 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { troubleshootButton.onDidClick(() => { process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace; process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name; - vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arc'), true, 'postgres/tsg100-troubleshoot-postgres'); + vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); }); content.addItem(troubleshootButton); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index 5ef316c396..809bc5675c 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -7,11 +7,11 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles, ResourceType } from '../../../constants'; -import { DuskyObjectModelsDatabase, DuskyObjectModelsDatabaseServiceArcPayload, V1Pod } from '../../../controller/generated/dusky/api'; +import { DuskyObjectModelsDatabase, V1Pod, DuskyObjectModelsDatabaseServiceArcPayload } from '../../../controller/generated/dusky/api'; import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel, PodRole } from '../../../models/postgresModel'; -import { promptForResourceDeletion } from '../../../common/utils'; +import { promptForResourceDeletion, promptAndConfirmPassword } from '../../../common/utils'; export class PostgresOverviewPage extends DashboardPage { private propertiesLoading?: azdata.LoadingComponent; @@ -28,7 +28,6 @@ export class PostgresOverviewPage extends DashboardPage { super(modelView); this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshEndpoints())); this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshProperties())); - this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshProperties())); this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => { this.refreshProperties(); @@ -178,10 +177,11 @@ export class PostgresOverviewPage extends DashboardPage { let name; try { name = await vscode.window.showInputBox({ prompt: loc.databaseName }); - if (name === undefined) { return; } - const db: DuskyObjectModelsDatabase = { name: name }; // TODO support other options (sharded, owner) - await this._postgresModel.createDatabase(db); - vscode.window.showInformationMessage(loc.databaseCreated(db.name ?? '')); + if (name) { + const db: DuskyObjectModelsDatabase = { name: name }; // TODO support other options (sharded, owner) + await this._postgresModel.createDatabase(db); + vscode.window.showInformationMessage(loc.databaseCreated(db.name ?? '')); + } } catch (error) { vscode.window.showErrorMessage(loc.databaseCreationFailed(name ?? '', error)); } finally { @@ -198,15 +198,16 @@ export class PostgresOverviewPage extends DashboardPage { resetPasswordButton.onDidClick(async () => { resetPasswordButton.enabled = false; try { - const password = await vscode.window.showInputBox({ prompt: loc.newPassword, password: true }); - if (password === undefined) { return; } - await this._postgresModel.update(s => { - s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload(); - s.arc.servicePassword = password; - }); - vscode.window.showInformationMessage(loc.passwordReset(this._postgresModel.fullName)); + const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); + if (password) { + await this._postgresModel.update(s => { + s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload(); + s.arc.servicePassword = password; + }); + vscode.window.showInformationMessage(loc.passwordReset); + } } catch (error) { - vscode.window.showErrorMessage(loc.passwordResetFailed(this._postgresModel.fullName, error)); + vscode.window.showErrorMessage(loc.passwordResetFailed); } finally { resetPasswordButton.enabled = true; } @@ -289,7 +290,7 @@ export class PostgresOverviewPage extends DashboardPage { this.properties!.propertyItems = [ { displayName: loc.name, value: this._postgresModel.name }, - { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this._postgresModel.password}@${endpoint.ip}:${endpoint.port}` }, + { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` }, { displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' }, { displayName: loc.postgresAdminUsername, value: 'postgres' }, { displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' }, diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 5271209bc8..ad154fd27f 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -18,7 +18,6 @@ export class PostgresPropertiesPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); - this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); } @@ -78,7 +77,7 @@ export class PostgresPropertiesPage extends DashboardPage { private refresh() { const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - const connectionString = `postgresql://postgres:${this._postgresModel.password}@${endpoint.ip}:${endpoint.port}`; + const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`; const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); this.keyValueContainer?.refresh([ @@ -86,7 +85,7 @@ export class PostgresPropertiesPage extends DashboardPage { new InputKeyValue(loc.postgresAdminUsername, 'postgres'), new TextKeyValue(loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), new LinkKeyValue(loc.dataController, this._controllerModel.namespace ?? '', _ => vscode.window.showInformationMessage('TODO: Go to data controller')), - new LinkKeyValue(loc.nodeConfiguration, this._postgresModel.configuration, _ => vscode.window.showInformationMessage('TODO: Go to configuration')), + new TextKeyValue(loc.nodeConfiguration, this._postgresModel.configuration), new TextKeyValue(loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''), new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '')