Skip to content

Commit

Permalink
feat: Indication that some of the connectors mandatory properties are…
Browse files Browse the repository at this point in the history
… not yet configured

Fixes: #126
  • Loading branch information
igarashitm authored and lordrip committed Jan 19, 2024
1 parent 89f4404 commit 2647614
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DataFormatEditor } from './DataFormatEditor';
import { LoadBalancerEditor } from './LoadBalancerEditor';
import { StepExpressionEditor } from './StepExpressionEditor';
import { CanvasNode } from './canvas.models';
import { ModelValidationService } from '../../../models/visualization/flows/support/model-validation.service';

interface CanvasFormProps {
selectedNode: CanvasNode;
Expand Down Expand Up @@ -53,10 +54,13 @@ export const CanvasForm: FunctionComponent<CanvasFormProps> = (props) => {

const handleOnChange = useCallback(
(newModel: Record<string, unknown>) => {
props.selectedNode.data?.vizNode?.updateModel(newModel);
if (props.selectedNode.data?.vizNode) {
props.selectedNode.data.vizNode.updateModel(newModel);
ModelValidationService.validateNodeStatus(visualComponentSchema, newModel, props.selectedNode.data.vizNode);
}
entitiesContext?.updateSourceCodeFromEntities();
},
[entitiesContext, props.selectedNode.data?.vizNode, props.selectedNode.id],
[entitiesContext, props.selectedNode.data?.vizNode, visualComponentSchema],
);

const isExpressionAwareStep = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NodeShape } from '@patternfly/react-topology';
import { NodeShape, NodeStatus } from '@patternfly/react-topology';
import { LayoutType } from './canvas.models';

export class CanvasDefaults {
static readonly DEFAULT_LAYOUT = LayoutType.DagreVertical;
static readonly DEFAULT_NODE_SHAPE = NodeShape.rect;
static readonly DEFAULT_NODE_DIAMETER = 75;
static readonly DEFAULT_GROUP_PADDING = 50;
static DEFAULT_NODE_STATUS = NodeStatus.default;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('CanvasService', () => {
type: 'node',
data: undefined,
shape: CanvasDefaults.DEFAULT_NODE_SHAPE,
status: CanvasDefaults.DEFAULT_NODE_STATUS,
width: CanvasDefaults.DEFAULT_NODE_DIAMETER,
height: CanvasDefaults.DEFAULT_NODE_DIAMETER,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class CanvasService {
width: CanvasDefaults.DEFAULT_NODE_DIAMETER,
height: CanvasDefaults.DEFAULT_NODE_DIAMETER,
shape: CanvasDefaults.DEFAULT_NODE_SHAPE,
status: options.data?.vizNode?.getNodeStatus(),
};
}

Expand Down
26 changes: 23 additions & 3 deletions packages/ui/src/components/Visualization/Custom/CustomNode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { DefaultNode, Node, WithSelectionProps, withContextMenu, withSelection } from '@patternfly/react-topology';
import { FunctionComponent } from 'react';
import {
DefaultNode,
Node,
NodeStatus,
withContextMenu,
withSelection,
WithSelectionProps,
} from '@patternfly/react-topology';
import { FunctionComponent, useState } from 'react';
import { AddStepMode } from '../../../models/visualization/base-visual-entity';
import { CanvasDefaults } from '../Canvas/canvas.defaults';
import { CanvasNode } from '../Canvas/canvas.models';
Expand All @@ -16,9 +23,22 @@ interface CustomNodeProps extends WithSelectionProps {
const CustomNode: FunctionComponent<CustomNodeProps> = ({ element, ...rest }) => {
const vizNode = element.getData()?.vizNode;
const label = vizNode?.getNodeLabel();
const [statusDecoratorTooltip, setStatusDecoratorTooltip] = useState(vizNode?.getNodeStatusMessage());
vizNode?.addNodeStatusListener((status: NodeStatus, message: string | undefined) => {
element.setNodeStatus(status);
setStatusDecoratorTooltip(message);
});

return (
<DefaultNode element={element} label={label} truncateLength={15} {...rest} showStatusDecorator>
<DefaultNode
element={element}
label={label}
truncateLength={15}
{...rest}
showStatusDecorator
statusDecoratorTooltip={statusDecoratorTooltip}
onStatusDecoratorClick={() => {}}
>
<g data-testid={`custom-node__${vizNode?.id}`} data-nodelabel={label}>
<foreignObject
x="0"
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/hooks/entities.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ describe('useEntities', () => {
id: route-1234
from:
id: from-1234
uri: timer:template
uri: timer
parameters:
period: "1000"
timerName: template
steps:
- log:
id: log-1234
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`createCamelResource should create an empty KameletResource if no args i
"id": "from-1234",
"parameters": {
"period": "{{period}}",
"timerName": "user",
},
"steps": [
{
Expand All @@ -17,7 +18,7 @@ exports[`createCamelResource should create an empty KameletResource if no args i
"to": "kamelet:sink",
},
],
"uri": "timer:user",
"uri": "timer",
},
"id": "kamelet-1234",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ exports[`KameletResource should convert to JSON 1`] = `
"id": "from-1234",
"parameters": {
"period": "{{period}}",
"timerName": "user",
},
"steps": [
{
Expand All @@ -50,7 +51,7 @@ exports[`KameletResource should convert to JSON 1`] = `
"to": "kamelet:sink",
},
],
"uri": "timer:user",
"uri": "timer",
},
},
"types": {
Expand Down Expand Up @@ -103,6 +104,7 @@ exports[`KameletResource should create a new KameletResource 1`] = `
"id": "from-1234",
"parameters": {
"period": "{{period}}",
"timerName": "user",
},
"steps": [
{
Expand All @@ -112,7 +114,7 @@ exports[`KameletResource should create a new KameletResource 1`] = `
"to": "kamelet:sink",
},
],
"uri": "timer:user",
"uri": "timer",
},
},
"types": {
Expand Down Expand Up @@ -167,6 +169,7 @@ exports[`KameletResource should get the visual entities (Camel Route Visual Enti
"id": "from-1234",
"parameters": {
"period": "{{period}}",
"timerName": "user",
},
"steps": [
{
Expand All @@ -176,7 +179,7 @@ exports[`KameletResource should get the visual entities (Camel Route Visual Enti
"to": "kamelet:sink",
},
],
"uri": "timer:user",
"uri": "timer",
},
"id": "kamelet-1234",
},
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/models/visualization/base-visual-entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSONSchemaType } from 'ajv';
import { DefinedComponent } from '../camel-catalog-index';
import { BaseCamelEntity, EntityType } from '../camel/entities';
import { NodeStatus } from '@patternfly/react-topology';

/**
* BaseVisualCamelEntity
Expand Down Expand Up @@ -94,6 +95,14 @@ export interface IVisualizationNode<T extends IVisualizationNodeData = IVisualiz
removeChild(): void;

populateLeafNodesIds(ids: string[]): void;

addNodeStatusListener(listener: (status: NodeStatus, message: string | undefined) => void): void;

getNodeStatus(): NodeStatus;

setNodeStatus(status: NodeStatus, message: string | undefined): void;

getNodeStatusMessage(): string | undefined;
}

export interface IVisualizationNodeData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ICamelElementLookupResult,
} from './support/camel-component-types';
import { EntityType } from '../../camel/entities';
import { ModelValidationService } from './support/model-validation.service';

export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity {
constructor(public route: RouteDefinition) {}
Expand Down Expand Up @@ -240,6 +241,9 @@ export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity
childrenVizNodes.forEach((childVizNode) => vizNode.addChild(childVizNode));
});

const schema = this.getComponentSchema(path);
const model = get(this.route, path);
ModelValidationService.validateNodeStatus(schema, model, vizNode);
return vizNode;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ModelValidationService } from './model-validation.service';
import * as componentCatalogMap from '@kaoto-next/camel-catalog/camel-catalog-aggregate-components.json';
import * as modelCatalogMap from '@kaoto-next/camel-catalog/camel-catalog-aggregate-models.json';
import * as patternCatalogMap from '@kaoto-next/camel-catalog/camel-catalog-aggregate-patterns.json';
import { createVisualizationNode } from '../../visualization-node';
import { NodeStatus } from '@patternfly/react-topology';
import { CamelComponentSchemaService } from './camel-component-schema.service';
import { CamelCatalogService } from '../camel-catalog.service';
import { CatalogKind } from '../../../catalog-kind';
import { ICamelComponentDefinition } from '../../../camel-components-catalog';
import { ICamelProcessorDefinition } from '../../../camel-processors-catalog';
describe('ModelValidationService', () => {
const camelRoute = {
route: {
id: 'route-8888',
from: {
uri: 'timer:tutorial',
steps: [
{
to: {
uri: 'activemq',
parameters: {},
},
},
],
},
},
};

beforeAll(() => {
CamelCatalogService.setCatalogKey(
CatalogKind.Component,
componentCatalogMap as unknown as Record<string, ICamelComponentDefinition>,
);
CamelCatalogService.setCatalogKey(
CatalogKind.Processor,
modelCatalogMap as unknown as Record<string, ICamelProcessorDefinition>,
);
CamelCatalogService.setCatalogKey(
CatalogKind.Pattern,
patternCatalogMap as unknown as Record<string, ICamelProcessorDefinition>,
);
});

describe('validateNodeStatus()', () => {
it('should set warning status if required parameter is missing', () => {
const model = camelRoute.route.from.steps[0].to;
const path = 'route.from.steps[0].to';
const schema = CamelComponentSchemaService.getVisualComponentSchema(path, model);
const vizNode = createVisualizationNode('dummy', {});
ModelValidationService.validateNodeStatus(schema, model, vizNode);
expect(vizNode.getNodeStatus()).toEqual(NodeStatus.warning);
expect(vizNode.getNodeStatusMessage()).toContain('destinationName');
});

it('should set default status if required parameter is set', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = { ...camelRoute.route.from.steps[0].to } as any;
model.parameters['destinationName'] = 'myQueue';
const path = 'route.from.steps[0].to';
const schema = CamelComponentSchemaService.getVisualComponentSchema(path, model);
const vizNode = createVisualizationNode('dummy', {});
ModelValidationService.validateNodeStatus(schema, model, vizNode);
expect(vizNode.getNodeStatus()).toEqual(NodeStatus.default);
expect(vizNode.getNodeStatusMessage()).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { IVisualizationNode, VisualComponentSchema } from '../../base-visual-entity';
import { NodeStatus } from '@patternfly/react-topology';
import { JSONSchemaType } from 'ajv';

export interface IValidationResult {
level: 'error' | 'warning' | 'info';
type: 'missingRequired';
parentPath: string;
propertyName: string;
message: string;
}

export class ModelValidationService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private static validateModel(schema: JSONSchemaType<unknown>, model: any, parentPath: string): IValidationResult[] {
const answer = [] as IValidationResult[];
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, propertyValue]) => {
const propertySchema = propertyValue as JSONSchemaType<unknown>;
// TODO
if (propertySchema.type === 'array') return;
if (propertySchema.type === 'object') {
const path = parentPath ? `${parentPath}.${propertyName}` : propertyName;
if (model) {
answer.push(...ModelValidationService.validateModel(propertySchema, model[propertyName], path));
}
return;
}
// check missing required parameter
if (
schema.required?.includes(propertyName) &&
propertySchema.default === undefined &&
(!model || !model[propertyName])
) {
answer.push({
level: 'error',
type: 'missingRequired',
parentPath: parentPath,
propertyName: propertyName,
message: `Missing required property ${propertyName}`,
});
}
});
}
return answer;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static validateNodeStatus(schema: VisualComponentSchema | undefined, model: any, vizNode: IVisualizationNode): void {
if (!schema?.schema) return;

const validationResult = ModelValidationService.validateModel(schema.schema, model, '');
const missingProperties = validationResult
.filter((result) => result.type === 'missingRequired')
.map((result) => result.propertyName);
if (missingProperties.length > 0) {
const message =
missingProperties.length > 1
? `${missingProperties.length} required properties are not yet configured: [ ${missingProperties} ]`
: `1 required property is not yet configured: [ ${missingProperties} ]`;
vizNode.setNodeStatus(NodeStatus.warning, message);
} else {
vizNode.setNodeStatus(NodeStatus.default, undefined);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ spec:
template:
from:
id: ${getCamelRandomId('from')}
uri: "timer:user"
uri: "timer"
parameters:
timerName: user
period: "{{period}}"
steps:
- to: https://random-data-api.com/api/v2/users
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export const routeTemplate = () => {
id: ${getCamelRandomId('route')}
from:
id: ${getCamelRandomId('from')}
uri: timer:template
uri: timer
parameters:
timerName: template
period: "1000"
steps:
- log:
Expand Down
Loading

0 comments on commit 2647614

Please sign in to comment.