Skip to content

Commit

Permalink
feat(viz): Update Source Code using the config form
Browse files Browse the repository at this point in the history
Currently, there's no mechanism for updating the Source Code
through the Design page.

This commit brings that functionality by leveraging the `path`
property from the `IVisualizationNode` object and lodash.set.

The new content is shaped by `uniforms`, using the composed
JSON Schema out of the Camel Components properties.

This is the first iteration, there's more work to be done, as
the serialization process depends on the actual entity.

relates to: #137
  • Loading branch information
lordrip committed Sep 27, 2023
1 parent c84d27d commit 1453212
Show file tree
Hide file tree
Showing 18 changed files with 503 additions and 42 deletions.
3 changes: 2 additions & 1 deletion packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Outlet } from 'react-router-dom';
import { Shell } from './layout/Shell';
import { CatalogSchemaLoaderProvider, EntitiesProvider } from './providers';
import { CatalogSchemaLoaderProvider } from './providers/catalog-schema-loader.provider';
import { EntitiesProvider } from './providers/entities.provider';

function App() {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { render } from '@testing-library/react';
import { JSONSchemaType } from 'ajv';
import { VisualizationNode } from '../../../models/visualization';
import { VisualComponentSchema } from '../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../providers/entities.provider';
import { CanvasForm } from './CanvasForm';
import { CanvasNode } from './canvas.models';
import { JSONSchemaType } from 'ajv';

describe('CanvasForm', () => {
it('should render', () => {
Expand Down Expand Up @@ -32,7 +33,11 @@ describe('CanvasForm', () => {
},
};

const { container } = render(<CanvasForm selectedNode={selectedNode} />);
const { container } = render(
<EntitiesContext.Provider value={undefined}>
<CanvasForm selectedNode={selectedNode} />
</EntitiesContext.Provider>,
);

expect(container).toMatchSnapshot();
});
Expand All @@ -48,7 +53,11 @@ describe('CanvasForm', () => {
},
};

const { container } = render(<CanvasForm selectedNode={selectedNode} />);
const { container } = render(
<EntitiesContext.Provider value={undefined}>
<CanvasForm selectedNode={selectedNode} />
</EntitiesContext.Provider>,
);

expect(container).toMatchSnapshot();
});
Expand All @@ -70,7 +79,11 @@ describe('CanvasForm', () => {
},
};

const { container } = render(<CanvasForm selectedNode={selectedNode} />);
const { container } = render(
<EntitiesContext.Provider value={undefined}>
<CanvasForm selectedNode={selectedNode} />
</EntitiesContext.Provider>,
);

expect(container).toMatchSnapshot();
});
Expand Down
17 changes: 11 additions & 6 deletions packages/ui/src/components/Visualization/Canvas/CanvasForm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { AutoFields, AutoForm, ErrorsField } from '@kaoto-next/uniforms-patternfly';
import { Title } from '@patternfly/react-core';
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
import { FunctionComponent, useCallback, useContext, useEffect, 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';

interface CanvasFormProps {
selectedNode: CanvasNode;
}

export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const formRef = useRef<typeof AutoForm>();
const schemaServiceRef = useRef(new SchemaService());

Expand All @@ -20,18 +22,21 @@ export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {

useEffect(() => {
formRef.current?.reset();

const visualComponentSchema = props.selectedNode.data?.vizNode?.getComponentSchema();
console.log('visualComponentSchema', visualComponentSchema);

setSchema(schemaServiceRef.current.getSchemaBridge(visualComponentSchema?.schema));
setModel(visualComponentSchema?.definition ?? {});
setComponentName(visualComponentSchema?.title);
}, [props.selectedNode.data?.vizNode]);

const handleOnChange = useCallback((newModel: Record<string, unknown>) => {
setModel(newModel);
}, []);
const handleOnChange = useCallback(
(newModel: Record<string, unknown>) => {
props.selectedNode.data?.vizNode?.updateModel(newModel);
entitiesContext?.updateCodeFromEntities();
setModel(newModel);
},
[entitiesContext, props.selectedNode.data?.vizNode],
);

return schema?.schema === undefined ? null : (
<ErrorBoundary fallback={<p>This node cannot be configured yet</p>}>
Expand Down
43 changes: 36 additions & 7 deletions packages/ui/src/hooks/entities.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { useCallback, useMemo, useState } from 'react';
import { parse } from 'yaml';
import { parse, stringify } from 'yaml';
import { isCamelRoute, isKameletBinding, isPipe } from '../camel-utils';
import { BaseCamelEntity } from '../models/camel-entities';
import { BaseVisualCamelEntity } from '../models/visualization/base-visual-entity';
import { CamelRoute, KameletBinding, Pipe } from '../models/visualization/flows';
import { isDefined } from '../utils';
import { EventNotifier, isDefined } from '../utils';

export const useEntities = () => {
export interface EntitiesContextResult {
code: string;
setCode: (code: string) => void;
entities: BaseCamelEntity[];
visualEntities: BaseVisualCamelEntity[];
updateCodeFromEntities: () => void;
eventNotifier: EventNotifier;
}

export const useEntities = (): EntitiesContextResult => {
const [sourceCode, setSourceCode] = useState<string>('');
const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);
const eventNotifier = useMemo(() => new EventNotifier(), []);

/** Set the Source Code and updates the Entities */
const setCode = useCallback((code: string) => {
try {
setSourceCode(code);
const result = parse(code);
const rawEntities = Array.isArray(result) ? result : [result];

const entities = rawEntities.reduce(
const entitiesHolder = rawEntities.reduce(
(acc, rawEntity) => {
const entity = getEntity(rawEntity);

Expand All @@ -35,23 +46,41 @@ export const useEntities = () => {
},
);

setEntities(entities.entities);
setVisualEntities(entities.visualEntities);
setEntities(entitiesHolder.entities);
setVisualEntities(entitiesHolder.visualEntities);

/** Notify subscribers that a `entities:update` happened */
eventNotifier.next('entities:update', undefined);
} catch (e) {
setEntities([]);
setVisualEntities([]);
console.error(e);
}
}, []);

/** Updates the Source Code whenever the entities are updated */
const updateCodeFromEntities = useCallback(() => {
const visualEntitiesCode = stringify(visualEntities, null, { indent: 2 });
const entitiesCode = stringify(entities, null, { indent: 2 });
const code = visualEntitiesCode + '\n' + entitiesCode;

/** Set the Source Code directly, without using `setCode` as updating the entities is already done */
setSourceCode(code);

/** Notify subscribers that a `code:update` happened */
eventNotifier.next('code:update', code);
}, [entities, eventNotifier, visualEntities]);

const value = useMemo(
() => ({
code: sourceCode,
setCode,
entities,
visualEntities,
updateCodeFromEntities,
eventNotifier,
}),
[sourceCode, setCode, entities, visualEntities],
[sourceCode, setCode, entities, visualEntities, updateCodeFromEntities, eventNotifier],
);

return value;
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/layout/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Page, Panel, PanelMain, PanelMainBody } from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react';
import { useLocalStorage } from '../hooks';
import { LocalStorageKeys } from '../models';
import { EntitiesContext } from '../providers';
import { EntitiesContext } from '../providers/entities.provider';
import { camelRouteYaml } from '../stubs/camel-route';
import { Navigation } from './Navigation';
import './Shell.scss';
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/models/visualization/base-visual-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity {
/** Given a path, get the component type and definition */
getComponentSchema: (path?: string) => VisualComponentSchema | undefined;

/** Retrieve the model from the underlying Camel entity */
toJSON: () => unknown;

/** Given a path, update the model */
updateModel(path: string | undefined, value: unknown): void;

/** Retrieve the steps from the underlying Camel entity */
getSteps: () => unknown[];

Expand All @@ -42,6 +48,8 @@ export interface IVisualizationNode {

getComponentSchema(): VisualComponentSchema | undefined;

updateModel(value: unknown): void;

getRootNode(): IVisualizationNode;

getParentNode(): IVisualizationNode | undefined;
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/models/visualization/flows/kamelet-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export class KameletBinding implements BaseVisualCamelEntity {
return undefined;
}

toJSON() {
return { route: this.route };
}

updateModel(): void {
return;
}

getSteps() {
const steps: KameletBindingSteps = this.route.spec?.steps;
const sink: KameletBindingStep = this.route.spec?.sink;
Expand Down
Loading

0 comments on commit 1453212

Please sign in to comment.