Skip to content

Commit

Permalink
Add App JMX plugin example
Browse files Browse the repository at this point in the history
This example demonstrates how you can create a plugin that reuses the views
from the builtin JMX plugin (Attributes, Operations, and Chart) into your own
plugin.
  • Loading branch information
tadayosi committed Mar 23, 2024
1 parent 6150724 commit 717c0ec
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 0 deletions.
3 changes: 3 additions & 0 deletions sample-plugin/craco.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ module.exports = {
login = false
res.redirect('/hawtio/login')
})
devServer.app.get('/hawtio/auth/config', (_, res) => {
res.send('{}')
})
devServer.app.get('/hawtio/proxy/enabled', (_, res) => res.send(String(proxyEnabled)))
devServer.app.get('/hawtio/plugin', (_, res) => res.send(JSON.stringify(plugin)))

Expand Down
1 change: 1 addition & 0 deletions sample-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@patternfly/react-table": "~4.113.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1"
},
"devDependencies": {
Expand Down
50 changes: 50 additions & 0 deletions sample-plugin/src/sample-plugin/app-jmx/AppJmx.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.app-jmx-split {
display: flex;
flex-direction: row;
}

.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}

.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
cursor: col-resize;
}

#app-jmx-tree-view .pf-c-tree-view__list {
height: 100%;
}

#app-jmx-tree-view .pf-c-tree-view__node {
padding-top: 1px;
padding-bottom: 1px;
}

#app-jmx-tree-view .pf-c-tree-view__node-toggle {
padding-top: 0px;
padding-bottom: 0px;
padding-left: 6px;
padding-right: 6px;
}

#app-jmx-tree-view .pf-c-tree-view__node-text {
padding-top: 2px;
font-size: smaller;
}

#app-jmx-tree-view .pf-c-tree-view__node-icon {
flex-shrink: 0;
}

#app-jmx-tree-view .pf-c-tree-view__node-text {
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

#app-jmx-content-main>article {
overflow: auto;
}
127 changes: 127 additions & 0 deletions sample-plugin/src/sample-plugin/app-jmx/AppJmx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Attributes, Chart, MBeanNode, Operations } from '@hawtio/react'
import {
EmptyState,
EmptyStateIcon,
Nav,
NavItem,
NavList,
PageGroup,
PageNavigation,
PageSection,
Spinner,
Text,
Title,
TreeView,
TreeViewProps
} from '@patternfly/react-core'
import { CubesIcon } from '@patternfly/react-icons'
import React, { useContext } from 'react'
import { NavLink, Navigate, Route, Routes, useLocation } from 'react-router-dom'
import Split from 'react-split'
import './AppJmx.css'
import { AppJmxContext, useAppJmx } from './context'
import { pluginPath } from './globals'

export const AppJmx: React.FunctionComponent = () => {
const { tree, loaded, selectedNode, setSelectedNode } = useAppJmx()

if (!loaded) {
return (
<PageSection>
<Spinner isSVG aria-label='Loading custom tree' />
</PageSection>
)
}

// You can use Split.js to implement a split view.
// For more information, see: https://split.js.org/
return (
<AppJmxContext.Provider value={{ tree, selectedNode, setSelectedNode }}>
<Split className='app-jmx-split' sizes={[30, 70]} minSize={200} gutterSize={5}>
<div>
<AppJmxTreeView />
</div>
<div>
<AppJmxContent />
</div>
</Split>
</AppJmxContext.Provider>
)
}

const AppJmxTreeView: React.FunctionComponent = () => {
const { tree, selectedNode, setSelectedNode } = useContext(AppJmxContext)

const onSelect: TreeViewProps['onSelect'] = (_, item) => {
setSelectedNode(item as MBeanNode)
}

return (
<TreeView
id='app-jmx-tree-view'
data={tree.getTree()}
hasGuides={true}
hasSelectableNodes={true}
onSelect={onSelect}
activeItems={selectedNode ? [selectedNode] : []}
/>
)
}

const AppJmxContent: React.FunctionComponent = () => {
const { selectedNode } = useContext(AppJmxContext)
const { pathname, search } = useLocation()

if (!selectedNode) {
return (
<PageSection variant='light' isFilled>
<EmptyState variant='full'>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel='h1' size='lg'>
Select MBean
</Title>
</EmptyState>
</PageSection>
)
}

const navItems = [
{ id: 'attributes', title: 'Attributes', component: Attributes },
{ id: 'operations', title: 'Operations', component: Operations },
{ id: 'chart', title: 'Chart', component: Chart }
]

const mbeanNav = (
<Nav aria-label='MBean Nav' variant='tertiary'>
<NavList>
{navItems.map(nav => (
<NavItem key={nav.id} isActive={pathname === `${pluginPath}/${nav.id}`}>
<NavLink to={{ pathname: nav.id, search }}>{nav.title}</NavLink>
</NavItem>
))}
</NavList>
</Nav>
)

const mbeanRoutes = navItems.map(nav => (
<Route key={nav.id} path={nav.id} element={React.createElement(nav.component)} />
))

return (
<React.Fragment>
<PageGroup>
<PageSection id='app-jmx-content-header' variant='light'>
<Title headingLevel='h1'>{selectedNode.name}</Title>
<Text component='small'>{selectedNode.objectName}</Text>
</PageSection>
<PageNavigation>{mbeanNav}</PageNavigation>
</PageGroup>
<PageSection id='app-jmx-content-main'>
<Routes>
{mbeanRoutes}
<Route key='root' path='/' element={<Navigate to={navItems[0]?.id ?? ''} />} />
</Routes>
</PageSection>
</React.Fragment>
)
}
56 changes: 56 additions & 0 deletions sample-plugin/src/sample-plugin/app-jmx/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { EVENT_REFRESH, MBeanNode, MBeanTree, PluginNodeSelectionContext, eventService, workspace } from '@hawtio/react'
import { createContext, useContext, useEffect, useState } from 'react'
import { jmxDomain, pluginName } from './globals'

/**
* Custom React hook for using the plugin-specific JMX MBean tree.
*/
export function useAppJmx() {
const [tree, setTree] = useState(MBeanTree.createEmpty(pluginName))
const [loaded, setLoaded] = useState(false)
const { selectedNode, setSelectedNode } = useContext(PluginNodeSelectionContext)

useEffect(() => {
const loadTree = async () => {
const tree = await populateTree()
setTree(tree)
setLoaded(true)
}
loadTree()

const listener = () => {
setLoaded(false)
loadTree()
}
eventService.onRefresh(listener)

return () => eventService.removeListener(EVENT_REFRESH, listener)
}, [])

return { tree, loaded, selectedNode, setSelectedNode }
}

async function populateTree(): Promise<MBeanTree> {
const tree = await workspace.getTree()
const root = tree.find(node => node.name === jmxDomain)
if (!root || !root.children || root.children.length === 0) {
return MBeanTree.createEmpty(pluginName)
}

return MBeanTree.createFromNodes(pluginName, [root])
}

type AppJmxContext = {
tree: MBeanTree
selectedNode: MBeanNode | null
setSelectedNode: (selected: MBeanNode | null) => void
}

// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AppJmxContext = createContext<AppJmxContext>({
tree: MBeanTree.createEmpty(pluginName),
selectedNode: null,
setSelectedNode: () => {
// no-op
},
})
9 changes: 9 additions & 0 deletions sample-plugin/src/sample-plugin/app-jmx/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Logger } from '@hawtio/react'

export const pluginName = 'app-jmx'
export const pluginTitle = 'App JMX'
export const pluginPath = '/app-jmx'

export const log = Logger.get(pluginName)

export const jmxDomain = 'java.lang'
20 changes: 20 additions & 0 deletions sample-plugin/src/sample-plugin/app-jmx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HawtioPlugin, hawtio } from '@hawtio/react'
import { log, pluginName, pluginPath, pluginTitle } from './globals'
import { AppJmx } from './AppJmx'

/**
* This example demonstrates how you can create a plugin that reuses the views
* from the builtin JMX plugin (Attributes, Operations, and Chart) into your own
* plugin.
*/
export const appJmx: HawtioPlugin = () => {
log.info('Loading', pluginName)

hawtio.addPlugin({
id: pluginName,
title: pluginTitle,
path: pluginPath,
component: AppJmx,
isActive: async () => true,
})
}
2 changes: 2 additions & 0 deletions sample-plugin/src/sample-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HawtioPlugin, configManager } from '@hawtio/react'
import { appJmx } from './app-jmx'
import { customTree } from './custom-tree'
import { simple } from './simple'

Expand All @@ -19,6 +20,7 @@ import { simple } from './simple'
export const plugin: HawtioPlugin = () => {
simple()
customTree()
appJmx()
}

// Register the custom plugin version to Hawtio
Expand Down
1 change: 1 addition & 0 deletions sample-plugin/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9041,6 +9041,7 @@ __metadata:
path-browserify: ^1.0.1
react: ^18.2.0
react-dom: ^18.2.0
react-router-dom: ^6.22.3
react-scripts: ^5.0.1
replace: ^1.2.2
languageName: unknown
Expand Down

0 comments on commit 717c0ec

Please sign in to comment.