From a8a104a570b2e09236252f7a8be5dbf5849e1a6b Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Tue, 14 Jan 2025 13:56:07 +0530 Subject: [PATCH] Fix(CanvasForm): fetching modified fields resolving refs --- .../src/components/Form/CustomAutoFields.tsx | 13 +- .../Form/customField/CustomNestField.tsx | 12 +- .../Form/dataFormat/DataFormatEditor.tsx | 2 +- .../Form/loadBalancer/LoadBalancerEditor.tsx | 6 +- .../ui/src/components/Form/schema-bridge.ts | 1 + .../stepExpression/StepExpressionEditor.tsx | 2 +- .../Canvas/Form/CanvasFormBody.tsx | 6 +- .../get-filtered-properties.test.ts.snap | 486 ++++++++++++++++++ ...ser-updated-properties-schema.test.ts.snap | 139 +++++ .../src/utils/get-filtered-properties.test.ts | 377 ++++++++++++++ .../ui/src/utils/get-filtered-properties.ts | 33 ++ ...get-user-updated-properties-schema.test.ts | 137 +---- .../get-user-updated-properties-schema.ts | 51 +- packages/ui/src/utils/index.ts | 1 + 14 files changed, 1118 insertions(+), 148 deletions(-) create mode 100644 packages/ui/src/utils/__snapshots__/get-filtered-properties.test.ts.snap create mode 100644 packages/ui/src/utils/__snapshots__/get-user-updated-properties-schema.test.ts.snap create mode 100644 packages/ui/src/utils/get-filtered-properties.test.ts create mode 100644 packages/ui/src/utils/get-filtered-properties.ts diff --git a/packages/ui/src/components/Form/CustomAutoFields.tsx b/packages/ui/src/components/Form/CustomAutoFields.tsx index 7d92a22cd..7165e6cc7 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, isDefined } from '../../utils'; +import { getFieldGroups, getFilteredProperties, isDefined } from '../../utils'; import './CustomAutoFields.scss'; import { CustomExpandableSection } from './customField/CustomExpandableSection'; import { NoFieldFound } from './NoFieldFound'; @@ -37,13 +37,12 @@ export function CustomAutoFields({ }, {}) : (rootField as KaotoSchemaDefinition['schema']).properties; - const filteredfields = Object.entries(schemaObject ?? {}).filter( - (field) => - (!omitFields!.includes(field[0]) && field[0].toLowerCase().includes(cleanQueryTerm)) || - (field[1] as { type: string }).type === 'object', + const filteredProperties = getFilteredProperties( + schemaObject as KaotoSchemaDefinition['schema']['properties'], + cleanQueryTerm, + omitFields, ); - - const propertiesArray = getFieldGroups(Object.fromEntries(filteredfields)); + 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 f80f0888f..3e2c30ad3 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) || (field[1] as { type: string }).type === 'object', + 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 38d699605..47c5f6929 100644 --- a/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx +++ b/packages/ui/src/components/Form/dataFormat/DataFormatEditor.tsx @@ -73,7 +73,7 @@ export const DataFormatEditor: FunctionComponent = (props } 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 9bf4b2435..51c2d2765 100644 --- a/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx +++ b/packages/ui/src/components/Form/loadBalancer/LoadBalancerEditor.tsx @@ -74,7 +74,11 @@ export const LoadBalancerEditor: FunctionComponent = (p } 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 c9b9d78ed..c9d23def0 100644 --- a/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx +++ b/packages/ui/src/components/Form/stepExpression/StepExpressionEditor.tsx @@ -77,7 +77,7 @@ export const StepExpressionEditor: FunctionComponent } 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 0bd33620b..ae45f21f1 100644 --- a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx @@ -36,7 +36,11 @@ export const CanvasFormBody: FunctionComponent = (props) => } 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/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-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..d011e8c89 --- /dev/null +++ b/packages/ui/src/utils/__snapshots__/get-user-updated-properties-schema.test.ts.snap @@ -0,0 +1,139 @@ +// 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", + ], + "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-filtered-properties.test.ts b/packages/ui/src/utils/get-filtered-properties.test.ts new file mode 100644 index 000000000..b64e66cf9 --- /dev/null +++ b/packages/ui/src/utils/get-filtered-properties.test.ts @@ -0,0 +1,377 @@ +import { KaotoSchemaDefinition } from '../models/kaoto-schema'; +import { getFilteredProperties } from './get-filtered-properties'; + +describe('getFilteredProperties()', () => { + const schemaObject = { + 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', + }, + }, + } as unknown as KaotoSchemaDefinition['schema']['properties']; + + it('should return only the filtered properties', () => { + const filteredSchema = getFilteredProperties(schemaObject, 'des'); + expect(filteredSchema).toMatchSnapshot(); + }); + + it('should return only the un-omitted properties', () => { + const filteredSchema = getFilteredProperties(schemaObject, '', ['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..327a61aec --- /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 (definition['type'] === 'object' && 'properties' in definition) { + const subFilteredSchema = getFilteredProperties(definition['properties'], filter); + if (Object.keys(subFilteredSchema!).length > 0) { + acc![property] = { ...definition, properties: subFilteredSchema }; + } + } else { + if ((!omitFields || !omitFields.includes(property)) && property.toLowerCase().includes(filter)) { + acc![property] = definition; + } + } + + return acc; + }, + {} as KaotoSchemaDefinition['schema']['properties'], + ); + + return filteredFormSchema; +} 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..b288b7d70 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 @@ -4,6 +4,20 @@ import { KaotoSchemaDefinition } from '../models/kaoto-schema'; describe('getUserUpdatedPropertiesSchema()', () => { const 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', @@ -21,6 +35,9 @@ describe('getUserUpdatedPropertiesSchema()', () => { title: 'Variable Receive', type: 'string', }, + testRef: { + $ref: '#/definitions/testRef', + }, parameters: { type: 'object', title: 'Endpoint Properties', @@ -146,6 +163,9 @@ describe('getUserUpdatedPropertiesSchema()', () => { id: 'from-2860', uri: 'test', variableReceive: 'test', + testRef: { + spec: 'test', + }, parameters: { period: '1000', timerName: 'template', @@ -179,122 +199,13 @@ describe('getUserUpdatedPropertiesSchema()', () => { ], }; - 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(schema.properties, inputModel, schema); + expect(processedSchema).toMatchSnapshot(); }); it('should return {}', () => { - const procesedSchema = getUserUpdatedPropertiesSchema(schema.properties!, {}); - expect(procesedSchema).toMatchObject({}); + const processedSchema = getUserUpdatedPropertiesSchema(schema.properties, {}, schema); + 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';