mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Postgres Resource Health Paage (#14575)
* Add podstatus to spec * Added image to table and fixed spacing. * Added pod status to spec * PR fixes * Added resource health page, created overiew box * Pod condtion table is up * Tryingt to fix how table refreshes * Fixed how drop down changes table * Overview box shows number of running and pending pods * overview box refresh fix * Updated summary section * PR fixes * Condensed create pod list function * Added enum * fixed refresh * Fixed refresh, fixed if all availble section add
This commit is contained in:
@@ -134,6 +134,8 @@ export const postgresArcProductName = localize('arc.postgresArcProductName', "Az
|
||||
export const coordinator = localize('arc.coordinator', "Coordinator");
|
||||
export const worker = localize('arc.worker', "Worker");
|
||||
export const monitor = localize('arc.monitor', "Monitor");
|
||||
export const available = localize('arc.available', "Available");
|
||||
export const issuesDetected = localize('arc.issuesDetected', "Issues Detected");
|
||||
export const newDatabase = localize('arc.newDatabase', "New Database");
|
||||
export const databaseName = localize('arc.databaseName', "Database name");
|
||||
export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password");
|
||||
@@ -152,6 +154,7 @@ export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgre
|
||||
export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by");
|
||||
export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure");
|
||||
export const computeAndStorageDescriptionPartFive = localize('arc.computeAndStorageDescriptionPartFive', "there are sufficient resources available");
|
||||
export const resourceHealthDescription = localize('arc.resourceHealthDescription', "Resource health can tell you if your resource is running as expected.");
|
||||
export const computeAndStorageDescriptionPartSix = localize('arc.computeAndStorageDescriptionPartSix', "in your Kubernetes cluster to honor this configuration.");
|
||||
export const node = localize('arc.node', "node");
|
||||
export const nodes = localize('arc.nodes', "nodes");
|
||||
@@ -169,14 +172,21 @@ export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
|
||||
export const enterANonEmptyPassword = localize('arc.enterANonEmptyPassword', "Enter a non empty password or press escape to exit.");
|
||||
export const thePasswordsDoNotMatch = localize('arc.thePasswordsDoNotMatch', "The passwords do not match. Confirm the password or press escape to exit.");
|
||||
export const passwordReset = localize('arc.passwordReset', "Password reset successfully");
|
||||
export const podOverview = localize('arc.podOverview', "Pod overview");
|
||||
export const condition = localize('arc.condition', "Condition");
|
||||
export const details = localize('arc.details', "Details");
|
||||
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
|
||||
export const lastTransition = localize('arc.lastTransition', "Last transition");
|
||||
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
|
||||
export const podsReady = localize('arc.podsReady', "pods ready");
|
||||
export const podsPresent = localize('arc.podsPresent', "Pods Present");
|
||||
export const podsUsedDescription = localize('arc.podsUsedDescription', "Select a pod in the dropdown below for detailed health information.");
|
||||
export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed.");
|
||||
export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql");
|
||||
export const podInitialized = localize('arc.podInitialized', "Pod is initialized.");
|
||||
export const podReady = localize('arc.podReady', "Pod is ready.");
|
||||
export const noPodIssuesDetected = localize('arc.noPodIssuesDetected', "There aren’t any known issues affecting this PostgreSQL Hyperscale instance.");
|
||||
export const podIssuesDetected = localize('arc.podIssuesDetected', "The pods listed below are experiencing issues that may affect performance or availability.");
|
||||
export const containerReady = localize('arc.containerReady', "Pod containers are ready.");
|
||||
export const podScheduled = localize('arc.podScheduled', "Pod is schedulable.");
|
||||
|
||||
export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); }
|
||||
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
|
||||
@@ -225,6 +235,7 @@ export function fetchEndpointsFailed(name: string, error: any): string { return
|
||||
export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function numberOfIssuesDetected(name: string, issues: number): string { return localize('arc.numberOfIssuesDetected', "• {0} ({1} issues)", name, issues); }
|
||||
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
|
||||
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "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); }
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
||||
import { PostgresComputeAndStoragePage } from './postgresComputeAndStoragePage';
|
||||
import { PostgresWorkerNodeParametersPage } from './postgresWorkerNodeParametersPage';
|
||||
import { PostgresPropertiesPage } from './postgresPropertiesPage';
|
||||
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
|
||||
|
||||
export class PostgresDashboard extends Dashboard {
|
||||
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
@@ -40,6 +41,7 @@ export class PostgresDashboard extends Dashboard {
|
||||
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel);
|
||||
|
||||
return [
|
||||
overviewPage.tab,
|
||||
@@ -55,6 +57,7 @@ export class PostgresDashboard extends Dashboard {
|
||||
{
|
||||
title: loc.supportAndTroubleshooting,
|
||||
tabs: [
|
||||
resourceHealthPage.tab,
|
||||
diagnoseAndSolveProblemsPage.tab,
|
||||
supportRequestPage.tab
|
||||
]
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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, iconSize } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export type PodHealthModel = {
|
||||
condition: string,
|
||||
details?: azdata.Component,
|
||||
lastUpdate: string
|
||||
};
|
||||
|
||||
export enum PodCondtionType {
|
||||
initialized = 'Initialized',
|
||||
ready = 'Ready',
|
||||
containersReady = 'ContainersReady',
|
||||
podScheduled = 'PodScheduled'
|
||||
}
|
||||
|
||||
export class PostgresResourceHealthPage extends DashboardPage {
|
||||
private podSummaryContainer!: azdata.DivContainer;
|
||||
|
||||
private podConditionsContainer!: azdata.DivContainer;
|
||||
private podConditionsLoading!: azdata.LoadingComponent;
|
||||
private podConditionsTable!: azdata.DeclarativeTableComponent;
|
||||
private podConditionsTableIndexes: Map<string, number[]> = new Map();
|
||||
|
||||
private podDropDown!: azdata.DropDownComponent;
|
||||
private coordinatorPodName!: string;
|
||||
private coordinatorData: PodHealthModel[] = [];
|
||||
private podsData: PodHealthModel[] = [];
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
|
||||
this.disposables.push(
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.resourceHealth;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-resource-health';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.health;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '10px 20px 0px 20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.resourceHealth,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.resourceHealthDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
|
||||
this.podSummaryContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
|
||||
this.refreshPodSummarySection();
|
||||
|
||||
content.addItem(this.podSummaryContainer);
|
||||
|
||||
// Pod Conditions
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.podsPresent,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.podsUsedDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-top': '10px' }
|
||||
}).component());
|
||||
|
||||
this.podConditionsContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.podConditionsTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.condition,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.details,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: '50%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow,
|
||||
'min-width': '150px'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.lastTransition,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: [this.coordinatorData.map(p => [p.condition, p.details, p.lastUpdate])]
|
||||
}).component();
|
||||
|
||||
this.podDropDown = this.modelView.modelBuilder.dropDown().withProps({ width: '150px' }).component();
|
||||
this.disposables.push(
|
||||
this.podDropDown.onValueChanged(() => {
|
||||
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(String(this.podDropDown.value)));
|
||||
})
|
||||
);
|
||||
|
||||
this.podConditionsContainer.addItem(this.podDropDown, { CSSStyles: { 'margin': '10px 0px 10px 0px' } });
|
||||
this.podConditionsContainer.addItem(this.podConditionsTable);
|
||||
this.podConditionsLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.podConditionsContainer)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.configLastUpdated
|
||||
}).component();
|
||||
|
||||
this.refreshPodCondtions();
|
||||
|
||||
content.addItem(this.podConditionsLoading, { CSSStyles: cssStyles.text });
|
||||
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Refresh
|
||||
const refreshButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.refresh,
|
||||
iconPath: IconPathHelper.refresh
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
refreshButton.onDidClick(async () => {
|
||||
refreshButton.enabled = false;
|
||||
try {
|
||||
this.podConditionsLoading!.loading = true;
|
||||
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 createPodList(): string[] {
|
||||
const podStatus = this._postgresModel.config?.status.podsStatus;
|
||||
let podNames: string[] = [];
|
||||
|
||||
podStatus?.forEach(p => {
|
||||
let podHealthModels: PodHealthModel[] = [];
|
||||
let indexes: number[] = [];
|
||||
|
||||
|
||||
p.conditions.forEach(c => {
|
||||
let message: string;
|
||||
let imageComponent = this.modelView.modelBuilder.image().withProps({
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
iconHeight: '15px',
|
||||
iconWidth: '15px'
|
||||
}).component();
|
||||
|
||||
if (c.status === 'False') {
|
||||
imageComponent.iconPath = IconPathHelper.fail;
|
||||
message = c.message ?? c.reason ?? '';
|
||||
} else {
|
||||
imageComponent.iconPath = IconPathHelper.success;
|
||||
|
||||
if (c.type === PodCondtionType.initialized) {
|
||||
message = loc.podInitialized;
|
||||
} else if (c.type === PodCondtionType.ready) {
|
||||
message = loc.podReady;
|
||||
} else if (c.type === PodCondtionType.containersReady) {
|
||||
message = loc.containerReady;
|
||||
} else if (c.type === PodCondtionType.podScheduled) {
|
||||
message = loc.podScheduled;
|
||||
} else {
|
||||
message = c.message ?? c.reason ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
const condtionContainer = this.modelView.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
|
||||
}).component();
|
||||
condtionContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
|
||||
condtionContainer.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: message,
|
||||
}).component());
|
||||
|
||||
indexes.push(this.podsData.length);
|
||||
this.podsData.push({
|
||||
condition: c.type,
|
||||
details: condtionContainer,
|
||||
lastUpdate: c.lastTransitionTime
|
||||
});
|
||||
});
|
||||
|
||||
if (p.role.toUpperCase() !== loc.coordinator.toUpperCase()) {
|
||||
podNames.push(p.name);
|
||||
} else {
|
||||
this.coordinatorData = podHealthModels;
|
||||
this.coordinatorPodName = p.name;
|
||||
podNames.unshift(p.name);
|
||||
}
|
||||
this.podConditionsTableIndexes.set(p.name, indexes);
|
||||
});
|
||||
|
||||
this.podConditionsTable.data = this.podsData.map(p => [p.condition, p.details, p.lastUpdate]);
|
||||
|
||||
return podNames;
|
||||
}
|
||||
|
||||
private findPodIssues(): string[] {
|
||||
const podStatus = this._postgresModel.config?.status.podsStatus;
|
||||
let issueCount = 0;
|
||||
let podIssuesDetected: string[] = [];
|
||||
|
||||
podStatus?.forEach(p => {
|
||||
p.conditions.forEach(c => {
|
||||
if (c.status === 'False') {
|
||||
issueCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (issueCount > 0) {
|
||||
podIssuesDetected.push(loc.numberOfIssuesDetected(p.name, issueCount));
|
||||
issueCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
return podIssuesDetected;
|
||||
}
|
||||
|
||||
private refreshPodSummarySection(): void {
|
||||
let podSummaryTitle = this.modelView.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: { 'alignItems': 'center', 'height': '15px', 'margin-top': '20px' }
|
||||
}).component();
|
||||
if (!this._postgresModel.config) {
|
||||
podSummaryTitle.addItem(this.modelView.modelBuilder.loadingComponent().component(), { CSSStyles: { 'margin-right': '5px' } });
|
||||
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.loading,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
this.podSummaryContainer.addItem(podSummaryTitle);
|
||||
} else {
|
||||
let components: azdata.Component[] = [];
|
||||
let imageComponent = this.modelView.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.success,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
iconHeight: '20px',
|
||||
iconWidth: '20px'
|
||||
}).component();
|
||||
|
||||
let podIssues = this.findPodIssues();
|
||||
if (podIssues.length === 0) {
|
||||
imageComponent.iconPath = IconPathHelper.success;
|
||||
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
|
||||
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.available,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-left': '0px' }
|
||||
}).component());
|
||||
components.push(podSummaryTitle);
|
||||
components.push(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.noPodIssuesDetected,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-top': '20px' }
|
||||
}).component());
|
||||
} else {
|
||||
imageComponent.iconPath = IconPathHelper.fail;
|
||||
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
|
||||
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.issuesDetected,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
components.push(podSummaryTitle);
|
||||
components.push(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.podIssuesDetected,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-top': '20px 0px 10px 0px' }
|
||||
}).component());
|
||||
components.push(...podIssues.map(i => {
|
||||
return this.modelView.modelBuilder.text().withProps({
|
||||
value: i,
|
||||
CSSStyles: { ...cssStyles.text, 'margin': '0px' }
|
||||
}).component();
|
||||
}));
|
||||
}
|
||||
this.podSummaryContainer.addItems(components);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshPodCondtions(): void {
|
||||
if (this._postgresModel.config) {
|
||||
this.podConditionsTableIndexes = new Map();
|
||||
this.podsData = [];
|
||||
this.podDropDown.values = this.createPodList();
|
||||
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(this.coordinatorPodName!));
|
||||
this.podConditionsLoading.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleConfigUpdated() {
|
||||
this.podSummaryContainer.clearItems();
|
||||
this.refreshPodSummarySection();
|
||||
this.refreshPodCondtions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user