Add refresh support to Arc Postgres pages (#10607)

This commit is contained in:
Brian Bergeron
2020-05-29 10:24:24 -07:00
committed by GitHub
parent 1305743479
commit e9e2a0b8b4
14 changed files with 348 additions and 181 deletions

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1328 0.296875C10.9974 0.53125 11.7891 0.898438 12.5078 1.39844C13.2266 1.89323 13.8438 2.48177 14.3594 3.16406C14.8802 3.84115 15.2839 4.59375 15.5703 5.42188C15.8568 6.24479 16 7.10417 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5625 12.6328 14.1458 13.1745 13.6562 13.6641C13.1719 14.1484 12.6328 14.5651 12.0391 14.9141C11.4453 15.2578 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2578 3.96094 14.9141C3.36719 14.5651 2.82552 14.1484 2.33594 13.6641C1.85156 13.1745 1.4349 12.6328 1.08594 12.0391C0.742188 11.4453 0.473958 10.8099 0.28125 10.1328C0.09375 9.45052 0 8.73958 0 8C0 7.27083 0.0963542 6.5625 0.289062 5.875C0.481771 5.1875 0.755208 4.54167 1.10938 3.9375C1.46875 3.32812 1.90365 2.77604 2.41406 2.28125C2.92448 1.78125 3.5 1.35417 4.14062 1H2V0H6V4H5V1.67969C4.39062 1.97135 3.83854 2.33854 3.34375 2.78125C2.85417 3.21875 2.4349 3.71354 2.08594 4.26562C1.73698 4.8125 1.46875 5.40365 1.28125 6.03906C1.09375 6.67448 1 7.32812 1 8C1 8.64062 1.08333 9.26042 1.25 9.85938C1.41667 10.4531 1.65104 11.0104 1.95312 11.5312C2.26042 12.0469 2.6276 12.5182 3.05469 12.9453C3.48177 13.3724 3.95312 13.7396 4.46875 14.0469C4.98958 14.349 5.54688 14.5833 6.14062 14.75C6.73438 14.9167 7.35417 15 8 15C8.64062 15 9.25781 14.9167 9.85156 14.75C10.4505 14.5833 11.0078 14.349 11.5234 14.0469C12.0443 13.7396 12.5182 13.3724 12.9453 12.9453C13.3724 12.5182 13.737 12.0469 14.0391 11.5312C14.3464 11.0104 14.5833 10.4531 14.75 9.85938C14.9167 9.26562 15 8.64583 15 8C15 7.21875 14.8724 6.46615 14.6172 5.74219C14.3672 5.01823 14.0156 4.35938 13.5625 3.76562C13.1094 3.17188 12.5677 2.65885 11.9375 2.22656C11.3125 1.78906 10.6224 1.46615 9.86719 1.25781L10.1328 0.296875Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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')
};
}
}

View File

@@ -23,10 +23,19 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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));
}
});
}

View File

@@ -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");

View File

@@ -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<EndpointModel[]>();
private readonly _onRegistrationsUpdated = new vscode.EventEmitter<RegistrationResponse[]>();
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('_');

View File

@@ -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<DuskyObjectModelsDatabaseService>();
private readonly _onPasswordUpdated = new vscode.EventEmitter<string>();
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;
}
/**

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<azdata.ButtonProperties>({
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();
]);
}
}

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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<azdata.PropertiesContainerComponentProperties>({
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<azdata.TextComponentProperties>({ 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<azdata.HyperlinkComponentProperties>({ label: kibanaUrl, url: kibanaUrl, }).component();
const grafanaLink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({ 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<azdata.DeclarativeTableProperties>({
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<azdata.TextComponentProperties>({ value: loc.serverGroupNodes, CSSStyles: titleCSS }).component());
const nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
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<azdata.ButtonProperties>({
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<azdata.ButtonProperties>({
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;
}
}

View File

@@ -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<azdata.ButtonProperties>({
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 ?? '')
]);
}
}