diff --git a/packages/hawtio/src/plugins/camel/CamelContent.tsx b/packages/hawtio/src/plugins/camel/CamelContent.tsx index 4835b322..99b69180 100644 --- a/packages/hawtio/src/plugins/camel/CamelContent.tsx +++ b/packages/hawtio/src/plugins/camel/CamelContent.tsx @@ -38,6 +38,7 @@ import { RouteDiagram } from './route-diagram/RouteDiagram' import { RouteDiagramContext, useRouteDiagramContext } from './route-diagram/route-diagram-context' import { CamelRoutes } from './routes/CamelRoutes' import { Source } from './routes/Source' +import { Properties } from './properties' import { Trace } from './trace' import { TypeConverters } from './type-converters' @@ -101,6 +102,7 @@ export const CamelContent: React.FunctionComponent = () => { !ccs.isEndpointsFolder(node) && (ccs.isRouteNode(node) || ccs.isRoutesFolder(node)), }, + { id: 'properties', title: 'Properties', component: , isApplicable: ccs.hasProperties }, { id: 'send', title: 'Send', component: , isApplicable: ccs.isEndpointNode }, { id: 'browse', diff --git a/packages/hawtio/src/plugins/camel/camel-content-service.ts b/packages/hawtio/src/plugins/camel/camel-content-service.ts index 316e98c1..afd12469 100644 --- a/packages/hawtio/src/plugins/camel/camel-content-service.ts +++ b/packages/hawtio/src/plugins/camel/camel-content-service.ts @@ -263,6 +263,10 @@ export function hasRestServices(node: MBeanNode): boolean { return registry ? true : false } +export function hasProperties(node: MBeanNode): boolean { + return isRouteNode(node) || isRouteXmlNode(node) +} + /** * Fetch the camel version and add it to the tree to avoid making a blocking call * elsewhere. diff --git a/packages/hawtio/src/plugins/camel/icons/Icons.tsx b/packages/hawtio/src/plugins/camel/icons/Icons.tsx index 2b95e7e7..808c4fd0 100644 --- a/packages/hawtio/src/plugins/camel/icons/Icons.tsx +++ b/packages/hawtio/src/plugins/camel/icons/Icons.tsx @@ -1,3 +1,4 @@ +import { log } from '../globals' import { CamelImageIcon } from './CamelImageIcon' import * as svg from './svg' @@ -18,7 +19,38 @@ for (const [key, value] of Object.entries(svg)) { export const IconNames = svg.IconNames -export function getIcon(name: string): JSX.Element { - const element: JSX.Element | undefined = elementMap.get(name) +export interface IconProperties { + size: number + inline: boolean +} + +export function getIcon(name: string, size?: number): JSX.Element { + let element: JSX.Element | undefined + if (!size) + // No size defined so return the default cached icon + element = elementMap.get(name) + else { + // Custom sized icons are not indexed by default but cached after first build + log.debug("Fetching custom sized icon '" + name + "' with size '" + size + "'") + + // Store the icon against the name & size + const customIconName = name + '_' + size + element = elementMap.get(customIconName) + + if (!element) { + // No icon in cache so build the icon then cache it + const iconKey = name.replace('Icon', '').toLowerCase() + Object.entries(svg) + .filter(([key, value]) => { + return iconKey === key + }) + .forEach(([key, value]) => { + log.debug("Building custom sized icon '" + name + "' with size '" + size + "'") + element = buildIcon(customIconName, value, size) + elementMap.set(customIconName, element) + }) + } + } + return element ? element : (elementMap.get(IconNames.GenericIcon) as JSX.Element) } diff --git a/packages/hawtio/src/plugins/camel/properties/Properties.tsx b/packages/hawtio/src/plugins/camel/properties/Properties.tsx new file mode 100644 index 00000000..f318cd86 --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/Properties.tsx @@ -0,0 +1,137 @@ +import { + Card, + CardBody, + CardHeader, + CardTitle, + Label, + LabelGroup, + Panel, + PanelMain, + PanelMainBody, + Skeleton, + Text, +} from '@patternfly/react-core' +import { InfoCircleIcon } from '@patternfly/react-icons' +import React, { useContext, useEffect, useState } from 'react' +import Logger from 'js-logger' +import { CamelContext } from '../context' +import { log, xmlNodeLocalName } from '../globals' +import { schemaService } from '../schema-service' +import { routesService } from '../routes-service' +import * as pps from './properties-service' +import { PropertiesList } from './PropertiesList' +import { Property } from './property' +import './properties.css' + +export const Properties: React.FunctionComponent = () => { + const { selectedNode } = useContext(CamelContext) + const [isReading, setIsReading] = useState(true) + + const [title, setTitle] = useState('') + const [icon, setIcon] = useState() + const [labels, setLabels] = useState([]) + const [description, setDescription] = useState('') + const [definedProperties, setDefinedProperties] = useState([]) + const [defaultProperties, setDefaultProperties] = useState([]) + const [undefinedProperties, setUndefinedProperties] = useState([]) + + useEffect(() => { + if (!selectedNode) return + + setIsReading(true) + + const init = async () => { + const localName: string = selectedNode.getProperty(xmlNodeLocalName) + const schemaKey = localName ? localName : selectedNode.name + const schema = schemaService.getSchema(schemaKey) + + let newTitle = localName + let newIcon = selectedNode.icon + let newDescription = '' + let groups: string[] = [] + + if (schema) { + newTitle = schema['title'] as string + newIcon = routesService.getIcon(schema, 24) + newDescription = schema['description'] as string + const groupStr = schema['group'] as string + groups = groupStr.split(',') + + if (log.enabledFor(Logger.DEBUG)) { + log.debug('Properties - schema:', JSON.stringify(schema, null, ' ')) + } + + const schemaProps = schema['properties'] as Record> + pps.populateProperties(selectedNode, schemaProps) + + setDefinedProperties(pps.getDefinedProperties(schemaProps)) + setDefaultProperties(pps.getDefaultProperties(schemaProps)) + setUndefinedProperties(pps.getUndefinedProperties(schemaProps)) + } + + setIcon(newIcon) + setTitle(newTitle) + setDescription(newDescription) + setLabels(groups) + + setIsReading(false) + } + + init() + }, [selectedNode]) + + if (!selectedNode) { + return ( + + + No selection has been made + + + ) + } + + if (isReading) { + return ( + + + + + + ) + } + + return ( + + + Properties + + + + + + {icon} + {title} + + {labels.map(label => ( + + ))} + + + + + {description && ( + + + {description} + + + )} + + + + + + ) +} diff --git a/packages/hawtio/src/plugins/camel/properties/PropertiesList.tsx b/packages/hawtio/src/plugins/camel/properties/PropertiesList.tsx new file mode 100644 index 00000000..ca47655e --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/PropertiesList.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Panel, + PanelHeader, + PanelMain, + PanelMainBody, +} from '@patternfly/react-core' +import { PropertiesTooltippedName } from './PropertiesTooltippedName' +import { Property } from './property' +import './properties.css' + +interface PropertiesListProps { + title: string + values: Property[] +} + +export const PropertiesList: React.FunctionComponent = props => { + return ( + + {props.title} + + {(!props.values || props.values.length === 0) && ( + No properties + )} + {props.values && props.values.length > 0 && ( + + + {props.values.map(p => { + return ( + + + + + {p.value} + + ) + })} + + + )} + + + ) +} diff --git a/packages/hawtio/src/plugins/camel/properties/PropertiesTooltippedName.tsx b/packages/hawtio/src/plugins/camel/properties/PropertiesTooltippedName.tsx new file mode 100644 index 00000000..de15a1d8 --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/PropertiesTooltippedName.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Tooltip } from '@patternfly/react-core' +import { InfoCircleIcon } from '@patternfly/react-icons' +import { Property } from './property' +import './properties.css' + +interface PropertiesTTNameProps { + property: Property +} + +export const PropertiesTooltippedName: React.FunctionComponent = props => { + const tooltipRef = React.useRef(null) + + return ( + + {props.property.name} + + + + + {props.property.description}} /> + + ) +} diff --git a/packages/hawtio/src/plugins/camel/properties/index.ts b/packages/hawtio/src/plugins/camel/properties/index.ts new file mode 100644 index 00000000..a44a0281 --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/index.ts @@ -0,0 +1 @@ +export { Properties } from './Properties' diff --git a/packages/hawtio/src/plugins/camel/properties/properties-service.ts b/packages/hawtio/src/plugins/camel/properties/properties-service.ts new file mode 100644 index 00000000..2c7fd09e --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/properties-service.ts @@ -0,0 +1,74 @@ +import { MBeanNode } from '@hawtiosrc/plugins/shared' +import { parseXML } from '@hawtiosrc/util/xml' +import { xmlNodeLocalName } from '../globals' +import { Property } from './property' + +export function populateProperties(node: MBeanNode, schemaProperties: Record>) { + // Extract the xml fragment from the node's property stash + const xml = node.getProperty('xml') + if (!xml) return + + // Extract the xml tag name from the node's property stash + const localName = node.getProperty(xmlNodeLocalName) + if (!localName) return + + // Parse the xml and find the root element using the localname + const xmlDoc = parseXML(xml) + const elements = xmlDoc.getElementsByTagName(localName) + + // Iterate the elements found (should only be 1) + for (const element of elements) { + // Iterate the element attributes + for (const attribute of element.attributes) { + // If any xml attribute has the same name as a schema property + // then assign the schema property the value, meaning this will + // become a defined property + const property = schemaProperties[attribute.name] + if (!property) continue + + property.value = attribute.value + } + } +} + +export function getDefinedProperties(schemaProperties: Record>): Property[] { + return Object.keys(schemaProperties) + .filter(key => { + const obj = schemaProperties[key] + return Object.keys(obj).includes('value') + }) + .map(key => { + const propertySchema = schemaProperties[key] + const name = propertySchema['title'] || key + return new Property(name, propertySchema['value'], propertySchema['description']) + }) + .sort(Property.sortByName) +} + +export function getDefaultProperties(schemaProperties: Record>): Property[] { + return Object.keys(schemaProperties) + .filter(key => { + const obj = schemaProperties[key] + return !Object.keys(obj).includes('value') && Object.keys(obj).includes('defaultValue') + }) + .map(key => { + const propertySchema = schemaProperties[key] + const name = propertySchema['title'] || key + return new Property(name, propertySchema['defaultValue'], propertySchema['description']) + }) + .sort(Property.sortByName) +} + +export function getUndefinedProperties(schemaProperties: Record>): Property[] { + return Object.keys(schemaProperties) + .filter(key => { + const obj = schemaProperties[key] + return !Object.keys(obj).includes('value') && !Object.keys(obj).includes('defaultValue') + }) + .map(key => { + const propertySchema = schemaProperties[key] + const name = propertySchema['title'] || key + return new Property(name, null, propertySchema['description']) + }) + .sort(Property.sortByName) +} diff --git a/packages/hawtio/src/plugins/camel/properties/properties.css b/packages/hawtio/src/plugins/camel/properties/properties.css new file mode 100644 index 00000000..8286cfde --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/properties.css @@ -0,0 +1,25 @@ +#properties-card-title-panel img { + margin-right: 1em; + vertical-align: middle; +} + +#properties-card-title-panel-labelgroup { + margin-left: 1em; +} + +.properties-list-panel { + margin-top: 1em !important; +} + +.properties-list-panel .pf-c-panel__header { + color: darkblue; +} + +.properties-no-properties { + font-style: italic !important; +} + +.properties-name-tooltip-button { + margin-left: 0.5em; + color: grey; +} diff --git a/packages/hawtio/src/plugins/camel/properties/property.ts b/packages/hawtio/src/plugins/camel/properties/property.ts new file mode 100644 index 00000000..6d0f8cc3 --- /dev/null +++ b/packages/hawtio/src/plugins/camel/properties/property.ts @@ -0,0 +1,10 @@ +export class Property { + constructor(public name: string, public value: string | null, public description: string) {} + + static sortByName(a: Property, b: Property) { + if (a.name < b.name) return -1 + if (a.name > b.name) return 1 + + return 0 + } +} diff --git a/packages/hawtio/src/plugins/camel/routes-service.test.ts b/packages/hawtio/src/plugins/camel/routes-service.test.ts index 396f02a1..0c30f299 100644 --- a/packages/hawtio/src/plugins/camel/routes-service.test.ts +++ b/packages/hawtio/src/plugins/camel/routes-service.test.ts @@ -96,7 +96,7 @@ describe('routes-service', () => { { id: 'quartz:simple?trigger.repeatInterval={{quartz.repeatInterval}}:from', name: 'quartz:simple?trigger.repeatInterval={{quartz.repeatInterval}}: from', - localName: 'from' + localName: 'from', }, { id: 'setBody2:setBody', name: 'setBody2: setBody', localName: 'setBody' }, { id: 'to3:to', name: 'to3: to', localName: 'to' }, diff --git a/packages/hawtio/src/plugins/camel/routes-service.ts b/packages/hawtio/src/plugins/camel/routes-service.ts index 4e0cc875..d2e3e211 100644 --- a/packages/hawtio/src/plugins/camel/routes-service.ts +++ b/packages/hawtio/src/plugins/camel/routes-service.ts @@ -61,7 +61,7 @@ export type RouteStats = Statistics & { } class RoutesService { - getIcon(nodeSettingsOrXmlNode: Record | Element): React.ReactNode { + getIcon(nodeSettingsOrXmlNode: Record | Element, size?: number): React.ReactNode { let nodeSettings: Record | null = null if (nodeSettingsOrXmlNode instanceof Element) { @@ -91,7 +91,7 @@ class RoutesService { // // Fetch the correct FunctionComponent icon from the icons module // - return icons.getIcon(iname) + return icons.getIcon(iname, size) } return null @@ -110,7 +110,7 @@ class RoutesService { */ const xmlId = routeXml.id const xmlUri = routeXml.getAttribute('uri') - const nodeName = (xmlId ? xmlId + ': ' : (xmlUri ? xmlUri + ': ' : '')) + routeXml.localName + const nodeName = (xmlId ? xmlId + ': ' : xmlUri ? xmlUri + ': ' : '') + routeXml.localName if (nodeSettings) { const node = new MBeanNode(null, nodeName, false)