Skip to content

Commit

Permalink
fix: Adds better error handling of workspace loading errors
Browse files Browse the repository at this point in the history
* Adds a status plugin for displaying errors messages if the workspace
  failed to load. Such errors can get lost in the console log and a blank
  page is the result in the client.

* workspace.ts
  * If an error occurs then add them to a collection in the workspace
  * Provides an api for quizzing the workspace on whether it has errored

* app-status
  * Plugin that is only active if
    * there is a current connection (no point in displaying if nothing
      has been connected to yet)
    * there are no mbeans collected from the jolokia connection
    * Errors were flagged from the workspace
  * Assuming the plugin is active, offer a component that provides
    notification of the workspace
 *
  • Loading branch information
phantomjinx committed Oct 16, 2024
1 parent d12490c commit e5474fb
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/hawtio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hawtio/react",
"version": "1.5.0",
"version": "1.5.1",
"description": "A Hawtio reimplementation based on TypeScript + React.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/hawtio/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class HawtioCore {
}

/**
* Adds an angular module to the list of modules to bootstrap.
* Adds a module to the list of modules to bootstrap.
*/
addPlugin(plugin: Plugin): HawtioCore {
log.info('Add plugin:', plugin.id)
Expand Down
3 changes: 3 additions & 0 deletions packages/hawtio/src/plugins/app-status/WorkspaceStatus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.workspace-alert {
margin-top: 1em;
}
76 changes: 76 additions & 0 deletions packages/hawtio/src/plugins/app-status/WorkspaceStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useEffect, useRef, useState } from 'react'
import {
Alert,
Card,
CardBody,
PageSection,
PageSectionVariants,
Panel,
PanelHeader,
PanelMain,
PanelMainBody,
} from '@patternfly/react-core'
import { HawtioLoadingCard, workspace } from '@hawtiosrc/plugins/shared'
import './WorkspaceStatus.css'

export const WorkspaceStatus: React.FunctionComponent = () => {
const timerRef = useRef<NodeJS.Timeout | null>(null)
const [errors, setErrors] = useState<Error[]>([])
const [loading, setLoading] = useState<boolean>(true)

useEffect(() => {
const waitLoading = async () => {
const hasErrors = await workspace.hasErrors()

if (hasErrors) {
const errors = [...(await workspace.getErrors())]
errors.reverse() // reverse so as to show latest first
setErrors(errors)
}

setLoading(false)
}

timerRef.current = setTimeout(waitLoading, 1000)

return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [])

if (loading) {
return (
<PageSection variant={PageSectionVariants.light}>
<Panel>
<PanelHeader>Waiting workspace to load ...</PanelHeader>
<PanelMain>
<PanelMainBody>
<HawtioLoadingCard />
</PanelMainBody>
</PanelMain>
</Panel>
</PageSection>
)
}

const hasCause = (error: Error) => {
if (!error || !error.cause) return false
return error.cause instanceof Error
}

return (
<PageSection variant={PageSectionVariants.light}>
<Card>
<CardBody>
<Alert variant='warning' title='Application returned no mbeans' />

{errors.map(error => (
<Alert variant='danger' title={error.message} className='workspace-alert'>
{hasCause(error) && <p>Cause: {(error.cause as Error).message}</p>}
</Alert>
))}
</CardBody>
</Card>
</PageSection>
)
}
6 changes: 6 additions & 0 deletions packages/hawtio/src/plugins/app-status/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Logger } from '@hawtiosrc/core/logging'

export const pluginId = 'appstatus'
export const pluginName = 'Application Status'
export const pluginPath = '/appstatus'
export const log = Logger.get(pluginName)
28 changes: 28 additions & 0 deletions packages/hawtio/src/plugins/app-status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HawtioPlugin, hawtio } from '@hawtiosrc/core'
import { connectService, workspace } from '@hawtiosrc/plugins/shared'
import { WorkspaceStatus } from './WorkspaceStatus'
import { pluginId, pluginPath, pluginName } from './globals'

/*
* Target application status plugin
* only active if the workspace contains no mbeans, ie, totally empty.
* and / or the workspace has produced errors.
* Will communicate this to the user with a notice component.
*/
export const appStatus: HawtioPlugin = () => {
hawtio.addPlugin({
id: pluginId,
title: pluginName,
path: pluginPath,
component: WorkspaceStatus,
isActive: async () => {
const connection = await connectService.getCurrentConnection()
const beans = await workspace.hasMBeans()
const errors = await workspace.hasErrors()

if (!connection) return false // no connection yet so no beans in workspace

return !beans || errors // either no beans or workspace has errors
},
})
}
4 changes: 3 additions & 1 deletion packages/hawtio/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { quartz } from './quartz'
import { rbac } from './rbac'
import { runtime } from './runtime'
import { springboot } from './springboot'
import { appStatus } from './app-status'

/**
* Registers the builtin plugins for Hawtio React.
Expand All @@ -28,10 +29,11 @@ export const registerPlugins: HawtioPlugin = () => {
logs()
quartz()
springboot()
appStatus()
}

// Export each plugin's entry point so that a custom console assembler can select which to bundle
export { camel, connect, jmx, keycloak, oidc, logs, quartz, rbac, runtime, springboot }
export { camel, connect, jmx, keycloak, oidc, logs, quartz, rbac, runtime, springboot, appStatus }

// Common plugin API
export * from './connect'
Expand Down
35 changes: 33 additions & 2 deletions packages/hawtio/src/plugins/shared/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const HAWTIO_REGISTRY_MBEAN = 'hawtio:type=Registry'
const HAWTIO_TREE_WATCHER_MBEAN = 'hawtio:type=TreeWatcher'

export interface IWorkspace {
hasErrors(): Promise<boolean>
getErrors(): Promise<Error[]>
refreshTree(): Promise<void>
getTree(): Promise<MBeanTree>
hasMBeans(): Promise<boolean>
Expand All @@ -26,9 +28,25 @@ class Workspace implements IWorkspace {
private pluginUpdateCounter?: number
private treeWatchRegisterHandle?: Promise<number>
private treeWatcherCounter?: number
private _errors: Error[] = []

async hasErrors(): Promise<boolean> {
await this.getTree()
return this._errors.length > 0
}

async getErrors(): Promise<Error[]> {
await this.getTree()
return this._errors
}

addError(error: Error) {
this._errors.push(error)
}

async refreshTree() {
this.tree = undefined
this._errors = []
await this.getTree()
eventService.refresh()
}
Expand All @@ -44,11 +62,13 @@ class Workspace implements IWorkspace {

private async loadTree(): Promise<MBeanTree> {
if (!(await userService.isLogin())) {
throw new Error('User needs to have logged in to use workspace')
this.addError(new Error('User needs to have logged in to use workspace'))
return MBeanTree.createEmpty(pluginName)
}

const config = await this.getConfig()
if (config.workspace === false || (typeof config.workspace !== 'boolean' && config.workspace?.length === 0)) {
// TODO Should this set the error??
return MBeanTree.createEmpty(pluginName)
}
const mbeanPaths = config.workspace && typeof config.workspace !== 'boolean' ? config.workspace : []
Expand All @@ -57,10 +77,17 @@ class Workspace implements IWorkspace {
const options: SimpleRequestOptions = {
ignoreErrors: true,
error: (response: JolokiaErrorResponse) => {
this.addError(
new Error(`Error - fetching JMX tree: ${response.error_type} ${response.error} ${response.error_value}`),
)
log.debug('Error - fetching JMX tree:', response)
},
fetchError: (response: Response | null, error: DOMException | TypeError | string | null) => {
const text = response?.statusText || error
const err = new Error(`Ajax error - fetching JMX tree: ${text}`)
err.cause = error
this.addError(err)

log.debug('Ajax error - fetching JMX tree:', text, '-', error)
},
}
Expand All @@ -77,7 +104,11 @@ class Workspace implements IWorkspace {

return tree
} catch (error) {
log.error('A request to list the JMX tree failed:', error)
const wkspError: Error = new Error('A request to list the JMX tree failed')
wkspError.cause = error
this.addError(wkspError)

log.error(wkspError.message, error)
return MBeanTree.createEmpty(pluginName)
}
}
Expand Down

0 comments on commit e5474fb

Please sign in to comment.