diff --git a/extensions/arc/images/refresh.svg b/extensions/arc/images/refresh.svg
new file mode 100644
index 0000000000..f03579110b
--- /dev/null
+++ b/extensions/arc/images/refresh.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts
index 28d14c3123..e2b897135a 100644
--- a/extensions/arc/src/constants.ts
+++ b/extensions/arc/src/constants.ts
@@ -27,6 +27,7 @@ export class IconPathHelper {
public static backup: IconPath;
public static properties: IconPath;
public static networking: IconPath;
+ public static refresh: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.context = context;
@@ -86,6 +87,10 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/security.svg'),
dark: context.asAbsolutePath('images/security.svg')
};
+ IconPathHelper.refresh = {
+ light: context.asAbsolutePath('images/refresh.svg'),
+ dark: context.asAbsolutePath('images/refresh.svg')
+ };
}
}
diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts
index a7dfa96414..e2cfb6040b 100644
--- a/extensions/arc/src/extension.ts
+++ b/extensions/arc/src/extension.ts
@@ -23,10 +23,19 @@ export async function activate(context: vscode.ExtensionContext): Promise
const dbNamespace = '';
const dbName = '';
- const controllerModel = new ControllerModel(controllerUrl, auth);
- const databaseModel = new PostgresModel(controllerUrl, auth, dbNamespace, dbName);
- const postgresDashboard = new PostgresDashboard(loc.postgresDashboard, controllerModel, databaseModel);
- await postgresDashboard.showDashboard();
+ try {
+ const controllerModel = new ControllerModel(controllerUrl, auth);
+ const postgresModel = new PostgresModel(controllerUrl, auth, dbNamespace, dbName);
+ const postgresDashboard = new PostgresDashboard(loc.postgresDashboard, controllerModel, postgresModel);
+
+ await Promise.all([
+ postgresDashboard.showDashboard(),
+ controllerModel.refresh(),
+ postgresModel.refresh()
+ ]);
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.failedToManagePostgres(`${dbNamespace}.${dbName}`, error));
+ }
});
}
diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts
index 5d6bd7a71b..dce75b36c4 100644
--- a/extensions/arc/src/localizedConstants.ts
+++ b/extensions/arc/src/localizedConstants.ts
@@ -46,6 +46,7 @@ export const feedback = localize('arc.feedback', 'Feedback');
export const selectConnectionString = localize('arc.selectConnectionString', 'Select from available client connection strings below');
export const vCores = localize('arc.vCores', 'vCores');
export const ram = localize('arc.ram', 'RAM');
+export const refresh = localize('arc.refresh', 'Refresh');
// Postgres constants
export const coordinatorEndpoint = localize('arc.coordinatorEndpoint', 'Coordinator endpoint');
@@ -66,14 +67,16 @@ export const node = localize('arc.node', 'node');
export const nodes = localize('arc.nodes', 'nodes');
export const storagePerNode = localize('arc.storagePerNode', 'storage per node');
-export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database '{0}' created", name); }
-export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database '{0}'. {1}", name, (error instanceof Error ? error.message : error)); }
-export function passwordReset(name: string): string { return localize('arc.passwordReset', "Password reset for service '{0}'", name); }
-export function passwordResetFailed(name: string, error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password for service '{0}'. {1}", name, (error instanceof Error ? error.message : error)); }
-export function deleteServicePrompt(name: string): string { return localize('arc.deleteServicePrompt', "Delete service '{0}'?", name); }
-export function serviceDeleted(name: string): string { return localize('arc.serviceDeleted', "Service '{0}' deleted", name); }
-export function serviceDeletionFailed(name: string, error: any): string { return localize('arc.serviceDeletionFailed', "Failed to delete service '{0}'. {1}", name, (error instanceof Error ? error.message : error)); }
-export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for '{0}'", name); }
+export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
+export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, (error instanceof Error ? error.message : error)); }
+export function passwordReset(name: string): string { return localize('arc.passwordReset', "Password reset for service {0}", name); }
+export function passwordResetFailed(name: string, error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password for service {0}. {1}", name, (error instanceof Error ? error.message : error)); }
+export function deleteServicePrompt(name: string): string { return localize('arc.deleteServicePrompt', "Delete service {0}?", name); }
+export function serviceDeleted(name: string): string { return localize('arc.serviceDeleted', "Service {0} deleted", name); }
+export function serviceDeletionFailed(name: string, error: any): string { return localize('arc.serviceDeletionFailed', "Failed to delete service {0}. {1}", name, (error instanceof Error ? error.message : error)); }
+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 refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", (error instanceof Error ? error.message : error)); }
+export function failedToManagePostgres(name: string, error: any): string { return localize('arc.failedToManagePostgres', "Failed to manage Postgres {0}. {1}", name, (error instanceof Error ? error.message : error)); }
export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts
index 44143fcae3..a7822e27d1 100644
--- a/extensions/arc/src/models/controllerModel.ts
+++ b/extensions/arc/src/models/controllerModel.ts
@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import * as vscode from 'vscode';
import { Authentication } from '../controller/auth';
import { EndpointsRouterApi, EndpointModel, RegistrationRouterApi, RegistrationResponse, TokenRouterApi } from '../controller/generated/v1/api';
@@ -10,9 +11,16 @@ export class ControllerModel {
private _endpointsRouter: EndpointsRouterApi;
private _tokenRouter: TokenRouterApi;
private _registrationRouter: RegistrationRouterApi;
- private _endpoints!: EndpointModel[];
- private _namespace!: string;
- private _registrations!: RegistrationResponse[];
+ private _endpoints?: EndpointModel[];
+ private _namespace?: string;
+ private _registrations?: RegistrationResponse[];
+
+ private readonly _onEndpointsUpdated = new vscode.EventEmitter();
+ private readonly _onRegistrationsUpdated = new vscode.EventEmitter();
+ public onEndpointsUpdated = this._onEndpointsUpdated.event;
+ public onRegistrationsUpdated = this._onRegistrationsUpdated.event;
+ public endpointsLastUpdated?: Date;
+ public registrationsLastUpdated?: Date;
constructor(controllerUrl: string, auth: Authentication) {
this._endpointsRouter = new EndpointsRouterApi(controllerUrl);
@@ -29,33 +37,36 @@ export class ControllerModel {
await Promise.all([
this._endpointsRouter.apiV1BdcEndpointsGet().then(response => {
this._endpoints = response.body;
+ this.endpointsLastUpdated = new Date();
+ this._onEndpointsUpdated.fire(this._endpoints);
}),
this._tokenRouter.apiV1TokenPost().then(async response => {
this._namespace = response.body.namespace!;
+ this._registrations = (await this._registrationRouter.apiV1RegistrationListResourcesNsGet(this._namespace)).body;
+ this.registrationsLastUpdated = new Date();
+ this._onRegistrationsUpdated.fire(this._registrations);
})
- ]).then(async _ => {
- this._registrations = (await this._registrationRouter.apiV1RegistrationListResourcesNsGet(this._namespace)).body;
- });
+ ]);
}
- public endpoints(): EndpointModel[] {
+ public endpoints(): EndpointModel[] | undefined {
return this._endpoints;
}
public endpoint(name: string): EndpointModel | undefined {
- return this._endpoints.find(e => e.name === name);
+ return this._endpoints?.find(e => e.name === name);
}
- public namespace(): string {
+ public namespace(): string | undefined {
return this._namespace;
}
- public registrations(): RegistrationResponse[] {
+ public registrations(): RegistrationResponse[] | undefined {
return this._registrations;
}
public registration(type: string, namespace: string, name: string): RegistrationResponse | undefined {
- return this._registrations.find(r => {
+ return this._registrations?.find(r => {
// Resources deployed outside the controller's namespace are named in the format 'namespace_name'
let instanceName = r.instanceName!;
const parts: string[] = instanceName.split('_');
diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts
index 440531d9eb..a63407c5a8 100644
--- a/extensions/arc/src/models/postgresModel.ts
+++ b/extensions/arc/src/models/postgresModel.ts
@@ -3,14 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
import { DuskyObjectModelsDatabaseService, DatabaseRouterApi, DuskyObjectModelsDatabase, V1Status } from '../controller/generated/dusky/api';
import { Authentication } from '../controller/auth';
export class PostgresModel {
private _databaseRouter: DatabaseRouterApi;
- private _service!: DuskyObjectModelsDatabaseService;
- private _password!: string;
+ private _service?: DuskyObjectModelsDatabaseService;
+ private _password?: string;
+ private readonly _onServiceUpdated = new vscode.EventEmitter();
+ private readonly _onPasswordUpdated = new vscode.EventEmitter();
+ public onServiceUpdated = this._onServiceUpdated.event;
+ public onPasswordUpdated = this._onPasswordUpdated.event;
+ public serviceLastUpdated?: Date;
+ public passwordLastUpdated?: Date;
constructor(controllerUrl: string, auth: Authentication, private _namespace: string, private _name: string) {
this._databaseRouter = new DatabaseRouterApi(controllerUrl);
@@ -33,23 +40,27 @@ export class PostgresModel {
}
/** Returns the service's spec */
- public service(): DuskyObjectModelsDatabaseService {
+ public service(): DuskyObjectModelsDatabaseService | undefined {
return this._service;
}
/** Returns the service's password */
- public password(): string {
+ public password(): string | undefined {
return this._password;
}
- /** Refreshes the service's model */
+ /** Refreshes the model */
public async refresh() {
await Promise.all([
this._databaseRouter.getDuskyDatabaseService(this._namespace, this._name).then(response => {
this._service = response.body;
+ this.serviceLastUpdated = new Date();
+ this._onServiceUpdated.fire(this._service);
}),
this._databaseRouter.getDuskyPassword(this._namespace, this._name).then(async response => {
this._password = response.body;
+ this.passwordLastUpdated = new Date();
+ this._onPasswordUpdated.fire(this._password!);
})
]);
}
@@ -82,7 +93,7 @@ export class PostgresModel {
/** Returns the number of nodes in the service */
public numNodes(): number {
- let nodes = this._service.spec.scale?.shards ?? 1;
+ let nodes = this._service?.spec.scale?.shards ?? 1;
if (nodes > 1) { nodes++; } // for multiple shards there is an additional node for the coordinator
return nodes;
}
@@ -92,10 +103,10 @@ export class PostgresModel {
* internal IP. If either field is not available it will be set to undefined.
*/
public endpoint(): { ip?: string, port?: number } {
- const externalIp = this._service.status?.externalIP;
- const internalIp = this._service.status?.internalIP;
- const externalPort = this._service.status?.externalPort;
- const internalPort = this._service.status?.internalPort;
+ const externalIp = this._service?.status?.externalIP;
+ const internalIp = this._service?.status?.internalIP;
+ const externalPort = this._service?.status?.externalPort;
+ const internalPort = this._service?.status?.internalPort;
return externalIp ? { ip: externalIp, port: externalPort ?? undefined }
: internalIp ? { ip: internalIp, port: internalPort ?? undefined }
@@ -105,26 +116,22 @@ export class PostgresModel {
/** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */
public configuration(): string {
const nodes = this.numNodes();
- const cpuLimit = this._service.spec.scheduling?.resources?.limits?.['cpu'];
- const ramLimit = this._service.spec.scheduling?.resources?.limits?.['memory'];
- const cpuRequest = this._service.spec.scheduling?.resources?.requests?.['cpu'];
- const ramRequest = this._service.spec.scheduling?.resources?.requests?.['memory'];
- const storage = this._service.spec.storage.volumeSize;
+ const cpuLimit = this._service?.spec.scheduling?.resources?.limits?.['cpu'];
+ const ramLimit = this._service?.spec.scheduling?.resources?.limits?.['memory'];
+ const cpuRequest = this._service?.spec.scheduling?.resources?.requests?.['cpu'];
+ const ramRequest = this._service?.spec.scheduling?.resources?.requests?.['memory'];
+ const storage = this._service?.spec.storage.volumeSize;
// Prefer limits if they're provided, otherwise use requests if they're provided
- let nodeConfiguration = `${nodes} ${nodes > 1 ? loc.nodes : loc.node}`;
- if (cpuLimit) {
- nodeConfiguration += `, ${this.formatCores(cpuLimit)} ${loc.vCores}`;
- } else if (cpuRequest) {
- nodeConfiguration += `, ${this.formatCores(cpuRequest)} ${loc.vCores}`;
+ let configuration = `${nodes} ${nodes > 1 ? loc.nodes : loc.node}`;
+ if (cpuLimit || cpuRequest) {
+ configuration += `, ${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`;
}
- if (ramLimit) {
- nodeConfiguration += `, ${this.formatMemory(ramLimit)} ${loc.ram}`;
- } else if (ramRequest) {
- nodeConfiguration += `, ${this.formatMemory(ramRequest)} ${loc.ram}`;
+ if (ramLimit || ramRequest) {
+ configuration += `, ${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`;
}
- if (storage) { nodeConfiguration += `, ${storage} ${loc.storagePerNode}`; }
- return nodeConfiguration;
+ if (storage) { configuration += `, ${storage} ${loc.storagePerNode}`; }
+ return configuration;
}
/**
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts
index 966821035a..e0cccd65df 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts
@@ -6,9 +6,9 @@
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
-import { PostgresDashboardPage } from './postgresDashboardPage';
+import { DashboardPage } from '../../components/dashboardPage';
-export class PostgresBackupPage extends PostgresDashboardPage {
+export class PostgresBackupPage extends DashboardPage {
protected get title(): string {
return loc.backup;
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts
index 539c5dc14a..f01148cb3d 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts
@@ -6,9 +6,9 @@
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
-import { PostgresDashboardPage } from './postgresDashboardPage';
+import { DashboardPage } from '../../components/dashboardPage';
-export class PostgresComputeStoragePage extends PostgresDashboardPage {
+export class PostgresComputeStoragePage extends DashboardPage {
protected get title(): string {
return loc.computeAndStorage;
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
index b0694548e5..678bc6c2aa 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
@@ -3,13 +3,23 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
-import { PostgresDashboardPage } from './postgresDashboardPage';
-import { KeyValueContainer, KeyValue, InputKeyValue } from '../../components/keyValueContainer';
+import { KeyValueContainer, InputKeyValue } from '../../components/keyValueContainer';
+import { DashboardPage } from '../../components/dashboardPage';
+import { PostgresModel } from '../../../models/postgresModel';
+
+export class PostgresConnectionStringsPage extends DashboardPage {
+ private keyValueContainer?: KeyValueContainer;
+
+ constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
+ super(modelView);
+ this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh()));
+ this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh()));
+ }
-export class PostgresConnectionStringsPage extends PostgresDashboardPage {
protected get title(): string {
return loc.connectionStrings;
}
@@ -46,10 +56,39 @@ export class PostgresConnectionStringsPage extends PostgresDashboardPage {
this.modelView.modelBuilder.flexContainer().withItems([info, link]).withLayout({ flexWrap: 'wrap' }).component(),
{ CSSStyles: { display: 'inline-flex', 'margin-bottom': '25px' } });
- const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
- const password = this.databaseModel.password();
+ this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, []);
+ content.addItem(this.keyValueContainer.container);
+ this.initialized = true;
+ return root;
+ }
- const pairs: KeyValue[] = [
+ protected get toolbarContainer(): azdata.ToolbarContainer {
+ const refreshButton = this.modelView.modelBuilder.button().withProperties({
+ label: loc.refresh,
+ 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;
+ }
+ });
+
+ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
+ { component: refreshButton }
+ ]).component();
+ }
+
+ private refresh() {
+ const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint();
+ const password = this._postgresModel.password();
+
+ this.keyValueContainer?.refresh([
new InputKeyValue('ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password=${password};Ssl Mode=Require;`),
new InputKeyValue('C++ (libpq)', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password=${password} sslmode=require`),
new InputKeyValue('JDBC', `jdbc:postgresql://${endpoint.ip}:${endpoint.port}/postgres?user=postgres&password=${password}&sslmode=require`),
@@ -59,14 +98,6 @@ export class PostgresConnectionStringsPage extends PostgresDashboardPage {
new InputKeyValue('Python', `dbname='postgres' user='postgres' host='${endpoint.ip}' password='${password}' port='${endpoint.port}' sslmode='true'`),
new InputKeyValue('Ruby', `host=${endpoint.ip}; dbname=postgres user=postgres password=${password} port=${endpoint.port} sslmode=require`),
new InputKeyValue('Web App', `Database=postgres; Data Source=${endpoint.ip}; User Id=postgres; Password=${password}`)
- ];
-
- const keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, pairs);
- content.addItem(keyValueContainer.container);
- return root;
- }
-
- protected get toolbarContainer(): azdata.ToolbarContainer {
- return this.modelView.modelBuilder.toolbarContainer().component();
+ ]);
}
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
index 3b311233aa..4b69e7d837 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
@@ -16,19 +16,17 @@ import { PostgresNetworkingPage } from './postgresNetworkingPage';
import { Dashboard } from '../../components/dashboard';
export class PostgresDashboard extends Dashboard {
- constructor(title: string, private _controllerModel: ControllerModel, private _databaseModel: PostgresModel) {
+ constructor(title: string, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(title);
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
- await Promise.all([this._controllerModel.refresh(), this._databaseModel.refresh()]);
-
- const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._databaseModel);
- const computeStoragePage = new PostgresComputeStoragePage(modelView, this._controllerModel, this._databaseModel);
- const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._controllerModel, this._databaseModel);
- const backupPage = new PostgresBackupPage(modelView, this._controllerModel, this._databaseModel);
- const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._databaseModel);
- const networkingPage = new PostgresNetworkingPage(modelView, this._controllerModel, this._databaseModel);
+ const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
+ const computeStoragePage = new PostgresComputeStoragePage(modelView);
+ const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
+ const backupPage = new PostgresBackupPage(modelView);
+ const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
+ const networkingPage = new PostgresNetworkingPage(modelView);
return [
overviewPage.tab,
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboardPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboardPage.ts
deleted file mode 100644
index f3664ee33a..0000000000
--- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboardPage.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the Source EULA. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as azdata from 'azdata';
-import { ControllerModel } from '../../../models/controllerModel';
-import { PostgresModel } from '../../../models/postgresModel';
-import { DashboardPage } from '../../components/dashboardPage';
-
-export abstract class PostgresDashboardPage extends DashboardPage {
- constructor(protected modelView: azdata.ModelView, protected controllerModel: ControllerModel, protected databaseModel: PostgresModel) {
- super(modelView);
- }
-}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts
index 6062d150da..ae91757a42 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts
@@ -6,9 +6,9 @@
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
-import { PostgresDashboardPage } from './postgresDashboardPage';
+import { DashboardPage } from '../../components/dashboardPage';
-export class PostgresNetworkingPage extends PostgresDashboardPage {
+export class PostgresNetworkingPage extends DashboardPage {
protected get title(): string {
return loc.networking;
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
index c6c3bfa712..5d03e12ac4 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
@@ -8,9 +8,32 @@ import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DuskyObjectModelsDatabase, DuskyObjectModelsDatabaseServiceArcPayload } from '../../../controller/generated/dusky/api';
-import { PostgresDashboardPage } from './postgresDashboardPage';
+import { DashboardPage } from '../../components/dashboardPage';
+import { ControllerModel } from '../../../models/controllerModel';
+import { PostgresModel } from '../../../models/postgresModel';
+
+export class PostgresOverviewPage extends DashboardPage {
+ private propertiesLoading?: azdata.LoadingComponent;
+ private kibanaLoading?: azdata.LoadingComponent;
+ private grafanaLoading?: azdata.LoadingComponent;
+ private nodesTableLoading?: azdata.LoadingComponent;
+
+ private properties?: azdata.PropertiesContainerComponent;
+ private kibanaLink?: azdata.HyperlinkComponent;
+ private grafanaLink?: azdata.HyperlinkComponent;
+ private nodesTable?: azdata.DeclarativeTableComponent;
+
+ constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
+ super(modelView);
+ this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshEndpoints()));
+ this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshProperties()));
+ this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshProperties()));
+ this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => {
+ this.refreshProperties();
+ this.refreshNodes();
+ }));
+ }
-export class PostgresOverviewPage extends PostgresDashboardPage {
protected get title(): string {
return loc.overview;
}
@@ -28,34 +51,18 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '10px 20px 0px 20px' } });
- const registration = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
- const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
- const essentials = this.modelView.modelBuilder.propertiesContainer().withProperties({
- propertyItems: [
- { displayName: loc.name, value: this.databaseModel.name() },
- { displayName: loc.serverGroupType, value: loc.postgresArcProductName },
- { displayName: loc.resourceGroup, value: registration?.resourceGroupName ?? 'None' },
- { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this.databaseModel.password()}@${endpoint.ip}:${endpoint.port}` },
- { displayName: loc.status, value: this.databaseModel.service().status?.state ?? '' },
- { displayName: loc.postgresAdminUsername, value: 'postgres' },
- { displayName: loc.dataController, value: this.controllerModel.namespace() },
- { displayName: loc.nodeConfiguration, value: this.databaseModel.configuration() },
- { displayName: loc.subscriptionId, value: registration?.subscriptionId ?? 'None' },
- { displayName: loc.postgresVersion, value: this.databaseModel.service().spec.engine.version?.toString() ?? '' }
- ]
- }).component();
- content.addItem(essentials, { CSSStyles: cssStyles.text });
+ // Properties
+ this.properties = this.modelView.modelBuilder.propertiesContainer().component();
+ this.propertiesLoading = this.modelView.modelBuilder.loadingComponent().withItem(this.properties).component();
+ content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text });
// Service endpoints
const titleCSS = { ...cssStyles.title, 'margin-block-start': '2em', 'margin-block-end': '0' };
content.addItem(this.modelView.modelBuilder.text().withProperties({ value: loc.serviceEndpoints, CSSStyles: titleCSS }).component());
-
- const kibanaQuery = `kubernetes_namespace:"${this.databaseModel.namespace()}" and cluster_name:"${this.databaseModel.name()}"`;
- const kibanaUrl = `${this.controllerModel.endpoint('logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
- const grafanaUrl = `${this.controllerModel.endpoint('metricsui')?.endpoint}/d/postgres-metrics?var-Namespace=${this.databaseModel.namespace()}&var-Name=${this.databaseModel.name()}`;
-
- const kibanaLink = this.modelView.modelBuilder.hyperlink().withProperties({ label: kibanaUrl, url: kibanaUrl, }).component();
- const grafanaLink = this.modelView.modelBuilder.hyperlink().withProperties({ label: grafanaUrl, url: grafanaUrl }).component();
+ this.kibanaLink = this.modelView.modelBuilder.hyperlink().component();
+ this.grafanaLink = this.modelView.modelBuilder.hyperlink().component();
+ this.kibanaLoading = this.modelView.modelBuilder.loadingComponent().withItem(this.kibanaLink).component();
+ this.grafanaLoading = this.modelView.modelBuilder.loadingComponent().withItem(this.grafanaLink).component();
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties({
width: '100%',
@@ -92,14 +99,14 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
}
],
data: [
- [loc.kibanaDashboard, kibanaLink, loc.kibanaDashboardDescription],
- [loc.grafanaDashboard, grafanaLink, loc.grafanaDashboardDescription]]
+ [loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
+ [loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
}).component();
content.addItem(endpointsTable);
// Server group nodes
content.addItem(this.modelView.modelBuilder.text().withProperties({ value: loc.serverGroupNodes, CSSStyles: titleCSS }).component());
- const nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties({
+ this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties({
width: '100%',
columns: [
{
@@ -130,15 +137,9 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
data: []
}).component();
- const nodes = this.databaseModel.numNodes();
- for (let i = 0; i < nodes; i++) {
- nodesTable.data.push([
- `${this.databaseModel.name()}-${i}`,
- i === 0 ? loc.coordinatorEndpoint : loc.worker,
- i === 0 ? `${endpoint.ip}:${endpoint.port}` : `${this.databaseModel.name()}-${i}.${this.databaseModel.name()}-svc.${this.databaseModel.namespace()}.svc.cluster.local`]);
- }
-
- content.addItem(nodesTable, { CSSStyles: { 'margin-bottom': '20px' } });
+ this.nodesTableLoading = this.modelView.modelBuilder.loadingComponent().withItem(this.nodesTable).component();
+ content.addItem(this.nodesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
+ this.initialized = true;
return root;
}
@@ -150,14 +151,18 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
}).component();
newDatabaseButton.onDidClick(async () => {
- const name = await vscode.window.showInputBox({ prompt: loc.databaseName });
- if (name === undefined) { return; }
- const db: DuskyObjectModelsDatabase = { name: name }; // TODO support other options (sharded, owner)
+ newDatabaseButton.enabled = false;
+ let name;
try {
- await this.databaseModel.createDatabase(db);
+ name = await vscode.window.showInputBox({ prompt: loc.databaseName });
+ if (name === undefined) { return; }
+ const db: DuskyObjectModelsDatabase = { name: name }; // TODO support other options (sharded, owner)
+ await this._postgresModel.createDatabase(db);
vscode.window.showInformationMessage(loc.databaseCreated(db.name));
} catch (error) {
- vscode.window.showErrorMessage(loc.databaseCreationFailed(db.name, error));
+ vscode.window.showErrorMessage(loc.databaseCreationFailed(name ?? '', error));
+ } finally {
+ newDatabaseButton.enabled = true;
}
});
@@ -168,16 +173,19 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
}).component();
resetPasswordButton.onDidClick(async () => {
- const password = await vscode.window.showInputBox({ prompt: loc.newPassword, password: true });
- if (password === undefined) { return; }
+ resetPasswordButton.enabled = false;
try {
- await this.databaseModel.update(s => {
+ const password = await vscode.window.showInputBox({ prompt: loc.newPassword, password: true });
+ if (password === undefined) { return; }
+ await this._postgresModel.update(s => {
s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload();
s.arc.servicePassword = password;
});
- vscode.window.showInformationMessage(loc.passwordReset(this.databaseModel.fullName()));
+ vscode.window.showInformationMessage(loc.passwordReset(this._postgresModel.fullName()));
} catch (error) {
- vscode.window.showErrorMessage(loc.passwordResetFailed(this.databaseModel.fullName(), error));
+ vscode.window.showErrorMessage(loc.passwordResetFailed(this._postgresModel.fullName(), error));
+ } finally {
+ resetPasswordButton.enabled = true;
}
});
@@ -188,15 +196,44 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
}).component();
deleteButton.onDidClick(async () => {
- const response = await vscode.window.showQuickPick([loc.yes, loc.no], {
- placeHolder: loc.deleteServicePrompt(this.databaseModel.fullName())
- });
- if (response !== loc.yes) { return; }
+ deleteButton.enabled = false;
try {
- await this.databaseModel.delete();
- vscode.window.showInformationMessage(loc.serviceDeleted(this.databaseModel.fullName()));
+ const response = await vscode.window.showQuickPick([loc.yes, loc.no], {
+ placeHolder: loc.deleteServicePrompt(this._postgresModel.fullName())
+ });
+ if (response !== loc.yes) { return; }
+ await this._postgresModel.delete();
+ vscode.window.showInformationMessage(loc.serviceDeleted(this._postgresModel.fullName()));
} catch (error) {
- vscode.window.showErrorMessage(loc.serviceDeletionFailed(this.databaseModel.fullName(), error));
+ vscode.window.showErrorMessage(loc.serviceDeletionFailed(this._postgresModel.fullName(), error));
+ } finally {
+ deleteButton.enabled = true;
+ }
+ });
+
+ // Refresh
+ const refreshButton = this.modelView.modelBuilder.button().withProperties({
+ label: loc.refresh,
+ 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;
+
+ await Promise.all([
+ this._postgresModel.refresh(),
+ this._controllerModel.refresh()
+ ]);
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.refreshFailed(error));
+ }
+ finally {
+ refreshButton.enabled = true;
}
});
@@ -207,27 +244,70 @@ export class PostgresOverviewPage extends PostgresDashboardPage {
}).component();
openInAzurePortalButton.onDidClick(async () => {
- const r = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
+ const r = this._controllerModel.registration('postgresInstances', this._postgresModel.namespace(), this._postgresModel.name());
if (r === undefined) {
- vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this.databaseModel.fullName()));
+ 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/postgresInstances/${r.instanceName}`));
}
});
- // TODO implement click
- const feedbackButton = this.modelView.modelBuilder.button().withProperties({
- label: loc.feedback,
- iconPath: IconPathHelper.heart
- }).component();
-
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: newDatabaseButton },
{ component: resetPasswordButton },
- { component: deleteButton, toolbarSeparatorAfter: true },
- { component: openInAzurePortalButton },
- { component: feedbackButton }
+ { component: deleteButton },
+ { component: refreshButton, toolbarSeparatorAfter: true },
+ { component: openInAzurePortalButton }
]).component();
}
+
+ private refreshProperties() {
+ const registration = this._controllerModel.registration('postgresInstances', this._postgresModel.namespace(), this._postgresModel.name());
+ const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint();
+
+ this.properties!.propertyItems = [
+ { displayName: loc.name, value: this._postgresModel.name() },
+ { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this._postgresModel.password()}@${endpoint.ip}:${endpoint.port}` },
+ { displayName: loc.status, value: this._postgresModel.service()?.status?.state ?? '' },
+ { displayName: loc.postgresAdminUsername, value: 'postgres' },
+ { displayName: loc.dataController, value: this._controllerModel?.namespace() ?? '' },
+ { displayName: loc.nodeConfiguration, value: this._postgresModel.configuration() },
+ { displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' },
+ { displayName: loc.postgresVersion, value: this._postgresModel.service()?.spec.engine.version?.toString() ?? '' }
+ ];
+
+ this.propertiesLoading!.loading = false;
+ }
+
+ private refreshEndpoints() {
+ const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace()}" and cluster_name:"${this._postgresModel.name()}"`;
+ const kibanaUrl = `${this._controllerModel.endpoint('logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
+ this.kibanaLink!.label = kibanaUrl;
+ this.kibanaLink!.url = kibanaUrl;
+
+ const grafanaUrl = `${this._controllerModel.endpoint('metricsui')?.endpoint}/d/postgres-metrics?var-Namespace=${this._postgresModel.namespace()}&var-Name=${this._postgresModel.name()}`;
+ this.grafanaLink!.label = grafanaUrl;
+ this.grafanaLink!.url = grafanaUrl;
+
+ this.kibanaLoading!.loading = false;
+ this.grafanaLoading!.loading = false;
+ }
+
+ private refreshNodes() {
+ const nodes = this._postgresModel.numNodes();
+ const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint();
+
+ const data: any[][] = [];
+ for (let i = 0; i < nodes; i++) {
+ data.push([
+ `${this._postgresModel.name()}-${i}`,
+ i === 0 ? loc.coordinatorEndpoint : loc.worker,
+ i === 0 ? `${endpoint.ip}:${endpoint.port}` :
+ `${this._postgresModel.name()}-${i}.${this._postgresModel.name()}-svc.${this._postgresModel.namespace()}.svc.cluster.local`]);
+ }
+
+ this.nodesTable!.data = data;
+ this.nodesTableLoading!.loading = false;
+ }
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
index 4a44636e7f..dd5a3125bb 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
@@ -7,11 +7,21 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
-import { PostgresDashboardPage } from './postgresDashboardPage';
-import { KeyValueContainer, KeyValue, InputKeyValue, LinkKeyValue, TextKeyValue } from '../../components/keyValueContainer';
+import { KeyValueContainer, InputKeyValue, LinkKeyValue, TextKeyValue } from '../../components/keyValueContainer';
+import { DashboardPage } from '../../components/dashboardPage';
+import { ControllerModel } from '../../../models/controllerModel';
+import { PostgresModel } from '../../../models/postgresModel';
+export class PostgresPropertiesPage extends DashboardPage {
+ private keyValueContainer?: KeyValueContainer;
+
+ constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
+ super(modelView);
+ this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh()));
+ this._postgresModel.onPasswordUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh()));
+ this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh()));
+ }
-export class PostgresPropertiesPage extends PostgresDashboardPage {
protected get title(): string {
return loc.properties;
}
@@ -34,27 +44,52 @@ export class PostgresPropertiesPage extends PostgresDashboardPage {
CSSStyles: { ...cssStyles.title, 'margin-bottom': '25px' }
}).component());
- const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
- const connectionString = `postgresql://postgres:${this.databaseModel.password()}@${endpoint.ip}:${endpoint.port}`;
- const registration = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
-
- const pairs: KeyValue[] = [
- new InputKeyValue(loc.coordinatorEndpoint, connectionString),
- new InputKeyValue(loc.postgresAdminUsername, 'postgres'),
- new TextKeyValue(loc.status, this.databaseModel.service().status?.state ?? 'Unknown'),
- new LinkKeyValue(loc.dataController, this.controllerModel.namespace(), _ => vscode.window.showInformationMessage('goto data controller')),
- new LinkKeyValue(loc.nodeConfiguration, this.databaseModel.configuration(), _ => vscode.window.showInformationMessage('goto configuration')),
- new TextKeyValue(loc.postgresVersion, this.databaseModel.service().spec.engine.version?.toString() ?? ''),
- new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''),
- new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '')
- ];
-
- const keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, pairs);
- content.addItem(keyValueContainer.container);
+ this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, []);
+ content.addItem(this.keyValueContainer.container);
+ this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
- return this.modelView.modelBuilder.toolbarContainer().component();
+ const refreshButton = this.modelView.modelBuilder.button().withProperties({
+ label: loc.refresh,
+ 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;
+ }
+ });
+
+ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
+ { component: refreshButton }
+ ]).component();
+ }
+
+ private refresh() {
+ const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint();
+ const connectionString = `postgresql://postgres:${this._postgresModel.password()}@${endpoint.ip}:${endpoint.port}`;
+ const registration = this._controllerModel.registration('postgresInstances', this._postgresModel.namespace(), this._postgresModel.name());
+
+ this.keyValueContainer?.refresh([
+ new InputKeyValue(loc.coordinatorEndpoint, connectionString),
+ new InputKeyValue(loc.postgresAdminUsername, 'postgres'),
+ new TextKeyValue(loc.status, this._postgresModel.service()?.status?.state ?? 'Unknown'),
+ new LinkKeyValue(loc.dataController, this._controllerModel.namespace() ?? '', _ => vscode.window.showInformationMessage('TODO: Go to data controller')),
+ new LinkKeyValue(loc.nodeConfiguration, this._postgresModel.configuration(), _ => vscode.window.showInformationMessage('TODO: Go to configuration')),
+ new TextKeyValue(loc.postgresVersion, this._postgresModel.service()?.spec.engine.version?.toString() ?? ''),
+ new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''),
+ new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '')
+ ]);
}
}