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) } }