From 717c0eceb8a2dedb0b1b76f80862fe355e15226f Mon Sep 17 00:00:00 2001 From: Tadayoshi Sato Date: Sat, 23 Mar 2024 21:13:26 +0900 Subject: [PATCH] Add App JMX plugin example 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. --- sample-plugin/craco.config.js | 3 + sample-plugin/package.json | 1 + .../src/sample-plugin/app-jmx/AppJmx.css | 50 +++++++ .../src/sample-plugin/app-jmx/AppJmx.tsx | 127 ++++++++++++++++++ .../src/sample-plugin/app-jmx/context.ts | 56 ++++++++ .../src/sample-plugin/app-jmx/globals.ts | 9 ++ .../src/sample-plugin/app-jmx/index.ts | 20 +++ sample-plugin/src/sample-plugin/index.ts | 2 + sample-plugin/yarn.lock | 1 + 9 files changed, 269 insertions(+) create mode 100644 sample-plugin/src/sample-plugin/app-jmx/AppJmx.css create mode 100644 sample-plugin/src/sample-plugin/app-jmx/AppJmx.tsx create mode 100644 sample-plugin/src/sample-plugin/app-jmx/context.ts create mode 100644 sample-plugin/src/sample-plugin/app-jmx/globals.ts create mode 100644 sample-plugin/src/sample-plugin/app-jmx/index.ts diff --git a/sample-plugin/craco.config.js b/sample-plugin/craco.config.js index e8776d5..2453c24 100644 --- a/sample-plugin/craco.config.js +++ b/sample-plugin/craco.config.js @@ -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))) diff --git a/sample-plugin/package.json b/sample-plugin/package.json index 2edc715..2ab9fc4 100644 --- a/sample-plugin/package.json +++ b/sample-plugin/package.json @@ -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": { diff --git a/sample-plugin/src/sample-plugin/app-jmx/AppJmx.css b/sample-plugin/src/sample-plugin/app-jmx/AppJmx.css new file mode 100644 index 0000000..c04fade --- /dev/null +++ b/sample-plugin/src/sample-plugin/app-jmx/AppJmx.css @@ -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; +} diff --git a/sample-plugin/src/sample-plugin/app-jmx/AppJmx.tsx b/sample-plugin/src/sample-plugin/app-jmx/AppJmx.tsx new file mode 100644 index 0000000..0d70723 --- /dev/null +++ b/sample-plugin/src/sample-plugin/app-jmx/AppJmx.tsx @@ -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 ( + + + + ) + } + + // You can use Split.js to implement a split view. + // For more information, see: https://split.js.org/ + return ( + + +
+ +
+
+ +
+
+
+ ) +} + +const AppJmxTreeView: React.FunctionComponent = () => { + const { tree, selectedNode, setSelectedNode } = useContext(AppJmxContext) + + const onSelect: TreeViewProps['onSelect'] = (_, item) => { + setSelectedNode(item as MBeanNode) + } + + return ( + + ) +} + +const AppJmxContent: React.FunctionComponent = () => { + const { selectedNode } = useContext(AppJmxContext) + const { pathname, search } = useLocation() + + if (!selectedNode) { + return ( + + + + + Select MBean + + + + ) + } + + const navItems = [ + { id: 'attributes', title: 'Attributes', component: Attributes }, + { id: 'operations', title: 'Operations', component: Operations }, + { id: 'chart', title: 'Chart', component: Chart } + ] + + const mbeanNav = ( + + ) + + const mbeanRoutes = navItems.map(nav => ( + + )) + + return ( + + + + {selectedNode.name} + {selectedNode.objectName} + + {mbeanNav} + + + + {mbeanRoutes} + } /> + + + + ) +} diff --git a/sample-plugin/src/sample-plugin/app-jmx/context.ts b/sample-plugin/src/sample-plugin/app-jmx/context.ts new file mode 100644 index 0000000..cfcaa62 --- /dev/null +++ b/sample-plugin/src/sample-plugin/app-jmx/context.ts @@ -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 { + 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({ + tree: MBeanTree.createEmpty(pluginName), + selectedNode: null, + setSelectedNode: () => { + // no-op + }, +}) diff --git a/sample-plugin/src/sample-plugin/app-jmx/globals.ts b/sample-plugin/src/sample-plugin/app-jmx/globals.ts new file mode 100644 index 0000000..d55bdf8 --- /dev/null +++ b/sample-plugin/src/sample-plugin/app-jmx/globals.ts @@ -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' diff --git a/sample-plugin/src/sample-plugin/app-jmx/index.ts b/sample-plugin/src/sample-plugin/app-jmx/index.ts new file mode 100644 index 0000000..eef1e5f --- /dev/null +++ b/sample-plugin/src/sample-plugin/app-jmx/index.ts @@ -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, + }) +} diff --git a/sample-plugin/src/sample-plugin/index.ts b/sample-plugin/src/sample-plugin/index.ts index 42d36f0..6be052a 100644 --- a/sample-plugin/src/sample-plugin/index.ts +++ b/sample-plugin/src/sample-plugin/index.ts @@ -1,4 +1,5 @@ import { HawtioPlugin, configManager } from '@hawtio/react' +import { appJmx } from './app-jmx' import { customTree } from './custom-tree' import { simple } from './simple' @@ -19,6 +20,7 @@ import { simple } from './simple' export const plugin: HawtioPlugin = () => { simple() customTree() + appJmx() } // Register the custom plugin version to Hawtio diff --git a/sample-plugin/yarn.lock b/sample-plugin/yarn.lock index 5ab7c07..c71b48f 100644 --- a/sample-plugin/yarn.lock +++ b/sample-plugin/yarn.lock @@ -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