From e5474fbe0468cebf07713a81d6c689e09ac481a5 Mon Sep 17 00:00:00 2001
From: phantomjinx
Date: Wed, 9 Oct 2024 13:42:02 +0100
Subject: [PATCH] fix: Adds better error handling of workspace loading errors
* 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
*
---
packages/hawtio/package.json | 2 +-
packages/hawtio/src/core/core.ts | 2 +-
.../plugins/app-status/WorkspaceStatus.css | 3 +
.../plugins/app-status/WorkspaceStatus.tsx | 76 +++++++++++++++++++
.../hawtio/src/plugins/app-status/globals.ts | 6 ++
.../hawtio/src/plugins/app-status/index.ts | 28 +++++++
packages/hawtio/src/plugins/index.ts | 4 +-
.../hawtio/src/plugins/shared/workspace.ts | 35 ++++++++-
8 files changed, 151 insertions(+), 5 deletions(-)
create mode 100644 packages/hawtio/src/plugins/app-status/WorkspaceStatus.css
create mode 100644 packages/hawtio/src/plugins/app-status/WorkspaceStatus.tsx
create mode 100644 packages/hawtio/src/plugins/app-status/globals.ts
create mode 100644 packages/hawtio/src/plugins/app-status/index.ts
diff --git a/packages/hawtio/package.json b/packages/hawtio/package.json
index 77f9fa5a4..4e08f982f 100644
--- a/packages/hawtio/package.json
+++ b/packages/hawtio/package.json
@@ -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",
diff --git a/packages/hawtio/src/core/core.ts b/packages/hawtio/src/core/core.ts
index cf808457a..de0b23558 100644
--- a/packages/hawtio/src/core/core.ts
+++ b/packages/hawtio/src/core/core.ts
@@ -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)
diff --git a/packages/hawtio/src/plugins/app-status/WorkspaceStatus.css b/packages/hawtio/src/plugins/app-status/WorkspaceStatus.css
new file mode 100644
index 000000000..f9f1728b8
--- /dev/null
+++ b/packages/hawtio/src/plugins/app-status/WorkspaceStatus.css
@@ -0,0 +1,3 @@
+.workspace-alert {
+ margin-top: 1em;
+}
diff --git a/packages/hawtio/src/plugins/app-status/WorkspaceStatus.tsx b/packages/hawtio/src/plugins/app-status/WorkspaceStatus.tsx
new file mode 100644
index 000000000..4a3ac6da9
--- /dev/null
+++ b/packages/hawtio/src/plugins/app-status/WorkspaceStatus.tsx
@@ -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(null)
+ const [errors, setErrors] = useState([])
+ const [loading, setLoading] = useState(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 (
+
+
+ Waiting workspace to load ...
+
+
+
+
+
+
+
+ )
+ }
+
+ const hasCause = (error: Error) => {
+ if (!error || !error.cause) return false
+ return error.cause instanceof Error
+ }
+
+ return (
+
+
+
+
+
+ {errors.map(error => (
+
+ {hasCause(error) && Cause: {(error.cause as Error).message}
}
+
+ ))}
+
+
+
+ )
+}
diff --git a/packages/hawtio/src/plugins/app-status/globals.ts b/packages/hawtio/src/plugins/app-status/globals.ts
new file mode 100644
index 000000000..fd039919b
--- /dev/null
+++ b/packages/hawtio/src/plugins/app-status/globals.ts
@@ -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)
diff --git a/packages/hawtio/src/plugins/app-status/index.ts b/packages/hawtio/src/plugins/app-status/index.ts
new file mode 100644
index 000000000..36be17d96
--- /dev/null
+++ b/packages/hawtio/src/plugins/app-status/index.ts
@@ -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
+ },
+ })
+}
diff --git a/packages/hawtio/src/plugins/index.ts b/packages/hawtio/src/plugins/index.ts
index 040c18b13..2c5d22390 100644
--- a/packages/hawtio/src/plugins/index.ts
+++ b/packages/hawtio/src/plugins/index.ts
@@ -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.
@@ -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'
diff --git a/packages/hawtio/src/plugins/shared/workspace.ts b/packages/hawtio/src/plugins/shared/workspace.ts
index 32098d1ac..c2cc05446 100644
--- a/packages/hawtio/src/plugins/shared/workspace.ts
+++ b/packages/hawtio/src/plugins/shared/workspace.ts
@@ -12,6 +12,8 @@ const HAWTIO_REGISTRY_MBEAN = 'hawtio:type=Registry'
const HAWTIO_TREE_WATCHER_MBEAN = 'hawtio:type=TreeWatcher'
export interface IWorkspace {
+ hasErrors(): Promise
+ getErrors(): Promise
refreshTree(): Promise
getTree(): Promise
hasMBeans(): Promise
@@ -26,9 +28,25 @@ class Workspace implements IWorkspace {
private pluginUpdateCounter?: number
private treeWatchRegisterHandle?: Promise
private treeWatcherCounter?: number
+ private _errors: Error[] = []
+
+ async hasErrors(): Promise {
+ await this.getTree()
+ return this._errors.length > 0
+ }
+
+ async getErrors(): Promise {
+ 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()
}
@@ -44,11 +62,13 @@ class Workspace implements IWorkspace {
private async loadTree(): Promise {
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 : []
@@ -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)
},
}
@@ -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)
}
}