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:
nasc17
2021-03-08 10:05:11 -08:00
committed by GitHub
parent cecc83c89d
commit dba5880f35
3 changed files with 351 additions and 2 deletions

View File

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

View File

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

View File

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