Skip to content

Commit

Permalink
feat(CanvasForm): Add support for ErrorHandler
Browse files Browse the repository at this point in the history
Currently, there's partial support for `oneOf` in the `uniforms` library.

This commit adds a new `OneOfField` component that shows a selector to
pick which schema should the form generate.

Also, if a model is already defined, the closest schema is selected
automatically.

fix: #560
relates: #948
  • Loading branch information
lordrip committed Mar 19, 2024
1 parent e62ffa9 commit 786a60c
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 15 deletions.
5 changes: 5 additions & 0 deletions packages/ui/src/components/Form/CustomAutoField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@kaoto-next/uniforms-patternfly';
import { createAutoField } from 'uniforms';
import { getValue } from '../../utils';
import { OneOfField } from './OneOf/OneOfField';
import { BeanReferenceField } from './bean/BeanReferenceField';
import { DisabledField } from './customField/DisabledField';
import { TypeaheadField } from './customField/TypeaheadField';
Expand All @@ -21,6 +22,10 @@ import { PropertiesField } from './properties/PropertiesField';
* In case a field is not supported, it will render a DisabledField
*/
export const CustomAutoField = createAutoField((props) => {
if (Array.isArray(props.oneOf) && props.oneOf.length > 0) {
return OneOfField;
}

if (props.options) {
return props.checkboxes && props.fieldType !== Array ? RadioField : TypeaheadField;
}
Expand Down
111 changes: 111 additions & 0 deletions packages/ui/src/components/Form/OneOf/OneOfField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { GuaranteedProps, connectField } from 'uniforms';
import { useAppliedSchema, useSchemaBridgeContext } from '../../../hooks';
import { KaotoSchemaDefinition } from '../../../models';
import { SchemaBridgeProvider } from '../../../providers/schema-bridge.provider';
import { ROOT_PATH, isDefined } from '../../../utils';
import { OneOfSchemas, getOneOfSchemaList } from '../../../utils/get-oneof-schema-list';
import { CustomAutoForm, CustomAutoFormRef } from '../CustomAutoForm';
import { SchemaService } from '../schema.service';
import { OneOfSchemaList } from './OneOfSchemaList';

interface OneOfComponentProps extends GuaranteedProps<unknown> {
oneOf: KaotoSchemaDefinition['schema'][];
}

const applyDefinitionsToSchema = (
schema?: KaotoSchemaDefinition['schema'],
rootSchema?: KaotoSchemaDefinition['schema'],
) => {
if (!isDefined(schema) || !isDefined(rootSchema)) {
return schema;
}

return Object.assign({}, schema, {
definitions: rootSchema.definitions,
});
};

const OneOfComponent: FunctionComponent<OneOfComponentProps> = ({ name: propsName, oneOf, onChange }) => {
const formRef = useRef<CustomAutoFormRef>(null);
const divRef = useRef<HTMLDivElement>(null);

const { schemaBridge, parentRef } = useSchemaBridgeContext();
const oneOfSchemas: OneOfSchemas[] = useMemo(
() => getOneOfSchemaList(oneOf, schemaBridge?.schema),
[oneOf, schemaBridge?.schema],
);

const appliedSchema = useAppliedSchema(propsName, oneOfSchemas);
const [selectedSchema, setSelectedSchema] = useState<KaotoSchemaDefinition['schema'] | undefined>(
applyDefinitionsToSchema(appliedSchema?.schema, schemaBridge?.schema),
);
const [selectedSchemaName, setSelectedSchemaName] = useState<string | undefined>(appliedSchema?.name);

const onSchemaChanged = useCallback(
(schemaName: string | undefined) => {
if (schemaName === selectedSchemaName) return;
/** Remove existing properties */
const path = propsName === '' ? ROOT_PATH : `${propsName}.${selectedSchemaName}`;
onChange(undefined, path);

if (!isDefined(schemaName)) {
setSelectedSchema(undefined);
setSelectedSchemaName(undefined);
return;
}

const selectedSchema = oneOfSchemas.find((schema) => schema.name === schemaName);
const schemaWithDefinitions = applyDefinitionsToSchema(selectedSchema?.schema, schemaBridge?.schema);
setSelectedSchema(schemaWithDefinitions);
setSelectedSchemaName(schemaName);
},
[onChange, oneOfSchemas, propsName, schemaBridge?.schema, selectedSchemaName],
);

useEffect(() => {
formRef.current?.form.reset();
}, []);

const handleOnChangeIndividualProp = useCallback(
(path: string, value: unknown) => {
const updatedPath = propsName === '' ? path : `${propsName}.${path}`;
onChange(value, updatedPath);
},
[onChange, propsName],
);

return (
<OneOfSchemaList
name={propsName}
oneOfSchemas={oneOfSchemas}
selectedSchemaName={selectedSchemaName}
onSchemaChanged={onSchemaChanged}
>
{isDefined(selectedSchema) && (
<SchemaBridgeProvider schema={selectedSchema} parentRef={divRef}>
{isDefined(parentRef?.current) &&
createPortal(
<>
<CustomAutoForm
key={propsName}
ref={formRef}
model={appliedSchema?.model ?? {}}
onChange={handleOnChangeIndividualProp}
sortFields={false}
omitFields={SchemaService.OMIT_FORM_FIELDS}
data-testid={`${propsName}-autoform`}
/>
<div data-testid={`${propsName}-form-placeholder`} ref={divRef} />
</>,
parentRef.current,
propsName,
)}
</SchemaBridgeProvider>
)}
</OneOfSchemaList>
);
};

export const OneOfField = connectField(OneOfComponent as unknown as Parameters<typeof connectField>[0]);
109 changes: 109 additions & 0 deletions packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
Chip,
Dropdown,
DropdownItem,
DropdownList,
MenuToggle,
MenuToggleElement,
Text,
TextContent,
TextVariants,
} from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren, Ref, useCallback, useEffect, useState } from 'react';
import { OneOfSchemas } from '../../../utils/get-oneof-schema-list';
import { isDefined } from '../../../utils/is-defined';
import { SchemaService } from '../schema.service';

interface OneOfComponentProps extends PropsWithChildren {
name: string;
oneOfSchemas: OneOfSchemas[];
selectedSchemaName?: string;
onSchemaChanged: (name: string | undefined) => void;
}

export const OneOfSchemaList: FunctionComponent<OneOfComponentProps> = ({
name,
oneOfSchemas,
selectedSchemaName,
onSchemaChanged,
children,
}) => {
const [isOpen, setIsOpen] = useState(false);

const onSelect = useCallback(
(_event: unknown, value: string | number | undefined) => {
setIsOpen(false);
onSchemaChanged(value as string);
},
[onSchemaChanged],
);

const onToggleClick = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen]);

const toggle = useCallback(
(toggleRef: Ref<MenuToggleElement>) => (
<MenuToggle ref={toggleRef} onClick={onToggleClick} isFullWidth isExpanded={isOpen}>
{selectedSchemaName || (
<TextContent>
<Text component={TextVariants.small}>{SchemaService.DROPDOWN_PLACEHOLDER}</Text>
</TextContent>
)}
</MenuToggle>
),
[isOpen, onToggleClick, selectedSchemaName],
);

const onClearChip = useCallback(() => {
onSchemaChanged(undefined);
}, [onSchemaChanged]);

useEffect(() => {
if (oneOfSchemas.length === 1 && isDefined(oneOfSchemas[0]) && !selectedSchemaName) {
onSchemaChanged(oneOfSchemas[0].name);
}
}, [onSchemaChanged, oneOfSchemas, selectedSchemaName]);

if (oneOfSchemas.length === 1) {
return children;
}

return (
<>
{isDefined(selectedSchemaName) ? (
<Chip key={`${selectedSchemaName}-chip`} onClick={onClearChip}>
{selectedSchemaName}
</Chip>
) : (
<Dropdown
id={`${name}-oneof-select`}
data-testid={`${name}-oneof-select`}
isOpen={isOpen}
selected={selectedSchemaName}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={toggle}
isScrollable
>
<DropdownList data-testid={`${name}-oneof-select-dropdownlist`}>
{oneOfSchemas.map((schemaDef) => {
return (
<DropdownItem
data-testid={`${name}-oneof-select-dropdownlist-${schemaDef.name}`}
key={schemaDef.name}
value={schemaDef.name}
description={schemaDef.description}
>
{schemaDef.name}
</DropdownItem>
);
})}
</DropdownList>
</Dropdown>
)}

{children}
</>
);
};
26 changes: 14 additions & 12 deletions packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface CanvasFormProps {
export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const formRef = useRef<CustomAutoFormRef>(null);
const divRef = useRef<HTMLDivElement>(null);

const visualComponentSchema = useMemo(() => {
const answer = props.selectedNode.data?.vizNode?.getComponentSchema();
Expand Down Expand Up @@ -75,25 +76,26 @@ export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {
nodeIcon={props.selectedNode.data?.vizNode?.data?.icon}
/>
</CardHeader>

<CardBody className="canvas-form__body">
{stepFeatures.isUnknownComponent ? (
<UnknownNode model={model} />
) : (
<>
<SchemaBridgeProvider schema={visualComponentSchema?.schema} parentRef={divRef}>
{stepFeatures.isExpressionAwareStep && <StepExpressionEditor selectedNode={props.selectedNode} />}
{stepFeatures.isDataFormatAwareStep && <DataFormatEditor selectedNode={props.selectedNode} />}
{stepFeatures.isLoadBalanceAwareStep && <LoadBalancerEditor selectedNode={props.selectedNode} />}
<SchemaBridgeProvider schema={visualComponentSchema?.schema}>
<CustomAutoForm
ref={formRef}
model={model}
onChange={handleOnChangeIndividualProp}
sortFields={false}
omitFields={SchemaService.OMIT_FORM_FIELDS}
data-testid="autoform"
/>
</SchemaBridgeProvider>
</>
<CustomAutoForm
key={props.selectedNode.id}
ref={formRef}
model={model}
onChange={handleOnChangeIndividualProp}
sortFields={false}
omitFields={SchemaService.OMIT_FORM_FIELDS}
data-testid="autoform"
/>
<div data-testid="root-form-placeholder" ref={divRef} />
</SchemaBridgeProvider>
)}
</CardBody>
</Card>
Expand Down
10 changes: 7 additions & 3 deletions packages/ui/src/models/camel/camel-route-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { TileFilter } from '../../components/Catalog';
import { createCamelPropertiesSorter, isDefined } from '../../utils';
import { AddStepMode } from '../visualization/base-visual-entity';
import { CamelRouteVisualEntity, isCamelFrom, isCamelRoute } from '../visualization/flows';
import { CamelErrorHandlerVisualEntity } from '../visualization/flows/camel-error-handler-visual-entity';
import { CamelOnExceptionVisualEntity } from '../visualization/flows/camel-on-exception-visual-entity';
import { FlowTemplateService } from '../visualization/flows/flow-templates-service';
import { NonVisualEntity } from '../visualization/flows/non-visual-entity';
import { CamelOnExceptionVisualEntity } from '../visualization/flows/camel-on-exception-visual-entity';
import { CamelComponentFilterService } from '../visualization/flows/support/camel-component-filter.service';
import { CamelRouteVisualEntityData } from '../visualization/flows/support/camel-component-types';
import { BeansEntity, isBeans } from '../visualization/metadata';
Expand All @@ -14,7 +15,7 @@ import { BaseCamelEntity } from './entities';
import { SourceSchemaType } from './source-schema-type';

export class CamelRouteResource implements CamelResource, BeansAwareResource {
static readonly SUPPORTED_ENTITIES = [CamelOnExceptionVisualEntity];
static readonly SUPPORTED_ENTITIES = [CamelOnExceptionVisualEntity, CamelErrorHandlerVisualEntity] as const;
static readonly PARAMETERS_ORDER = ['id', 'description', 'uri', 'parameters', 'steps'];
readonly sortFn = createCamelPropertiesSorter(CamelRouteResource.PARAMETERS_ORDER) as (
a: unknown,
Expand Down Expand Up @@ -53,7 +54,9 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource {

getVisualEntities(): CamelRouteVisualEntity[] {
return this.entities.filter(
(entity) => entity instanceof CamelRouteVisualEntity || entity instanceof CamelOnExceptionVisualEntity,
(entity) =>
entity instanceof CamelRouteVisualEntity ||
CamelRouteResource.SUPPORTED_ENTITIES.some((SupportedEntity) => entity instanceof SupportedEntity),
) as CamelRouteVisualEntity[];
}

Expand Down Expand Up @@ -117,6 +120,7 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource {

for (const Entity of CamelRouteResource.SUPPORTED_ENTITIES) {
if (Entity.isApplicable(rawItem)) {
// @ts-expect-error When iterating over the entities, we know that the entity is applicable but TS doesn't, hence causing an error
return new Entity(rawItem);
}
}
Expand Down
Loading

0 comments on commit 786a60c

Please sign in to comment.