-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(connect): add Discover tab - by Jolokia Discovery MBean
- Loading branch information
Showing
11 changed files
with
493 additions
and
26 deletions.
There are no files selected for viewing
203 changes: 201 additions & 2 deletions
203
packages/hawtio/src/plugins/connect/discover/Discover.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,204 @@ | ||
import React from 'react' | ||
import { HawtioEmptyCard, HawtioLoadingCard, connectService } from '@hawtiosrc/plugins/shared' | ||
import { formatTimestamp } from '@hawtiosrc/util/dates' | ||
import { | ||
Button, | ||
Card, | ||
CardActions, | ||
CardBody, | ||
CardHeader, | ||
CardTitle, | ||
DescriptionList, | ||
DescriptionListDescription, | ||
DescriptionListGroup, | ||
DescriptionListTerm, | ||
Gallery, | ||
SearchInput, | ||
Text, | ||
Toolbar, | ||
ToolbarContent, | ||
ToolbarItem, | ||
} from '@patternfly/react-core' | ||
import React, { useContext, useEffect, useState } from 'react' | ||
import { ADD, UPDATE } from '../connections' | ||
import { ConnectContext } from '../context' | ||
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' | ||
|
||
export const Discover: React.FunctionComponent = () => { | ||
return null | ||
const [discoverable, setDiscoverable] = useState(false) | ||
const [discovering, setDiscovering] = useState(true) | ||
const [agents, setAgents] = useState<Agent[]>([]) | ||
|
||
// Filter | ||
const [filter, setFilter] = useState('') | ||
const [filteredAgents, setFilteredAgents] = useState<Agent[]>([]) | ||
|
||
useEffect(() => { | ||
if (!discovering) { | ||
return | ||
} | ||
|
||
const isDiscoverable = async () => { | ||
const discoverable = await discoverService.isDiscoverable() | ||
setDiscoverable(discoverable) | ||
setDiscovering(false) | ||
} | ||
isDiscoverable() | ||
|
||
setDiscovering(true) | ||
const discoverAgents = async () => { | ||
const agents = await discoverService.discoverAgents() | ||
log.debug('Discover - agents:', agents) | ||
setAgents(agents) | ||
setFilteredAgents(agents) | ||
setDiscovering(false) | ||
} | ||
discoverAgents() | ||
}, [discovering]) | ||
|
||
if (discovering) { | ||
return <HawtioLoadingCard message='Please wait, discovering agents...' /> | ||
} | ||
|
||
if (!discoverable) { | ||
return <HawtioEmptyCard message='Agent discovery is not available' /> | ||
} | ||
|
||
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 clearFilter = () => { | ||
setFilter('') | ||
setFilteredAgents(agents) | ||
} | ||
|
||
const refresh = () => { | ||
setDiscovering(true) | ||
} | ||
|
||
const toolbar = ( | ||
<Toolbar id='connect-discover-toolbar'> | ||
<ToolbarContent> | ||
<ToolbarItem id='connect-discover-toolbar-filter'> | ||
<SearchInput | ||
id='connect-discover-toolbar-filter-input' | ||
aria-label='Filter Agents' | ||
placeholder='Filter agents...' | ||
value={filter} | ||
onChange={(_, value) => setFilter(value)} | ||
onSearch={applyFilter} | ||
onClear={clearFilter} | ||
/> | ||
</ToolbarItem> | ||
<ToolbarItem variant='separator' /> | ||
<ToolbarItem> | ||
<Button variant='secondary' onClick={refresh} isSmall> | ||
Refresh | ||
</Button> | ||
</ToolbarItem> | ||
</ToolbarContent> | ||
</Toolbar> | ||
) | ||
|
||
return ( | ||
<React.Fragment> | ||
<Card style={{ marginBottom: '1rem' }}>{toolbar}</Card> | ||
<Gallery hasGutter minWidths={{ default: '400px' }}> | ||
{filteredAgents.map((agent, index) => ( | ||
<AgentCard key={`agent-${index}-${agent.agent_id}`} agent={agent} /> | ||
))} | ||
</Gallery> | ||
</React.Fragment> | ||
) | ||
} | ||
|
||
const PRODUCT_LOGO: Record<string, string> = { | ||
jetty: jettyLogo, | ||
tomcat: tomcatLogo, | ||
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) | ||
} | ||
|
||
const productLogo = (agent: Agent) => { | ||
return PRODUCT_LOGO[agent.server_product?.toLowerCase() ?? 'generic'] ?? PRODUCT_LOGO.generic | ||
} | ||
|
||
return ( | ||
<Card isCompact id={`connect-discover-agent-card-${agent.agent_id}`}> | ||
<CardHeader> | ||
<img src={productLogo(agent)} alt={agent.server_product} style={{ maxWidth: '30px', paddingRight: '0.5rem' }} /> | ||
{discoverService.hasName(agent) && ( | ||
<CardTitle> | ||
{agent.server_vendor} {agent.server_product} {agent.server_version} | ||
</CardTitle> | ||
)} | ||
{agent.command && ( | ||
<CardTitle> | ||
<Text component='pre'>{agent.command}</Text> | ||
</CardTitle> | ||
)} | ||
<CardActions> | ||
<Button variant='primary' onClick={connect} isSmall> | ||
Connect | ||
</Button> | ||
</CardActions> | ||
</CardHeader> | ||
<CardBody> | ||
<DescriptionList isCompact isHorizontal> | ||
<DescriptionListGroup> | ||
<DescriptionListTerm>Agent ID</DescriptionListTerm> | ||
<DescriptionListDescription>{agent.agent_id}</DescriptionListDescription> | ||
</DescriptionListGroup> | ||
<DescriptionListGroup> | ||
<DescriptionListTerm>Agent Version</DescriptionListTerm> | ||
<DescriptionListDescription>{agent.agent_version}</DescriptionListDescription> | ||
</DescriptionListGroup> | ||
<DescriptionListGroup> | ||
<DescriptionListTerm>Agent Description</DescriptionListTerm> | ||
<DescriptionListDescription>{agent.agent_description}</DescriptionListDescription> | ||
</DescriptionListGroup> | ||
{agent.startTime && ( | ||
<DescriptionListGroup> | ||
<DescriptionListTerm>JVM Started</DescriptionListTerm> | ||
<DescriptionListDescription>{formatTimestamp(new Date(agent.startTime))}</DescriptionListDescription> | ||
</DescriptionListGroup> | ||
)} | ||
{agent.url && ( | ||
<DescriptionListGroup> | ||
<DescriptionListTerm>Agent URL</DescriptionListTerm> | ||
<DescriptionListDescription> | ||
<Text component='a' href={agent.url} target='_blank'> | ||
{agent.url} | ||
</Text> | ||
</DescriptionListDescription> | ||
</DescriptionListGroup> | ||
)} | ||
</DescriptionList> | ||
</CardBody> | ||
</Card> | ||
) | ||
} |
90 changes: 90 additions & 0 deletions
90
packages/hawtio/src/plugins/connect/discover/discover-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { Connection, INITIAL_CONNECTION, connectService, jolokiaService, workspace } from '@hawtiosrc/plugins/shared' | ||
import { isBlank } from '@hawtiosrc/util/strings' | ||
import { log } from '../globals' | ||
|
||
/** | ||
* @see https://jolokia.org/reference/html/mbeans.html#mbean-discovery | ||
*/ | ||
export type Agent = { | ||
// Properties from Jolokia API | ||
agent_id?: string | ||
agent_description?: string | ||
agent_version?: string | ||
url?: string | ||
secured?: boolean | ||
server_vendor?: string | ||
server_product?: string | ||
server_version?: string | ||
|
||
// Properties that Hawtio attaches | ||
startTime?: number | ||
command?: string | ||
} | ||
|
||
class DiscoverService { | ||
async isDiscoverable(): Promise<boolean> { | ||
return (await this.hasLocalMBean()) || (await this.hasDiscoveryMBean()) | ||
} | ||
|
||
private hasLocalMBean(): Promise<boolean> { | ||
return workspace.treeContainsDomainAndProperties('hawtio', { type: 'JVMList' }) | ||
} | ||
|
||
private hasDiscoveryMBean(): Promise<boolean> { | ||
return workspace.treeContainsDomainAndProperties('jolokia', { type: 'Discovery' }) | ||
} | ||
|
||
async discoverAgents(): Promise<Agent[]> { | ||
// 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 | ||
} | ||
|
||
return [] | ||
} | ||
|
||
private async fetchMoreJvmDetails(agents: Agent[]) { | ||
for (const agent of agents) { | ||
if (!agent.url || agent.secured) { | ||
continue | ||
} | ||
// One-off Jolokia instance to connect to the agent | ||
const jolokia = connectService.createJolokia(this.toConnection(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 | ||
agent.command = jolokia.getAttribute('java.lang:type=Runtime', 'SystemProperties', 'sun.java.command') as string | ||
} | ||
} | ||
} | ||
|
||
toConnection(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) | ||
return conn | ||
} | ||
|
||
const url = new URL(agent.url) | ||
conn.scheme = url.protocol.substring(0, url.protocol.length - 1) // strip last ':' | ||
conn.host = url.hostname | ||
conn.port = parseInt(url.port) | ||
conn.path = url.pathname | ||
|
||
log.debug('Discover - connection from agent:', conn) | ||
return conn | ||
} | ||
|
||
hasName(agent: Agent): boolean { | ||
return [agent.server_vendor, agent.server_product, agent.server_version].every(s => !isBlank(s)) | ||
} | ||
} | ||
|
||
export const discoverService = new DiscoverService() |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.