Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime): Initial Runtime plugin #556

Merged
merged 9 commits into from
Sep 20, 2023
2 changes: 2 additions & 0 deletions packages/hawtio/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { jmx } from './jmx'
import { logs } from './logs'
import { quartz } from './quartz'
import { rbac } from './rbac'
import { runtime } from './runtime'

export const registerPlugins: HawtioPlugin = () => {
// Auth plugins should be loaded before other plugins
Expand All @@ -15,6 +16,7 @@ export const registerPlugins: HawtioPlugin = () => {
jmx()
rbac()
camel()
runtime()
logs()
quartz()
}
Expand Down
92 changes: 92 additions & 0 deletions packages/hawtio/src/plugins/runtime/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Card, CardBody, CardHeader, Grid, GridItem, Title } from '@patternfly/react-core'
import React, { useEffect, useState } from 'react'
import { runtimeService } from './runtime-service'
import { Metric } from './types'
import { ChartBullet } from '@patternfly/react-charts'

export const Metrics: React.FunctionComponent = () => {
const [metrics, setMetrics] = useState<Record<string, Metric>>({})

useEffect(() => {
const registerMetricsRequests = () => {
let metricsRecord: Record<string, Metric> = {}
runtimeService.registerMetrics(metric => {
metricsRecord = { ...metricsRecord, [metric.name]: metric }
setMetrics(metricsRecord)
})
}

const readMetrics = async () => {
const metricsList = await runtimeService.loadMetrics()
let metricsRecord: Record<string, Metric> = {}
metricsList.forEach(metric => (metricsRecord = { ...metricsRecord, [metric.name]: metric }))
setMetrics(metricsRecord)
}

readMetrics()
registerMetricsRequests()
return () => runtimeService.unregisterAll()
}, [])

return (
<Grid hasGutter span={6}>
<GridItem>
<Card>
<CardHeader>
<Title headingLevel='h2'>System</Title>
</CardHeader>
<CardBody>
{Object.values(metrics)
.filter(m => m.type === 'System')
.map((metric, index) => {
return (
<div key={index}>
{metric.name} :
<span>
{metric.value} {metric.unit ?? ''}
{metric.available && ' of ' + metric.available + ' ' + (metric.unit ?? '')}
</span>
{metric.chart && (
<ChartBullet
ariaDesc={metric.unit}
ariaTitle={metric.value + ' ' + metric.unit}
comparativeWarningMeasureData={[{ name: 'Warning', y: 0.9 * (metric.available as number) }]}
constrainToVisibleArea
maxDomain={{ y: metric.available as number }}
name={metric.name}
primarySegmentedMeasureData={[{ name: metric.unit, y: metric.value }]}
width={600}
/>
)}
</div>
)
})}
</CardBody>
</Card>
</GridItem>
<GridItem>
<Card>
<CardHeader>
<Title headingLevel='h2'>JVM</Title>
</CardHeader>

<CardBody>
{Object.values(metrics)
.filter(m => m.type === 'JVM')
.map((metric, index) => {
return (
<div key={index}>
{metric.name} :
<span>
{metric.value} {metric.unit ?? ''}
{metric.available && 'of' + metric.available + ' ' + (metric.unit ?? '')}
</span>
</div>
)
})}
</CardBody>
</Card>
</GridItem>
</Grid>
)
}
63 changes: 63 additions & 0 deletions packages/hawtio/src/plugins/runtime/Runtime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
PageSection,
PageSectionVariants,
NavItem,
Title,
PageGroup,
PageNavigation,
Nav,
NavList,
Card,
} from '@patternfly/react-core'
import React from 'react'

import { SysProps } from './SysProps'
import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom'
import { Metrics } from './Metrics'
import { Threads } from './Threads'

type NavItem = {
id: string
title: string
component: JSX.Element
}
export const Runtime: React.FunctionComponent = () => {
const location = useLocation()

const navItems: NavItem[] = [
{ id: 'sysprops', title: 'System Properties', component: <SysProps /> },
{ id: 'metrics', title: 'Metrics', component: <Metrics /> },
{ id: 'threads', title: 'Threads', component: <Threads /> },
]

return (
<React.Fragment>
<PageSection variant={PageSectionVariants.light}>
<Title headingLevel='h1'>Runtime</Title>
</PageSection>
<PageGroup>
<PageNavigation>
<Nav aria-label='Runtime Nav' variant='tertiary'>
<NavList>
{navItems.map(navItem => (
<NavItem key={navItem.id} isActive={location.pathname === `/runtime/${navItem.id}`}>
<NavLink to={navItem.id}>{navItem.title}</NavLink>
</NavItem>
))}
</NavList>
</Nav>
</PageNavigation>
</PageGroup>
<PageSection>
<Card isFullHeight>
<Routes>
{navItems.map(navItem => (
<Route key={navItem.id} path={navItem.id} element={navItem.component} />
))}
<Route path='/' element={<Navigate to='sysprops' />} />
</Routes>
</Card>
</PageSection>
</React.Fragment>
)
}
89 changes: 89 additions & 0 deletions packages/hawtio/src/plugins/runtime/SysProps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { render, screen, waitFor, within } from '@testing-library/react'
import { SysProps } from './SysProps'
import { SystemProperty } from './types'
import { runtimeService } from './runtime-service'
import userEvent from '@testing-library/user-event'

function getMockedProperties(): SystemProperty[] {
return [
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' },
{ key: 'key3', value: 'value3' },
]
}

describe('SysProps.tsx', () => {
tadayosi marked this conversation as resolved.
Show resolved Hide resolved
jest.spyOn(runtimeService, 'loadSystemProperties').mockResolvedValue(getMockedProperties())
const renderSysProps = () => {
return render(<SysProps />)
}

test('System properties are displayed correctly', async () => {
renderSysProps()
await waitFor(() => {
expect(screen.getByText('value3')).toBeInTheDocument()
})
for (const property of getMockedProperties()) {
expect(screen.getByText(property.key)).toBeInTheDocument()
expect(screen.getByText(property.value)).toBeInTheDocument()
}
})

test('Statistics can be filtered', async () => {
renderSysProps()
const input = within(screen.getByTestId('filter-input')).getByRole('textbox')

const prop = getMockedProperties()[2] as SystemProperty
expect(input).toBeInTheDocument()
await userEvent.type(input, prop.key)

expect(input).toHaveValue(prop.key)
expect(screen.getByText(prop.key)).toBeInTheDocument()
expect(screen.queryByText('key1')).not.toBeInTheDocument()
expect(screen.getByText(prop.value)).toBeInTheDocument()

// search according the value
const dropdown = screen.getByTestId('attribute-select-toggle')
await userEvent.click(dropdown)
await userEvent.click(screen.getAllByText('Value')[0] as HTMLElement)

await userEvent.clear(input)
await userEvent.type(input, prop.key)
expect(screen.getByText('No results found.')).toBeInTheDocument()

await userEvent.clear(input)
await userEvent.type(input, prop.value)
await waitFor(() => {
expect(screen.getByText(prop.key)).toBeInTheDocument()
})

expect(screen.queryByText('key1')).not.toBeInTheDocument()
expect(screen.getByText(prop.value)).toBeInTheDocument()
})

test('Properties can be sorted', async () => {
renderSysProps()
const changeOrder = async (header: string) => {
const element = within(screen.getByTestId(header)).getByRole('button')

expect(element).toBeInTheDocument()

await userEvent.click(element)
await userEvent.click(element)
}
const testProperty = (index: number, expected: SystemProperty) => {
expect(within(screen.getByTestId('row' + index)).getAllByText(expected.key)[0]).toBeInTheDocument()
expect(within(screen.getByTestId('row' + index)).getAllByText(expected.value)[0]).toBeInTheDocument()
}

await waitFor(() => {
expect(screen.getByText('value1')).toBeInTheDocument()
})
await changeOrder('name-header')
testProperty(0, getMockedProperties()[2] as SystemProperty)
testProperty(2, getMockedProperties()[0] as SystemProperty)
await changeOrder('value-header')
testProperty(0, getMockedProperties()[2] as SystemProperty)
// testProperty(2, getMockedProperties()[0] as SystemProperty)
})
})
Loading
Loading