diff --git a/packages/hawtio/src/plugins/connect/discover/Discover.tsx b/packages/hawtio/src/plugins/connect/discover/Discover.tsx index 721024358..ce5183d0d 100644 --- a/packages/hawtio/src/plugins/connect/discover/Discover.tsx +++ b/packages/hawtio/src/plugins/connect/discover/Discover.tsx @@ -1,10 +1,13 @@ -import { HawtioEmptyCard, HawtioLoadingCard, connectService } from '@hawtiosrc/plugins/shared' +import { Connection, HawtioEmptyCard, HawtioLoadingCard, connectService } from '@hawtiosrc/plugins/shared' import { formatTimestamp } from '@hawtiosrc/util/dates' import { + ActionList, + ActionListItem, Button, Card, CardActions, CardBody, + CardFooter, CardHeader, CardTitle, DescriptionList, @@ -12,10 +15,15 @@ import { DescriptionListGroup, DescriptionListTerm, Gallery, + Label, SearchInput, + Select, + SelectOption, + SelectProps, Text, Toolbar, ToolbarContent, + ToolbarGroup, ToolbarItem, } from '@patternfly/react-core' import React, { useContext, useEffect, useState } from 'react' @@ -25,82 +33,158 @@ import { log } from '../globals' import javaLogo from '../img/java-logo.svg' import jettyLogo from '../img/jetty-logo.svg' import tomcatLogo from '../img/tomcat-logo.svg' -import { Agent, discoverService } from './discover-service' +import { Agent, Jvm, discoverService } from './discover-service' export const Discover: React.FunctionComponent = () => { - const [discoverable, setDiscoverable] = useState(false) + const { connections, dispatch } = useContext(ConnectContext) + + const [agentDiscoverable, setAgentDiscoverable] = useState(false) + const [jvmListable, setJvmListable] = useState(false) const [discovering, setDiscovering] = useState(true) const [agents, setAgents] = useState([]) + const [jvms, setJvms] = useState([]) // Filter const [filter, setFilter] = useState('') + const [label, setLabel] = useState<'Agent' | 'JVM'>('Agent') + const [isSelectLabelOpen, setIsSelectLabelOpen] = useState(false) const [filteredAgents, setFilteredAgents] = useState([]) + const [filteredJvms, setFilteredJvms] = useState([]) useEffect(() => { - if (!discovering) { - return - } - const isDiscoverable = async () => { - const discoverable = await discoverService.isDiscoverable() - setDiscoverable(discoverable) - setDiscovering(false) + const discoverable = await discoverService.hasDiscoveryMBean() + setAgentDiscoverable(discoverable) + const listable = await discoverService.hasLocalMBean() + setJvmListable(listable) + + if (!discoverable && listable) { + setLabel('JVM') + } + + if (!discoverable && !listable) { + setDiscovering(false) + } } isDiscoverable() + }, []) + + useEffect(() => { + if (!discovering) { + return + } - setDiscovering(true) - const discoverAgents = async () => { + const discover = async () => { const agents = await discoverService.discoverAgents() log.debug('Discover - agents:', agents) setAgents(agents) setFilteredAgents(agents) + + const jvms = await discoverService.listJvms() + log.debug('Discover - JVMs:', jvms) + setJvms(jvms) + setFilteredJvms(jvms) + setDiscovering(false) } - discoverAgents() + discover() }, [discovering]) + if (!agentDiscoverable && !jvmListable) { + return + } + if (discovering) { return } - if (!discoverable) { - return + const selectLabel: SelectProps['onSelect'] = (_, value) => { + setLabel(value as typeof label) + setIsSelectLabelOpen(!isSelectLabelOpen) } const applyFilter = () => { - const filtered = agents.filter(agent => - Object.values(agent).some(value => typeof value === 'string' && value.includes(filter)), - ) - log.debug('Discover - apply filter:', filter, filtered) - setFilteredAgents(filtered) + const matchesIgnoringCase = (value: unknown) => + typeof value === 'string' && value.toLowerCase().includes(filter.toLowerCase()) + + const filteredAgents = agents.filter(agent => Object.values(agent).some(matchesIgnoringCase)) + setFilteredAgents(filteredAgents) + + const filteredJvms = jvms.filter(jvm => Object.values(jvm).some(matchesIgnoringCase)) + setFilteredJvms(filteredJvms) + + log.debug('Discover - apply filter:', filter, 'agents:', filteredAgents, 'JVMs:', filteredJvms) } const clearFilter = () => { setFilter('') setFilteredAgents(agents) + setFilteredJvms(jvms) + } + + const reset = () => { + setAgents([]) + setFilteredAgents([]) + setJvms([]) + setFilteredJvms([]) } - const refresh = () => { - setDiscovering(true) + const refresh = (delay = false) => { + reset() + if (delay) { + // Delay refreshing to show users a pseudo-sense of updating + setTimeout(() => setDiscovering(true), 100) + } else { + setDiscovering(true) + } + } + + const connect = (conn: Connection) => { + log.debug('Discover - connect to:', conn) + + // Save the connection before connecting + if (connections[conn.name]) { + dispatch({ type: UPDATE, name: conn.name, connection: conn }) + } else { + dispatch({ type: ADD, connection: conn }) + } + + connectService.connect(conn) } const toolbar = ( - - setFilter(value)} - onSearch={applyFilter} - onClear={clearFilter} - /> - + + + + + + setFilter(value)} + onSearch={applyFilter} + onClear={clearFilter} + /> + + - @@ -112,9 +196,14 @@ export const Discover: React.FunctionComponent = () => { {toolbar} - {filteredAgents.map((agent, index) => ( - - ))} + {label === 'Agent' && + filteredAgents.map((agent, index) => ( + + ))} + {label === 'JVM' && + filteredJvms.map((jvm, index) => ( + + ))} ) @@ -126,45 +215,30 @@ const PRODUCT_LOGO: Record = { generic: javaLogo, } -export const AgentCard: React.FunctionComponent<{ agent: Agent }> = ({ agent }) => { - const { connections, dispatch } = useContext(ConnectContext) - - const connect = () => { - const conn = discoverService.toConnection(agent) - log.debug('Discover - connect to:', conn) - - // Save the connection before connecting - if (connections[conn.name]) { - dispatch({ type: UPDATE, name: conn.name, connection: conn }) - } else { - dispatch({ type: ADD, connection: conn }) - } - - connectService.connect(conn) - } - +export const AgentCard: React.FunctionComponent<{ + agent: Agent + connect: (conn: Connection) => void +}> = ({ agent, connect }) => { const productLogo = (agent: Agent) => { return PRODUCT_LOGO[agent.server_product?.toLowerCase() ?? 'generic'] ?? PRODUCT_LOGO.generic } + const title = discoverService.hasName(agent) + ? `${agent.server_vendor} ${agent.server_product} ${agent.server_version}` + : agent.agent_id + return ( {agent.server_product} - {discoverService.hasName(agent) && ( - - {agent.server_vendor} {agent.server_product} {agent.server_version} - - )} + {title} {agent.command && ( {agent.command} )} - + @@ -199,6 +273,91 @@ export const AgentCard: React.FunctionComponent<{ agent: Agent }> = ({ agent }) )} + + + + + ) +} + +export const JvmCard: React.FunctionComponent<{ + jvm: Jvm + connect: (conn: Connection) => void + refresh: () => void +}> = ({ jvm, connect, refresh }) => { + const stopAgent = () => { + discoverService.stopAgent(jvm.id) + refresh() + } + + const startAgent = () => { + discoverService.startAgent(jvm.id) + refresh() + } + + return ( + + + {jvm.alias} + + {jvm.alias} + + + + + + + + + PID + {jvm.id} + + + Name + {jvm.displayName} + + {jvm.agentUrl && ( + + Agent URL + + + {jvm.agentUrl} + + + + )} + + + + + + + + {jvm.agentUrl && ( + + + + + + + + + )} + + ) } diff --git a/packages/hawtio/src/plugins/connect/discover/discover-service.ts b/packages/hawtio/src/plugins/connect/discover/discover-service.ts index b690c78bc..3e928f0a3 100644 --- a/packages/hawtio/src/plugins/connect/discover/discover-service.ts +++ b/packages/hawtio/src/plugins/connect/discover/discover-service.ts @@ -21,33 +21,52 @@ export type Agent = { command?: string } +/** + * @see https://github.com/hawtio/hawtio/blob/3.x/plugins/hawtio-local-jvm-mbean/src/main/java/io/hawt/jvm/local/VMDescriptorDTO.java + */ +export type Jvm = { + id: string + alias: string + displayName: string + agentUrl: string | null + port: number + hostname: string | null + scheme: string | null + path: string | null +} + class DiscoverService { - async isDiscoverable(): Promise { - return (await this.hasLocalMBean()) || (await this.hasDiscoveryMBean()) + hasDiscoveryMBean(): Promise { + return workspace.treeContainsDomainAndProperties('jolokia', { type: 'Discovery' }) } - private hasLocalMBean(): Promise { + hasLocalMBean(): Promise { return workspace.treeContainsDomainAndProperties('hawtio', { type: 'JVMList' }) } - private hasDiscoveryMBean(): Promise { - return workspace.treeContainsDomainAndProperties('jolokia', { type: 'Discovery' }) - } - async discoverAgents(): Promise { // Jolokia 1.x: 'jolokia:type=Discovery' // Jolokia 2.x: 'jolokia:type=Discovery,agent=...' const discoveryMBean = (await workspace.findMBeans('jolokia', { type: 'Discovery' }))[0] - if (discoveryMBean && discoveryMBean.objectName) { - // Use 10 sec timeout - const agents = (await jolokiaService.execute(discoveryMBean.objectName, 'lookupAgentsWithTimeout(int)', [ - 10 * 1000, - ])) as Agent[] - await this.fetchMoreJvmDetails(agents) - return agents + if (!discoveryMBean?.objectName) { + return [] } - return [] + // Use 10 sec timeout + const agents = (await jolokiaService.execute(discoveryMBean.objectName, 'lookupAgentsWithTimeout(int)', [ + 10 * 1000, + ])) as Agent[] + await this.fetchMoreJvmDetails(agents) + return agents + } + + async listJvms(): Promise { + const jvmListMBean = (await workspace.findMBeans('hawtio', { type: 'JVMList' }))[0] + if (!jvmListMBean?.objectName) { + return [] + } + + return (await jolokiaService.execute(jvmListMBean.objectName, 'listLocalJVMs()')) as Jvm[] } private async fetchMoreJvmDetails(agents: Agent[]) { @@ -56,7 +75,7 @@ class DiscoverService { continue } // One-off Jolokia instance to connect to the agent - const jolokia = connectService.createJolokia(this.toConnection(agent)) + const jolokia = connectService.createJolokia(this.agentToConnection(agent)) agent.startTime = jolokia.getAttribute('java.lang:type=Runtime', 'StartTime') as number if (!this.hasName(agent)) { // Only look for command if agent vm is not known @@ -65,7 +84,11 @@ class DiscoverService { } } - toConnection(agent: Agent): Connection { + hasName(agent: Agent): boolean { + return [agent.server_vendor, agent.server_product, agent.server_version].every(s => !isBlank(s)) + } + + agentToConnection(agent: Agent): Connection { const conn = { ...INITIAL_CONNECTION, name: agent.agent_description ?? `discover-${agent.agent_id}` } if (!agent.url) { log.warn('No URL available to connect to agent:', agent) @@ -82,8 +105,41 @@ class DiscoverService { return conn } - hasName(agent: Agent): boolean { - return [agent.server_vendor, agent.server_product, agent.server_version].every(s => !isBlank(s)) + jvmToConnection(jvm: Jvm): Connection { + const conn = { ...INITIAL_CONNECTION, name: `local-${jvm.port}` } + if (!jvm.scheme || !jvm.hostname || jvm.port === 0 || !jvm.path) { + log.warn('Lack of information to connect to JVM:', jvm) + return conn + } + + conn.scheme = jvm.scheme + conn.host = jvm.hostname + conn.port = jvm.port + conn.path = jvm.path + log.debug('Discover - connection from JVM:', conn) + return conn + } + + isConnectable(jvm: Jvm): boolean { + return [jvm.scheme, jvm.hostname, jvm.path].every(s => s && !isBlank(s)) && jvm.port !== 0 + } + + async stopAgent(pid: string) { + const jvmListMBean = (await workspace.findMBeans('hawtio', { type: 'JVMList' }))[0] + if (!jvmListMBean?.objectName) { + return + } + log.debug('Discover - stop JVM agent:', jvmListMBean, pid) + await jolokiaService.execute(jvmListMBean.objectName, 'stopAgent(java.lang.String)', [pid]) + } + + async startAgent(pid: string) { + const jvmListMBean = (await workspace.findMBeans('hawtio', { type: 'JVMList' }))[0] + if (!jvmListMBean?.objectName) { + return + } + log.debug('Discover - start JVM agent:', jvmListMBean, pid) + await jolokiaService.execute(jvmListMBean.objectName, 'startAgent(java.lang.String)', [pid]) } }