diff --git a/packages/ui/src/components/Form/CustomAutoFields.tsx b/packages/ui/src/components/Form/CustomAutoFields.tsx index 1d25b6453..3559f9e7f 100644 --- a/packages/ui/src/components/Form/CustomAutoFields.tsx +++ b/packages/ui/src/components/Form/CustomAutoFields.tsx @@ -4,7 +4,7 @@ import { ComponentType, createElement, useContext } from 'react'; import { useForm } from 'uniforms'; import { CatalogKind, KaotoSchemaDefinition } from '../../models'; import { CanvasFormTabsContext, FilteredFieldContext } from '../../providers'; -import { getFieldGroups } from '../../utils'; +import { getFieldGroups, getFilteredProperties, isDefined } from '../../utils'; import './CustomAutoFields.scss'; import { CustomExpandableSection } from './customField/CustomExpandableSection'; import { NoFieldFound } from './NoFieldFound'; @@ -29,16 +29,20 @@ export function CustomAutoFields({ const { filteredFieldText, isGroupExpanded } = useContext(FilteredFieldContext); const canvasFormTabsContext = useContext(CanvasFormTabsContext); const oneOf = (rootField as KaotoSchemaDefinition['schema']).oneOf; - const cleanQueryTerm = filteredFieldText.replace(/\s/g, '').toLowerCase(); - const actualFields = (fields ?? schema.getSubfields()).filter( - (field) => !omitFields!.includes(field) && (field === 'parameters' || field.toLowerCase().includes(cleanQueryTerm)), + const schemaObject = isDefined(fields) + ? fields.reduce((acc: { [name: string]: unknown }, name) => { + acc[name] = schema.getField(name); + return acc; + }, {}) + : (rootField as KaotoSchemaDefinition['schema']).properties; + + const filteredProperties = getFilteredProperties( + schemaObject as KaotoSchemaDefinition['schema']['properties'], + cleanQueryTerm, + omitFields, ); - const actualFieldsSchema = actualFields.reduce((acc: { [name: string]: unknown }, name) => { - acc[name] = schema.getField(name); - return acc; - }, {}); - const propertiesArray = getFieldGroups(actualFieldsSchema); + const propertiesArray = getFieldGroups(filteredProperties); if ( canvasFormTabsContext?.selectedTab !== 'All' && diff --git a/packages/ui/src/components/Form/customField/CustomNestField.tsx b/packages/ui/src/components/Form/customField/CustomNestField.tsx index 3e59dfb7d..bafc16a31 100644 --- a/packages/ui/src/components/Form/customField/CustomNestField.tsx +++ b/packages/ui/src/components/Form/customField/CustomNestField.tsx @@ -21,10 +21,11 @@ import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; import { useContext } from 'react'; import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms'; import { FilteredFieldContext } from '../../../providers'; -import { getFieldGroups } from '../../../utils'; +import { getFieldGroups, getFilteredProperties } from '../../../utils'; import { CustomAutoField } from '../CustomAutoField'; import { CustomExpandableSection } from './CustomExpandableSection'; import './CustomNestField.scss'; +import { KaotoSchemaDefinition } from '../../../models'; export type CustomNestFieldProps = HTMLFieldProps< object, @@ -51,12 +52,11 @@ export const CustomNestField = connectField( }: CustomNestFieldProps) => { const { filteredFieldText, isGroupExpanded } = useContext(FilteredFieldContext); const cleanQueryTerm = filteredFieldText.replace(/\s/g, '').toLowerCase(); - const filteredProperties = Object.entries(props.properties ?? {}).filter((field) => - field[0].toLowerCase().includes(cleanQueryTerm), + const filteredProperties = getFilteredProperties( + props.properties as KaotoSchemaDefinition['schema']['properties'], + cleanQueryTerm, ); - const actualProperties = Object.fromEntries(filteredProperties); - const propertiesArray = getFieldGroups(actualProperties); - + const propertiesArray = getFieldGroups(filteredProperties); if (propertiesArray.common.length === 0 && Object.keys(propertiesArray.groups).length === 0) return null; return ( diff --git a/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx b/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx index 6b43fb2f9..47c5f6929 100644 --- a/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx +++ b/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx @@ -67,13 +67,13 @@ export const DataFormatEditor: FunctionComponent = (props const processedSchema = useMemo(() => { if (props.formMode === 'Required') { - return getRequiredPropertiesSchema(dataFormatSchema ?? {}); + return getRequiredPropertiesSchema(dataFormatSchema, dataFormatSchema); } else if (props.formMode === 'All') { return dataFormatSchema; } else if (props.formMode === 'Modified') { return { ...dataFormatSchema, - properties: getUserUpdatedPropertiesSchema(dataFormatSchema?.properties ?? {}, dataFormatModel ?? {}), + properties: getUserUpdatedPropertiesSchema(dataFormatSchema?.properties, dataFormatModel, dataFormatSchema), }; } }, [props.formMode, dataFormat]); diff --git a/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx b/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx index 40a1358e9..51c2d2765 100644 --- a/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx +++ b/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx @@ -68,13 +68,17 @@ export const LoadBalancerEditor: FunctionComponent = (p const processedSchema = useMemo(() => { if (props.formMode === 'Required') { - return getRequiredPropertiesSchema(loadBalancerSchema ?? {}); + return getRequiredPropertiesSchema(loadBalancerSchema, loadBalancerSchema); } else if (props.formMode === 'All') { return loadBalancerSchema; } else if (props.formMode === 'Modified') { return { ...loadBalancerSchema, - properties: getUserUpdatedPropertiesSchema(loadBalancerSchema?.properties ?? {}, loadBalancerModel ?? {}), + properties: getUserUpdatedPropertiesSchema( + loadBalancerSchema?.properties, + loadBalancerModel, + loadBalancerSchema, + ), }; } }, [props.formMode, loadBalancer]); diff --git a/packages/ui/src/components/Form/schema-bridge.ts b/packages/ui/src/components/Form/schema-bridge.ts index 2c987503b..b67f0f58c 100644 --- a/packages/ui/src/components/Form/schema-bridge.ts +++ b/packages/ui/src/components/Form/schema-bridge.ts @@ -42,6 +42,7 @@ export class SchemaBridge extends JSONSchemaBridge { } else if (definition.$ref) { /** Resolve $ref if needed */ Object.assign(definition, resolveRefIfNeeded(definition, this.schema)); + delete definition['$ref']; } } diff --git a/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx b/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx index 844d62dfc..c9d23def0 100644 --- a/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx +++ b/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx @@ -71,13 +71,13 @@ export const StepExpressionEditor: FunctionComponent const processedSchema = useMemo(() => { if (props.formMode === 'Required') { - return getRequiredPropertiesSchema(languageSchema ?? {}); + return getRequiredPropertiesSchema(languageSchema, languageSchema); } else if (props.formMode === 'All') { return languageSchema; } else if (props.formMode === 'Modified') { return { ...languageSchema, - properties: getUserUpdatedPropertiesSchema(languageSchema?.properties ?? {}, languageModel ?? {}), + properties: getUserUpdatedPropertiesSchema(languageSchema?.properties, languageModel, languageSchema), }; } }, [props.formMode, language]); diff --git a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx index d8f7b9b94..ae45f21f1 100644 --- a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx @@ -32,11 +32,15 @@ export const CanvasFormBody: FunctionComponent = (props) => const model = visualComponentSchema?.definition; let processedSchema = visualComponentSchema?.schema; if (selectedTab === 'Required') { - processedSchema = getRequiredPropertiesSchema(visualComponentSchema?.schema ?? {}); + processedSchema = getRequiredPropertiesSchema(visualComponentSchema?.schema, visualComponentSchema?.schema); } else if (selectedTab === 'Modified') { processedSchema = { ...visualComponentSchema?.schema, - properties: getUserUpdatedPropertiesSchema(visualComponentSchema?.schema.properties ?? {}, model), + properties: getUserUpdatedPropertiesSchema( + visualComponentSchema?.schema.properties, + model, + visualComponentSchema?.schema, + ), }; } diff --git a/packages/ui/src/stubs/rest-schema-properties.ts b/packages/ui/src/stubs/rest-schema-properties.ts new file mode 100644 index 000000000..cc8ec2bf3 --- /dev/null +++ b/packages/ui/src/stubs/rest-schema-properties.ts @@ -0,0 +1,364 @@ +import { KaotoSchemaDefinition } from '../models/kaoto-schema'; + +export const restSchemaProperties: KaotoSchemaDefinition['schema']['properties'] = { + id: { + type: 'string', + title: 'Id', + description: 'Sets the id of this node', + }, + description: { + type: 'string', + title: 'Description', + description: 'Sets the description of this node', + }, + disabled: { + type: 'boolean', + title: 'Disabled', + description: + 'Whether to disable this REST service from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.', + default: false, + }, + path: { + type: 'string', + title: 'Path', + description: 'Path of the rest service, such as /foo', + }, + consumes: { + type: 'string', + title: 'Consumes', + description: + 'To define the content type what the REST service consumes (accept as input), such as application/xml or application/json. This option will override what may be configured on a parent level', + }, + produces: { + type: 'string', + title: 'Produces', + description: + 'To define the content type what the REST service produces (uses for output), such as application/xml or application/json This option will override what may be configured on a parent level', + }, + bindingMode: { + type: 'string', + title: 'Binding Mode', + description: + 'Sets the binding mode to use. This option will override what may be configured on a parent level The default value is auto', + default: 'off', + enum: ['off', 'auto', 'json', 'xml', 'json_xml'], + }, + skipBindingOnErrorCode: { + type: 'boolean', + title: 'Skip Binding On Error Code', + description: + 'Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do. This option will override what may be configured on a parent level', + default: false, + }, + clientRequestValidation: { + type: 'boolean', + title: 'Client Request Validation', + description: + 'Whether to enable validation of the client request to check: 1) Content-Type header matches what the Rest DSL consumes; returns HTTP Status 415 if validation error. 2) Accept header matches what the Rest DSL produces; returns HTTP Status 406 if validation error. 3) Missing required data (query parameters, HTTP headers, body); returns HTTP Status 400 if validation error. 4) Parsing error of the message body (JSon, XML or Auto binding mode must be enabled); returns HTTP Status 400 if validation error.', + default: false, + }, + enableCORS: { + type: 'boolean', + title: 'Enable CORS', + description: + 'Whether to enable CORS headers in the HTTP response. This option will override what may be configured on a parent level The default value is false.', + default: false, + }, + enableNoContentResponse: { + type: 'boolean', + title: 'Enable No Content Response', + description: + 'Whether to return HTTP 204 with an empty body when a response contains an empty JSON object or XML root object. The default value is false.', + default: false, + }, + apiDocs: { + type: 'boolean', + title: 'Api Docs', + description: + 'Whether to include or exclude this rest operation in API documentation. This option will override what may be configured on a parent level. The default value is true.', + default: true, + }, + tag: { + type: 'string', + title: 'Tag', + description: 'To configure a special tag for the operations within this rest definition.', + }, + openApi: { + title: 'Open Api', + description: 'To use OpenApi as contract-first with Camel Rest DSL.', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'Sets the description of this node', + }, + disabled: { + type: 'boolean', + title: 'Disabled', + description: + 'Whether to disable all the REST services from the OpenAPI contract from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.', + }, + id: { + type: 'string', + title: 'Id', + description: 'Sets the id of this node', + }, + missingOperation: { + type: 'string', + title: 'Missing Operation', + description: + 'Whether to fail, ignore or return a mock response for OpenAPI operations that are not mapped to a corresponding route.', + default: 'fail', + enum: ['fail', 'ignore', 'mock'], + }, + mockIncludePattern: { + type: 'string', + title: 'Mock Include Pattern', + description: + 'Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma.', + default: 'classpath:camel-mock/**', + }, + routeId: { + type: 'string', + title: 'Route Id', + description: 'Sets the id of the route', + }, + specification: { + type: 'string', + title: 'Specification', + description: 'Path to the OpenApi specification file.', + }, + }, + required: ['specification'], + }, + securityDefinitions: { + title: 'Rest Security Definitions', + description: 'To configure rest security definitions.', + type: 'object', + additionalProperties: false, + properties: { + apiKey: { + title: 'Api Key', + description: 'Rest security basic auth definition', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + inCookie: { + type: 'boolean', + title: 'In Cookie', + description: 'To use a cookie as the location of the API key.', + }, + inHeader: { + type: 'boolean', + title: 'In Header', + description: 'To use header as the location of the API key.', + }, + inQuery: { + type: 'boolean', + title: 'In Query', + description: 'To use query parameter as the location of the API key.', + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + name: { + type: 'string', + title: 'Name', + description: 'The name of the header or query parameter to be used.', + }, + }, + required: ['key', 'name'], + }, + basicAuth: { + title: 'Basic Auth', + description: 'Rest security basic auth definition', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + }, + required: ['key'], + }, + bearer: { + title: 'Bearer Token', + description: 'Rest security bearer token authentication definition', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + format: { + type: 'string', + title: 'Format', + description: 'A hint to the client to identify how the bearer token is formatted.', + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + }, + required: ['key'], + }, + mutualTLS: { + title: 'Mutual TLS', + description: 'Rest security mutual TLS authentication definition', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + }, + required: ['key'], + }, + oauth2: { + title: 'Oauth2', + description: 'Rest security OAuth2 definition', + type: 'object', + additionalProperties: false, + properties: { + authorizationUrl: { + type: 'string', + title: 'Authorization Url', + description: + 'The authorization URL to be used for this flow. This SHOULD be in the form of a URL. Required for implicit and access code flows', + }, + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + flow: { + type: 'string', + title: 'Flow', + description: + 'The flow used by the OAuth2 security scheme. Valid values are implicit, password, application or accessCode.', + enum: ['implicit', 'password', 'application', 'clientCredentials', 'accessCode', 'authorizationCode'], + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + refreshUrl: { + type: 'string', + title: 'Refresh Url', + description: 'The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL.', + }, + scopes: { + type: 'array', + title: 'Scopes', + description: 'The available scopes for an OAuth2 security scheme', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.RestPropertyDefinition', + }, + }, + tokenUrl: { + type: 'string', + title: 'Token Url', + description: + 'The token URL to be used for this flow. This SHOULD be in the form of a URL. Required for password, application, and access code flows.', + }, + }, + required: ['key'], + }, + openIdConnect: { + title: 'Open Id Connect', + description: 'Rest security OpenID Connect definition', + type: 'object', + additionalProperties: false, + properties: { + description: { + type: 'string', + title: 'Description', + description: 'A short description for security scheme.', + }, + key: { + type: 'string', + title: 'Key', + description: 'Key used to refer to this security definition', + }, + url: { + type: 'string', + title: 'Url', + description: 'OpenId Connect URL to discover OAuth2 configuration values.', + }, + }, + required: ['key', 'url'], + }, + }, + }, + securityRequirements: { + type: 'array', + title: 'Security Requirements', + description: 'Sets the security requirement(s) for all endpoints.', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.SecurityDefinition', + }, + }, + delete: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.DeleteDefinition', + }, + }, + get: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.GetDefinition', + }, + }, + head: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.HeadDefinition', + }, + }, + patch: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.PatchDefinition', + }, + }, + post: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.PostDefinition', + }, + }, + put: { + type: 'array', + items: { + $ref: '#/definitions/org.apache.camel.model.rest.PutDefinition', + }, + }, +}; diff --git a/packages/ui/src/stubs/test-schema.ts b/packages/ui/src/stubs/test-schema.ts new file mode 100644 index 000000000..05678a8e2 --- /dev/null +++ b/packages/ui/src/stubs/test-schema.ts @@ -0,0 +1,202 @@ +import { KaotoSchemaDefinition } from '../models/kaoto-schema'; + +/** + * This is not a real schema, this is a hybrid custom made schema. + */ +export const testSchema: KaotoSchemaDefinition['schema'] = { + type: 'object', + definitions: { + testRef: { + type: 'object', + title: 'testRef', + properties: { + spec: { + type: 'string', + title: 'Specification', + description: 'Path to the OpenApi specification file.', + }, + }, + required: ['spec'], + }, + }, + properties: { + id: { + title: 'Id', + type: 'string', + }, + description: { + title: 'Description', + type: 'string', + }, + uri: { + title: 'Uri', + type: 'string', + }, + variableReceive: { + title: 'Variable Receive', + type: 'string', + }, + testRef: { + $ref: '#/definitions/testRef', + }, + parameters: { + type: 'object', + title: 'Endpoint Properties', + description: 'Endpoint properties description', + properties: { + timerName: { + title: 'Timer Name', + type: 'string', + }, + delay: { + title: 'Delay', + type: 'string', + default: '1000', + }, + fixedRate: { + title: 'Fixed Rate', + type: 'boolean', + default: false, + }, + includeMetadata: { + title: 'Include Metadata', + type: 'boolean', + default: false, + }, + period: { + title: 'Period', + type: 'string', + default: '1000', + }, + repeatCount: { + title: 'Repeat Count', + type: 'integer', + }, + exceptionHandler: { + title: 'Exception Handler', + type: 'string', + $comment: 'class:org.apache.camel.spi.ExceptionHandler', + }, + exchangePattern: { + title: 'Exchange Pattern', + type: 'string', + enum: ['InOnly', 'InOut'], + }, + synchronous: { + title: 'Synchronous', + type: 'boolean', + default: false, + }, + timer: { + title: 'Timer', + type: 'string', + $comment: 'class:java.util.Timer', + }, + runLoggingLevel: { + title: 'Run Logging Level', + type: 'string', + default: 'TRACE', + enum: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF'], + }, + }, + required: ['timerName', 'fixedRate', 'repeatCount', 'exceptionHandler', 'runLoggingLevel'], + }, + kameletProperties: { + title: 'Properties', + type: 'array', + description: 'Configure properties on the Kamelet', + items: { + type: 'object', + properties: { + name: { + title: 'Property name', + description: 'Name of the property', + type: 'string', + }, + title: { + title: 'Title', + description: 'Display name of the property', + type: 'string', + }, + description: { + title: 'Description', + description: 'Simple text description of the property', + type: 'string', + }, + type: { + title: 'Property type', + description: 'Set the expected type for this property', + type: 'string', + enum: ['string', 'number', 'boolean'], + default: 'string', + }, + default: { + title: 'Default', + description: 'Default value for the property', + type: 'string', + }, + 'x-descriptors': { + title: 'X-descriptors', + description: 'Specific aids for the visual tools', + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['name', 'type'], + }, + }, + labels: { + additionalProperties: { + default: '', + type: 'string', + }, + title: 'Additional Labels', + description: + 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels', + type: 'object', + }, + }, + required: ['id', 'uri', 'labels'], +}; + +export const inputModelForTestSchema: Record = { + id: 'from-2860', + uri: 'test', + variableReceive: 'test', + testRef: { + spec: 'test', + }, + parameters: { + period: '1000', + timerName: 'template', + exceptionHandler: '#test', + exchangePattern: 'InOnly', + fixedRate: true, + runLoggingLevel: 'OFF', + repeatCount: '2', + time: undefined, + timer: '#testbean', + }, + kameletProperties: [ + { + name: 'period', + title: 'Period', + description: 'The time interval between two events', + type: 'integer', + default: 5000, + }, + ], + labels: { + test: 'test', + }, + steps: [ + { + log: { + id: 'log-2942', + message: 'template message', + }, + }, + ], +}; diff --git a/packages/ui/src/utils/__snapshots__/get-filtered-properties.test.ts.snap b/packages/ui/src/utils/__snapshots__/get-filtered-properties.test.ts.snap new file mode 100644 index 000000000..79cd27e94 --- /dev/null +++ b/packages/ui/src/utils/__snapshots__/get-filtered-properties.test.ts.snap @@ -0,0 +1,486 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getFilteredProperties() should return only the filtered properties 1`] = ` +{ + "description": { + "description": "Sets the description of this node", + "title": "Description", + "type": "string", + }, + "openApi": { + "additionalProperties": false, + "description": "To use OpenApi as contract-first with Camel Rest DSL.", + "properties": { + "description": { + "description": "Sets the description of this node", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "specification", + ], + "title": "Open Api", + "type": "object", + }, + "securityDefinitions": { + "additionalProperties": false, + "description": "To configure rest security definitions.", + "properties": { + "apiKey": { + "additionalProperties": false, + "description": "Rest security basic auth definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + "name", + ], + "title": "Api Key", + "type": "object", + }, + "basicAuth": { + "additionalProperties": false, + "description": "Rest security basic auth definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Basic Auth", + "type": "object", + }, + "bearer": { + "additionalProperties": false, + "description": "Rest security bearer token authentication definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Bearer Token", + "type": "object", + }, + "mutualTLS": { + "additionalProperties": false, + "description": "Rest security mutual TLS authentication definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Mutual TLS", + "type": "object", + }, + "oauth2": { + "additionalProperties": false, + "description": "Rest security OAuth2 definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Oauth2", + "type": "object", + }, + "openIdConnect": { + "additionalProperties": false, + "description": "Rest security OpenID Connect definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + }, + "required": [ + "key", + "url", + ], + "title": "Open Id Connect", + "type": "object", + }, + }, + "title": "Rest Security Definitions", + "type": "object", + }, +} +`; + +exports[`getFilteredProperties() should return only the un-omitted properties 1`] = ` +{ + "apiDocs": { + "default": true, + "description": "Whether to include or exclude this rest operation in API documentation. This option will override what may be configured on a parent level. The default value is true.", + "title": "Api Docs", + "type": "boolean", + }, + "bindingMode": { + "default": "off", + "description": "Sets the binding mode to use. This option will override what may be configured on a parent level The default value is auto", + "enum": [ + "off", + "auto", + "json", + "xml", + "json_xml", + ], + "title": "Binding Mode", + "type": "string", + }, + "clientRequestValidation": { + "default": false, + "description": "Whether to enable validation of the client request to check: 1) Content-Type header matches what the Rest DSL consumes; returns HTTP Status 415 if validation error. 2) Accept header matches what the Rest DSL produces; returns HTTP Status 406 if validation error. 3) Missing required data (query parameters, HTTP headers, body); returns HTTP Status 400 if validation error. 4) Parsing error of the message body (JSon, XML or Auto binding mode must be enabled); returns HTTP Status 400 if validation error.", + "title": "Client Request Validation", + "type": "boolean", + }, + "consumes": { + "description": "To define the content type what the REST service consumes (accept as input), such as application/xml or application/json. This option will override what may be configured on a parent level", + "title": "Consumes", + "type": "string", + }, + "description": { + "description": "Sets the description of this node", + "title": "Description", + "type": "string", + }, + "disabled": { + "default": false, + "description": "Whether to disable this REST service from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.", + "title": "Disabled", + "type": "boolean", + }, + "enableCORS": { + "default": false, + "description": "Whether to enable CORS headers in the HTTP response. This option will override what may be configured on a parent level The default value is false.", + "title": "Enable CORS", + "type": "boolean", + }, + "enableNoContentResponse": { + "default": false, + "description": "Whether to return HTTP 204 with an empty body when a response contains an empty JSON object or XML root object. The default value is false.", + "title": "Enable No Content Response", + "type": "boolean", + }, + "head": { + "items": { + "$ref": "#/definitions/org.apache.camel.model.rest.HeadDefinition", + }, + "type": "array", + }, + "id": { + "description": "Sets the id of this node", + "title": "Id", + "type": "string", + }, + "openApi": { + "additionalProperties": false, + "description": "To use OpenApi as contract-first with Camel Rest DSL.", + "properties": { + "description": { + "description": "Sets the description of this node", + "title": "Description", + "type": "string", + }, + "disabled": { + "description": "Whether to disable all the REST services from the OpenAPI contract from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.", + "title": "Disabled", + "type": "boolean", + }, + "id": { + "description": "Sets the id of this node", + "title": "Id", + "type": "string", + }, + "missingOperation": { + "default": "fail", + "description": "Whether to fail, ignore or return a mock response for OpenAPI operations that are not mapped to a corresponding route.", + "enum": [ + "fail", + "ignore", + "mock", + ], + "title": "Missing Operation", + "type": "string", + }, + "mockIncludePattern": { + "default": "classpath:camel-mock/**", + "description": "Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma.", + "title": "Mock Include Pattern", + "type": "string", + }, + "routeId": { + "description": "Sets the id of the route", + "title": "Route Id", + "type": "string", + }, + "specification": { + "description": "Path to the OpenApi specification file.", + "title": "Specification", + "type": "string", + }, + }, + "required": [ + "specification", + ], + "title": "Open Api", + "type": "object", + }, + "path": { + "description": "Path of the rest service, such as /foo", + "title": "Path", + "type": "string", + }, + "produces": { + "description": "To define the content type what the REST service produces (uses for output), such as application/xml or application/json This option will override what may be configured on a parent level", + "title": "Produces", + "type": "string", + }, + "securityDefinitions": { + "additionalProperties": false, + "description": "To configure rest security definitions.", + "properties": { + "apiKey": { + "additionalProperties": false, + "description": "Rest security basic auth definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "inCookie": { + "description": "To use a cookie as the location of the API key.", + "title": "In Cookie", + "type": "boolean", + }, + "inHeader": { + "description": "To use header as the location of the API key.", + "title": "In Header", + "type": "boolean", + }, + "inQuery": { + "description": "To use query parameter as the location of the API key.", + "title": "In Query", + "type": "boolean", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + "name": { + "description": "The name of the header or query parameter to be used.", + "title": "Name", + "type": "string", + }, + }, + "required": [ + "key", + "name", + ], + "title": "Api Key", + "type": "object", + }, + "basicAuth": { + "additionalProperties": false, + "description": "Rest security basic auth definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Basic Auth", + "type": "object", + }, + "bearer": { + "additionalProperties": false, + "description": "Rest security bearer token authentication definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "format": { + "description": "A hint to the client to identify how the bearer token is formatted.", + "title": "Format", + "type": "string", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Bearer Token", + "type": "object", + }, + "mutualTLS": { + "additionalProperties": false, + "description": "Rest security mutual TLS authentication definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Mutual TLS", + "type": "object", + }, + "oauth2": { + "additionalProperties": false, + "description": "Rest security OAuth2 definition", + "properties": { + "authorizationUrl": { + "description": "The authorization URL to be used for this flow. This SHOULD be in the form of a URL. Required for implicit and access code flows", + "title": "Authorization Url", + "type": "string", + }, + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "flow": { + "description": "The flow used by the OAuth2 security scheme. Valid values are implicit, password, application or accessCode.", + "enum": [ + "implicit", + "password", + "application", + "clientCredentials", + "accessCode", + "authorizationCode", + ], + "title": "Flow", + "type": "string", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + "refreshUrl": { + "description": "The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL.", + "title": "Refresh Url", + "type": "string", + }, + "scopes": { + "description": "The available scopes for an OAuth2 security scheme", + "items": { + "$ref": "#/definitions/org.apache.camel.model.rest.RestPropertyDefinition", + }, + "title": "Scopes", + "type": "array", + }, + "tokenUrl": { + "description": "The token URL to be used for this flow. This SHOULD be in the form of a URL. Required for password, application, and access code flows.", + "title": "Token Url", + "type": "string", + }, + }, + "required": [ + "key", + ], + "title": "Oauth2", + "type": "object", + }, + "openIdConnect": { + "additionalProperties": false, + "description": "Rest security OpenID Connect definition", + "properties": { + "description": { + "description": "A short description for security scheme.", + "title": "Description", + "type": "string", + }, + "key": { + "description": "Key used to refer to this security definition", + "title": "Key", + "type": "string", + }, + "url": { + "description": "OpenId Connect URL to discover OAuth2 configuration values.", + "title": "Url", + "type": "string", + }, + }, + "required": [ + "key", + "url", + ], + "title": "Open Id Connect", + "type": "object", + }, + }, + "title": "Rest Security Definitions", + "type": "object", + }, + "securityRequirements": { + "description": "Sets the security requirement(s) for all endpoints.", + "items": { + "$ref": "#/definitions/org.apache.camel.model.rest.SecurityDefinition", + }, + "title": "Security Requirements", + "type": "array", + }, + "skipBindingOnErrorCode": { + "default": false, + "description": "Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do. This option will override what may be configured on a parent level", + "title": "Skip Binding On Error Code", + "type": "boolean", + }, + "tag": { + "description": "To configure a special tag for the operations within this rest definition.", + "title": "Tag", + "type": "string", + }, +} +`; diff --git a/packages/ui/src/utils/__snapshots__/get-required-properties-schema.test.ts.snap b/packages/ui/src/utils/__snapshots__/get-required-properties-schema.test.ts.snap new file mode 100644 index 000000000..93a76acf7 --- /dev/null +++ b/packages/ui/src/utils/__snapshots__/get-required-properties-schema.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getRequiredPropertiesSchema() should return only the properties which are Required 1`] = ` +{ + "definitions": { + "testRef": { + "properties": { + "spec": { + "description": "Path to the OpenApi specification file.", + "title": "Specification", + "type": "string", + }, + }, + "required": [ + "spec", + ], + "title": "testRef", + "type": "object", + }, + }, + "properties": { + "id": { + "title": "Id", + "type": "string", + }, + "labels": { + "additionalProperties": { + "default": "", + "type": "string", + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "title": "Additional Labels", + "type": "object", + }, + "parameters": { + "description": "Endpoint properties description", + "properties": { + "exceptionHandler": { + "$comment": "class:org.apache.camel.spi.ExceptionHandler", + "title": "Exception Handler", + "type": "string", + }, + "fixedRate": { + "default": false, + "title": "Fixed Rate", + "type": "boolean", + }, + "repeatCount": { + "title": "Repeat Count", + "type": "integer", + }, + "runLoggingLevel": { + "default": "TRACE", + "enum": [ + "TRACE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "OFF", + ], + "title": "Run Logging Level", + "type": "string", + }, + "timerName": { + "title": "Timer Name", + "type": "string", + }, + }, + "required": [ + "timerName", + "fixedRate", + "repeatCount", + "exceptionHandler", + "runLoggingLevel", + ], + "title": "Endpoint Properties", + "type": "object", + }, + "testRef": { + "properties": { + "spec": { + "description": "Path to the OpenApi specification file.", + "title": "Specification", + "type": "string", + }, + }, + "required": [ + "spec", + ], + "title": "testRef", + "type": "object", + }, + "uri": { + "title": "Uri", + "type": "string", + }, + }, + "required": [ + "id", + "uri", + "labels", + ], + "type": "object", +} +`; diff --git a/packages/ui/src/utils/__snapshots__/get-user-updated-properties-schema.test.ts.snap b/packages/ui/src/utils/__snapshots__/get-user-updated-properties-schema.test.ts.snap new file mode 100644 index 000000000..0378c619a --- /dev/null +++ b/packages/ui/src/utils/__snapshots__/get-user-updated-properties-schema.test.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUserUpdatedPropertiesSchema() should return only the properties which are user Modified 1`] = ` +{ + "id": { + "title": "Id", + "type": "string", + }, + "kameletProperties": { + "description": "Configure properties on the Kamelet", + "items": { + "properties": { + "default": { + "description": "Default value for the property", + "title": "Default", + "type": "string", + }, + "description": { + "description": "Simple text description of the property", + "title": "Description", + "type": "string", + }, + "name": { + "description": "Name of the property", + "title": "Property name", + "type": "string", + }, + "title": { + "description": "Display name of the property", + "title": "Title", + "type": "string", + }, + "type": { + "default": "string", + "description": "Set the expected type for this property", + "enum": [ + "string", + "number", + "boolean", + ], + "title": "Property type", + "type": "string", + }, + "x-descriptors": { + "description": "Specific aids for the visual tools", + "items": { + "type": "string", + }, + "title": "X-descriptors", + "type": "array", + }, + }, + "required": [ + "name", + "type", + ], + "type": "object", + }, + "title": "Properties", + "type": "array", + }, + "parameters": { + "description": "Endpoint properties description", + "properties": { + "exceptionHandler": { + "$comment": "class:org.apache.camel.spi.ExceptionHandler", + "title": "Exception Handler", + "type": "string", + }, + "exchangePattern": { + "enum": [ + "InOnly", + "InOut", + ], + "title": "Exchange Pattern", + "type": "string", + }, + "fixedRate": { + "default": false, + "title": "Fixed Rate", + "type": "boolean", + }, + "repeatCount": { + "title": "Repeat Count", + "type": "integer", + }, + "runLoggingLevel": { + "default": "TRACE", + "enum": [ + "TRACE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "OFF", + ], + "title": "Run Logging Level", + "type": "string", + }, + "timer": { + "$comment": "class:java.util.Timer", + "title": "Timer", + "type": "string", + }, + "timerName": { + "title": "Timer Name", + "type": "string", + }, + }, + "required": [ + "timerName", + "fixedRate", + "repeatCount", + "exceptionHandler", + "runLoggingLevel", + ], + "title": "Endpoint Properties", + "type": "object", + }, + "testRef": { + "properties": { + "spec": { + "description": "Path to the OpenApi specification file.", + "title": "Specification", + "type": "string", + }, + }, + "required": [ + "spec", + ], + "title": "testRef", + "type": "object", + }, + "uri": { + "title": "Uri", + "type": "string", + }, + "variableReceive": { + "title": "Variable Receive", + "type": "string", + }, +} +`; diff --git a/packages/ui/src/utils/get-field-groups.ts b/packages/ui/src/utils/get-field-groups.ts index c4b2f470c..1e58d9001 100644 --- a/packages/ui/src/utils/get-field-groups.ts +++ b/packages/ui/src/utils/get-field-groups.ts @@ -1,6 +1,9 @@ import { getValue } from './get-value'; +import { isDefined } from './is-defined'; + +export const getFieldGroups = (fields?: { [name: string]: unknown }) => { + if (!isDefined(fields)) return { common: [], groups: {} }; -export const getFieldGroups = (fields: { [name: string]: unknown }) => { const propertiesArray = Object.entries(fields).reduce( (acc, [name, definition]) => { const group: string = getValue(definition, 'group', ''); diff --git a/packages/ui/src/utils/get-filtered-properties.test.ts b/packages/ui/src/utils/get-filtered-properties.test.ts new file mode 100644 index 000000000..ccdca5699 --- /dev/null +++ b/packages/ui/src/utils/get-filtered-properties.test.ts @@ -0,0 +1,21 @@ +import { restSchemaProperties } from '../stubs/rest-schema-properties'; +import { getFilteredProperties } from './get-filtered-properties'; + +describe('getFilteredProperties()', () => { + it('should return only the filtered properties', () => { + const filteredSchema = getFilteredProperties(restSchemaProperties, 'des'); + expect(filteredSchema).toMatchSnapshot(); + }); + + it('should return only the un-omitted properties', () => { + const filteredSchema = getFilteredProperties(restSchemaProperties, '', [ + 'get', + 'post', + 'put', + 'delete', + 'patch', + 'patch', + ]); + expect(filteredSchema).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/utils/get-filtered-properties.ts b/packages/ui/src/utils/get-filtered-properties.ts new file mode 100644 index 000000000..e3ff9a99d --- /dev/null +++ b/packages/ui/src/utils/get-filtered-properties.ts @@ -0,0 +1,33 @@ +import { KaotoSchemaDefinition } from '../models'; +import { isDefined } from './is-defined'; + +/** + * Extracts the schema recursively containing only the filtered properties. + */ +export function getFilteredProperties( + properties: KaotoSchemaDefinition['schema']['properties'], + filter: string, + omitFields?: string[], +): KaotoSchemaDefinition['schema']['properties'] { + if (!isDefined(properties)) return {}; + + const filteredFormSchema = Object.entries(properties).reduce( + (acc, [property, definition]) => { + if (!omitFields?.includes(property)) { + if (definition['type'] === 'object' && 'properties' in definition) { + const subFilteredSchema = getFilteredProperties(definition['properties'], filter); + if (subFilteredSchema && Object.keys(subFilteredSchema).length > 0) { + acc![property] = { ...definition, properties: subFilteredSchema }; + } + } else if (property.toLowerCase().includes(filter)) { + acc![property] = definition; + } + } + + return acc; + }, + {} as KaotoSchemaDefinition['schema']['properties'], + ); + + return filteredFormSchema; +} diff --git a/packages/ui/src/utils/get-required-properties-schema.test.ts b/packages/ui/src/utils/get-required-properties-schema.test.ts index 18fd64dd0..42d0465cf 100644 --- a/packages/ui/src/utils/get-required-properties-schema.test.ts +++ b/packages/ui/src/utils/get-required-properties-schema.test.ts @@ -1,212 +1,14 @@ import { getRequiredPropertiesSchema } from './get-required-properties-schema'; -import { KaotoSchemaDefinition } from '../models/kaoto-schema'; +import { testSchema } from '../stubs/test-schema'; describe('getRequiredPropertiesSchema()', () => { - const schema = { - type: 'object', - properties: { - id: { - title: 'Id', - type: 'string', - }, - description: { - title: 'Description', - type: 'string', - }, - uri: { - title: 'Uri', - type: 'string', - }, - variableReceive: { - title: 'Variable Receive', - type: 'string', - }, - parameters: { - type: 'object', - title: 'Endpoint Properties', - description: 'Endpoint properties description', - properties: { - timerName: { - title: 'Timer Name', - type: 'string', - }, - delay: { - title: 'Delay', - type: 'string', - default: '1000', - }, - fixedRate: { - title: 'Fixed Rate', - type: 'boolean', - default: false, - }, - includeMetadata: { - title: 'Include Metadata', - type: 'boolean', - default: false, - }, - period: { - title: 'Period', - type: 'string', - default: '1000', - }, - repeatCount: { - title: 'Repeat Count', - type: 'integer', - }, - exceptionHandler: { - title: 'Exception Handler', - type: 'string', - $comment: 'class:org.apache.camel.spi.ExceptionHandler', - }, - exchangePattern: { - title: 'Exchange Pattern', - type: 'string', - enum: ['InOnly', 'InOut'], - }, - synchronous: { - title: 'Synchronous', - type: 'boolean', - default: false, - }, - timer: { - title: 'Timer', - type: 'string', - $comment: 'class:java.util.Timer', - }, - runLoggingLevel: { - title: 'Run Logging Level', - type: 'string', - default: 'TRACE', - enum: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF'], - }, - }, - required: ['timerName', 'fixedRate', 'repeatCount', 'exceptionHandler', 'runLoggingLevel'], - }, - kameletProperties: { - title: 'Properties', - type: 'array', - description: 'Configure properties on the Kamelet', - items: { - type: 'object', - properties: { - name: { - title: 'Property name', - description: 'Name of the property', - type: 'string', - }, - title: { - title: 'Title', - description: 'Display name of the property', - type: 'string', - }, - description: { - title: 'Description', - description: 'Simple text description of the property', - type: 'string', - }, - type: { - title: 'Property type', - description: 'Set the expected type for this property', - type: 'string', - enum: ['string', 'number', 'boolean'], - default: 'string', - }, - default: { - title: 'Default', - description: 'Default value for the property', - type: 'string', - }, - 'x-descriptors': { - title: 'X-descriptors', - description: 'Specific aids for the visual tools', - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['name', 'type'], - }, - }, - labels: { - additionalProperties: { - default: '', - type: 'string', - }, - title: 'Additional Labels', - description: - 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels', - type: 'object', - }, - }, - required: ['id', 'uri', 'labels'], - } as unknown as KaotoSchemaDefinition['schema']; - - const expectedSchema = { - type: 'object', - properties: { - id: { - title: 'Id', - type: 'string', - }, - uri: { - title: 'Uri', - type: 'string', - }, - parameters: { - type: 'object', - title: 'Endpoint Properties', - description: 'Endpoint properties description', - properties: { - timerName: { - title: 'Timer Name', - type: 'string', - }, - fixedRate: { - title: 'Fixed Rate', - type: 'boolean', - default: false, - }, - repeatCount: { - title: 'Repeat Count', - type: 'integer', - }, - exceptionHandler: { - title: 'Exception Handler', - type: 'string', - $comment: 'class:org.apache.camel.spi.ExceptionHandler', - }, - runLoggingLevel: { - title: 'Run Logging Level', - type: 'string', - default: 'TRACE', - enum: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF'], - }, - }, - required: ['timerName', 'fixedRate', 'repeatCount', 'exceptionHandler', 'runLoggingLevel'], - }, - labels: { - additionalProperties: { - default: '', - type: 'string', - }, - title: 'Additional Labels', - description: - 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels', - type: 'object', - }, - }, - required: ['id', 'uri', 'labels'], - }; - - it('should return only the properties which are user Modified', () => { - const procesedSchema = getRequiredPropertiesSchema(schema); - expect(procesedSchema).toMatchObject(expectedSchema); + it('should return only the properties which are Required', () => { + const procesedSchema = getRequiredPropertiesSchema(testSchema, testSchema); + expect(procesedSchema).toMatchSnapshot(); }); it('should return {}', () => { - const procesedSchema = getRequiredPropertiesSchema({}); + const procesedSchema = getRequiredPropertiesSchema({}, testSchema); expect(procesedSchema).toMatchObject({}); }); }); diff --git a/packages/ui/src/utils/get-required-properties-schema.ts b/packages/ui/src/utils/get-required-properties-schema.ts index 10f918fe2..1c61c839c 100644 --- a/packages/ui/src/utils/get-required-properties-schema.ts +++ b/packages/ui/src/utils/get-required-properties-schema.ts @@ -1,30 +1,45 @@ import { KaotoSchemaDefinition } from '../models'; import { isDefined } from './is-defined'; +import { resolveRefIfNeeded } from './resolve-ref-if-needed'; -export function getRequiredPropertiesSchema(schema: KaotoSchemaDefinition['schema']): KaotoSchemaDefinition['schema'] { - if (!isDefined(schema)) return {}; +/** + * Extracts a schema containing only the required properties. + * Recursively resolves `$ref` if necessary. + */ +export function getRequiredPropertiesSchema( + schema?: KaotoSchemaDefinition['schema'], + resolveFromSchema?: KaotoSchemaDefinition['schema'], +): KaotoSchemaDefinition['schema'] { + if (!isDefined(schema) || !isDefined(resolveFromSchema)) return {}; const schemaProperties = schema.properties; const requiredProperties = schema.required as string[]; - if (isDefined(requiredProperties) && isDefined(schemaProperties)) { - const requiredFormSchema = Object.entries(schemaProperties).reduce( - (acc, [property, definition]) => { - if (definition['type'] === 'object' && 'properties' in definition) { - const subSchema = getRequiredPropertiesSchema(definition); - if (Object.keys(subSchema.properties as object).length > 0) { - acc[property] = subSchema; - } - } else { - if (requiredProperties.indexOf(property) > -1) acc[property] = definition; + if (!isDefined(schemaProperties)) { + return { ...schema, properties: {} }; + } + + const requiredFormSchema = Object.entries(schemaProperties).reduce( + (acc, [property, definition]) => { + if ('$ref' in definition) { + const objectDefinition = resolveRefIfNeeded(definition, resolveFromSchema); + const subSchema = getRequiredPropertiesSchema(objectDefinition, resolveFromSchema); + if (Object.keys(subSchema.properties as object).length > 0) { + acc[property] = subSchema; } + } else if (definition['type'] === 'object' && 'properties' in definition) { + const subSchema = getRequiredPropertiesSchema(definition, resolveFromSchema); + if (Object.keys(subSchema.properties as object).length > 0) { + acc[property] = subSchema; + } + } else if (isDefined(requiredProperties) && requiredProperties.indexOf(property) > -1) { + acc[property] = definition; + } - return acc; - }, - {} as KaotoSchemaDefinition['schema'], - ); - return { ...schema, properties: requiredFormSchema }; - } + return acc; + }, + {} as KaotoSchemaDefinition['schema'], + ); - return { ...schema, properties: {} }; + return { ...schema, properties: requiredFormSchema }; } diff --git a/packages/ui/src/utils/get-user-updated-properties-schema.test.ts b/packages/ui/src/utils/get-user-updated-properties-schema.test.ts index 7cf7854db..73586b2c6 100644 --- a/packages/ui/src/utils/get-user-updated-properties-schema.test.ts +++ b/packages/ui/src/utils/get-user-updated-properties-schema.test.ts @@ -1,300 +1,18 @@ import { getUserUpdatedPropertiesSchema } from './get-user-updated-properties-schema'; -import { KaotoSchemaDefinition } from '../models/kaoto-schema'; +import { inputModelForTestSchema, testSchema } from '../stubs/test-schema'; describe('getUserUpdatedPropertiesSchema()', () => { - const schema = { - type: 'object', - properties: { - id: { - title: 'Id', - type: 'string', - }, - description: { - title: 'Description', - type: 'string', - }, - uri: { - title: 'Uri', - type: 'string', - }, - variableReceive: { - title: 'Variable Receive', - type: 'string', - }, - parameters: { - type: 'object', - title: 'Endpoint Properties', - description: 'Endpoint properties description', - properties: { - timerName: { - title: 'Timer Name', - type: 'string', - }, - delay: { - title: 'Delay', - type: 'string', - default: '1000', - }, - fixedRate: { - title: 'Fixed Rate', - type: 'boolean', - default: false, - }, - includeMetadata: { - title: 'Include Metadata', - type: 'boolean', - default: false, - }, - period: { - title: 'Period', - type: 'string', - default: '1000', - }, - repeatCount: { - title: 'Repeat Count', - type: 'integer', - }, - exceptionHandler: { - title: 'Exception Handler', - type: 'string', - $comment: 'class:org.apache.camel.spi.ExceptionHandler', - }, - exchangePattern: { - title: 'Exchange Pattern', - type: 'string', - enum: ['InOnly', 'InOut'], - }, - synchronous: { - title: 'Synchronous', - type: 'boolean', - default: false, - }, - timer: { - title: 'Timer', - type: 'string', - $comment: 'class:java.util.Timer', - }, - runLoggingLevel: { - title: 'Run Logging Level', - type: 'string', - default: 'TRACE', - enum: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF'], - }, - }, - required: ['timerName'], - }, - kameletProperties: { - title: 'Properties', - type: 'array', - description: 'Configure properties on the Kamelet', - items: { - type: 'object', - properties: { - name: { - title: 'Property name', - description: 'Name of the property', - type: 'string', - }, - title: { - title: 'Title', - description: 'Display name of the property', - type: 'string', - }, - description: { - title: 'Description', - description: 'Simple text description of the property', - type: 'string', - }, - type: { - title: 'Property type', - description: 'Set the expected type for this property', - type: 'string', - enum: ['string', 'number', 'boolean'], - default: 'string', - }, - default: { - title: 'Default', - description: 'Default value for the property', - type: 'string', - }, - 'x-descriptors': { - title: 'X-descriptors', - description: 'Specific aids for the visual tools', - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['name', 'type'], - }, - }, - labels: { - additionalProperties: { - default: '', - type: 'string', - }, - title: 'Additional Labels', - description: - 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels', - type: 'object', - }, - }, - } as unknown as KaotoSchemaDefinition['schema']; - - const inputModel: Record = { - id: 'from-2860', - uri: 'test', - variableReceive: 'test', - parameters: { - period: '1000', - timerName: 'template', - exceptionHandler: '#test', - exchangePattern: 'InOnly', - fixedRate: true, - runLoggingLevel: 'OFF', - repeatCount: '2', - time: undefined, - timer: '#testbean', - }, - kameletProperties: [ - { - name: 'period', - title: 'Period', - description: 'The time interval between two events', - type: 'integer', - default: 5000, - }, - ], - labels: { - test: 'test', - }, - steps: [ - { - log: { - id: 'log-2942', - message: 'template message', - }, - }, - ], - }; - - const expectedSchema = { - id: { - title: 'Id', - type: 'string', - }, - uri: { - title: 'Uri', - type: 'string', - }, - parameters: { - type: 'object', - title: 'Endpoint Properties', - description: 'Endpoint properties description', - properties: { - timerName: { - title: 'Timer Name', - type: 'string', - }, - fixedRate: { - title: 'Fixed Rate', - type: 'boolean', - default: false, - }, - repeatCount: { - title: 'Repeat Count', - type: 'integer', - }, - exceptionHandler: { - title: 'Exception Handler', - type: 'string', - $comment: 'class:org.apache.camel.spi.ExceptionHandler', - }, - exchangePattern: { - title: 'Exchange Pattern', - type: 'string', - enum: ['InOnly', 'InOut'], - }, - timer: { - title: 'Timer', - type: 'string', - $comment: 'class:java.util.Timer', - }, - runLoggingLevel: { - title: 'Run Logging Level', - type: 'string', - default: 'TRACE', - enum: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF'], - }, - }, - required: ['timerName'], - }, - kameletProperties: { - title: 'Properties', - type: 'array', - description: 'Configure properties on the Kamelet', - items: { - type: 'object', - properties: { - name: { - title: 'Property name', - description: 'Name of the property', - type: 'string', - }, - title: { - title: 'Title', - description: 'Display name of the property', - type: 'string', - }, - description: { - title: 'Description', - description: 'Simple text description of the property', - type: 'string', - }, - type: { - title: 'Property type', - description: 'Set the expected type for this property', - type: 'string', - enum: ['string', 'number', 'boolean'], - default: 'string', - }, - default: { - title: 'Default', - description: 'Default value for the property', - type: 'string', - }, - 'x-descriptors': { - title: 'X-descriptors', - description: 'Specific aids for the visual tools', - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['name', 'type'], - }, - }, - labels: { - additionalProperties: { - default: '', - type: 'string', - }, - title: 'Additional Labels', - description: - 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels', - type: 'object', - }, - }; - it('should return only the properties which are user Modified', () => { - const procesedSchema = getUserUpdatedPropertiesSchema(schema.properties!, inputModel); - expect(procesedSchema).toMatchObject(expectedSchema); + const processedSchema = getUserUpdatedPropertiesSchema( + testSchema['properties'], + inputModelForTestSchema, + testSchema, + ); + expect(processedSchema).toMatchSnapshot(); }); it('should return {}', () => { - const procesedSchema = getUserUpdatedPropertiesSchema(schema.properties!, {}); - expect(procesedSchema).toMatchObject({}); + const processedSchema = getUserUpdatedPropertiesSchema(testSchema['properties'], {}, testSchema); + expect(processedSchema).toMatchObject({}); }); }); diff --git a/packages/ui/src/utils/get-user-updated-properties-schema.ts b/packages/ui/src/utils/get-user-updated-properties-schema.ts index 901f3b7cb..58f1de6e3 100644 --- a/packages/ui/src/utils/get-user-updated-properties-schema.ts +++ b/packages/ui/src/utils/get-user-updated-properties-schema.ts @@ -1,11 +1,13 @@ import { KaotoSchemaDefinition } from '../models'; import { isDefined } from './is-defined'; +import { resolveRefIfNeeded } from './resolve-ref-if-needed'; export function getUserUpdatedPropertiesSchema( - schemaProperties: KaotoSchemaDefinition['schema'], - inputModel: Record, + schemaProperties?: KaotoSchemaDefinition['schema']['properties'], + inputModel?: Record, + resolveFromSchema?: KaotoSchemaDefinition['schema']['properties'], ): KaotoSchemaDefinition['schema'] { - if (!isDefined(schemaProperties) || !isDefined(inputModel)) return {}; + if (!isDefined(schemaProperties) || !isDefined(inputModel) || !isDefined(resolveFromSchema)) return {}; const nonDefaultFormSchema = Object.entries(schemaProperties).reduce( (acc, [property, definition]) => { @@ -17,31 +19,44 @@ export function getUserUpdatedPropertiesSchema( definition['type'] === 'number' ) { if ('default' in definition) { - if (!(definition['default'] == inputModel[property])) { - acc[property] = definition; + if (definition['default'] != inputModel[property]) { + acc![property] = definition; } } else { - acc[property] = definition; + acc![property] = definition; } - } else if (definition['type'] === 'object' && Object.keys(inputModel[property] as object).length > 0) { - if ('properties' in definition) { - const subSchema = getUserUpdatedPropertiesSchema( - definition['properties'], - inputModel[property] as Record, - ); - acc[property] = { ...definition, properties: subSchema }; - } else { - acc[property] = definition; + } else if ( + definition['type'] === 'object' && + 'properties' in definition && + Object.keys(inputModel[property] as object).length > 0 + ) { + const subSchema = getUserUpdatedPropertiesSchema( + definition['properties'], + inputModel[property] as Record, + resolveFromSchema, + ); + if (Object.keys(subSchema).length > 0) { + acc![property] = { ...definition, properties: subSchema }; + } + } else if ('$ref' in definition) { + const objectDefinition = resolveRefIfNeeded(definition, resolveFromSchema); + const subSchema = getUserUpdatedPropertiesSchema( + objectDefinition['properties'] as KaotoSchemaDefinition['schema']['properties'], + inputModel[property] as Record, + resolveFromSchema, + ); + if (Object.keys(subSchema).length > 0) { + acc![property] = { ...objectDefinition, properties: subSchema }; } } else if (definition['type'] === 'array' && (inputModel[property] as unknown[]).length > 0) { - acc[property] = definition; + acc![property] = definition; } } return acc; }, - {} as KaotoSchemaDefinition['schema'], + {} as KaotoSchemaDefinition['schema']['properties'], ); - return nonDefaultFormSchema; + return nonDefaultFormSchema!; } diff --git a/packages/ui/src/utils/index.ts b/packages/ui/src/utils/index.ts index e2178ee3d..b5ba2342b 100644 --- a/packages/ui/src/utils/index.ts +++ b/packages/ui/src/utils/index.ts @@ -6,6 +6,7 @@ export * from './event-notifier'; export * from './get-array-property'; export * from './get-custom-schema-from-kamelet'; export * from './get-field-groups'; +export * from './get-filtered-properties'; export * from './get-required-properties-schema'; export * from './get-serialized-model'; export * from './get-user-updated-properties-schema';