Some general cleanup/fixes for Arc dashboards (#11066)

* Some general cleanup/fixes for Arc dashboards

* more disposables

* refresh controller model for miaa dashboard too

* fixes
This commit is contained in:
Charles Gagnon
2020-06-24 09:06:28 -07:00
committed by GitHub
parent ca4ab55380
commit 2ba0de10df
20 changed files with 378 additions and 257 deletions

View File

@@ -16,19 +16,28 @@ A gui-based experience to deploy an Azure Arc data controller as well as resourc
### Management Dashboards ### 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 ## Usage Guide
### Deployment Wizards ### 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. * 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 * 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 * 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. * 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 ## 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. 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.

View File

@@ -8,6 +8,8 @@ import * as azurecore from '../../../azurecore/src/azurecore';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import { IconPathHelper, IconPath, ResourceType, Connectionmode } from '../constants'; 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. * Converts the resource type name into the localized Display Name for that type.
* @param resourceType The resource type name to convert * @param resourceType The resource type name to convert

View File

@@ -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 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 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 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 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 { export function numVCores(vCores: string | undefined): string {
@@ -130,15 +129,20 @@ export function numVCores(vCores: string | undefined): string {
return '-'; 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); } export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); }
// Errors // 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 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 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 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 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 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)); } export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); }

View File

@@ -10,6 +10,7 @@ import { parseEndpoint, parseInstanceName } from '../common/utils';
import { ResourceType } from '../constants'; import { ResourceType } from '../constants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import * as loc from '../localizedConstants';
export type ControllerInfo = { export type ControllerInfo = {
url: string, url: string,
@@ -88,6 +89,13 @@ export class ControllerModel {
this._endpoints = response.body; this._endpoints = response.body;
this.endpointsLastUpdated = new Date(); this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints); 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._tokenRouter.apiV1TokenPost().then(async response => {
this._namespace = response.body.namespace!; this._namespace = response.body.namespace!;
@@ -95,7 +103,14 @@ export class ControllerModel {
this._controllerRegistration = this._registrations.find(r => r.instanceType === ResourceType.dataControllers); this._controllerRegistration = this._registrations.find(r => r.instanceType === ResourceType.dataControllers);
this.registrationsLastUpdated = new Date(); this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations); 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;
}),
]); ]);
} }

View File

@@ -12,6 +12,8 @@ import { ResourceModel } from './resourceModel';
import { ResourceInfo, Registration } from './controllerModel'; import { ResourceInfo, Registration } from './controllerModel';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { Deferred } from '../common/promise'; import { Deferred } from '../common/promise';
import * as loc from '../localizedConstants';
import { UserCancelledError } from '../common/utils';
export type DatabaseModel = { name: string, status: string }; export type DatabaseModel = { name: string, status: string };
@@ -26,7 +28,7 @@ export class MiaaModel extends ResourceModel {
private _activeConnectionId: string | undefined = undefined; private _activeConnectionId: string | undefined = undefined;
private readonly _onPasswordUpdated = new vscode.EventEmitter<string>(); private readonly _onPasswordUpdated = new vscode.EventEmitter<string>();
private readonly _onStatusUpdated = new vscode.EventEmitter<HybridSqlNsNameGetResponse>(); private readonly _onStatusUpdated = new vscode.EventEmitter<HybridSqlNsNameGetResponse | undefined>();
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>(); private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
public onPasswordUpdated = this._onPasswordUpdated.event; public onPasswordUpdated = this._onPasswordUpdated.event;
public onStatusUpdated = this._onStatusUpdated.event; public onStatusUpdated = this._onStatusUpdated.event;
@@ -77,8 +79,16 @@ export class MiaaModel extends ResourceModel {
const instanceRefresh = this._sqlInstanceRouter.apiV1HybridSqlNsNameGet(this.info.namespace, this.info.name).then(response => { const instanceRefresh = this._sqlInstanceRouter.apiV1HybridSqlNsNameGet(this.info.namespace, this.info.name).then(response => {
this._status = response.body; this._status = response.body;
this._onStatusUpdated.fire(this._status); 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<any>[] = [instanceRefresh]; const promises: Thenable<any>[] = [instanceRefresh];
try {
await this.getConnectionProfile(); await this.getConnectionProfile();
if (this._connectionProfile) { if (this._connectionProfile) {
// We haven't connected yet so do so now and then store the ID for the active connection // We haven't connected yet so do so now and then store the ID for the active connection
@@ -106,7 +116,24 @@ export class MiaaModel extends ResourceModel {
}); });
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); await Promise.all(promises);
this._refreshPromise.resolve();
} catch (err) {
this._refreshPromise.reject(err);
throw err;
} finally { } finally {
this._refreshPromise = undefined; this._refreshPromise = undefined;
} }
@@ -133,7 +160,7 @@ export class MiaaModel extends ResourceModel {
connection = existingConnection; connection = existingConnection;
} else { } else {
// We need the password so prompt the user for it // We need the password so prompt the user for it
const connectionProfile = { const connectionProfile: azdata.IConnectionProfile = {
serverName: existingConnection.options['serverName'], serverName: existingConnection.options['serverName'],
databaseName: existingConnection.options['databaseName'], databaseName: existingConnection.options['databaseName'],
authenticationType: existingConnection.options['authenticationType'], authenticationType: existingConnection.options['authenticationType'],
@@ -162,7 +189,8 @@ export class MiaaModel extends ResourceModel {
} }
if (connection) { 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'], serverName: connection.options['serverName'] || connection.options['server'],
databaseName: connection.options['databaseName'] || connection.options['database'], databaseName: connection.options['databaseName'] || connection.options['database'],
authenticationType: connection.options['authenticationType'], authenticationType: connection.options['authenticationType'],
@@ -177,9 +205,15 @@ export class MiaaModel extends ResourceModel {
groupId: undefined, groupId: undefined,
options: connection.options options: connection.options
}; };
this.info.connectionId = connection.connectionId; this.updateConnectionProfile(profile);
await this._treeDataProvider.saveControllers(); } else {
throw new UserCancelledError();
}
} }
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
await this._treeDataProvider.saveControllers();
} }
} }

View File

@@ -4,12 +4,21 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { InitializingComponent } from './initializingComponent'; import { InitializingComponent } from './initializingComponent';
export abstract class DashboardPage extends InitializingComponent { export abstract class DashboardPage extends InitializingComponent {
protected disposables: vscode.Disposable[] = [];
constructor(protected modelView: azdata.ModelView) { constructor(protected modelView: azdata.ModelView) {
super(); super();
this.disposables.push(modelView.onClosed(() => {
// Clean up best we can
this.disposables.forEach(d => {
try { d.dispose(); } catch { }
});
}));
} }
public get tab(): azdata.DashboardTab { public get tab(): azdata.DashboardTab {

View File

@@ -15,6 +15,12 @@ export class ControllerDashboard extends Dashboard {
super(loc.arcControllerDashboard); super(loc.arcControllerDashboard);
} }
public async showDashboard(): Promise<void> {
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)[]> { protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel); const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel);
return [ return [

View File

@@ -145,15 +145,35 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
iconPath: IconPathHelper.add iconPath: IconPathHelper.add
}).component(); }).component();
this.disposables.push(
newInstance.onDidClick(async () => { newInstance.onDidClick(async () => {
await vscode.commands.executeCommand('azdata.resource.deploy'); await vscode.commands.executeCommand('azdata.resource.deploy');
}); }));
// Refresh
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
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<azdata.ButtonProperties>({ const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.openInAzurePortal, label: loc.openInAzurePortal,
iconPath: IconPathHelper.openInTab iconPath: IconPathHelper.openInTab
}).component(); }).component();
this.disposables.push(
openInAzurePortalButton.onDidClick(async () => { openInAzurePortalButton.onDidClick(async () => {
const r = this._controllerModel.controllerRegistration; const r = this._controllerModel.controllerRegistration;
if (r) { if (r) {
@@ -162,11 +182,12 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
} else { } else {
vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._controllerModel.namespace, 'controller')); vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._controllerModel.namespace, 'controller'));
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
[ [
{ component: newInstance, toolbarSeparatorAfter: true }, { component: newInstance },
{ component: refreshButton, toolbarSeparatorAfter: true },
{ component: openInAzurePortalButton } { component: openInAzurePortalButton }
] ]
).component(); ).component();

View File

@@ -18,11 +18,10 @@ export class MiaaConnectionStringsPage extends DashboardPage {
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView); 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._instanceRegistration = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name);
this.eventuallyRunOnInitialized(() => this.updateConnectionStrings()); this.eventuallyRunOnInitialized(() => this.updateConnectionStrings());
}); }));
this.refresh().catch(err => console.error(err));
} }
protected async refresh(): Promise<void> { protected async refresh(): Promise<void> {

View File

@@ -17,6 +17,13 @@ export class MiaaDashboard extends Dashboard {
super(loc.miaaDashboard); super(loc.miaaDashboard);
} }
public async showDashboard(): Promise<void> {
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)[]> { protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel); const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel); const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);

View File

@@ -40,18 +40,16 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView); super(modelView);
this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin; this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin;
this.disposables.push(
this._controllerModel.onRegistrationsUpdated((_: Registration[]) => { this._controllerModel.onRegistrationsUpdated((_: Registration[]) => {
this.eventuallyRunOnInitialized(() => { this.eventuallyRunOnInitialized(() => {
this.handleRegistrationsUpdated().catch(e => console.log(e)); this.handleRegistrationsUpdated().catch(e => console.log(e));
}); });
}); }),
this._controllerModel.onEndpointsUpdated(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated(endpoints))); this._controllerModel.onEndpointsUpdated(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated(endpoints))),
this._miaaModel.onStatusUpdated(status => this.eventuallyRunOnInitialized(() => this.handleMiaaStatusUpdated(status))); this._miaaModel.onStatusUpdated(status => this.eventuallyRunOnInitialized(() => this.handleMiaaStatusUpdated(status))),
this._miaaModel.onDatabasesUpdated(databases => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated(databases))); this._miaaModel.onDatabasesUpdated(databases => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated(databases)))
);
this.refresh().catch(e => {
console.log(e);
});
} }
public get title(): string { public get title(): string {
@@ -171,6 +169,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
iconPath: IconPathHelper.delete iconPath: IconPathHelper.delete
}).component(); }).component();
this.disposables.push(
deleteButton.onDidClick(async () => { deleteButton.onDidClick(async () => {
deleteButton.enabled = false; deleteButton.enabled = false;
try { try {
@@ -183,13 +182,38 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
} finally { } finally {
deleteButton.enabled = true; deleteButton.enabled = true;
} }
}); }));
// Refresh
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
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<azdata.ButtonProperties>({ const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.openInAzurePortal, label: loc.openInAzurePortal,
iconPath: IconPathHelper.openInTab iconPath: IconPathHelper.openInTab
}).component(); }).component();
this.disposables.push(
openInAzurePortalButton.onDidClick(async () => { openInAzurePortalButton.onDidClick(async () => {
const r = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name); const r = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.info.namespace, this._miaaModel.info.name);
if (r) { if (r) {
@@ -198,11 +222,12 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
} else { } else {
vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._miaaModel.info.namespace, this._miaaModel.info.name)); vscode.window.showErrorMessage(loc.couldNotFindRegistration(this._miaaModel.info.namespace, this._miaaModel.info.name));
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
[ [
{ component: deleteButton, toolbarSeparatorAfter: true }, { component: deleteButton },
{ component: refreshButton, toolbarSeparatorAfter: true },
{ component: openInAzurePortalButton } { component: openInAzurePortalButton }
] ]
).component(); ).component();
@@ -221,8 +246,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
} }
} }
private async handleMiaaStatusUpdated(status: HybridSqlNsNameGetResponse): Promise<void> { private async handleMiaaStatusUpdated(status: HybridSqlNsNameGetResponse | undefined): Promise<void> {
this._instanceProperties.status = status.status || '-'; this._instanceProperties.status = status?.status || '-';
this.refreshDisplayedProperties(); this.refreshDisplayedProperties();
} }

View File

@@ -12,18 +12,11 @@ import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel'; import { PostgresModel } from '../../../models/postgresModel';
export class PostgresConnectionStringsPage extends DashboardPage { export class PostgresConnectionStringsPage extends DashboardPage {
private disposables: vscode.Disposable[] = [];
private keyValueContainer?: KeyValueContainer; private keyValueContainer?: KeyValueContainer;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView); super(modelView);
modelView.onClosed(() =>
this.disposables.forEach(d => {
try { d.dispose(); }
catch { }
}));
this.disposables.push(this._postgresModel.onServiceUpdated( this.disposables.push(this._postgresModel.onServiceUpdated(
() => this.eventuallyRunOnInitialized(() => this.refresh()))); () => this.eventuallyRunOnInitialized(() => this.refresh())));
} }
@@ -77,6 +70,7 @@ export class PostgresConnectionStringsPage extends DashboardPage {
iconPath: IconPathHelper.refresh iconPath: IconPathHelper.refresh
}).component(); }).component();
this.disposables.push(
refreshButton.onDidClick(async () => { refreshButton.onDidClick(async () => {
refreshButton.enabled = false; refreshButton.enabled = false;
try { try {
@@ -86,7 +80,7 @@ export class PostgresConnectionStringsPage extends DashboardPage {
} finally { } finally {
refreshButton.enabled = true; refreshButton.enabled = true;
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton } { component: refreshButton }

View File

@@ -21,6 +21,12 @@ export class PostgresDashboard extends Dashboard {
super(loc.postgresDashboard); super(loc.postgresDashboard);
} }
public async showDashboard(): Promise<void> {
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)[]> { protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);

View File

@@ -48,11 +48,12 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
width: '160px' width: '160px'
}).component(); }).component();
this.disposables.push(
troubleshootButton.onDidClick(() => { troubleshootButton.onDidClick(() => {
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace; process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace;
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name; process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name;
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
}); }));
content.addItem(troubleshootButton); content.addItem(troubleshootButton);
return root; return root;

View File

@@ -14,7 +14,6 @@ import { PostgresModel, PodRole } from '../../../models/postgresModel';
import { promptForResourceDeletion, promptAndConfirmPassword } from '../../../common/utils'; import { promptForResourceDeletion, promptAndConfirmPassword } from '../../../common/utils';
export class PostgresOverviewPage extends DashboardPage { export class PostgresOverviewPage extends DashboardPage {
private disposables: vscode.Disposable[] = [];
private propertiesLoading?: azdata.LoadingComponent; private propertiesLoading?: azdata.LoadingComponent;
private kibanaLoading?: azdata.LoadingComponent; private kibanaLoading?: azdata.LoadingComponent;
@@ -29,25 +28,23 @@ export class PostgresOverviewPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView); super(modelView);
modelView.onClosed(() => this.disposables.push(
this.disposables.forEach(d => { this._controllerModel.onEndpointsUpdated(
try { d.dispose(); }
catch { }
}));
this.disposables.push(this._controllerModel.onEndpointsUpdated(
() => this.eventuallyRunOnInitialized(() => this.refreshEndpoints()))); () => this.eventuallyRunOnInitialized(() => this.refreshEndpoints())));
this.disposables.push(this._controllerModel.onRegistrationsUpdated( this.disposables.push(
this._controllerModel.onRegistrationsUpdated(
() => this.eventuallyRunOnInitialized(() => this.refreshProperties()))); () => this.eventuallyRunOnInitialized(() => this.refreshProperties())));
this.disposables.push(this._postgresModel.onServiceUpdated( this.disposables.push(
this._postgresModel.onServiceUpdated(
() => this.eventuallyRunOnInitialized(() => { () => this.eventuallyRunOnInitialized(() => {
this.refreshProperties(); this.refreshProperties();
this.refreshNodes(); this.refreshNodes();
}))); })));
this.disposables.push(this._postgresModel.onPodsUpdated( this.disposables.push(
this._postgresModel.onPodsUpdated(
() => this.eventuallyRunOnInitialized(() => { () => this.eventuallyRunOnInitialized(() => {
this.refreshProperties(); this.refreshProperties();
this.refreshNodes(); this.refreshNodes();
@@ -186,6 +183,7 @@ export class PostgresOverviewPage extends DashboardPage {
iconPath: IconPathHelper.add iconPath: IconPathHelper.add
}).component(); }).component();
this.disposables.push(
newDatabaseButton.onDidClick(async () => { newDatabaseButton.onDidClick(async () => {
newDatabaseButton.enabled = false; newDatabaseButton.enabled = false;
let name; let name;
@@ -201,7 +199,7 @@ export class PostgresOverviewPage extends DashboardPage {
} finally { } finally {
newDatabaseButton.enabled = true; newDatabaseButton.enabled = true;
} }
}); }));
// Reset password // Reset password
const resetPasswordButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ const resetPasswordButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
@@ -209,6 +207,7 @@ export class PostgresOverviewPage extends DashboardPage {
iconPath: IconPathHelper.edit iconPath: IconPathHelper.edit
}).component(); }).component();
this.disposables.push(
resetPasswordButton.onDidClick(async () => { resetPasswordButton.onDidClick(async () => {
resetPasswordButton.enabled = false; resetPasswordButton.enabled = false;
try { try {
@@ -225,7 +224,7 @@ export class PostgresOverviewPage extends DashboardPage {
} finally { } finally {
resetPasswordButton.enabled = true; resetPasswordButton.enabled = true;
} }
}); }));
// Delete service // Delete service
const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
@@ -233,6 +232,7 @@ export class PostgresOverviewPage extends DashboardPage {
iconPath: IconPathHelper.delete iconPath: IconPathHelper.delete
}).component(); }).component();
this.disposables.push(
deleteButton.onDidClick(async () => { deleteButton.onDidClick(async () => {
deleteButton.enabled = false; deleteButton.enabled = false;
try { try {
@@ -245,7 +245,7 @@ export class PostgresOverviewPage extends DashboardPage {
} finally { } finally {
deleteButton.enabled = true; deleteButton.enabled = true;
} }
}); }));
// Refresh // Refresh
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
@@ -253,6 +253,7 @@ export class PostgresOverviewPage extends DashboardPage {
iconPath: IconPathHelper.refresh iconPath: IconPathHelper.refresh
}).component(); }).component();
this.disposables.push(
refreshButton.onDidClick(async () => { refreshButton.onDidClick(async () => {
refreshButton.enabled = false; refreshButton.enabled = false;
try { try {
@@ -271,7 +272,7 @@ export class PostgresOverviewPage extends DashboardPage {
finally { finally {
refreshButton.enabled = true; refreshButton.enabled = true;
} }
}); }));
// Open in Azure portal // Open in Azure portal
const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
@@ -279,6 +280,7 @@ export class PostgresOverviewPage extends DashboardPage {
iconPath: IconPathHelper.openInTab iconPath: IconPathHelper.openInTab
}).component(); }).component();
this.disposables.push(
openInAzurePortalButton.onDidClick(async () => { openInAzurePortalButton.onDidClick(async () => {
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
if (!r) { if (!r) {
@@ -287,7 +289,7 @@ export class PostgresOverviewPage extends DashboardPage {
vscode.env.openExternal(vscode.Uri.parse( vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`));
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: newDatabaseButton }, { component: newDatabaseButton },

View File

@@ -13,18 +13,11 @@ import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel'; import { PostgresModel } from '../../../models/postgresModel';
export class PostgresPropertiesPage extends DashboardPage { export class PostgresPropertiesPage extends DashboardPage {
private disposables: vscode.Disposable[] = [];
private keyValueContainer?: KeyValueContainer; private keyValueContainer?: KeyValueContainer;
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView); super(modelView);
modelView.onClosed(() =>
this.disposables.forEach(d => {
try { d.dispose(); }
catch { }
}));
this.disposables.push(this._postgresModel.onServiceUpdated( this.disposables.push(this._postgresModel.onServiceUpdated(
() => this.eventuallyRunOnInitialized(() => this.refresh()))); () => this.eventuallyRunOnInitialized(() => this.refresh())));
@@ -66,6 +59,7 @@ export class PostgresPropertiesPage extends DashboardPage {
iconPath: IconPathHelper.refresh iconPath: IconPathHelper.refresh
}).component(); }).component();
this.disposables.push(
refreshButton.onDidClick(async () => { refreshButton.onDidClick(async () => {
refreshButton.enabled = false; refreshButton.enabled = false;
try { try {
@@ -79,7 +73,7 @@ export class PostgresPropertiesPage extends DashboardPage {
finally { finally {
refreshButton.enabled = true; refreshButton.enabled = true;
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton } { component: refreshButton }

View File

@@ -12,7 +12,6 @@ import { PostgresModel } from '../../../models/postgresModel';
import { fromNow } from '../../../common/date'; import { fromNow } from '../../../common/date';
export class PostgresResourceHealthPage extends DashboardPage { export class PostgresResourceHealthPage extends DashboardPage {
private disposables: vscode.Disposable[] = [];
private interval: NodeJS.Timeout; private interval: NodeJS.Timeout;
private podsUpdated?: azdata.TextComponent; private podsUpdated?: azdata.TextComponent;
private podsTable?: azdata.DeclarativeTableComponent; private podsTable?: azdata.DeclarativeTableComponent;
@@ -21,15 +20,11 @@ export class PostgresResourceHealthPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView); super(modelView);
this.disposables.push(
modelView.onClosed(() => { modelView.onClosed(() => {
try { clearInterval(this.interval); } try { clearInterval(this.interval); }
catch { } catch { }
}));
this.disposables.forEach(d => {
try { d.dispose(); }
catch { }
});
});
this.disposables.push(this._postgresModel.onServiceUpdated( this.disposables.push(this._postgresModel.onServiceUpdated(
() => this.eventuallyRunOnInitialized(() => this.refresh()))); () => this.eventuallyRunOnInitialized(() => this.refresh())));
@@ -144,6 +139,7 @@ export class PostgresResourceHealthPage extends DashboardPage {
iconPath: IconPathHelper.refresh iconPath: IconPathHelper.refresh
}).component(); }).component();
this.disposables.push(
refreshButton.onDidClick(async () => { refreshButton.onDidClick(async () => {
refreshButton.enabled = false; refreshButton.enabled = false;
try { try {
@@ -153,7 +149,7 @@ export class PostgresResourceHealthPage extends DashboardPage {
} finally { } finally {
refreshButton.enabled = true; refreshButton.enabled = true;
} }
}); }));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton } { component: refreshButton }

View File

@@ -49,6 +49,7 @@ export class PostgresSupportRequestPage extends DashboardPage {
width: '205px' width: '205px'
}).component(); }).component();
this.disposables.push(
supportRequestButton.onDidClick(() => { supportRequestButton.onDidClick(() => {
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
if (!r) { if (!r) {
@@ -57,7 +58,7 @@ export class PostgresSupportRequestPage extends DashboardPage {
vscode.env.openExternal(vscode.Uri.parse( 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`)); `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`));
} }
}); }));
content.addItem(supportRequestButton); content.addItem(supportRequestButton);
return root; return root;

View File

@@ -21,8 +21,6 @@ export class MiaaTreeNode extends TreeNode {
public async openDashboard(): Promise<void> { public async openDashboard(): Promise<void> {
const miaaDashboard = new MiaaDashboard(this._controllerModel, this.model); const miaaDashboard = new MiaaDashboard(this._controllerModel, this.model);
await Promise.all([ await miaaDashboard.showDashboard();
miaaDashboard.showDashboard(),
this.model.refresh()]);
} }
} }

View File

@@ -21,8 +21,6 @@ export class PostgresTreeNode extends ResourceTreeNode {
public async openDashboard(): Promise<void> { public async openDashboard(): Promise<void> {
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model); const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
await Promise.all([ await postgresDashboard.showDashboard();
postgresDashboard.showDashboard(),
this._model.refresh()]);
} }
} }