Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Indication that some of the connectors mandatory properties are… #659

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading