Skip to content

Commit

Permalink
feat: Support configuring expression in Canvas form
Browse files Browse the repository at this point in the history
Fixes: #270
  • Loading branch information
igarashitm committed Nov 10, 2023
1 parent 365f7a0 commit 29a822c
Show file tree
Hide file tree
Showing 16 changed files with 926 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PipeErrorHandlerEditor } from './PipeErrorHandlerEditor';
import { within } from '@testing-library/dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { pipeErrorHandlerJson } from '../../stubs/PipeErrorHandler';
import * as pipeErrorHandlerSchema from '@kaoto-next/camel-catalog/PipeErrorHandler.json';

describe('PipeErrorHandlerEditor', () => {
it('should render', () => {
Expand All @@ -13,8 +13,8 @@ describe('PipeErrorHandlerEditor', () => {
},
},
};
render(<PipeErrorHandlerEditor model={model} onChangeModel={() => {}} schema={pipeErrorHandlerJson} />);
const element = screen.getByTestId('metadata-editor-form-Log ErrorHandler');
render(<PipeErrorHandlerEditor model={model} onChangeModel={() => {}} schema={pipeErrorHandlerSchema} />);
const element = screen.getByTestId('metadata-editor-form-Log Pipe ErrorHandler');
expect(element).toBeTruthy();
const inputs = screen.getAllByTestId('num-field');
expect(inputs.length).toBe(2);
Expand All @@ -23,8 +23,8 @@ describe('PipeErrorHandlerEditor', () => {
});

it('should not render a form if model is empty', () => {
render(<PipeErrorHandlerEditor model={{}} onChangeModel={() => {}} schema={pipeErrorHandlerJson} />);
const element = screen.queryByTestId('metadata-editor-form-Log ErrorHandler');
render(<PipeErrorHandlerEditor model={{}} onChangeModel={() => {}} schema={pipeErrorHandlerSchema} />);
const element = screen.queryByTestId('metadata-editor-form-Log Pipe ErrorHandler');
expect(element).toBeFalsy();
});

Expand All @@ -36,19 +36,19 @@ describe('PipeErrorHandlerEditor', () => {
onChangeModel={(m) => {
model = m;
}}
schema={pipeErrorHandlerJson}
schema={pipeErrorHandlerSchema}
/>,
);
const button = screen.getByRole('button');
fireEvent(button!, new MouseEvent('click', { bubbles: true }));
const options = screen.getAllByTestId(/pipe-error-handler-select-option.*/);
options.forEach((option) => {
if (option.innerHTML.includes('Log ErrorHandler')) {
if (option.innerHTML.includes('Log Pipe ErrorHandler')) {
const button = within(option).getByRole('option');
fireEvent(button, new MouseEvent('click', { bubbles: true }));
}
});
const element = screen.getByTestId('metadata-editor-form-Log ErrorHandler');
const element = screen.getByTestId('metadata-editor-form-Log Pipe ErrorHandler');
expect(element).toBeTruthy();
expect(model.log).toBeTruthy();
});
Expand Down
11 changes: 8 additions & 3 deletions packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { AutoFields, AutoForm, ErrorsField } from '@kaoto-next/uniforms-patternfly';
import { Title } from '@patternfly/react-core';
import { FunctionComponent, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { FunctionComponent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from '../../ErrorBoundary';
import { SchemaService } from '../../Form';
import { CustomAutoField } from '../../Form/CustomAutoField';
import { CanvasNode } from './canvas.models';
import { EntitiesContext } from '../../../providers/entities.provider';
import { ExpressionEditor } from './ExpressionEditor';

interface CanvasFormProps {
selectedNode: CanvasNode;
Expand Down Expand Up @@ -38,12 +39,16 @@ export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {
[entitiesContext, props.selectedNode.data?.vizNode],
);

const isExpressionAwareStep = useMemo(() => {
return schema?.schema?.properties?.expression !== undefined;
}, [schema]);

return schema?.schema === undefined ? null : (
<ErrorBoundary fallback={<p>This node cannot be configured yet</p>}>
<Title headingLevel="h1">{componentName}</Title>

{isExpressionAwareStep && <ExpressionEditor selectedNode={props.selectedNode} />}
<AutoForm ref={formRef} schema={schema} model={model} onChangeModel={handleOnChange}>
<AutoFields autoField={CustomAutoField} />
<AutoFields autoField={CustomAutoField} omitFields={['expression']} />
<ErrorsField />
</AutoForm>
</ErrorBoundary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as yamlDslSchema from '@kaoto-next/camel-catalog/camelYamlDsl.json';
import * as languageCatalog from '@kaoto-next/camel-catalog/camel-catalog-aggregate-languages.json';
import { fireEvent, render, screen } from '@testing-library/react';
import { ExpressionEditor } from './ExpressionEditor';
import { CamelCatalogService } from '../../../models/visualization/flows';
import { CatalogKind, ICamelLanguageDefinition } from '../../../models';
import { CanvasNode } from './canvas.models';
import { JSONSchemaType } from 'ajv';
import { IVisualizationNode, VisualComponentSchema } from '../../../models/visualization/base-visual-entity';
import { useSchemasStore } from '../../../store';
import { act } from 'react-dom/test-utils';

describe('ExpressionEditor', () => {
let mockNode: CanvasNode;
beforeAll(() => {
/* eslint-disable @typescript-eslint/no-explicit-any */
delete (yamlDslSchema as any).default;
delete (languageCatalog as any).default;
CamelCatalogService.setCatalogKey(
CatalogKind.Language,
languageCatalog as unknown as Record<string, ICamelLanguageDefinition>,
);

act(() => {
useSchemasStore.setState({
schemas: {
camelYamlDsl: {
name: 'camelYamlDsl',
tags: ['camel'],
version: '1.0.0',
uri: '',
schema: yamlDslSchema as unknown as Record<string, unknown>,
},
},
});
});

const visualComponentSchema: VisualComponentSchema = {
title: 'My Node',
schema: {
type: 'object',
properties: {
name: {
type: 'string',
},
},
} as unknown as JSONSchemaType<unknown>,
definition: {
name: 'my node',
},
};

mockNode = {
id: '1',
type: 'node',
data: {
vizNode: {
getComponentSchema: () => visualComponentSchema,
updateModel: (_value: unknown) => {},
} as IVisualizationNode,
},
};
});

it('should render', () => {
render(<ExpressionEditor selectedNode={mockNode} />);
const buttons = screen.getAllByRole('button');
fireEvent.click(buttons[1]);
const jsonpath = screen.getByTestId('expression-dropdownitem-jsonpath');
fireEvent.click(jsonpath.getElementsByTagName('button')[0]);
const form = screen.getByTestId('metadata-editor-form-expression');
expect(form.innerHTML).toContain('Suppress Exceptions');
});
});
126 changes: 126 additions & 0 deletions packages/ui/src/components/Visualization/Canvas/ExpressionEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { FunctionComponent, Ref, useCallback, useContext, useMemo, useState } from 'react';
import {
Card,
CardBody,
CardExpandableContent,
CardHeader,
CardTitle,
Dropdown,
DropdownItem,
DropdownList,
MenuToggle,
MenuToggleElement,
} from '@patternfly/react-core';
import { MetadataEditor } from '../../MetadataEditor';
import { EntitiesContext } from '../../../providers';
import { CanvasNode } from './canvas.models';
import { ExpressionService } from './expression.service';
import { useSchemasStore } from '../../../store';

interface ExpressionEditorProps {
selectedNode: CanvasNode;
}

export const ExpressionEditor: FunctionComponent<ExpressionEditorProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const [isOpen, setIsOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(true);
const camelYamlDslSchema = useSchemasStore((state) => state.schemas['camelYamlDsl'].schema) as Record<
string,
unknown
>;
const languageCatalogMap = useMemo(() => {
return ExpressionService.getLanguageMap(camelYamlDslSchema);
}, [camelYamlDslSchema]);

const visualComponentSchema = props.selectedNode.data?.vizNode?.getComponentSchema();
if (visualComponentSchema) {
if (!visualComponentSchema.definition) {
visualComponentSchema.definition = {};
}
}
const model = visualComponentSchema?.definition;
const { language, model: expressionModel } = ExpressionService.parseExpressionModel(languageCatalogMap, model);
const languageSchema = useMemo(() => {
return ExpressionService.getLanguageSchema(language!);
}, [language]);

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

const handleOnChange = useCallback(
(selectedLanguage: string, newExpressionModel: Record<string, unknown>) => {
if (!model) return;
ExpressionService.setExpressionModel(languageCatalogMap, model, selectedLanguage, newExpressionModel);
props.selectedNode.data?.vizNode?.updateModel(model);
entitiesContext?.updateCodeFromEntities();
},
[entitiesContext, languageCatalogMap, model, props.selectedNode.data?.vizNode],
);

const onSelect = useCallback(
(_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
setIsOpen(false);
if (value === language!.language.modelName) return;
handleOnChange(value as string, {});
},
[handleOnChange, language],
);

const toggle = useCallback(
(toggleRef: Ref<MenuToggleElement>) => (
<MenuToggle ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen}>
{language!.language.title}
</MenuToggle>
),
[language, isOpen, onToggleClick],
);

return (
languageCatalogMap &&
model &&
language && (
<Card isCompact={true} isExpanded={isExpanded}>
<CardHeader onExpand={() => setIsExpanded(!isExpanded)}>
<CardTitle>Expression</CardTitle>
</CardHeader>
<CardExpandableContent>
<CardBody>
<Dropdown
id="expression-select"
data-testid="expression-dropdown"
isOpen={isOpen}
selected={language.language.modelName}
onSelect={onSelect}
toggle={toggle}
isScrollable={true}
>
<DropdownList data-testid="expression-dropdownlist">
{Object.values(languageCatalogMap).map((language) => {
return (
<DropdownItem
data-testid={`expression-dropdownitem-${language.language.modelName}`}
key={language.language.title}
value={language.language.modelName}
description={language.language.description}
>
{language.language.title}
</DropdownItem>
);
})}
</DropdownList>
</Dropdown>
<MetadataEditor
data-testid="expression-editor"
name={'expression'}
schema={languageSchema}
metadata={expressionModel}
onChangeModel={(model) => handleOnChange(language.language.modelName, model)}
/>
</CardBody>
</CardExpandableContent>
</Card>
)
);
};
Loading

0 comments on commit 29a822c

Please sign in to comment.