diff --git a/extensions/arc/README.md b/extensions/arc/README.md index 6d7ad772e2..eea2df9518 100644 --- a/extensions/arc/README.md +++ b/extensions/arc/README.md @@ -16,19 +16,28 @@ A gui-based experience to deploy an Azure Arc data controller as well as resourc ### Management Dashboards -After connecting to an existing Azure Arc data controller in the *Azure Arc Controllers* view of the *Connections* viewlet a list of the active resources registered to the controller is shown, which allow launching a dashboard for further management capabilities. +After connecting to an existing Azure Arc data controller in the **Azure Arc Controllers** view of the **Connections** viewlet a list of the active resources registered to the controller is shown, which allow launching a dashboard for further management capabilities. ## Usage Guide ### Deployment Wizards -* After installing this extension click on '...' to the right of the Connections section in the left Panel and click on 'New Deployment...'. +* After installing this extension open the **Connections** viewlet +* Click on '...' in the top right corner of the viewlet panel and click on 'New Deployment...'. * This opens a dialog box that shows several deployment tiles. This extension adds tiles for the supported resources listed above. * Click on that tile, accept any license agreements, and choose the appropriate 'Resource Type' to install * A required tools check will run, if any required tools are missing then instructions will be given for installing those tools -* Once the check has passed successfully then click the *Select* button +* Once the check has passed successfully then click the **Select** button * This opens up a new dialog where you can enter input parameters specific to the deployment selected and then open a notebook that does the actual deployment. -* + +### Controller View/Management Dashboards + +* The **Azure Arc Controllers** view can be found in the **Connections** viewlet +* You can create a new controller by clicking the + button in the view title bar. This will launch the deployment wizard detailed above +* You can connect to an existing controller by clicking the plug button in the view title bar or the **Connect Controller** button in the view area when no controllers are registered +* You will then be prompted for the connection information to the controller, if it's successful then a node will be added to the tree for that controller +* Right click on one of the nodes to launch a dashboard for that resource - either the Data Controller or an instance deployed on it + ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index 3478d133e4..97d6b96fa8 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -8,6 +8,8 @@ import * as azurecore from '../../../azurecore/src/azurecore'; import * as loc from '../localizedConstants'; import { IconPathHelper, IconPath, ResourceType, Connectionmode } from '../constants'; +export class UserCancelledError extends Error { } + /** * Converts the resource type name into the localized Display Name for that type. * @param resourceType The resource type name to convert diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 4d92fff6e8..45bba03021 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -120,7 +120,6 @@ export const lastUpdated = localize('arc.lastUpdated', "Last updated"); export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", 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); } export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); } export function numVCores(vCores: string | undefined): string { @@ -130,15 +129,20 @@ export function numVCores(vCores: string | undefined): string { return '-'; } } -export function couldNotFindRegistration(namespace: string, name: string) { return localize('arc.couldNotFindRegistration', "Could not find controller registration for {0} ({1})", name, namespace); } -export function resourceDeletionWarning(namespace: string, name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}.{1}' type the name '{1}' below to proceed.", namespace, name); } -export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); } export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); } // Errors +export const connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information"); 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 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 fetchStatusFailed(name: string, error: any): string { return localize('arc.fetchStatusFailed', "An unexpected error occured retrieving the status for resource '{0}'. {1}", name, getErrorMessage(error)); } +export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occured retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); } +export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occured retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); } +export function couldNotFindRegistration(namespace: string, name: string) { return localize('arc.couldNotFindRegistration', "Could not find controller registration for {0} ({1})", name, namespace); } +export function resourceDeletionWarning(namespace: string, name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}.{1}' type the name '{1}' below to proceed.", namespace, name); } +export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "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); } export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); } diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index 9e540e5f65..0c6bfb341c 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -10,6 +10,7 @@ import { parseEndpoint, parseInstanceName } from '../common/utils'; import { ResourceType } from '../constants'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; +import * as loc from '../localizedConstants'; export type ControllerInfo = { url: string, @@ -88,6 +89,13 @@ export class ControllerModel { this._endpoints = response.body; this.endpointsLastUpdated = new Date(); this._onEndpointsUpdated.fire(this._endpoints); + }).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) + vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.url, err)); + this._onEndpointsUpdated.fire(this._endpoints); + throw err; }), this._tokenRouter.apiV1TokenPost().then(async response => { this._namespace = response.body.namespace!; @@ -95,7 +103,14 @@ export class ControllerModel { this._controllerRegistration = this._registrations.find(r => r.instanceType === ResourceType.dataControllers); this.registrationsLastUpdated = new Date(); this._onRegistrationsUpdated.fire(this._registrations); - }) + }).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) + vscode.window.showErrorMessage(loc.fetchRegistrationsFailed(this.info.url, err)); + this._onRegistrationsUpdated.fire(this._registrations); + throw err; + }), ]); } diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index 5b85d37a24..7f8b179aed 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -12,6 +12,8 @@ import { ResourceModel } from './resourceModel'; import { ResourceInfo, Registration } from './controllerModel'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { Deferred } from '../common/promise'; +import * as loc from '../localizedConstants'; +import { UserCancelledError } from '../common/utils'; export type DatabaseModel = { name: string, status: string }; @@ -26,7 +28,7 @@ export class MiaaModel extends ResourceModel { private _activeConnectionId: string | undefined = undefined; private readonly _onPasswordUpdated = new vscode.EventEmitter(); - private readonly _onStatusUpdated = new vscode.EventEmitter(); + private readonly _onStatusUpdated = new vscode.EventEmitter(); private readonly _onDatabasesUpdated = new vscode.EventEmitter(); public onPasswordUpdated = this._onPasswordUpdated.event; public onStatusUpdated = this._onStatusUpdated.event; @@ -77,36 +79,61 @@ export class MiaaModel extends ResourceModel { const instanceRefresh = this._sqlInstanceRouter.apiV1HybridSqlNsNameGet(this.info.namespace, this.info.name).then(response => { this._status = response.body; this._onStatusUpdated.fire(this._status); + }).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) + vscode.window.showErrorMessage(loc.fetchStatusFailed(this.info.name, err)); + this._onStatusUpdated.fire(undefined); + throw err; }); const promises: Thenable[] = [instanceRefresh]; - 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); + try { + 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; } - this._activeConnectionId = result.connectionId; - } - const provider = azdata.dataprotocol.getProvider(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider); - const databasesRefresh = azdata.connection.getUriForConnection(this._activeConnectionId).then(ownerUri => { - provider.getDatabases(ownerUri).then(databases => { - 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._onDatabasesUpdated.fire(this._databases); + const provider = azdata.dataprotocol.getProvider(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider); + const databasesRefresh = azdata.connection.getUriForConnection(this._activeConnectionId).then(ownerUri => { + provider.getDatabases(ownerUri).then(databases => { + 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._onDatabasesUpdated.fire(this._databases); + }); }); - }); - promises.push(databasesRefresh); + promises.push(databasesRefresh); + } + } 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.showErrorMessage(loc.connectionRequired); + } else { + vscode.window.showErrorMessage(loc.fetchStatusFailed(this.info.name, err)); + } + this._onDatabasesUpdated.fire(this._databases); + throw err; } + await Promise.all(promises); + this._refreshPromise.resolve(); + } catch (err) { + this._refreshPromise.reject(err); + throw err; } finally { this._refreshPromise = undefined; } @@ -133,7 +160,7 @@ export class MiaaModel extends ResourceModel { connection = existingConnection; } else { // We need the password so prompt the user for it - const connectionProfile = { + const connectionProfile: azdata.IConnectionProfile = { serverName: existingConnection.options['serverName'], databaseName: existingConnection.options['databaseName'], authenticationType: existingConnection.options['authenticationType'], @@ -162,7 +189,8 @@ export class MiaaModel extends ResourceModel { } if (connection) { - this._connectionProfile = { + const profile = { + // The option name might be different here based on where it came from serverName: connection.options['serverName'] || connection.options['server'], databaseName: connection.options['databaseName'] || connection.options['database'], authenticationType: connection.options['authenticationType'], @@ -177,9 +205,15 @@ export class MiaaModel extends ResourceModel { groupId: undefined, options: connection.options }; - this.info.connectionId = connection.connectionId; - await this._treeDataProvider.saveControllers(); + this.updateConnectionProfile(profile); + } else { + throw new UserCancelledError(); } + } + private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { + this._connectionProfile = connectionProfile; + this.info.connectionId = connectionProfile.id; + await this._treeDataProvider.saveControllers(); } } diff --git a/extensions/arc/src/ui/components/dashboardPage.ts b/extensions/arc/src/ui/components/dashboardPage.ts index 64a58fd9ce..c335166953 100644 --- a/extensions/arc/src/ui/components/dashboardPage.ts +++ b/extensions/arc/src/ui/components/dashboardPage.ts @@ -4,12 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import { InitializingComponent } from './initializingComponent'; export abstract class DashboardPage extends InitializingComponent { + protected disposables: vscode.Disposable[] = []; + constructor(protected modelView: azdata.ModelView) { super(); + this.disposables.push(modelView.onClosed(() => { + // Clean up best we can + this.disposables.forEach(d => { + try { d.dispose(); } catch { } + }); + })); } public get tab(): azdata.DashboardTab { diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboard.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboard.ts index 76c1eb2c10..bed32b3ed2 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboard.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboard.ts @@ -15,6 +15,12 @@ export class ControllerDashboard extends Dashboard { super(loc.arcControllerDashboard); } + public async showDashboard(): Promise { + await super.showDashboard(); + // Kick off the model refresh but don't wait on it since that's all handled with callbacks anyways + this._controllerModel.refresh().catch(err => console.log(`Error refreshing Controller dashboard ${err}`)); + } + protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel); return [ diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts index 9b3ae16d85..4ff1c1a0c4 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts @@ -145,28 +145,49 @@ export class ControllerDashboardOverviewPage extends DashboardPage { iconPath: IconPathHelper.add }).component(); - newInstance.onDidClick(async () => { - await vscode.commands.executeCommand('azdata.resource.deploy'); - }); + this.disposables.push( + newInstance.onDidClick(async () => { + await vscode.commands.executeCommand('azdata.resource.deploy'); + })); + + // Refresh + const refreshButton = this.modelView.modelBuilder.button().withProperties({ + label: loc.refresh, + iconPath: IconPathHelper.refresh + }).component(); + + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + this._propertiesLoadingComponent!.loading = true; + this._arcResourcesLoadingComponent!.loading = true; + await this._controllerModel.refresh(); + } finally { + refreshButton.enabled = true; + } + })); const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties({ label: loc.openInAzurePortal, iconPath: IconPathHelper.openInTab }).component(); - openInAzurePortalButton.onDidClick(async () => { - const r = this._controllerModel.controllerRegistration; - if (r) { - vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${r.instanceName}`)); - } else { - vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._controllerModel.namespace, 'controller')); - } - }); + this.disposables.push( + openInAzurePortalButton.onDidClick(async () => { + const r = this._controllerModel.controllerRegistration; + if (r) { + vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${r.instanceName}`)); + } else { + vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._controllerModel.namespace, 'controller')); + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( [ - { component: newInstance, toolbarSeparatorAfter: true }, + { component: newInstance }, + { component: refreshButton, toolbarSeparatorAfter: true }, { component: openInAzurePortalButton } ] ).component(); diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts index c7b6baa992..bb69b2572f 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts @@ -18,11 +18,10 @@ export class MiaaConnectionStringsPage extends DashboardPage { constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { super(modelView); - this._controllerModel.onRegistrationsUpdated(_ => { + this.disposables.push(this._controllerModel.onRegistrationsUpdated(_ => { this._instanceRegistration = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name); this.eventuallyRunOnInitialized(() => this.updateConnectionStrings()); - }); - this.refresh().catch(err => console.error(err)); + })); } protected async refresh(): Promise { diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts index 9a1a024224..a85d84ade5 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts @@ -17,6 +17,13 @@ export class MiaaDashboard extends Dashboard { super(loc.miaaDashboard); } + public async showDashboard(): Promise { + await super.showDashboard(); + // Kick off the model refreshes but don't wait on it since that's all handled with callbacks anyways + this._controllerModel.refresh().catch(err => console.log(`Error refreshing controller model for MIAA dashboard ${err}`)); + this._miaaModel.refresh().catch(err => console.log(`Error refreshing MIAA model for MIAA dashboard ${err}`)); + } + protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel); const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel); diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts index 30163d7520..54427da4c5 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts @@ -40,18 +40,16 @@ export class MiaaDashboardOverviewPage extends DashboardPage { constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { super(modelView); this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin; - this._controllerModel.onRegistrationsUpdated((_: Registration[]) => { - this.eventuallyRunOnInitialized(() => { - this.handleRegistrationsUpdated().catch(e => console.log(e)); - }); - }); - this._controllerModel.onEndpointsUpdated(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated(endpoints))); - this._miaaModel.onStatusUpdated(status => this.eventuallyRunOnInitialized(() => this.handleMiaaStatusUpdated(status))); - this._miaaModel.onDatabasesUpdated(databases => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated(databases))); - - this.refresh().catch(e => { - console.log(e); - }); + this.disposables.push( + this._controllerModel.onRegistrationsUpdated((_: Registration[]) => { + this.eventuallyRunOnInitialized(() => { + this.handleRegistrationsUpdated().catch(e => console.log(e)); + }); + }), + this._controllerModel.onEndpointsUpdated(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated(endpoints))), + this._miaaModel.onStatusUpdated(status => this.eventuallyRunOnInitialized(() => this.handleMiaaStatusUpdated(status))), + this._miaaModel.onDatabasesUpdated(databases => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated(databases))) + ); } public get title(): string { @@ -171,38 +169,65 @@ export class MiaaDashboardOverviewPage extends DashboardPage { iconPath: IconPathHelper.delete }).component(); - deleteButton.onDidClick(async () => { - deleteButton.enabled = false; - try { - if (await promptForResourceDeletion(this._miaaModel.info.namespace, this._miaaModel.info.name)) { - await this._controllerModel.miaaDelete(this._miaaModel.info.namespace, this._miaaModel.info.name); - vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name)); + this.disposables.push( + deleteButton.onDidClick(async () => { + deleteButton.enabled = false; + try { + if (await promptForResourceDeletion(this._miaaModel.info.namespace, this._miaaModel.info.name)) { + await this._controllerModel.miaaDelete(this._miaaModel.info.namespace, this._miaaModel.info.name); + vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name)); + } + } catch (error) { + vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error)); + } finally { + deleteButton.enabled = true; } - } catch (error) { - vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error)); - } finally { - deleteButton.enabled = true; - } - }); + })); + + // Refresh + const refreshButton = this.modelView.modelBuilder.button().withProperties({ + label: loc.refresh, + iconPath: IconPathHelper.refresh + }).component(); + + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + this._propertiesLoading!.loading = true; + this._kibanaLoading!.loading = true; + this._grafanaLoading!.loading = true; + this._databasesTableLoading!.loading = true; + + await Promise.all([ + this._miaaModel.refresh(), + this._controllerModel.refresh() + ]); + } finally { + refreshButton.enabled = true; + } + })); const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties({ label: loc.openInAzurePortal, iconPath: IconPathHelper.openInTab }).component(); - openInAzurePortalButton.onDidClick(async () => { - const r = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name); - if (r) { - vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.sqlManagedInstances}/${r.instanceName}`)); - } else { - vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._miaaModel.info.namespace, this._miaaModel.info.name)); - } - }); + this.disposables.push( + openInAzurePortalButton.onDidClick(async () => { + const r = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name); + if (r) { + vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.sqlManagedInstances}/${r.instanceName}`)); + } else { + vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._miaaModel.info.namespace, this._miaaModel.info.name)); + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( [ - { component: deleteButton, toolbarSeparatorAfter: true }, + { component: deleteButton }, + { component: refreshButton, toolbarSeparatorAfter: true }, { component: openInAzurePortalButton } ] ).component(); @@ -221,8 +246,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage { } } - private async handleMiaaStatusUpdated(status: HybridSqlNsNameGetResponse): Promise { - this._instanceProperties.status = status.status || '-'; + private async handleMiaaStatusUpdated(status: HybridSqlNsNameGetResponse | undefined): Promise { + this._instanceProperties.status = status?.status || '-'; this.refreshDisplayedProperties(); } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 9638835a92..f16e0754e8 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -12,18 +12,11 @@ import { DashboardPage } from '../../components/dashboardPage'; import { PostgresModel } from '../../../models/postgresModel'; export class PostgresConnectionStringsPage extends DashboardPage { - private disposables: vscode.Disposable[] = []; private keyValueContainer?: KeyValueContainer; constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); - modelView.onClosed(() => - this.disposables.forEach(d => { - try { d.dispose(); } - catch { } - })); - this.disposables.push(this._postgresModel.onServiceUpdated( () => this.eventuallyRunOnInitialized(() => this.refresh()))); } @@ -77,16 +70,17 @@ export class PostgresConnectionStringsPage extends DashboardPage { iconPath: IconPathHelper.refresh }).component(); - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - await this._postgresModel.refresh(); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } finally { - refreshButton.enabled = true; - } - }); + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + await this._postgresModel.refresh(); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } finally { + refreshButton.enabled = true; + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: refreshButton } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index 738aca22cc..756da5722c 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -21,6 +21,12 @@ export class PostgresDashboard extends Dashboard { super(loc.postgresDashboard); } + public async showDashboard(): Promise { + await super.showDashboard(); + // Kick off the model refresh but don't wait on it since that's all handled with callbacks anyways + this._postgresModel.refresh().catch(err => console.log(`Error refreshing Postgres dashboard ${err}`)); + } + protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index 8137f9cc0d..bb884d3b2b 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -48,11 +48,12 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { width: '160px' }).component(); - 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/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); - }); + this.disposables.push( + 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/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); + })); content.addItem(troubleshootButton); return root; diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index b6cdacce34..0ff05ff3ff 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -14,7 +14,6 @@ import { PostgresModel, PodRole } from '../../../models/postgresModel'; import { promptForResourceDeletion, promptAndConfirmPassword } from '../../../common/utils'; export class PostgresOverviewPage extends DashboardPage { - private disposables: vscode.Disposable[] = []; private propertiesLoading?: azdata.LoadingComponent; private kibanaLoading?: azdata.LoadingComponent; @@ -29,29 +28,27 @@ export class PostgresOverviewPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - modelView.onClosed(() => - this.disposables.forEach(d => { - try { d.dispose(); } - catch { } - })); + this.disposables.push( + this._controllerModel.onEndpointsUpdated( + () => this.eventuallyRunOnInitialized(() => this.refreshEndpoints()))); - this.disposables.push(this._controllerModel.onEndpointsUpdated( - () => this.eventuallyRunOnInitialized(() => this.refreshEndpoints()))); + this.disposables.push( + this._controllerModel.onRegistrationsUpdated( + () => this.eventuallyRunOnInitialized(() => this.refreshProperties()))); - this.disposables.push(this._controllerModel.onRegistrationsUpdated( - () => this.eventuallyRunOnInitialized(() => this.refreshProperties()))); + this.disposables.push( + this._postgresModel.onServiceUpdated( + () => this.eventuallyRunOnInitialized(() => { + this.refreshProperties(); + this.refreshNodes(); + }))); - this.disposables.push(this._postgresModel.onServiceUpdated( - () => this.eventuallyRunOnInitialized(() => { - this.refreshProperties(); - this.refreshNodes(); - }))); - - this.disposables.push(this._postgresModel.onPodsUpdated( - () => this.eventuallyRunOnInitialized(() => { - this.refreshProperties(); - this.refreshNodes(); - }))); + this.disposables.push( + this._postgresModel.onPodsUpdated( + () => this.eventuallyRunOnInitialized(() => { + this.refreshProperties(); + this.refreshNodes(); + }))); } protected get title(): string { @@ -186,22 +183,23 @@ export class PostgresOverviewPage extends DashboardPage { iconPath: IconPathHelper.add }).component(); - newDatabaseButton.onDidClick(async () => { - newDatabaseButton.enabled = false; - let name; - try { - name = await vscode.window.showInputBox({ prompt: loc.databaseName }); - 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 ?? '')); + this.disposables.push( + newDatabaseButton.onDidClick(async () => { + newDatabaseButton.enabled = false; + let name; + try { + name = await vscode.window.showInputBox({ prompt: loc.databaseName }); + 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 { + newDatabaseButton.enabled = true; } - } catch (error) { - vscode.window.showErrorMessage(loc.databaseCreationFailed(name ?? '', error)); - } finally { - newDatabaseButton.enabled = true; - } - }); + })); // Reset password const resetPasswordButton = this.modelView.modelBuilder.button().withProperties({ @@ -209,23 +207,24 @@ export class PostgresOverviewPage extends DashboardPage { iconPath: IconPathHelper.edit }).component(); - resetPasswordButton.onDidClick(async () => { - resetPasswordButton.enabled = false; - try { - 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); + this.disposables.push( + resetPasswordButton.onDidClick(async () => { + resetPasswordButton.enabled = false; + try { + 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(error)); + } finally { + resetPasswordButton.enabled = true; } - } catch (error) { - vscode.window.showErrorMessage(loc.passwordResetFailed(error)); - } finally { - resetPasswordButton.enabled = true; - } - }); + })); // Delete service const deleteButton = this.modelView.modelBuilder.button().withProperties({ @@ -233,19 +232,20 @@ export class PostgresOverviewPage extends DashboardPage { iconPath: IconPathHelper.delete }).component(); - deleteButton.onDidClick(async () => { - deleteButton.enabled = false; - try { - if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) { - await this._postgresModel.delete(); - vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName)); + this.disposables.push( + deleteButton.onDidClick(async () => { + deleteButton.enabled = false; + try { + if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) { + await this._postgresModel.delete(); + vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName)); + } + } catch (error) { + vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error)); + } finally { + deleteButton.enabled = true; } - } catch (error) { - vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error)); - } finally { - deleteButton.enabled = true; - } - }); + })); // Refresh const refreshButton = this.modelView.modelBuilder.button().withProperties({ @@ -253,25 +253,26 @@ export class PostgresOverviewPage extends DashboardPage { iconPath: IconPathHelper.refresh }).component(); - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - this.propertiesLoading!.loading = true; - this.kibanaLoading!.loading = true; - this.grafanaLoading!.loading = true; - this.nodesTableLoading!.loading = true; + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + this.propertiesLoading!.loading = true; + this.kibanaLoading!.loading = true; + this.grafanaLoading!.loading = true; + this.nodesTableLoading!.loading = true; - await Promise.all([ - this._postgresModel.refresh(), - this._controllerModel.refresh() - ]); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } - finally { - refreshButton.enabled = true; - } - }); + await Promise.all([ + this._postgresModel.refresh(), + this._controllerModel.refresh() + ]); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } + finally { + refreshButton.enabled = true; + } + })); // Open in Azure portal const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties({ @@ -279,15 +280,16 @@ export class PostgresOverviewPage extends DashboardPage { iconPath: IconPathHelper.openInTab }).component(); - openInAzurePortalButton.onDidClick(async () => { - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); - } else { - vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); - } - }); + this.disposables.push( + openInAzurePortalButton.onDidClick(async () => { + const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); + if (!r) { + vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); + } else { + vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: newDatabaseButton }, diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index dd24f3866c..9bde9492b1 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -13,18 +13,11 @@ import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; export class PostgresPropertiesPage extends DashboardPage { - private disposables: vscode.Disposable[] = []; private keyValueContainer?: KeyValueContainer; constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - modelView.onClosed(() => - this.disposables.forEach(d => { - try { d.dispose(); } - catch { } - })); - this.disposables.push(this._postgresModel.onServiceUpdated( () => this.eventuallyRunOnInitialized(() => this.refresh()))); @@ -66,20 +59,21 @@ export class PostgresPropertiesPage extends DashboardPage { iconPath: IconPathHelper.refresh }).component(); - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - await Promise.all([ - this._postgresModel.refresh(), - this._controllerModel.refresh() - ]); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } - finally { - refreshButton.enabled = true; - } - }); + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + await Promise.all([ + this._postgresModel.refresh(), + this._controllerModel.refresh() + ]); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } + finally { + refreshButton.enabled = true; + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: refreshButton } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts index e828af562b..3b68be566e 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts @@ -12,7 +12,6 @@ import { PostgresModel } from '../../../models/postgresModel'; import { fromNow } from '../../../common/date'; export class PostgresResourceHealthPage extends DashboardPage { - private disposables: vscode.Disposable[] = []; private interval: NodeJS.Timeout; private podsUpdated?: azdata.TextComponent; private podsTable?: azdata.DeclarativeTableComponent; @@ -21,15 +20,11 @@ export class PostgresResourceHealthPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); - modelView.onClosed(() => { - try { clearInterval(this.interval); } - catch { } - - this.disposables.forEach(d => { - try { d.dispose(); } + this.disposables.push( + modelView.onClosed(() => { + try { clearInterval(this.interval); } catch { } - }); - }); + })); this.disposables.push(this._postgresModel.onServiceUpdated( () => this.eventuallyRunOnInitialized(() => this.refresh()))); @@ -144,16 +139,17 @@ export class PostgresResourceHealthPage extends DashboardPage { iconPath: IconPathHelper.refresh }).component(); - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - await this._postgresModel.refresh(); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } finally { - refreshButton.enabled = true; - } - }); + this.disposables.push( + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + await this._postgresModel.refresh(); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } finally { + refreshButton.enabled = true; + } + })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ { component: refreshButton } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts index e7b8e76270..61869a6baf 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts @@ -49,15 +49,16 @@ export class PostgresSupportRequestPage extends DashboardPage { width: '205px' }).component(); - supportRequestButton.onDidClick(() => { - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); - } else { - vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`)); - } - }); + this.disposables.push( + supportRequestButton.onDidClick(() => { + const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); + if (!r) { + vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); + } else { + vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`)); + } + })); content.addItem(supportRequestButton); return root; diff --git a/extensions/arc/src/ui/tree/miaaTreeNode.ts b/extensions/arc/src/ui/tree/miaaTreeNode.ts index 1c99974372..7fe90b87ad 100644 --- a/extensions/arc/src/ui/tree/miaaTreeNode.ts +++ b/extensions/arc/src/ui/tree/miaaTreeNode.ts @@ -21,8 +21,6 @@ export class MiaaTreeNode extends TreeNode { public async openDashboard(): Promise { const miaaDashboard = new MiaaDashboard(this._controllerModel, this.model); - await Promise.all([ - miaaDashboard.showDashboard(), - this.model.refresh()]); + await miaaDashboard.showDashboard(); } } diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts index 757c0da1d2..56851a955d 100644 --- a/extensions/arc/src/ui/tree/postgresTreeNode.ts +++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts @@ -21,8 +21,6 @@ export class PostgresTreeNode extends ResourceTreeNode { public async openDashboard(): Promise { const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model); - await Promise.all([ - postgresDashboard.showDashboard(), - this._model.refresh()]); + await postgresDashboard.showDashboard(); } }