diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index b99603386a..fa61919cdc 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -197,8 +197,8 @@ export function updated(when: string): string { return localize('arc.updated', " export function validationMin(min: number): string { return localize('arc.validationMin', "Value must be greater than or equal to {0}.", min); } // 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 miaaConnectionRequired = localize('arc.miaaConnectionRequired', "A connection is required to list the databases on this instance."); 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)); } diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index 2f5bdfd8fd..5a067ead07 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -28,8 +28,8 @@ export class MiaaModel extends ResourceModel { private readonly _azdataApi: azdataExt.IExtension; public onConfigUpdated = this._onConfigUpdated.event; public onDatabasesUpdated = this._onDatabasesUpdated.event; - public configLastUpdated?: Date; - public databasesLastUpdated?: Date; + public configLastUpdated: Date | undefined; + public databasesLastUpdated: Date | undefined; private _refreshPromise: Deferred | undefined = undefined; @@ -91,22 +91,16 @@ export class MiaaModel extends ResourceModel { // If we have an external endpoint configured then fetch the databases now if (this._config.status.externalEndpoint) { - this.getDatabases().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.connectionRequired); - } else { - vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this.info.name, err)); - } - this.databasesLastUpdated = new Date(); + this.getDatabases(false).catch(_err => { + // If an error occurs still fire the event so callers can know to + // update (e.g. so dashboards don't show the loading icon forever) + + this.databasesLastUpdated = undefined; this._onDatabasesUpdated.fire(this._databases); - throw err; }); } else { // Otherwise just fire the event so dashboards can update appropriately - this.databasesLastUpdated = new Date(); + this.databasesLastUpdated = undefined; this._onDatabasesUpdated.fire(this._databases); } @@ -120,9 +114,9 @@ export class MiaaModel extends ResourceModel { } } - private async getDatabases(): Promise { + public async getDatabases(promptForConnection: boolean = true): Promise { if (!this._connectionProfile) { - await this.getConnectionProfile(); + await this.getConnectionProfile(promptForConnection); } // We haven't connected yet so do so now and then store the ID for the active connection @@ -182,6 +176,7 @@ export class MiaaModel extends ResourceModel { protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { this._connectionProfile = connectionProfile; + this._activeConnectionId = connectionProfile.id; this.info.connectionId = connectionProfile.id; this._miaaInfo.userName = connectionProfile.userName; await this._treeDataProvider.saveControllers(); diff --git a/extensions/arc/src/models/resourceModel.ts b/extensions/arc/src/models/resourceModel.ts index adb6fc6b8f..c635041d55 100644 --- a/extensions/arc/src/models/resourceModel.ts +++ b/extensions/arc/src/models/resourceModel.ts @@ -35,7 +35,7 @@ export abstract class ResourceModel { * 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 { + protected async getConnectionProfile(promptForConnection: boolean = true): Promise { let connectionProfile: azdata.IConnectionProfile | undefined = this.createConnectionProfile(); // If we have the ID stored then try to retrieve the password from previous connections @@ -50,7 +50,11 @@ export abstract class ResourceModel { if (connectionProfile.userName) { const result = await azdata.connection.connect(connectionProfile, false, false); if (!result.connected) { - await this.promptForConnection(connectionProfile); + if (promptForConnection) { + await this.promptForConnection(connectionProfile); + } else { + throw new Error(result.errorMessage); + } } else { this.updateConnectionProfile(connectionProfile); } @@ -63,8 +67,13 @@ export abstract class ResourceModel { } if (!connectionProfile?.userName || !connectionProfile?.password) { - // Need to prompt user for password since we don't have one stored - await this.promptForConnection(connectionProfile); + if (promptForConnection) { + // Need to prompt user for password since we don't have one stored + await this.promptForConnection(connectionProfile); + } else { + throw new Error('Missing username/password for connection profile'); + } + } } diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts index 1cb8238b7c..2c2118db41 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts @@ -14,13 +14,13 @@ import { ControllerModel } from '../../../models/controllerModel'; import { MiaaModel } from '../../../models/miaaModel'; import { DashboardPage } from '../../components/dashboardPage'; import { ResourceType } from 'arc'; +import { UserCancelledError } from '../../../common/api'; export class MiaaDashboardOverviewPage extends DashboardPage { private _propertiesLoading!: azdata.LoadingComponent; private _kibanaLoading!: azdata.LoadingComponent; private _grafanaLoading!: azdata.LoadingComponent; - private _databasesTableLoading!: azdata.LoadingComponent; private _propertiesContainer!: azdata.PropertiesContainerComponent; private _kibanaLink!: azdata.HyperlinkComponent; @@ -29,6 +29,11 @@ export class MiaaDashboardOverviewPage extends DashboardPage { private _databasesMessage!: azdata.TextComponent; private _openInAzurePortalButton!: azdata.ButtonComponent; + private _databasesContainer!: azdata.DivContainer; + private _connectToServerLoading!: azdata.LoadingComponent; + private _connectToServerButton!: azdata.ButtonComponent; + private _databasesTableLoading!: azdata.LoadingComponent; + private readonly _azdataApi: azdataExt.IExtension; private readonly _azurecoreApi: azurecore.IExtension; @@ -84,6 +89,28 @@ export class MiaaDashboardOverviewPage extends DashboardPage { this._grafanaLink = this.modelView.modelBuilder.hyperlink().component(); this._grafanaLoading = this.modelView.modelBuilder.loadingComponent().component(); + this._databasesContainer = this.modelView.modelBuilder.divContainer().component(); + + const connectToServerText = this.modelView.modelBuilder.text().withProps({ + value: loc.miaaConnectionRequired + }).component(); + + this._connectToServerButton = this.modelView.modelBuilder.button().withProps({ + label: loc.connectToServer, + enabled: false, + CSSStyles: { 'max-width': '125px', 'margin-left': '40%' } + }).component(); + + const connectToServerContainer = this.modelView.modelBuilder.divContainer().component(); + + + connectToServerContainer.addItem(connectToServerText, { CSSStyles: { 'text-align': 'center', 'margin-top': '20px' } }); + connectToServerContainer.addItem(this._connectToServerButton); + + this._connectToServerLoading = this.modelView.modelBuilder.loadingComponent().withItem(connectToServerContainer).component(); + + this._databasesContainer.addItem(this._connectToServerLoading, { CSSStyles: { 'margin-top': '20px' } }); + this._databasesTableLoading = this.modelView.modelBuilder.loadingComponent().component(); this._databasesTable = this.modelView.modelBuilder.declarativeTable().withProperties({ width: '100%', @@ -180,7 +207,18 @@ export class MiaaDashboardOverviewPage extends DashboardPage { // Databases rootContainer.addItem(this.modelView.modelBuilder.text().withProperties({ value: loc.databases, CSSStyles: titleCSS }).component()); - rootContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } }); + this.disposables.push( + this._connectToServerButton!.onDidClick(async () => { + this._connectToServerButton!.enabled = false; + this._databasesTableLoading!.loading = true; + try { + await this.callGetDatabases(); + } catch { + this._connectToServerButton!.enabled = true; + } + }) + ); + rootContainer.addItem(this._databasesContainer); rootContainer.addItem(this._databasesMessage); this.initialized = true; @@ -283,6 +321,19 @@ export class MiaaDashboardOverviewPage extends DashboardPage { ).component(); } + private async callGetDatabases(): Promise { + try { + await this._miaaModel.getDatabases(); + } catch (error) { + if (error instanceof UserCancelledError) { + vscode.window.showWarningMessage(loc.miaaConnectionRequired); + } else { + vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this._miaaModel.info.name, error)); + } + throw error; + } + } + private handleRegistrationsUpdated(): void { const config = this._controllerModel.controllerConfig; if (this._openInAzurePortalButton) { @@ -301,6 +352,9 @@ export class MiaaDashboardOverviewPage extends DashboardPage { this._instanceProperties.externalEndpoint = this._miaaModel.config.status.externalEndpoint || loc.notConfigured; this._instanceProperties.vCores = this._miaaModel.config.spec.limits?.vcores?.toString() || ''; this._databasesMessage.value = !this._miaaModel.config.status.externalEndpoint ? loc.noExternalEndpoint : ''; + if (!this._miaaModel.config.status.externalEndpoint) { + this._databasesContainer.removeItem(this._connectToServerLoading); + } } this.refreshDisplayedProperties(); @@ -312,7 +366,20 @@ export class MiaaDashboardOverviewPage extends DashboardPage { this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin; this.refreshDisplayedProperties(); this._databasesTable.data = this._miaaModel.databases.map(d => [d.name, getDatabaseStateDisplayText(d.status)]); - this._databasesTableLoading.loading = !this._miaaModel.databasesLastUpdated; + this._databasesTableLoading.loading = false; + + if (this._miaaModel.databasesLastUpdated) { + // We successfully connected so now can remove the button and replace it with the actual databases table + this._databasesContainer.removeItem(this._connectToServerLoading); + this._databasesContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } }); + } else { + // If we don't have an endpoint then there's no point in showing the connect button - but the logic + // to display text informing the user of this is already handled by the handleMiaaConfigUpdated + if (this._miaaModel?.config?.status.externalEndpoint) { + this._connectToServerLoading.loading = false; + this._connectToServerButton.enabled = true; + } + } } private refreshDisplayedProperties(): void {