Skip to content

Commit

Permalink
feat(connect): Improve remote connections handling (closes #906) (#932)
Browse files Browse the repository at this point in the history
Signed-off-by: Grzegorz Grzybek <gr.grzybek@gmail.com>
  • Loading branch information
grgrzybek committed May 9, 2024
1 parent 95be600 commit e8ad5b3
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 89 deletions.
2 changes: 1 addition & 1 deletion packages/hawtio/src/core/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class ConfigManager {
async filterEnabledPlugins(plugins: Plugin[]): Promise<Plugin[]> {
const enabledPlugins: Plugin[] = []
for (const plugin of plugins) {
if (await this.isRouteEnabled(plugin.path)) {
if ((plugin.path == null && (await plugin.isActive())) || (await this.isRouteEnabled(plugin.path!))) {
enabledPlugins.push(plugin)
} else {
log.debug(`Plugin "${plugin.id}" disabled by hawtconfig.json`)
Expand Down
20 changes: 17 additions & 3 deletions packages/hawtio/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,20 @@ export function isUniversalHeaderItem(item: HeaderItem): item is UniversalHeader
* Internal representation of a Hawtio plugin.
*/
export interface Plugin {
/**
* Mandatory, unique plugin identifier
*/
id: string
title: string
path: string

/**
* Title to be displayed in left PageSidebar
*/
title?: string

/**
* Path for plugin's main component. Optional if the plugin only contributes header elements for example
*/
path?: string

/**
* The order to be shown in the Hawtio sidebar.
Expand All @@ -62,8 +73,11 @@ export interface Plugin {
*/
isLogin?: boolean

/**
* Plugins main component to be displayed
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: React.ComponentType<any>
component?: React.ComponentType<any>

headerItems?: HeaderItem[]

Expand Down
33 changes: 33 additions & 0 deletions packages/hawtio/src/plugins/connect/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useEffect, useState } from 'react'
import { connectService } from '@hawtiosrc/plugins/shared/connect-service'
import { PluggedIcon, UnpluggedIcon } from '@patternfly/react-icons'

/**
* Component to be displayed in HawtioHeaderToolbar for remote connection tabs
* @constructor
*/
export const ConnectionStatus: React.FunctionComponent = () => {
const [reachable, setReachable] = useState(false)

const connectionId = connectService.getCurrentConnectionId()
const connectionName = connectService.getCurrentConnectionName()

useEffect(() => {
const check = async () => {
const connection = await connectService.getCurrentConnection()
if (connection) {
connectService.checkReachable(connection).then(result => setReachable(result))
}
}
check() // initial fire
const timer = setInterval(check, 20000)
return () => clearInterval(timer)
}, [connectionId])

return (
<>
{reachable ? <PluggedIcon color='green' /> : <UnpluggedIcon color='red' />}
{connectionName ? connectionName : ''}
</>
)
}
71 changes: 40 additions & 31 deletions packages/hawtio/src/plugins/connect/connections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Connection, Connections } from '@hawtiosrc/plugins/shared'
import { connectService } from '@hawtiosrc/plugins/shared/connect-service'

export const ADD = 'ADD'
export const UPDATE = 'UPDATE'
Expand All @@ -8,62 +9,70 @@ export const RESET = 'RESET'

export type ConnectionsAction =
| { type: typeof ADD; connection: Connection }
| { type: typeof UPDATE; name: string; connection: Connection }
| { type: typeof DELETE; name: string }
| { type: typeof UPDATE; id: string; connection: Connection }
| { type: typeof DELETE; id: string }
| { type: typeof IMPORT; connections: Connection[] }
| { type: typeof RESET }

function addConnection(state: Connections, connection: Connection): Connections {
if (state[connection.name]) {
// TODO: error handling
return state
// generate ID
if (!connection.id) {
connectService.generateId(connection, state)
}
return { ...state, [connection.name]: connection }
}

function updateConnection(state: Connections, name: string, connection: Connection): Connections {
if (name === connection.name) {
// normal update
if (!state[connection.name]) {
// TODO: error handling
return state
}
return { ...state, [connection.name]: connection }
}
return { ...state, [connection.id]: connection }
}

// name change
if (state[connection.name]) {
// TODO: error handling
return state
}
return Object.fromEntries(
Object.entries(state).map(([k, v]) => (k === name ? [connection.name, connection] : [k, v])),
)
function updateConnection(state: Connections, id: string, connection: Connection): Connections {
// name change is handled correctly, because we use id
return { ...state, [id]: connection }
}

function deleteConnection(state: Connections, name: string): Connections {
function deleteConnection(state: Connections, id: string): Connections {
const newState = { ...state }
delete newState[name]
delete newState[id]
return newState
}

function importConnections(state: Connections, imported: Connection[]): Connections {
return imported.reduce((newState, conn) => {
// if there's a connection with given ID, change it, otherwise, add new one
if (!conn.id) {
// importing old format without ID
connectService.generateId(conn, state)
}
let exists = false
for (const c in state) {
if (c === conn.id) {
exists = true
break
}
}
if (exists) {
return updateConnection(state, conn.id, conn)
} else {
return addConnection(newState, conn)
}
}, state)
}

export function reducer(state: Connections, action: ConnectionsAction): Connections {
switch (action.type) {
case ADD: {
const { connection } = action
return addConnection(state, connection)
}
case UPDATE: {
const { name, connection } = action
return updateConnection(state, name, connection)
const { id, connection } = action
return updateConnection(state, id, connection)
}
case DELETE: {
const { name } = action
return deleteConnection(state, name)
const { id } = action
return deleteConnection(state, id)
}
case IMPORT: {
const { connections } = action
return connections.reduce((newState, conn) => addConnection(newState, conn), state)
return importConnections(state, connections)
}
case RESET:
return {}
Expand Down
4 changes: 2 additions & 2 deletions packages/hawtio/src/plugins/connect/discover/Discover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ export const Discover: React.FunctionComponent = () => {
log.debug('Discover - connect to:', conn)

// Save the connection before connecting
if (connections[conn.name]) {
dispatch({ type: UPDATE, name: conn.name, connection: conn })
if (connections[conn.id]) {
dispatch({ type: UPDATE, id: conn.id, connection: conn })
} else {
dispatch({ type: ADD, connection: conn })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isBlank } from '@hawtiosrc/util/strings'
import { log } from '../globals'

/**
* @see https://jolokia.org/reference/html/mbeans.html#mbean-discovery
* @see https://jolokia.org/reference/html/manual/jolokia_mbeans.html#mbean-discovery
*/
export type Agent = {
// Properties from Jolokia API
Expand Down Expand Up @@ -89,7 +89,11 @@ class DiscoverService {
}

agentToConnection(agent: Agent): Connection {
const conn = { ...INITIAL_CONNECTION, name: agent.agent_description ?? `discover-${agent.agent_id}` }
const conn = {
...INITIAL_CONNECTION,
id: agent.agent_id ?? `discover-${agent.agent_id}`,
name: agent.agent_description ?? `discover-${agent.agent_id}`,
}
if (!agent.url) {
log.warn('No URL available to connect to agent:', agent)
return conn
Expand Down
1 change: 1 addition & 0 deletions packages/hawtio/src/plugins/connect/globals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Logger } from '@hawtiosrc/core/logging'

export const pluginId = 'connect'
export const statusPluginId = 'connectStatus'
export const pluginTitle = 'Connect'
export const pluginPath = '/connect'
export const pluginName = 'hawtio-connect'
Expand Down
17 changes: 14 additions & 3 deletions packages/hawtio/src/plugins/connect/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { hawtio, HawtioPlugin } from '@hawtiosrc/core'
import { hawtio, HawtioPlugin, UniversalHeaderItem } from '@hawtiosrc/core'
import { helpRegistry } from '@hawtiosrc/help/registry'
import { preferencesRegistry } from '@hawtiosrc/preferences/registry'
import { Connect } from './Connect'
import { ConnectPreferences } from './ConnectPreferences'
import { pluginId, pluginPath, pluginTitle } from './globals'
import { pluginId, pluginPath, pluginTitle, statusPluginId } from './globals'
import help from './help.md'
import { isActive, registerUserHooks } from './init'
import { isActive, isConnectionStatusActive, registerUserHooks } from './init'
import { ConnectionStatus } from '@hawtiosrc/plugins/connect/ConnectionStatus'

const order = 11

const connectStatusItem: UniversalHeaderItem = {
component: ConnectionStatus,
universal: true,
}

export const connect: HawtioPlugin = () => {
registerUserHooks()
hawtio.addPlugin({
Expand All @@ -19,6 +25,11 @@ export const connect: HawtioPlugin = () => {
component: Connect,
isActive,
})
hawtio.addPlugin({
id: statusPluginId,
headerItems: [connectStatusItem],
isActive: isConnectionStatusActive,
})
helpRegistry.add(pluginId, pluginTitle, help, order)
preferencesRegistry.add(pluginId, pluginTitle, ConnectPreferences, order)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/hawtio/src/plugins/connect/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ describe('isActive', () => {

test('/proxy/enabled returns not false & connection name is not set', async () => {
fetchMock.mockResponse('true')
connectService.getCurrentConnectionName = jest.fn(() => null)
connectService.getCurrentConnectionId = jest.fn(() => null)

await expect(isActive()).resolves.toEqual(true)
})

test('/proxy/enabled returns not false & connection name is set', async () => {
fetchMock.mockResponse('')
connectService.getCurrentConnectionName = jest.fn(() => 'test-connection')
connectService.getCurrentConnectionId = jest.fn(() => 'test-connection')

await expect(isActive()).resolves.toEqual(false)
})
Expand Down
13 changes: 12 additions & 1 deletion packages/hawtio/src/plugins/connect/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ export async function isActive(): Promise<boolean> {

// The connect login path is exceptionally allowlisted to provide login form for
// remote Jolokia endpoints requiring authentication.
return connectService.getCurrentConnectionName() === null || isConnectLogin()
return connectService.getCurrentConnectionId() === null || isConnectLogin()
}

export async function isConnectionStatusActive(): Promise<boolean> {
const proxyEnabled = await isProxyEnabled()
if (!proxyEnabled) {
return false
}

// for "main" hawtio page, where this plugin is fully active, we don't have to show the connection status
// but for actually connected tab, we want the status in the header
return connectService.getCurrentConnectionId() !== null
}

async function isProxyEnabled(): Promise<boolean> {
Expand Down
25 changes: 17 additions & 8 deletions packages/hawtio/src/plugins/connect/remote/ConnectionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const ConnectionModal: React.FunctionComponent<{

const validate = () => {
const result = { ...emptyResult }
const { name, host, port } = connection
const { id: cid, name, host, port } = connection
let valid = true

// Name
Expand All @@ -69,12 +69,17 @@ export const ConnectionModal: React.FunctionComponent<{
validated: 'error',
}
valid = false
} else if (name !== input.name && connections[name]) {
result.name = {
text: `Connection name '${connection.name.trim()}' is already in use`,
validated: 'error',
} else if (name !== input.name) {
for (const id in connections) {
if (id !== cid && connections[id]?.name === name) {
result.name = {
text: `Connection name '${connection.name.trim()}' is already in use`,
validated: 'error',
}
valid = false
break
}
}
valid = false
}

// Host
Expand All @@ -86,6 +91,7 @@ export const ConnectionModal: React.FunctionComponent<{
valid = false
} else if (host.indexOf(':') !== -1) {
result.host = {
// TODO: IPv6
text: "Invalid character ':'",
validated: 'error',
}
Expand Down Expand Up @@ -115,12 +121,15 @@ export const ConnectionModal: React.FunctionComponent<{
switch (mode) {
case 'add':
dispatch({ type: ADD, connection })
setConnection(input)
break
case 'edit':
dispatch({ type: UPDATE, name: input.name, connection })
dispatch({ type: UPDATE, id: input.id, connection })
setConnection(connection)
break
}
clear()
setValidations(emptyResult)
onClose()
}

const clear = () => {
Expand Down
22 changes: 18 additions & 4 deletions packages/hawtio/src/plugins/connect/remote/ImportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,24 @@ export const ImportModal: React.FunctionComponent<{
}

const importConnections = () => {
const connections = JSON.parse(fileContent)
dispatch({ type: 'IMPORT', connections })
clearAndClose()
eventService.notify({ type: 'success', message: 'Connections imported successfully' })
try {
const connections = JSON.parse(fileContent)
if (Array.isArray(connections)) {
dispatch({ type: 'IMPORT', connections })
clearAndClose()
eventService.notify({ type: 'success', message: 'Connections imported successfully' })
} else {
clearAndClose()
eventService.notify({ type: 'danger', message: 'Unexpected connections data format' })
}
} catch (e) {
clearAndClose()
let msg = 'Invalid connections data format'
if (e instanceof Error) {
msg = (e as Error).message
}
eventService.notify({ type: 'danger', message: msg })
}
}

return (
Expand Down
Loading

0 comments on commit e8ad5b3

Please sign in to comment.