From fcb70d3c85dc48b935f82a7887584f543204d550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20R=C3=A1ez=20Rodr=C3=ADguez?= Date: Tue, 11 Apr 2023 12:50:17 +0200 Subject: [PATCH] feat(jmx): Tabular attribute table (#215) * wip(#125): Progress on multiattribute table. Have to implement a better way to know which are JMX nodes * feat(#125): Tabular attribute table * feat(#125): Added NodeName table and better filtering * feat(#125): Refactored filtering logic * feat(#125): Added test for helper function. Created helper function class. Simplified JMXContent component taking into account new logic * feat(#125): Removed NodeNameTable and using JMXContentMBeans. * feat(#125): Moved helper function to proper class. Redefined paths to components * feat(#125): Changed humanize labels to use function implementation * feat(#125): Removed array creation: map handles it * feat(#125): Added extra test cases. Some refactoring * feat(#125): Extra refinement * feat(#125): Added extra tests * feat(#125): Used strings isBlank function * feat(#125): Fixed logic after changing to using isBlank function --- .../hawtio/src/plugins/jmx/JmxContent.tsx | 34 ++++--- .../shared/attributes/AttributeTable.css | 3 + .../shared/attributes/AttributeTable.tsx | 90 +++++++++++++++++++ .../src/plugins/shared/attributes/index.ts | 1 + packages/hawtio/src/util/strings.test.ts | 19 +++- packages/hawtio/src/util/strings.ts | 21 +++++ 6 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 packages/hawtio/src/plugins/shared/attributes/AttributeTable.css create mode 100644 packages/hawtio/src/plugins/shared/attributes/AttributeTable.tsx diff --git a/packages/hawtio/src/plugins/jmx/JmxContent.tsx b/packages/hawtio/src/plugins/jmx/JmxContent.tsx index 2cbac74b..5c360eb2 100644 --- a/packages/hawtio/src/plugins/jmx/JmxContent.tsx +++ b/packages/hawtio/src/plugins/jmx/JmxContent.tsx @@ -19,7 +19,7 @@ import { NavLink, Navigate, Route, Routes, useLocation } from 'react-router-dom' import { MBeanTreeContext } from './context' import { Chart } from '@hawtiosrc/plugins/shared/chart' import { Operations } from '@hawtiosrc/plugins/shared/operations' -import { Attributes } from '@hawtiosrc/plugins/shared/attributes' +import { Attributes, AttributeTable } from '@hawtiosrc/plugins/shared/attributes' import { JmxContentMBeans, MBeanNode } from '@hawtiosrc/plugins/shared' export const JmxContent: React.FunctionComponent = () => { @@ -39,10 +39,21 @@ export const JmxContent: React.FunctionComponent = () => { ) } - const mBeanApplicable = (node: MBeanNode) => node.objectName + const mBeanApplicable = (node: MBeanNode) => Boolean(node.objectName) + const mBeanCollectionApplicable = (node: MBeanNode) => Boolean(node.children?.every(child => child.objectName)) + const ALWAYS = (node: MBeanNode) => true + + const tableSelector: (node: MBeanNode) => React.FunctionComponent = (node: MBeanNode) => { + const tablePriorityList: { condition: (node: MBeanNode) => boolean; element: React.FunctionComponent }[] = [ + { condition: mBeanApplicable, element: Attributes }, + { condition: mBeanCollectionApplicable, element: AttributeTable }, + ] + + return tablePriorityList.find(entry => entry.condition(node))?.element ?? JmxContentMBeans + } const allNavItems = [ - { id: 'attributes', title: 'Attributes', component: Attributes, isApplicable: mBeanApplicable }, + { id: 'attributes', title: 'Attributes', component: tableSelector(selectedNode), isApplicable: ALWAYS }, { id: 'operations', title: 'Operations', component: Operations, isApplicable: mBeanApplicable }, { id: 'chart', title: 'Chart', component: Chart, isApplicable: mBeanApplicable }, ] @@ -73,18 +84,15 @@ export const JmxContent: React.FunctionComponent = () => { {selectedNode.name} {selectedNode.objectName} - {navItems.length > 0 && {mbeanNav}} + {mbeanNav} - {navItems.length > 0 && ( - - - {mbeanRoutes} - } /> - - - )} - {navItems.length === 0 && !selectedNode.objectName && } + + + {mbeanRoutes} + } /> + + ) diff --git a/packages/hawtio/src/plugins/shared/attributes/AttributeTable.css b/packages/hawtio/src/plugins/shared/attributes/AttributeTable.css new file mode 100644 index 00000000..d1f251b3 --- /dev/null +++ b/packages/hawtio/src/plugins/shared/attributes/AttributeTable.css @@ -0,0 +1,3 @@ +.attribute-table > tr > th { + max-width: none; +} diff --git a/packages/hawtio/src/plugins/shared/attributes/AttributeTable.tsx b/packages/hawtio/src/plugins/shared/attributes/AttributeTable.tsx new file mode 100644 index 00000000..20bb1dee --- /dev/null +++ b/packages/hawtio/src/plugins/shared/attributes/AttributeTable.tsx @@ -0,0 +1,90 @@ +import { useContext, useEffect, useState } from 'react' +import { Card, CardBody, Text } from '@patternfly/react-core' +import { Table, TableBody, TableHeader, TableProps } from '@patternfly/react-table' +import { PluginNodeSelectionContext } from '@hawtiosrc/plugins/selectionNodeContext' +import { AttributeValues } from '@hawtiosrc/plugins/connect/jolokia-service' +import { attributeService } from './attribute-service' +import './AttributeTable.css' +import { JmxContentMBeans } from '@hawtiosrc/plugins/shared/JmxContentMBeans' +import { humanizeLabels } from '@hawtiosrc/util/strings' + +export const AttributeTable: React.FunctionComponent = () => { + const { selectedNode } = useContext(PluginNodeSelectionContext) + const [attributesList, setAttributesList] = useState([{}]) + const [isReading, setIsReading] = useState(false) + + useEffect(() => { + if (!selectedNode) { + return + } + + const readAttributes = async () => { + const childrenMbeansAttributes: AttributeValues[] = [] + setIsReading(true) + if (selectedNode.children) + for (const mbean of selectedNode.children) { + const objectName = mbean.objectName + if (objectName) { + const attrs = await attributeService.read(objectName) + childrenMbeansAttributes.push(attrs) + } + } + setAttributesList([...childrenMbeansAttributes]) + setIsReading(false) + } + readAttributes() + }, [selectedNode]) + + if (!selectedNode) { + return null + } + + if (isReading) { + return ( + + + Reading attributes... + + + ) + } + + const checkIfAllMBeansHaveSameAttributes = (attributesList: AttributeValues[]) => { + if (attributesList.length <= 1) { + return true + } + + const firstMBeanAttributesElements = attributesList[0].length + + if (!attributesList.every(mbeanAttributes => mbeanAttributes.length === firstMBeanAttributesElements)) { + return false + } + + const labelSet: Set = new Set() + Object.keys(attributesList[0]).forEach(label => labelSet.add(label)) + + return attributesList.every(attributes => Object.keys(attributes).every(label => labelSet.has(label))) + } + + if ( + attributesList.some(attribute => Object.entries(attribute).length === 0) || + !checkIfAllMBeansHaveSameAttributes(attributesList) + ) { + return + } + + const labels = Object.keys(attributesList[0]) + const columns: TableProps['cells'] = labels.map(label => humanizeLabels(label)) + const rows: TableProps['rows'] = attributesList.map(attribute => + labels.map(label => JSON.stringify(attribute[label])), + ) + + return ( + + + + +
+
+ ) +} diff --git a/packages/hawtio/src/plugins/shared/attributes/index.ts b/packages/hawtio/src/plugins/shared/attributes/index.ts index bf968b43..d95b305c 100644 --- a/packages/hawtio/src/plugins/shared/attributes/index.ts +++ b/packages/hawtio/src/plugins/shared/attributes/index.ts @@ -1,2 +1,3 @@ export { Attributes } from './Attributes' export { AttributeModal } from './AttributeModal' +export { AttributeTable } from './AttributeTable' diff --git a/packages/hawtio/src/util/strings.test.ts b/packages/hawtio/src/util/strings.test.ts index f76d2c05..f306b0ce 100644 --- a/packages/hawtio/src/util/strings.test.ts +++ b/packages/hawtio/src/util/strings.test.ts @@ -1,4 +1,4 @@ -import { isString, parseBoolean, toString, trimQuotes } from './strings' +import { humanizeLabels, isString, parseBoolean, toString, trimQuotes } from './strings' describe('strings', () => { test('isString', () => { @@ -41,4 +41,21 @@ describe('strings', () => { expect(parseBoolean('FaLsE')).toBeFalsy() expect(parseBoolean('0')).toBeFalsy() }) + + test('humanizeLabels()', () => { + expect(humanizeLabels('ObjectName')).toEqual('Object Name') + expect(humanizeLabels('XHTTPRequest')).toEqual('XHTTP Request') + expect(humanizeLabels('MBeanName')).toEqual('MBean Name') + expect(humanizeLabels('MBeanHTML')).toEqual('MBean HTML') + + expect(humanizeLabels('object-name')).toEqual('Object Name') + expect(humanizeLabels('double--dashes')).toEqual('Double Dashes') + expect(humanizeLabels('mbean-name')).toEqual('MBean Name') + + expect(humanizeLabels('-Object-Name-')).toEqual('Object Name') + + expect(humanizeLabels('')).toEqual('') + expect(humanizeLabels(' ')).toEqual('') + expect(humanizeLabels('Object Name')).toEqual('Object Name') + }) }) diff --git a/packages/hawtio/src/util/strings.ts b/packages/hawtio/src/util/strings.ts index 37809e8f..5f489b92 100644 --- a/packages/hawtio/src/util/strings.ts +++ b/packages/hawtio/src/util/strings.ts @@ -99,3 +99,24 @@ export function parseBoolean(value: string): boolean { return /^true$/i.test(value) || parseInt(value) === 1 } + +/** + * Will format a property to a standard human readable string with its spaces. + * It will respect MBean and leave it together + * @param str The property to transform + * @returns The property with its proper spaces + */ +export function humanizeLabels(str: string): string { + return str + .split('-') + .filter(str => !isBlank(str)) + .map(str => str.replace(/^./, str => str.toUpperCase())) + .join(' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') + .replace('M Bean', 'MBean') + .replace('Mbean', 'MBean') + .replace(/^./, str => str.toUpperCase()) + .replace(/ +/, ' ') + .trim() +}