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 => (
+ }>
+ {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)