Skip to content

Commit

Permalink
fix(Properties View): Implement Properties View for Camel Plugin
Browse files Browse the repository at this point in the history
* CamelContent
* camel-content-service
 * Adds view to navigation tabs

* routes-service
* Icons
 * Implement creation of custom size versions of the icons then cache them

* Properties
 * View that displays schema properties of the route xml nodes

Fixes: #387
  • Loading branch information
phantomjinx committed Jun 29, 2023
1 parent 7dedd9d commit 699d8a6
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 6 deletions.
2 changes: 2 additions & 0 deletions packages/hawtio/src/plugins/camel/CamelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -101,6 +102,7 @@ export const CamelContent: React.FunctionComponent = () => {
!ccs.isEndpointsFolder(node) &&
(ccs.isRouteNode(node) || ccs.isRoutesFolder(node)),
},
{ id: 'properties', title: 'Properties', component: <Properties />, isApplicable: ccs.hasProperties },
{ id: 'send', title: 'Send', component: <SendMessage />, isApplicable: ccs.isEndpointNode },
{
id: 'browse',
Expand Down
4 changes: 4 additions & 0 deletions packages/hawtio/src/plugins/camel/camel-content-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 34 additions & 2 deletions packages/hawtio/src/plugins/camel/icons/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log } from '../globals'
import { CamelImageIcon } from './CamelImageIcon'
import * as svg from './svg'

Expand All @@ -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)
}
137 changes: 137 additions & 0 deletions packages/hawtio/src/plugins/camel/properties/Properties.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ReactNode>()
const [labels, setLabels] = useState<string[]>([])
const [description, setDescription] = useState('')
const [definedProperties, setDefinedProperties] = useState<Property[]>([])
const [defaultProperties, setDefaultProperties] = useState<Property[]>([])
const [undefinedProperties, setUndefinedProperties] = useState<Property[]>([])

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<string, Record<string, string>>
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 (
<Card>
<CardBody>
<Text component='p'>No selection has been made</Text>
</CardBody>
</Card>
)
}

if (isReading) {
return (
<Card>
<CardBody>
<Skeleton data-testid='loading' screenreaderText='Loading...' />
</CardBody>
</Card>
)
}

return (
<Card isFullHeight>
<CardHeader>
<CardTitle>Properties</CardTitle>
</CardHeader>
<CardBody id='properties-card-body'>
<Panel variant='raised'>
<PanelMain>
<PanelMainBody id='properties-card-title-panel'>
{icon}
<span>{title}</span>
<LabelGroup id='properties-card-title-panel-labelgroup'>
{labels.map(label => (
<Label key={label} icon={<InfoCircleIcon />}>
{label}
</Label>
))}
</LabelGroup>
</PanelMainBody>
</PanelMain>
</Panel>
{description && (
<Panel variant='raised'>
<PanelMain>
<PanelMainBody id='properties-card-description-panel'>{description}</PanelMainBody>
</PanelMain>
</Panel>
)}
<PropertiesList title='Defined Properties' values={definedProperties} />
<PropertiesList title='Default Properties' values={defaultProperties} />
<PropertiesList title='Undefined Properties' values={undefinedProperties} />
</CardBody>
</Card>
)
}
48 changes: 48 additions & 0 deletions packages/hawtio/src/plugins/camel/properties/PropertiesList.tsx
Original file line number Diff line number Diff line change
@@ -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<PropertiesListProps> = props => {
return (
<Panel variant='raised' className='properties-list-panel'>
<PanelHeader>{props.title}</PanelHeader>
<PanelMain>
{(!props.values || props.values.length === 0) && (
<PanelMainBody className='properties-no-properties'>No properties</PanelMainBody>
)}
{props.values && props.values.length > 0 && (
<PanelMainBody>
<DescriptionList columnModifier={{ default: '2Col' }}>
{props.values.map(p => {
return (
<DescriptionListGroup key={p.name}>
<DescriptionListTerm>
<PropertiesTooltippedName property={p} />
</DescriptionListTerm>
<DescriptionListDescription>{p.value}</DescriptionListDescription>
</DescriptionListGroup>
)
})}
</DescriptionList>
</PanelMainBody>
)}
</PanelMain>
</Panel>
)
}
Original file line number Diff line number Diff line change
@@ -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<PropertiesTTNameProps> = props => {
const tooltipRef = React.useRef<HTMLSpanElement>(null)

return (
<React.Fragment>
{props.property.name}

<span ref={tooltipRef} className='properties-name-tooltip-button'>
<InfoCircleIcon />
</span>
<Tooltip id='tooltip-ref1' reference={tooltipRef} content={<div>{props.property.description}</div>} />
</React.Fragment>
)
}
1 change: 1 addition & 0 deletions packages/hawtio/src/plugins/camel/properties/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Properties } from './Properties'
74 changes: 74 additions & 0 deletions packages/hawtio/src/plugins/camel/properties/properties-service.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, string>>) {
// 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<string, Record<string, string>>): 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<string, Record<string, string>>): 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<string, Record<string, string>>): 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)
}
25 changes: 25 additions & 0 deletions packages/hawtio/src/plugins/camel/properties/properties.css
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions packages/hawtio/src/plugins/camel/properties/property.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 699d8a6

Please sign in to comment.