Skip to content

Commit

Permalink
feat(springboot-plugin) Add Info and Health pages
Browse files Browse the repository at this point in the history
  • Loading branch information
mmelko committed Nov 15, 2023
1 parent 1898cb0 commit 846e77e
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 35 deletions.
165 changes: 145 additions & 20 deletions packages/hawtio/src/plugins/springboot/Health.tsx
Original file line number Diff line number Diff line change
@@ -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<string, number[]> = {
diskSpace: [7, 2],
ping: [5, 0],
camelHealth: [5, 1],
}
const ComponentDetails: React.FunctionComponent<{ componentDetails: HealthComponentDetail[] }> = ({
componentDetails,
}) => {
return (
<TableComposable variant='compact' borders={false}>
<Tbody style={{ fontSize: 'xx-small' }}>
{componentDetails.map((detail, index) => {
return (
<Tr key={'row' + detail.key + index}>
<Td key={detail.key + index}>{humanizeLabels(detail.key)}:</Td>
<Td>
{typeof detail.value === 'string' ? detail.value : <ComponentDetails componentDetails={detail.value} />}
</Td>
</Tr>
)
})}
</Tbody>
</TableComposable>
)
}
const HealthStatusIcon: React.FunctionComponent<{ status: string }> = ({ status }) => {
switch (status) {
case 'UP':
return <CheckCircleIcon color={'green'} />
case 'DOWN':
return <ExclamationCircleIcon color={'red'} />
case 'OUT_OF_SERVICE':
return <ExclamationTriangleIcon color={'orange'} />
case 'UNKNOWN':
return <QuestionCircleIcon />
default:
return <InfoCircleIcon color={'blue'} />
}
}

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 (
<Grid height={'100%'}>
<GridItem span={6}>
<ComponentDetails componentDetails={componentDetails} />
</GridItem>
<GridItem span={6}>
<ChartDonutUtilization
ariaDesc='Storage capacity'
ariaTitle='Donut utilization chart example'
constrainToVisibleArea
data={{ x: 'Used Space', y: usedPercentage }}
name='chart2'
subTitle='of available space'
title={`${usedPercentage}% used`}
thresholds={[{ value: 90 }]}
width={435}
/>
</GridItem>
</Grid>
)
}
export const Health: React.FunctionComponent = () => {
loadHealth()
const [healthData, setHealthData] = useState<HealthData>()

useEffect(() => {
loadHealth().then(healthData => {
setHealthData(healthData)
})
}, [])

return (
<PageSection variant='light'>
<Grid hasGutter span={12}>
<GridItem>
<Card>
<CardHeader>
<Title headingLevel='h2'>System</Title>
</CardHeader>
<CardBody></CardBody>
</Card>
<Card>
<CardHeader>
<Title headingLevel='h2'>System</Title>
</CardHeader>
<CardBody></CardBody>
</Card>
</GridItem>
</Grid>
<PageSection variant='default'>
{healthData && (
<Grid hasGutter span={4}>
<GridItem span={12}>
<Card>
<CardHeader>
<Flex>
<HealthStatusIcon status={healthData?.status} />
<Title headingLevel='h3'>
<span>Overall status: {healthData?.status}</span>
</Title>
</Flex>
</CardHeader>
</Card>
</GridItem>
{healthData?.components.map(component => (
<GridItem
// span={12}
span={componentSpanRecord[component.name]![0] as gridSpans}
key={component.name}
mdRowSpan={componentSpanRecord[component.name]![1] as gridSpans}
>
<Card>
<CardHeader>
<Title headingLevel='h3'>{humanizeLabels(component.name!)}</Title>
</CardHeader>
<CardBody>
<Flex>
<FlexItem>
<HealthStatusIcon status={component.status} />
</FlexItem>
<FlexItem>Status: {component.status}</FlexItem>
{component.details &&
(component.name === 'diskSpace' ? (
<DiskComponentDetails componentDetails={component.details} />
) : (
<ComponentDetails componentDetails={component.details} />
))}
</Flex>
</CardBody>
</Card>
</GridItem>
))}
</Grid>
)}
</PageSection>
)
}
45 changes: 38 additions & 7 deletions packages/hawtio/src/plugins/springboot/Info.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<PageSection variant='light'>
Info - TO BE DONE
</PageSection>
)
export const Info: React.FunctionComponent = () => {
const [systemProperties, setSystemProperties] = useState<{ key: string; value: string }[]>([])

useEffect(() => {
getInfo().then(res => {
setSystemProperties(res)
})
}, [])

return (
<PageSection variant='light'>
<FormGroup>
<TableComposable aria-label='Message Table' variant='compact' height='80vh' isStriped isStickyHeader>
<Thead>
<Tr>
<Th data-testid={'name-header'}>Property Name</Th>
<Th data-testid={'value-header'}>Property Value</Th>
</Tr>
</Thead>
<Tbody>
{systemProperties.map((prop, index) => {
return (
<Tr key={'row' + index} data-testid={'row' + index}>
<Td style={{ width: '20%' }}>{prop.key}</Td>
<Td style={{ flex: 3 }}>{prop.value}</Td>
</Tr>
)
})}
</Tbody>
</TableComposable>
</FormGroup>
</PageSection>
)
}
56 changes: 48 additions & 8 deletions packages/hawtio/src/plugins/springboot/springboot-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,51 @@
import { SystemProperty } from '@hawtiosrc/plugins/runtime/types'
import { jolokiaService } from '@hawtiosrc/plugins'
import {
HealthComponent,
HealthData,
JolokiaHealthData,
Logger,
LoggerConfiguration,
} from '@hawtiosrc/plugins/springboot/types'

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
export async function loadHealth(): Promise<HealthData> {
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: key, value: 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(entry => ({
key: entry[0],
value: entry[1],
}))
return properties
}
29 changes: 29 additions & 0 deletions packages/hawtio/src/plugins/springboot/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export interface HealthComponent {
name: string
status: string
details?: HealthComponentDetail[]
}
export interface HealthComponentDetail {
key: string
value: string | HealthComponentDetail[]
}
export interface HealthData {
components: HealthComponent[]
status: string
}

export interface JolokiaHealthData {
status: string
components: {
[name: string]: {
details?: {
[key: string]:
| string
| {
[key: string]: string
}
}
status: string
}
}
}

0 comments on commit 846e77e

Please sign in to comment.