-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Beans): Port Bean Configuration UI
Fixes: #8
- Loading branch information
1 parent
517a16d
commit 4b6cc23
Showing
32 changed files
with
1,745 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { isBeans } from './is-beans'; | ||
import { beansJson } from '../stubs/beans'; | ||
|
||
describe('isBeans', () => { | ||
it.each([ | ||
[beansJson, true], | ||
[undefined, false], | ||
[null, false], | ||
[true, false], | ||
[false, false], | ||
])('should mark %s as isBeans: %s', (beans, result) => { | ||
expect(isBeans(beans)).toEqual(result); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { BeansDeserializer } from '@kaoto-next/camel-catalog/types'; | ||
import { isDefined } from '../utils'; | ||
|
||
/** Very basic check to determine whether this object is a Beans */ | ||
export const isBeans = (rawEntity: unknown): rawEntity is {beans: BeansDeserializer} => { | ||
if (!isDefined(rawEntity) || Array.isArray(rawEntity) || typeof rawEntity !== 'object') { | ||
return false; | ||
} | ||
|
||
return 'beans' in rawEntity! && Array.isArray((rawEntity! as any).beans); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/ui/src/components/Form/properties/AddPropertyButtons.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { AddPropertyButtons } from './AddPropertyButtons.tsx'; | ||
import { screen } from '@testing-library/dom'; | ||
import { fireEvent, render } from '@testing-library/react'; | ||
|
||
describe('AddPropertyButtons.tsx', () => { | ||
test('Add string property button', () => { | ||
const events: boolean[] = []; | ||
render( | ||
<AddPropertyButtons | ||
path={['foo', 'bar']} | ||
createPlaceholder={(isObject) => events.push(isObject)} | ||
/>, | ||
); | ||
const element = screen.getByTestId('properties-add-string-property-foo-bar-btn'); | ||
expect(events.length).toBe(0); | ||
fireEvent.click(element); | ||
expect(events.length).toBe(1); | ||
expect(events[0]).toBeFalsy(); | ||
}); | ||
|
||
test('Add object property button', () => { | ||
const events: boolean[] = []; | ||
render( | ||
<AddPropertyButtons | ||
path={['foo', 'bar']} | ||
createPlaceholder={(isObject) => events.push(isObject)} | ||
/>, | ||
); | ||
const element = screen.getByTestId('properties-add-object-property-foo-bar-btn'); | ||
expect(events.length).toBe(0); | ||
fireEvent.click(element); | ||
expect(events.length).toBe(1); | ||
expect(events[0]).toBeTruthy(); | ||
}); | ||
}); |
52 changes: 52 additions & 0 deletions
52
packages/ui/src/components/Form/properties/AddPropertyButtons.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Button, Split, SplitItem, Tooltip } from '@patternfly/react-core'; | ||
import { FolderPlusIcon, PlusCircleIcon } from '@patternfly/react-icons'; | ||
|
||
type AddPropertyPopoverProps = { | ||
showLabel?: boolean; | ||
path: string[]; | ||
disabled?: boolean; | ||
createPlaceholder: (isObject: boolean) => void; | ||
}; | ||
|
||
/** | ||
* A set of "add string property" and "add object property" buttons which triggers creating a placeholder. | ||
* @param props | ||
* @constructor | ||
*/ | ||
export function AddPropertyButtons({ | ||
showLabel = false, | ||
path, | ||
disabled = false, | ||
createPlaceholder, | ||
}: AddPropertyPopoverProps) { | ||
return ( | ||
<Split> | ||
<SplitItem> | ||
<Tooltip content="Add string property"> | ||
<Button | ||
data-testid={`properties-add-string-property-${path.join('-')}-btn`} | ||
variant={'link'} | ||
icon={<PlusCircleIcon />} | ||
isDisabled={disabled} | ||
onClick={() => createPlaceholder(false)} | ||
> | ||
{showLabel && 'Add string property'} | ||
</Button> | ||
</Tooltip> | ||
</SplitItem> | ||
<SplitItem> | ||
<Tooltip content="Add object property"> | ||
<Button | ||
data-testid={`properties-add-object-property-${path.join('-')}-btn`} | ||
variant={'link'} | ||
icon={<FolderPlusIcon />} | ||
isDisabled={disabled} | ||
onClick={() => createPlaceholder(true)} | ||
> | ||
{showLabel && 'Add object property'} | ||
</Button> | ||
</Tooltip> | ||
</SplitItem> | ||
</Split> | ||
); | ||
} |
236 changes: 236 additions & 0 deletions
236
packages/ui/src/components/Form/properties/PropertiesField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
import {AddPropertyButtons} from './AddPropertyButtons.tsx'; | ||
import {PropertyRow} from './PropertyRow.tsx'; | ||
import {wrapField} from '@kaoto-next/uniforms-patternfly'; | ||
import { | ||
EmptyState, | ||
EmptyStateBody, ExpandableSection, | ||
Stack, | ||
StackItem | ||
} from '@patternfly/react-core'; | ||
import { | ||
Table, | ||
TableVariant, | ||
Tbody, | ||
Td, | ||
TdProps, | ||
Th, | ||
Thead, | ||
Tr, | ||
} from '@patternfly/react-table'; | ||
import {ReactNode, useState} from 'react'; | ||
import {HTMLFieldProps, connectField} from 'uniforms'; | ||
|
||
export type PropertiesFieldProps = HTMLFieldProps<any, HTMLDivElement>; | ||
|
||
/** | ||
* The uniforms custom field for editing generic properties where it has type "object" in the schema, | ||
* but it doesn't have "properties" declared. | ||
* @param props | ||
* @constructor | ||
*/ | ||
export const PropertiesField = connectField((props: PropertiesFieldProps) => { | ||
const [isFieldExpanded, setFieldExpanded] = useState<boolean>(false); | ||
const [expandedNodes, setExpandedNodes] = useState<string[]>([]); | ||
const [placeholderState, setPlaceholderState] = useState<PlaceholderState | null>(null); | ||
const propertiesModel = props.value ? {...props.value} : {}; | ||
|
||
type PlaceholderState = { | ||
isObject: boolean; | ||
parentNodeId: string; | ||
}; | ||
|
||
function handleModelChange() { | ||
setPlaceholderState(null); | ||
props.onChange(propertiesModel, props.name); | ||
} | ||
|
||
function getNodeId(path: string[]) { | ||
return path.join('-'); | ||
} | ||
|
||
function handleCreatePlaceHolder(state: PlaceholderState) { | ||
setPlaceholderState({...state}); | ||
if (state.parentNodeId && state.parentNodeId.length > 0) { | ||
expandedNodes.includes(state.parentNodeId) || | ||
setExpandedNodes([...expandedNodes, state.parentNodeId]); | ||
} | ||
} | ||
|
||
function renderRows( | ||
[node, ...remainingNodes]: [string, any][], | ||
parentModel: any, | ||
parentPath: string[] = [], | ||
level = 1, | ||
posinset = 1, | ||
rowIndex = 0, | ||
isHidden = false, | ||
): ReactNode[] { | ||
if (!node) { | ||
// placeholder is rendered as a last sibling | ||
const placeholderTreeRow: TdProps['treeRow'] = { | ||
rowIndex, | ||
onCollapse: () => { | ||
}, | ||
props: { | ||
isRowSelected: true, | ||
isExpanded: false, | ||
isHidden: false, | ||
'aria-level': level, | ||
'aria-posinset': posinset, | ||
'aria-setsize': 0, | ||
}, | ||
}; | ||
|
||
return placeholderState && placeholderState.parentNodeId === getNodeId(parentPath) | ||
? [ | ||
<PropertyRow | ||
isPlaceholder={true} | ||
key="placeholder" | ||
propertyName={props.name} | ||
nodeName="" | ||
nodeValue={placeholderState.isObject ? {} : ''} | ||
path={parentPath} | ||
parentModel={parentModel} | ||
treeRow={placeholderTreeRow} | ||
isObject={placeholderState.isObject} | ||
onChangeModel={handleModelChange} | ||
createPlaceholder={() => { | ||
}} | ||
/>, | ||
] | ||
: []; | ||
} | ||
|
||
const nodeName = node[0]; | ||
const nodeValue = node[1]; | ||
const path = parentPath.slice(); | ||
path.push(nodeName); | ||
const nodeId = getNodeId(path); | ||
const isExpanded = expandedNodes.includes(nodeId); | ||
|
||
const childRows = | ||
typeof nodeValue === 'object' | ||
? renderRows( | ||
Object.entries(nodeValue), | ||
nodeValue, | ||
path, | ||
level + 1, | ||
1, | ||
rowIndex + 1, | ||
!isExpanded || isHidden, | ||
) | ||
: []; | ||
|
||
const siblingRows = renderRows( | ||
remainingNodes, | ||
parentModel, | ||
parentPath, | ||
level, | ||
posinset + 1, | ||
rowIndex + 1 + childRows.length, | ||
isHidden, | ||
); | ||
|
||
const treeRow: TdProps['treeRow'] = { | ||
onCollapse: () => | ||
setExpandedNodes((prevExpanded) => { | ||
const otherExpandedNodeIds = prevExpanded.filter((id) => id !== nodeId); | ||
return isExpanded ? otherExpandedNodeIds : [...otherExpandedNodeIds, nodeId]; | ||
}), | ||
rowIndex, | ||
props: { | ||
isExpanded, | ||
isHidden, | ||
'aria-level': level, | ||
'aria-posinset': posinset, | ||
'aria-setsize': typeof nodeValue === 'object' ? Object.keys(nodeValue).length : 0, | ||
}, | ||
}; | ||
|
||
return [ | ||
<PropertyRow | ||
key={`${props.name}-${getNodeId(path)}`} | ||
propertyName={props.name} | ||
nodeName={nodeName} | ||
nodeValue={nodeValue} | ||
path={path} | ||
parentModel={parentModel} | ||
treeRow={treeRow} | ||
isObject={typeof nodeValue === 'object'} | ||
onChangeModel={handleModelChange} | ||
createPlaceholder={(isObject) => { | ||
handleCreatePlaceHolder({ | ||
isObject: isObject, | ||
parentNodeId: getNodeId(path), | ||
}); | ||
}} | ||
/>, | ||
...childRows, | ||
...siblingRows, | ||
]; | ||
} | ||
|
||
return wrapField( | ||
props, | ||
<ExpandableSection | ||
toggleText={''} | ||
onToggle={(_event, isExpanded) => setFieldExpanded(isExpanded)} | ||
isExpanded={isFieldExpanded} | ||
> | ||
<Stack hasGutter> | ||
<StackItem isFilled> | ||
<Table | ||
isTreeTable={true} | ||
aria-label={props.name} | ||
variant={TableVariant.compact} | ||
borders={true} | ||
isStickyHeader | ||
> | ||
<Thead> | ||
<Tr key={`${props.name}-header`}> | ||
<Th modifier="nowrap">NAME</Th> | ||
<Th modifier="nowrap">VALUE</Th> | ||
<Td modifier="nowrap" isActionCell> | ||
<AddPropertyButtons | ||
path={[]} | ||
disabled={props.disabled} | ||
createPlaceholder={(isObject) => | ||
handleCreatePlaceHolder({ | ||
isObject: isObject, | ||
parentNodeId: '', | ||
}) | ||
} | ||
/> | ||
</Td> | ||
</Tr> | ||
</Thead> | ||
<Tbody> | ||
{Object.keys(propertiesModel).length > 0 || placeholderState | ||
? renderRows(Object.entries(propertiesModel), propertiesModel) | ||
: !props.disabled && ( | ||
<Tr key={`${props.name}-empty`}> | ||
<Td colSpan={3}> | ||
<EmptyState> | ||
<EmptyStateBody>No {props.name}</EmptyStateBody> | ||
<AddPropertyButtons | ||
showLabel={true} | ||
path={[]} | ||
disabled={props.disabled} | ||
createPlaceholder={(isObject) => | ||
handleCreatePlaceHolder({ | ||
isObject: isObject, | ||
parentNodeId: '', | ||
}) | ||
} | ||
/> | ||
</EmptyState> | ||
</Td> | ||
</Tr> | ||
)} | ||
</Tbody> | ||
</Table> | ||
</StackItem> | ||
</Stack> | ||
</ExpandableSection>, | ||
); | ||
}); |
Oops, something went wrong.