From 8dbc221dd8b77f251b70ba5bf5331711f7579103 Mon Sep 17 00:00:00 2001 From: mmelko Date: Wed, 15 Nov 2023 16:54:09 +0100 Subject: [PATCH] feat(springboot-plugin) Add Info and Health pages --- .../hawtio/src/plugins/springboot/Health.tsx | 165 +++++++++++++++--- .../hawtio/src/plugins/springboot/Info.tsx | 45 ++++- .../src/plugins/springboot/SpringBoot.tsx | 72 ++++++++ .../src/plugins/springboot/Springboot.tsx | 54 ------ .../hawtio/src/plugins/springboot/Trace.tsx | 10 +- .../hawtio/src/plugins/springboot/help.md | 4 +- .../hawtio/src/plugins/springboot/index.ts | 10 +- .../plugins/springboot/springboot-service.ts | 58 ++++-- .../hawtio/src/plugins/springboot/types.ts | 29 +++ 9 files changed, 342 insertions(+), 105 deletions(-) create mode 100644 packages/hawtio/src/plugins/springboot/SpringBoot.tsx delete mode 100644 packages/hawtio/src/plugins/springboot/Springboot.tsx diff --git a/packages/hawtio/src/plugins/springboot/Health.tsx b/packages/hawtio/src/plugins/springboot/Health.tsx index b7dd9123..ca31cbe5 100644 --- a/packages/hawtio/src/plugins/springboot/Health.tsx +++ b/packages/hawtio/src/plugins/springboot/Health.tsx @@ -1,27 +1,152 @@ -import React from 'react' -import { Card, CardBody, CardHeader, Grid, GridItem, PageSection, Title } from '@patternfly/react-core' +import React, { useEffect, useState } from 'react' +import { + Card, + CardBody, + CardHeader, + Flex, + FlexItem, + Grid, + GridItem, + gridSpans, + PageSection, + Title, +} from '@patternfly/react-core' import { loadHealth } from '@hawtiosrc/plugins/springboot/springboot-service' +import { HealthComponentDetail, HealthData } from '@hawtiosrc/plugins/springboot/types' +import { TableComposable, Tbody, Td, Tr } from '@patternfly/react-table' +import { humanizeLabels } from '@hawtiosrc/util/strings' +import { ChartDonutUtilization } from '@patternfly/react-charts' +import { + CheckCircleIcon, + ExclamationCircleIcon, + ExclamationTriangleIcon, + InfoCircleIcon, + QuestionCircleIcon, +} from '@patternfly/react-icons' +const componentSpanRecord: Record = { + diskSpace: [7, 2], + ping: [5, 0], + camelHealth: [5, 1], + camel: [5, 1], +} +const ComponentDetails: React.FunctionComponent<{ componentDetails: HealthComponentDetail[] }> = ({ + componentDetails, +}) => { + return ( + + + {componentDetails.map((detail, index) => { + return ( + + {humanizeLabels(detail.key)}: + + {typeof detail.value === 'string' ? detail.value : } + + + ) + })} + + + ) +} +const HealthStatusIcon: React.FunctionComponent<{ status: string }> = ({ status }) => { + switch (status) { + case 'UP': + return + case 'DOWN': + return + case 'OUT_OF_SERVICE': + return + case 'UNKNOWN': + return + default: + return + } +} + +const DiskComponentDetails: React.FunctionComponent<{ componentDetails: HealthComponentDetail[] }> = ({ + componentDetails, +}) => { + const total = Number.parseInt(componentDetails.find(k => k.key === 'total')!.value as string) + const free = Number.parseInt(componentDetails.find(k => k.key === 'free')!.value as string) + const usedPercentage = Math.round(((total - free) * 100) / total) + + return ( + + + + + + + + + ) +} export const Health: React.FunctionComponent = () => { - loadHealth() + const [healthData, setHealthData] = useState() + + useEffect(() => { + loadHealth().then(healthData => { + setHealthData(healthData) + }) + }, []) + return ( - - - - - - System - - - - - - System - - - - - + + {healthData && ( + + + + + + + + <span>Overall status: {healthData?.status}</span> + + + + + + {healthData?.components.map(component => ( + + + + {humanizeLabels(component.name!)} + + + + + + + Status: {component.status} + {component.details && + (component.name === 'diskSpace' ? ( + + ) : ( + + ))} + + + + + ))} + + )} ) } diff --git a/packages/hawtio/src/plugins/springboot/Info.tsx b/packages/hawtio/src/plugins/springboot/Info.tsx index dbecc38b..96381066 100644 --- a/packages/hawtio/src/plugins/springboot/Info.tsx +++ b/packages/hawtio/src/plugins/springboot/Info.tsx @@ -1,8 +1,39 @@ -import React from "react" -import {PageSection} from "@patternfly/react-core" +import React, { useEffect, useState } from 'react' +import { FormGroup, PageSection } from '@patternfly/react-core' +import { getInfo } from '@hawtiosrc/plugins/springboot/springboot-service' +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table' -export const Info:React.FunctionComponent = () => ( - - Info - TO BE DONE - -) +export const Info: React.FunctionComponent = () => { + const [systemProperties, setSystemProperties] = useState<{ key: string; value: string }[]>([]) + + useEffect(() => { + getInfo().then(res => { + setSystemProperties(res) + }) + }, []) + + return ( + + + + + + Property Name + Property Value + + + + {systemProperties.map((prop, index) => { + return ( + + {prop.key} + {prop.value} + + ) + })} + + + + + ) +} diff --git a/packages/hawtio/src/plugins/springboot/SpringBoot.tsx b/packages/hawtio/src/plugins/springboot/SpringBoot.tsx new file mode 100644 index 00000000..32093282 --- /dev/null +++ b/packages/hawtio/src/plugins/springboot/SpringBoot.tsx @@ -0,0 +1,72 @@ +import { Nav, NavItem, NavList, PageGroup, PageNavigation, PageSection, Title } from '@patternfly/react-core' +import React, { useEffect, useState } from 'react' + +import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom' +import { Health } from '@hawtiosrc/plugins/springboot/Health' +import { Info } from '@hawtiosrc/plugins/springboot/Info' +import { Loggers } from '@hawtiosrc/plugins/springboot/Loggers' +import { Trace } from '@hawtiosrc/plugins/springboot/Trace' +import { hasEndpoint } from '@hawtiosrc/plugins/springboot/springboot-service' + +type NavItem = { + id: string + title: string + component: JSX.Element +} +export const SpringBoot: React.FunctionComponent = () => { + const location = useLocation() + const [navItems, setNavItems] = useState([]) + + useEffect(() => { + const initNavItems = async () => { + const nav: NavItem[] = [] + if (await hasEndpoint('Health')) { + nav.push({ id: 'health', title: 'Health', component: }) + } + + if (await hasEndpoint('Info')) { + nav.push({ id: 'info', title: 'Info', component: }) + } + + if (await hasEndpoint('Loggers')) { + nav.push({ id: 'loggers', title: 'Loggers', component: }) + } + + if (await hasEndpoint('Trace')) { + nav.push({ id: 'trace', title: 'Trace', component: }) + } + + setNavItems([...nav]) + } + initNavItems() + }, []) + + return ( + + + Spring Boot + + + + + + + + + {navItems.map(navItem => ( + + ))} + } /> + + + + ) +} diff --git a/packages/hawtio/src/plugins/springboot/Springboot.tsx b/packages/hawtio/src/plugins/springboot/Springboot.tsx deleted file mode 100644 index 68eec361..00000000 --- a/packages/hawtio/src/plugins/springboot/Springboot.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Nav, NavItem, NavList, PageGroup, PageNavigation, PageSection, Title } from '@patternfly/react-core' -import React from 'react' - -import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom' -import {Health} from "@hawtiosrc/plugins/springboot/Health" -import {Info} from "@hawtiosrc/plugins/springboot/Info" -import {Loggers} from "@hawtiosrc/plugins/springboot/Loggers" -import {Trace} from "@hawtiosrc/plugins/springboot/Trace" - - -type NavItem = { - id: string - title: string - component: JSX.Element -} -export const Springboot: React.FunctionComponent = () => { - const location = useLocation() - - const navItems: NavItem[] = [ - { id: 'health', title: 'Health', component: }, - { id: 'info', title: 'Info', component: }, - { id: 'loggers', title: 'Loggers', component: }, - { id: 'trace', title: 'Trace', component: }, - ] - - return ( - - - Springboot - - - - - - - - - {navItems.map(navItem => ( - - ))} - } /> - - - - ) -} diff --git a/packages/hawtio/src/plugins/springboot/Trace.tsx b/packages/hawtio/src/plugins/springboot/Trace.tsx index ab58d6e3..c5a0e294 100644 --- a/packages/hawtio/src/plugins/springboot/Trace.tsx +++ b/packages/hawtio/src/plugins/springboot/Trace.tsx @@ -1,8 +1,4 @@ -import React from "react" -import {PageSection} from "@patternfly/react-core" +import React from 'react' +import { PageSection } from '@patternfly/react-core' -export const Trace:React.FunctionComponent = () => ( - - Trace - TO BE DONE - -) +export const Trace: React.FunctionComponent = () => Trace - TO BE DONE diff --git a/packages/hawtio/src/plugins/springboot/help.md b/packages/hawtio/src/plugins/springboot/help.md index c3cfafc4..1d327dce 100644 --- a/packages/hawtio/src/plugins/springboot/help.md +++ b/packages/hawtio/src/plugins/springboot/help.md @@ -8,11 +8,11 @@ Displays the current health status of the application together with details retu ### Info -Displays relevant information regarding the Spring boot application. It is the output of the Info actuator endpoint. +Displays relevant information regarding the Spring boot application. It is the output of the Info actuator endpoint. ### Loggers -Lists all the available loggers in the application. You can modify the level of a logger and the changes will take effect immediately. +Lists all the available loggers in the application. You can modify the level of a logger and the changes will take effect immediately. ### Trace diff --git a/packages/hawtio/src/plugins/springboot/index.ts b/packages/hawtio/src/plugins/springboot/index.ts index 05f4096a..ffcef010 100644 --- a/packages/hawtio/src/plugins/springboot/index.ts +++ b/packages/hawtio/src/plugins/springboot/index.ts @@ -4,15 +4,15 @@ import { pluginId, pluginPath } from './globals' import { workspace } from '@hawtiosrc/plugins' import { helpRegistry } from '@hawtiosrc/help' import help from './help.md' -import {Springboot} from "@hawtiosrc/plugins/springboot/Springboot" +import { SpringBoot } from '@hawtiosrc/plugins/springboot/SpringBoot' export const springboot: HawtioPlugin = () => { hawtio.addPlugin({ id: pluginId, - title: 'Springboot', + title: 'Spring Boot', path: pluginPath, - component: Springboot, - isActive: async () => workspace.hasMBeans(), + component: SpringBoot, + isActive: async () => workspace.treeContainsDomainAndProperties('org.springframework.boot'), }) - helpRegistry.add(pluginId, 'Runtime', help, 16) + helpRegistry.add(pluginId, 'Spring Boot', help, 17) } diff --git a/packages/hawtio/src/plugins/springboot/springboot-service.ts b/packages/hawtio/src/plugins/springboot/springboot-service.ts index 24d6d0a7..23d75f44 100644 --- a/packages/hawtio/src/plugins/springboot/springboot-service.ts +++ b/packages/hawtio/src/plugins/springboot/springboot-service.ts @@ -1,11 +1,49 @@ -import { SystemProperty } from '@hawtiosrc/plugins/runtime/types' -import { jolokiaService } from '@hawtiosrc/plugins' - -export async function loadHealth() { - const attr = await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Health', 'health') - console.log(attr) - // for (const [k, v] of Object.entries(attr as object)) { - // systemProperties.push({ key: k, value: v }) - // } - //return systemProperties +import { jolokiaService, workspace } from '@hawtiosrc/plugins' +import { HealthComponent, HealthData, JolokiaHealthData } from './types' + +export async function loadHealth(): Promise { + const data = (await jolokiaService.execute( + 'org.springframework.boot:type=Endpoint,name=Health', + 'health', + )) as JolokiaHealthData + let healthComponents: HealthComponent[] = [] + + healthComponents = Object.entries(data.components).map(([componentName, component]) => { + let details + + if (component.details) { + details = Object.entries(component.details).map(([detailKey, detailValue]) => { + const typ = typeof detailValue + const value = ['string', 'number', 'boolean'].includes(typ) + ? detailValue.toString() + : Object.entries(detailValue).map(([key, value]) => ({ key, value })) + + return { + key: detailKey, + value: value, + } + }) + } + + return { + name: componentName, + status: component.status, + details: component.details ? details : undefined, + } + }) + + return { status: data.status, components: healthComponents } +} + +export async function getInfo() { + const res = await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Info', 'info') + const properties: { key: string; value: string }[] = Object.entries(res as object).map(([key, value]) => ({ + key, + value, + })) + return properties +} + +export async function hasEndpoint(name: string): Promise { + return await workspace.treeContainsDomainAndProperties('org.springframework.boot', { type: 'Endpoint', name: name }) } diff --git a/packages/hawtio/src/plugins/springboot/types.ts b/packages/hawtio/src/plugins/springboot/types.ts index e69de29b..5bfeffa6 100644 --- a/packages/hawtio/src/plugins/springboot/types.ts +++ b/packages/hawtio/src/plugins/springboot/types.ts @@ -0,0 +1,29 @@ +export type HealthComponent = { + name: string + status: string + details?: HealthComponentDetail[] +} +export type HealthComponentDetail = { + key: string + value: string | HealthComponentDetail[] +} +export type HealthData = { + components: HealthComponent[] + status: string +} + +export type JolokiaHealthData = { + status: string + components: { + [name: string]: { + details?: { + [key: string]: + | string + | { + [key: string]: string + } + } + status: string + } + } +}