diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java index d0efb442..5256bad0 100644 --- a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java @@ -226,6 +226,8 @@ public String getDataFormatCatalog() throws Exception { var json = JsonMapper.asJsonObject(dataFormatCatalog).toJson(); var catalogTree = (ObjectNode) jsonMapper.readTree(json); catalogTree.set("propertiesSchema", dataFormatSchema); + // setting required property to all the dataformats schema + setRequiredToPropertiesSchema(dataFormatSchema, catalogTree); answer.set(dataFormatName, catalogTree); } StringWriter writer = new StringWriter(); @@ -267,6 +269,8 @@ public String getLanguageCatalog() throws Exception { var json = JsonMapper.asJsonObject(languageCatalog).toJson(); var catalogTree = (ObjectNode) jsonMapper.readTree(json); catalogTree.set("propertiesSchema", languageSchema); + // setting required property to all the languages schema + setRequiredToPropertiesSchema(languageSchema, catalogTree); answer.set(languageName, catalogTree); } StringWriter writer = new StringWriter(); @@ -709,6 +713,8 @@ public String getLoadBalancerCatalog() throws Exception { var json = JsonMapper.asJsonObject(loadBalancerCatalog).toJson(); var catalogTree = (ObjectNode) jsonMapper.readTree(json); catalogTree.set("propertiesSchema", loadBalancerSchema); + // setting required property to all the load-balancers schema + setRequiredToPropertiesSchema(loadBalancerSchema, catalogTree); answer.set(loadBalancerName, catalogTree); } StringWriter writer = new StringWriter(); @@ -716,4 +722,17 @@ public String getLoadBalancerCatalog() throws Exception { jsonMapper.writeTree(jsonGenerator, answer); return writer.toString(); } + + private void setRequiredToPropertiesSchema(ObjectNode camelYamlDslSchema, ObjectNode catalogModel) { + List required = new ArrayList<>(); + var camelYamlDslProperties = camelYamlDslSchema.withObject("/properties").properties().stream() + .map(Map.Entry::getKey).toList(); + for (var propertyName : camelYamlDslProperties) { + var catalogPropertySchema = catalogModel.withObject("/properties").withObject("/" + propertyName); + if (catalogPropertySchema.has("required") && catalogPropertySchema.get("required").asBoolean()) { + required.add(propertyName); + } + } + catalogModel.withObject("/propertiesSchema").set("required", jsonMapper.valueToTree(required)); + } } diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java index a97ffba8..53f961b9 100644 --- a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java @@ -15,42 +15,34 @@ */ package io.kaoto.camelcatalog.generator; -import java.io.StringWriter; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.StringWriter; +import java.util.*; + /** * Process camelYamlDsl.json file, aka Camel YAML DSL JSON schema. */ public class CamelYamlDslSchemaProcessor { private static final String PROCESSOR_DEFINITION = "org.apache.camel.model.ProcessorDefinition"; + private static final String TOKENIZER_DEFINITION = "org.apache.camel.model.TokenizerDefinition"; private static final String ROUTE_CONFIGURATION_DEFINITION = "org.apache.camel.model.RouteConfigurationDefinition"; private static final String LOAD_BALANCE_DEFINITION = "org.apache.camel.model.LoadBalanceDefinition"; - private static final String EXPRESSION_SUB_ELEMENT_DEFINITION = "org.apache.camel.model.ExpressionSubElementDefinition"; + private static final String EXPRESSION_SUB_ELEMENT_DEFINITION = + "org.apache.camel.model.ExpressionSubElementDefinition"; private static final String SAGA_DEFINITION = "org.apache.camel.model.SagaDefinition"; private static final String PROPERTY_EXPRESSION_DEFINITION = "org.apache.camel.model.PropertyExpressionDefinition"; private static final String ERROR_HANDLER_DEFINITION = "org.apache.camel.model.ErrorHandlerDefinition"; - private static final String ERROR_HANDLER_DESERIALIZER = "org.apache.camel.dsl.yaml.deserializers.ErrorHandlerBuilderDeserializer"; + private static final String ERROR_HANDLER_DESERIALIZER = + "org.apache.camel.dsl.yaml.deserializers.ErrorHandlerBuilderDeserializer"; private final ObjectMapper jsonMapper; private final ObjectNode yamlDslSchema; - private final List processorBlocklist = List.of( - "org.apache.camel.model.KameletDefinition" - // reactivate entries once we have a better handling of how to add WHEN and - // OTHERWISE without Catalog - // "Otherwise", - // "when", - // "doCatch", - // ""doFinally" - ); + private final List processorBlocklist = List.of("org.apache.camel.model.KameletDefinition"); /** * The processor properties those should be handled separately, i.e. remove from @@ -67,8 +59,7 @@ public class CamelYamlDslSchemaProcessor { List.of("uri", "parameters"), "org.apache.camel.model.WireTapDefinition", List.of("uri", "parameters")); - private final List processorReferenceBlockList = List.of( - PROCESSOR_DEFINITION); + private final List processorReferenceBlockList = List.of(PROCESSOR_DEFINITION); public CamelYamlDslSchemaProcessor(ObjectMapper mapper, ObjectNode yamlDslSchema) throws Exception { this.jsonMapper = mapper; @@ -107,11 +98,14 @@ private String doProcessSubSchema( var answer = (ObjectNode) prop.getValue().deepCopy(); if (answer.has("$ref") && definitions.has(getNameFromRef(answer))) { answer = definitions.withObject("/" + getNameFromRef(answer)).deepCopy(); - } + extractSingleOneOfFromAnyOf(answer); + removeEmptyProperties(answer); + removeNotFromOneOf(answer); answer.set("$schema", rootSchema.get("$schema")); populateDefinitions(answer, definitions); + var writer = new StringWriter(); try { JsonGenerator gen = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); @@ -122,6 +116,58 @@ private String doProcessSubSchema( } } + /** + * Remove the empty properties from the definition, as it's not supported by the + * form library. This happens when the properties are supposed to be handled + * separately, in a oneOf definition, for example. + * An example: + * ``` + * { + * properties: { + * deadLetterChannel: { }, + * defaultErrorHandler: { } + * } + * } + * + * @param definitionWithEmptyProperties the definition containing the empty properties to be removed + */ + private void removeEmptyProperties(ObjectNode definitionWithEmptyProperties) { + if (!definitionWithEmptyProperties.has("properties")) { + return; + } + + var propertiesObject = definitionWithEmptyProperties.withObject("/properties"); + List propToRemove = new ArrayList<>(); + propertiesObject.fields().forEachRemaining(field -> { + if (field.getValue().isEmpty()) { + propToRemove.add(field.getKey()); + } + }); + propToRemove.forEach(propertiesObject::remove); + } + + /** + * Remove "not" from the OneOf definition, as it's not supported by the + * form library. The "not" is only useful for the Source code editor. + * + * @param definitionContainingOneOf the definition to be cleaned + */ + private void removeNotFromOneOf(ObjectNode definitionContainingOneOf) { + if (!definitionContainingOneOf.has("oneOf")) { + return; + } + + ArrayNode cleanAnyOf = jsonMapper.createArrayNode(); + var oneOfDefinitionArray = definitionContainingOneOf.withArray("/oneOf"); + for (var def : oneOfDefinitionArray) { + if (def.has("not")) { + continue; + } + cleanAnyOf.add(def); + } + definitionContainingOneOf.set("oneOf", cleanAnyOf); + } + private String getNameFromRef(ObjectNode parent) { var ref = parent.get("$ref").asText(); return ref.contains("items") ? ref.replace("#/items/definitions/", "") @@ -152,9 +198,7 @@ private void populateDefinitions(ObjectNode schema, ObjectNode definitions) { * root definitions. * It's a workaround for the current Camel YAML DSL JSON schema, where some * AnyOf definition - * contains only one OneOf definition. This can be removed once - * https://github.com/KaotoIO/kaoto/issues/948 - * is resolved. + * contains only one OneOf definition. * This is done mostly for the errorHandler definition, f.i. * ``` * { @@ -214,7 +258,7 @@ private void extractSingleOneOfFromAnyOf(ObjectNode definition) { * "$comment" in the property schema * * - * @return + * @return A map of processor definitions */ public Map getProcessors() throws Exception { var definitions = yamlDslSchema @@ -235,8 +279,16 @@ public Map getProcessors() throws Exception { var processor = relocatedDefinitions.withObject("/" + processorFQCN); processor = extractFromOneOf(processorFQCN, processor); processor.remove("oneOf"); - processor = extractFromAnyOfOneOf(processorFQCN, processor); - processor.remove("anyOf"); + + /* Preparation for TokenizerDefinition, this could be propagated to all EIPs in the future */ + if (processorFQCN.equals(TOKENIZER_DEFINITION)) { + removeEmptyProperties(processor); + extractSingleOneOfFromAnyOf(processor); + removeNotFromOneOf(processor); + } + + processAndRemoveAnyOfForSubCatalogs(processorFQCN, processor); + var processorProperties = processor.withObject("/properties"); Set propToRemove = new HashSet<>(); var propertyBlockList = processorPropertyBlockList.get(processorFQCN); @@ -316,10 +368,12 @@ private ObjectNode extractFromOneOf(String name, ObjectNode definition) throws E for (var def : oneOf) { if (def.get("type").asText().equals("object")) { var objectDef = (ObjectNode) def; - if (definition.has("title")) + if (definition.has("title")) { objectDef.set("title", definition.get("title")); - if (definition.has("description")) + } + if (definition.has("description")) { objectDef.set("description", definition.get("description")); + } return objectDef; } } @@ -328,9 +382,19 @@ private ObjectNode extractFromOneOf(String name, ObjectNode definition) throws E name)); } - private ObjectNode extractFromAnyOfOneOf(String name, ObjectNode definition) throws Exception { + /** + * Process the "anyOf" definition for the sub-catalogs, such as expressions, languages, + * data formats, and errorHandler. + * It puts a "$comment" in the schema to indicate the type of the sub-catalog and then removes + * the "anyOf" definition. + * + * @param name the FQCN of the definition, for instance "org.apache.camel.model.LoadBalanceDefinition" + * @param definition the definition that potentially could have "anyOf" definition, referencing to another + * sub-catalogs + */ + private void processAndRemoveAnyOfForSubCatalogs(String name, ObjectNode definition) { if (!definition.has("anyOf")) { - return definition; + return; } var anyOfOneOf = definition.withArray("/anyOf").get(0).withArray("/oneOf"); for (var def : anyOfOneOf) { @@ -355,7 +419,6 @@ private ObjectNode extractFromAnyOfOneOf(String name, ObjectNode definition) thr } } definition.remove("anyOf"); - return definition; } private void sanitizeDefinitions(String processorFQCN, ObjectNode processor) throws Exception { @@ -373,7 +436,7 @@ private void sanitizeDefinitions(String processorFQCN, ObjectNode processor) thr var definition = (ObjectNode) entry.getValue(); definition = extractFromOneOf(definitionName, definition); - definition = extractFromAnyOfOneOf(definitionName, definition); + processAndRemoveAnyOfForSubCatalogs(definitionName, definition); var definitionProperties = definition.withObject("/properties"); var propToRemove = new HashSet(); for (var property : definitionProperties.properties()) { @@ -529,7 +592,7 @@ public Map getLanguages() throws Exception { *
  • If the processor property is expression aware, it puts "expression" as a * "$comment" in the property schema
  • * - * @return + * @return A map of the entity name and the schema */ public Map getEntities() throws Exception { var definitions = yamlDslSchema @@ -548,8 +611,7 @@ public Map getEntities() throws Exception { var yamlInDefinition = relocatedDefinitions.withObject("/" + yamlInFQCN); yamlInDefinition = extractFromOneOf(yamlInFQCN, yamlInDefinition); yamlInDefinition.remove("oneOf"); - yamlInDefinition = extractFromAnyOfOneOf(yamlInFQCN, yamlInDefinition); - yamlInDefinition.remove("anyOf"); + processAndRemoveAnyOfForSubCatalogs(yamlInFQCN, yamlInDefinition); Set propToRemove = new HashSet<>(); var yamlInProperties = yamlInDefinition.withObject("/properties"); for (var yamlInPropertyEntry : yamlInProperties.properties()) { diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java index 378a4d2f..0ae89cfd 100644 --- a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java @@ -19,8 +19,6 @@ import java.nio.file.Path; import java.nio.file.Paths; -import org.apache.commons.io.FilenameUtils; - public class Util { public static String generateHash(byte[] content) throws Exception { if (content == null) @@ -44,8 +42,7 @@ public static String getNormalizedFolder(String folder) { // Resolve the relative path Path absolutePath = currentDirectory.resolve(folder); - String normalizedfolder = FilenameUtils.separatorsToUnix(absolutePath.toString()); - return normalizedfolder; + return absolutePath.toString(); } } diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandOptionsTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandOptionsTest.java index 3db10ba2..421cdec1 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandOptionsTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandOptionsTest.java @@ -41,7 +41,7 @@ public void testConfigureWithAllRequiredOptions() throws ParseException { generateCommandOptions.configure(args); String outputDir = Util.getNormalizedFolder("outputDir"); - assertEquals(outputDir, configBean.getOutputFolder().toString()); + assertEquals(outputDir, configBean.getOutputFolder().toPath().toString()); assertEquals("catalogName", configBean.getCatalogsName()); assertEquals("kameletsVersion", configBean.getKameletsVersion()); } diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandTest.java index d569d79c..f0b365e4 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/commands/GenerateCommandTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -84,7 +85,8 @@ void testGeneratorCalledWithCorrectParameters() { File expectedFolder = new File(tempDir, "camel-main/4.8.0"); verify(builder, times(1)).withOutputDirectory(expectedFolder); - assertEquals(catalogDefinition.getFileName(), "camel-main/4.8.0/index.json"); + String expectedFile = Path.of("camel-main", "4.8.0", "index.json").toString(); + assertEquals(catalogDefinition.getFileName(), expectedFile); } } @@ -123,7 +125,9 @@ void testCatalogLibraryOutput() { assertEquals(catalogLibraryEntry.name(), "test-camel-catalog"); assertEquals(catalogLibraryEntry.version(), "4.8.0"); assertEquals(catalogLibraryEntry.runtime(), "Main"); - assertEquals(catalogLibraryEntry.fileName(), "camel-main/4.8.0/index.json"); + + String expectedFile = Path.of("camel-main", "4.8.0", "index.json").toString(); + assertEquals(catalogLibraryEntry.fileName(), expectedFile); } } -} \ No newline at end of file +} diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessorTest.java index 91af4940..2d8d6810 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessorTest.java @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class CamelCatalogProcessorTest { +class CamelCatalogProcessorTest { private static final List ALLOWED_ENUM_TYPES = List.of("integer", "number", "string"); private final CamelCatalogProcessor processor; private final ObjectNode componentCatalog; @@ -41,7 +41,7 @@ public class CamelCatalogProcessorTest { private final ObjectNode entityCatalog; private final ObjectNode loadBalancerCatalog; - public CamelCatalogProcessorTest() throws Exception { + CamelCatalogProcessorTest() throws Exception { CamelCatalog catalog = new DefaultCamelCatalog(); ObjectMapper jsonMapper = new ObjectMapper(); var is = YamlRoutesBuilderLoader.class.getClassLoader().getResourceAsStream("schema/camelYamlDsl.json"); @@ -60,7 +60,7 @@ public CamelCatalogProcessorTest() throws Exception { } @Test - public void testProcessCatalog() throws Exception { + void testProcessCatalog() throws Exception { var catalogMap = processor.processCatalog(); assertEquals(processor.getComponentCatalog(), catalogMap.get("components")); assertEquals(processor.getDataFormatCatalog(), catalogMap.get("dataformats")); @@ -72,7 +72,7 @@ public void testProcessCatalog() throws Exception { } @Test - public void testGetComponentCatalog() throws Exception { + void testGetComponentCatalog() throws Exception { assertTrue(componentCatalog.size() > 300); var directModel = componentCatalog .withObject("/direct") @@ -123,7 +123,7 @@ public void testGetComponentCatalog() throws Exception { } @Test - public void testComponentEnumParameter() throws Exception { + void testComponentEnumParameter() throws Exception { checkEnumParameters(componentCatalog); } @@ -154,7 +154,7 @@ private void checkEnumDuplicate(String entityName, String propertyName, ArrayNod } @Test - public void testGetDataFormatCatalog() throws Exception { + void testGetDataFormatCatalog() throws Exception { var customModel = dataFormatCatalog .withObject("/custom") .withObject("/model"); @@ -170,40 +170,48 @@ public void testGetDataFormatCatalog() throws Exception { assertEquals("Custom", customPropertiesSchema.get("title").asText()); var refProperty = customPropertiesSchema.withObject("/properties").withObject("/ref"); assertEquals("Ref", refProperty.get("title").asText()); + var customPropertiesSchemaRequiredFields = customPropertiesSchema.withArray("/required"); + assertFalse(customPropertiesSchemaRequiredFields.isEmpty()); + assertEquals(1, customPropertiesSchemaRequiredFields.size(), "Size should be 1"); } @Test - public void testDataFormatEnumParameter() throws Exception { + void testDataFormatEnumParameter() throws Exception { checkEnumParameters(dataFormatCatalog); } @Test - public void testGetLanguageCatalog() throws Exception { + void testGetLanguageCatalog() throws Exception { assertFalse(languageCatalog.has("file")); - var customModel = languageCatalog + var languageModel = languageCatalog .withObject("/language") .withObject("/model"); - assertEquals("model", customModel.get("kind").asText()); - assertEquals("Language", customModel.get("title").asText()); - var customProperties = languageCatalog + assertEquals("model", languageModel.get("kind").asText()); + assertEquals("Language", languageModel.get("title").asText()); + var languageProperties = languageCatalog .withObject("/language") .withObject("/properties"); - assertEquals("Language", customProperties.withObject("/language").get("displayName").asText()); - var customPropertiesSchema = languageCatalog + assertEquals("Language", languageProperties.withObject("/language").get("displayName").asText()); + var languagePropertiesSchema = languageCatalog .withObject("/language") .withObject("/propertiesSchema"); - assertEquals("Language", customPropertiesSchema.get("title").asText()); - var languageProperty = customPropertiesSchema.withObject("/properties").withObject("/language"); + assertEquals("Language", languagePropertiesSchema.get("title").asText()); + var languageProperty = languagePropertiesSchema.withObject("/properties").withObject("/language"); assertEquals("Language", languageProperty.get("title").asText()); + var languagePropertiesSchemaRequiredFields = languagePropertiesSchema.withArray("/required"); + assertFalse(languagePropertiesSchemaRequiredFields.isEmpty()); + assertEquals(2, languagePropertiesSchemaRequiredFields.size(), "Size should be 2"); + assertEquals("expression", languagePropertiesSchemaRequiredFields.get(0).asText()); + assertEquals("language", languagePropertiesSchemaRequiredFields.get(1).asText()); } @Test - public void testLanguageEnumParameter() throws Exception { + void testLanguageEnumParameter() throws Exception { checkEnumParameters(languageCatalog); } @Test - public void testGetModelCatalog() throws Exception { + void testGetModelCatalog() throws Exception { assertTrue(modelCatalog.size() > 200); var aggregateModel = modelCatalog .withObject("/aggregate") @@ -213,12 +221,12 @@ public void testGetModelCatalog() throws Exception { } @Test - public void testModelEnumParameter() throws Exception { + void testModelEnumParameter() throws Exception { checkEnumParameters(modelCatalog); } @Test - public void testGetPatternCatalog() throws Exception { + void testGetPatternCatalog() throws Exception { assertTrue(processorCatalog.size() > 65 && processorCatalog.size() < 80); var choiceModel = processorCatalog.withObject("/choice").withObject("/model"); assertEquals("choice", choiceModel.get("name").asText()); @@ -235,17 +243,18 @@ public void testGetPatternCatalog() throws Exception { } @Test - public void testRouteConfigurationCatalog() throws Exception { - List.of("intercept", "interceptFrom", "interceptSendToEndpoint", "onCompletion", "onException").forEach(name -> assertTrue(entityCatalog.has(name), name)); + void testRouteConfigurationCatalog() throws Exception { + List.of("intercept", "interceptFrom", "interceptSendToEndpoint", "onCompletion", "onException") + .forEach(name -> assertTrue(entityCatalog.has(name), name)); } @Test - public void testPatternEnumParameter() throws Exception { + void testPatternEnumParameter() throws Exception { checkEnumParameters(processorCatalog); } @Test - public void testGetEntityCatalog() throws Exception { + void testGetEntityCatalog() throws Exception { List.of( "bean", "beans", @@ -262,8 +271,7 @@ public void testGetEntityCatalog() throws Exception { "templatedRoute", "restConfiguration", "rest", - "routeTemplateBean" - ).forEach(name -> assertTrue(entityCatalog.has(name), name)); + "routeTemplateBean").forEach(name -> assertTrue(entityCatalog.has(name), name)); var bean = entityCatalog.withObject("/bean"); var beanScriptLanguage = bean.withObject("/propertiesSchema") .withObject("/properties") @@ -284,32 +292,42 @@ public void testGetEntityCatalog() throws Exception { } @Test - public void testEntityEnumParameter() throws Exception { + void testEntityEnumParameter() throws Exception { checkEnumParameters(entityCatalog); } @Test - public void testGetLoadBalancerCatalog() throws Exception { + void testGetLoadBalancerCatalog() throws Exception { assertFalse(loadBalancerCatalog.isEmpty()); var failoverModel = loadBalancerCatalog.withObject("/failoverLoadBalancer/model"); assertEquals("failoverLoadBalancer", failoverModel.get("name").asText()); var failoverSchema = loadBalancerCatalog.withObject("/failoverLoadBalancer/propertiesSchema"); + var failoverSchemaRequiredFields = failoverSchema.withArray("/required"); + assertTrue(failoverSchemaRequiredFields.isEmpty()); var maximumFailoverAttempts = failoverSchema.withObject("/properties/maximumFailoverAttempts"); assertEquals("string", maximumFailoverAttempts.get("type").asText()); assertEquals("-1", maximumFailoverAttempts.get("default").asText()); + var roundRobinSchema = loadBalancerCatalog.withObject("/roundRobinLoadBalancer/propertiesSchema"); + var roundRobinSchemaRequiredFields = roundRobinSchema.withArray("/required"); + assertTrue(roundRobinSchemaRequiredFields.isEmpty()); var roundRobinId = roundRobinSchema.withObject("/properties/id"); assertEquals("string", roundRobinId.get("type").asText()); + var customModel = loadBalancerCatalog.withObject("/customLoadBalancer/model"); assertEquals("Custom Load Balancer", customModel.get("title").asText()); var customSchema = loadBalancerCatalog.withObject("/customLoadBalancer/propertiesSchema"); + var customSchemaRequiredFields = customSchema.withArray("/required"); + assertFalse(customSchemaRequiredFields.isEmpty()); + assertEquals(1, customSchemaRequiredFields.size(), "Size should be 1"); + assertEquals("ref", customSchemaRequiredFields.get(0).asText()); assertEquals("Custom Load Balancer", customSchema.get("title").asText()); var customRef = customSchema.withObject("/properties/ref"); assertEquals("Ref", customRef.get("title").asText()); } @Test - public void testLoadBalancerEnumParameter() throws Exception { + void testLoadBalancerEnumParameter() throws Exception { checkEnumParameters(loadBalancerCatalog); } } diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparatorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparatorTest.java index 1a8d1192..b8cffab3 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparatorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparatorTest.java @@ -26,16 +26,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class CamelYamlDSLKeysComparatorTest { +class CamelYamlDSLKeysComparatorTest { private DefaultCamelCatalog api; @BeforeEach - public void setUp() { + void setUp() { this.api = new DefaultCamelCatalog(); } @Test - public void sort_keys_using_the_catalog_index() throws Exception { + void sort_keys_using_the_catalog_index() throws Exception { var aggregateCatalogModel = (EipModel) api.model(Kind.eip, "aggregate"); List aggregateKeysFromCamelYAMLDsl = List.of("aggregateController", "aggregationRepository", diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java index 0d6f07a3..2681264e 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java @@ -18,26 +18,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.camel.dsl.yaml.YamlRoutesBuilderLoader; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; import static org.junit.jupiter.api.Assertions.*; -public class CamelYamlDslSchemaProcessorTest { - private final ObjectMapper jsonMapper; - private final ObjectNode yamlDslSchema; - private final CamelYamlDslSchemaProcessor processor; +class CamelYamlDslSchemaProcessorTest { + private ObjectMapper jsonMapper; + private CamelYamlDslSchemaProcessor processor; - public CamelYamlDslSchemaProcessorTest() throws Exception { + @BeforeEach + void setUp() throws Exception { jsonMapper = new ObjectMapper(); var is = YamlRoutesBuilderLoader.class.getClassLoader().getResourceAsStream("schema/camelYamlDsl.json"); - yamlDslSchema = (ObjectNode) jsonMapper.readTree(is); + ObjectNode yamlDslSchema = (ObjectNode) jsonMapper.readTree(is); + processor = new CamelYamlDslSchemaProcessor(jsonMapper, yamlDslSchema); } @Test - public void testProcessSubSchema() throws Exception { + void testProcessSubSchema() throws Exception { var subSchemaMap = processor.processSubSchema(); assertTrue(subSchemaMap.size() > 10 && subSchemaMap.size() < 20); var beansSchema = jsonMapper.readTree(subSchemaMap.get("beans")); @@ -52,7 +54,7 @@ public void testProcessSubSchema() throws Exception { } @Test - public void testExtractSingleOneOfFromAnyOf() throws Exception { + void testExtractSingleOneOfFromAnyOf() throws Exception { var subSchemaMap = processor.processSubSchema(); var errorHandlerSchema = jsonMapper.readTree(subSchemaMap.get("errorHandler")); @@ -60,11 +62,45 @@ public void testExtractSingleOneOfFromAnyOf() throws Exception { assertTrue(errorHandlerSchema.has("oneOf")); assertTrue(errorHandlerSchema.get("oneOf").isArray()); - assertEquals(7, errorHandlerSchema.get("oneOf").size()); + assertEquals(6, errorHandlerSchema.get("oneOf").size()); + } + + @Test + void testRemoveEmptyProperties() throws Exception { + var tokenizerRootSchema = (ObjectNode) jsonMapper.readTree( + getClass().getClassLoader().getResourceAsStream("camel-4.9.0-tokenizer-schema.json")); + processor = new CamelYamlDslSchemaProcessor(jsonMapper, tokenizerRootSchema); + + var subSchemaMap = processor.processSubSchema(); + var tokenizerSchema = jsonMapper.readTree(subSchemaMap.get("tokenizer")); + var properties = tokenizerSchema.withObject("/properties"); + + assertFalse(properties.has("langChain4jCharacterTokenizer"), "The langChain4jCharacterTokenizer empty property should not exist, since is empty"); + assertFalse(properties.has("langChain4jLineTokenizer"), "The langChain4jLineTokenizer empty property should not exist, since is empty"); + assertFalse(properties.has("langChain4jParagraphTokenizer"), "The langChain4jParagraphTokenizer empty property should not exist, since is empty"); + assertFalse(properties.has("langChain4jSentenceTokenizer"), "The langChain4jSentenceTokenizer empty property should not exist, since is empty"); + assertFalse(properties.has("langChain4jWordTokenizer"), "The langChain4jWordTokenizer empty property should not exist, since is empty"); + } + + @Test + void testRemoveNotFromOneOf() throws Exception { + var tokenizerRootSchema = (ObjectNode) jsonMapper.readTree( + getClass().getClassLoader().getResourceAsStream("camel-4.9.0-tokenizer-schema.json")); + processor = new CamelYamlDslSchemaProcessor(jsonMapper, tokenizerRootSchema); + + var subSchemaMap = processor.processSubSchema(); + var tokenizerSchema = jsonMapper.readTree(subSchemaMap.get("tokenizer")); + var oneOfArray = tokenizerSchema.withArray("/oneOf"); + + assertEquals(5, oneOfArray.size()); + oneOfArray.forEach(oneOf -> { + assertTrue(oneOf.has("properties")); + assertFalse(oneOf.has("not")); + }); } @Test - public void testGetDataFormats() throws Exception { + void testGetDataFormats() throws Exception { var dataFormatMap = processor.getDataFormats(); assertTrue(dataFormatMap.size() > 30 && dataFormatMap.size() < 50); var customDataFormat = dataFormatMap.get("custom"); @@ -78,10 +114,11 @@ public void testGetDataFormats() throws Exception { } @Test - public void testGetDataFormatYaml() throws Exception { + void testGetDataFormatYaml() throws Exception { var dataFormatMap = processor.getDataFormats(); var yamlDataFormat = dataFormatMap.get("yaml"); - var typeFilterDefinition = yamlDataFormat.withObject("/definitions").withObject("org.apache.camel.model.dataformat.YAMLTypeFilterDefinition"); + var typeFilterDefinition = yamlDataFormat.withObject("/definitions") + .withObject("org.apache.camel.model.dataformat.YAMLTypeFilterDefinition"); assertEquals("object", typeFilterDefinition.get("type").asText()); var propType = typeFilterDefinition.withObject("/properties").withObject("/type"); assertEquals("string", propType.get("type").asText()); @@ -89,7 +126,7 @@ public void testGetDataFormatYaml() throws Exception { } @Test - public void testGetLanguages() throws Exception { + void testGetLanguages() throws Exception { var languageMap = processor.getLanguages(); assertTrue(languageMap.size() > 20 && languageMap.size() < 30); var customLanguage = languageMap.get("language"); @@ -103,7 +140,7 @@ public void testGetLanguages() throws Exception { } @Test - public void testGetProcessors() throws Exception { + void testGetProcessors() throws Exception { var processorMap = processor.getProcessors(); assertTrue(processorMap.size() > 50 && processorMap.size() < 100); var aggregate = processorMap.get("org.apache.camel.model.AggregateDefinition"); @@ -141,13 +178,15 @@ public void testGetProcessors() throws Exception { var actionDef = saga.withObject("/definitions").withObject("/org.apache.camel.model.SagaActionUriDefinition"); assertFalse(actionDef.has("oneOf")); assertEquals("object", actionDef.withObject("/properties").withObject("/parameters").get("type").asText()); - var propExpDef = saga.withObject("/definitions").withObject("/org.apache.camel.model.PropertyExpressionDefinition"); + var propExpDef = + saga.withObject("/definitions").withObject("/org.apache.camel.model.PropertyExpressionDefinition"); assertEquals("object", propExpDef.withObject("/properties").withObject("/expression").get("type").asText()); - assertEquals("expression", propExpDef.withObject("/properties").withObject("/expression").get("$comment").asText()); + assertEquals("expression", + propExpDef.withObject("/properties").withObject("/expression").get("$comment").asText()); } @Test - public void testGetEntities() throws Exception { + void testGetEntities() throws Exception { var entityMap = processor.getEntities(); List.of( "beans", @@ -163,12 +202,11 @@ public void testGetEntities() throws Exception { "routeTemplate", "templatedRoute", "restConfiguration", - "rest" - ).forEach(name -> assertTrue(entityMap.containsKey(name), name)); + "rest").forEach(name -> assertTrue(entityMap.containsKey(name), name)); } @Test - public void testGetLoadBalancers() throws Exception { + void testGetLoadBalancers() throws Exception { var lbMap = processor.getLoadBalancers(); assertTrue(lbMap.containsKey("customLoadBalancer")); var customLb = lbMap.get("customLoadBalancer"); diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessorTest.java index 3b2cc857..cf43f2d2 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessorTest.java @@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -public class K8sSchemaProcessorTest { +class K8sSchemaProcessorTest { private static final String[] K8S_DEFINITIONS = new String[] { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", @@ -32,14 +32,14 @@ public class K8sSchemaProcessorTest { private final ObjectMapper jsonMapper; private final K8sSchemaProcessor processor; - public K8sSchemaProcessorTest() throws Exception { + K8sSchemaProcessorTest() throws Exception { jsonMapper = new ObjectMapper(); var openapiSpec = (ObjectNode) jsonMapper.readTree(getClass().getClassLoader().getResourceAsStream("kubernetes-api-v1-openapi.json")); processor = new K8sSchemaProcessor(jsonMapper, openapiSpec); } @Test - public void test() throws Exception { + void test() throws Exception { var schemaMap = processor.processK8sDefinitions(List.of(K8S_DEFINITIONS)); var objectMeta = (ObjectNode) jsonMapper.readTree(schemaMap.get("ObjectMeta")); assertTrue(objectMeta.withObject("/properties").has("annotations")); diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/KameletProcessorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/KameletProcessorTest.java index 7700b8d3..9ca62069 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/KameletProcessorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/KameletProcessorTest.java @@ -29,8 +29,8 @@ import static org.junit.jupiter.api.Assertions.*; -public class KameletProcessorTest { - private static final List ALLOWED_ENUM_TYPES = List.of("integer", "number", "string" ); +class KameletProcessorTest { + private static final List ALLOWED_ENUM_TYPES = List.of("integer", "number", "string"); private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());; private ObjectNode processKamelet(String name) throws Exception { @@ -59,7 +59,7 @@ private Map getAllKameletFiles() throws Exception { } @Test - public void test() throws Exception { + void test() throws Exception { var beerSource = processKamelet("beer-source"); assertTrue(beerSource.has("propertiesSchema")); var periodProp = beerSource.withObject("/propertiesSchema") @@ -92,7 +92,7 @@ public void test() throws Exception { } @Test - public void testEnumParameters() throws Exception { + void testEnumParameters() throws Exception { for (var kamelet : getAllKameletFiles().values()) { var schema = kamelet.withObject("/propertiesSchema"); var title = schema.get("title"); diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/UtilTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/UtilTest.java index 3cc3e4b0..7b3ff2d8 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/UtilTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/UtilTest.java @@ -25,11 +25,11 @@ import static org.junit.jupiter.api.Assertions.*; -public class UtilTest { +class UtilTest { private final List testFiles = List.of("testfile1.txt", "testfile2.txt"); @Test - public void testGenerateHash() throws Exception { + void testGenerateHash() throws Exception { var fileHashMap = new HashMap(); for (var file : testFiles) { var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(file); @@ -51,7 +51,7 @@ public void testGenerateHash() throws Exception { } @Test - public void testGenerateHashFromPath() throws Exception { + void testGenerateHashFromPath() throws Exception { var url = Thread.currentThread().getContextClassLoader().getResource(testFiles.get(0)); if (url == null) throw new Exception("no test file available"); var testFilePath = Path.of(url.toURI()); @@ -65,7 +65,7 @@ public void testGenerateHashFromPath() throws Exception { } @Test - public void testGenerateHashFromString() throws Exception { + void testGenerateHashFromString() throws Exception { var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(testFiles.get(0)); if (is == null) throw new Exception("no test file available"); var testFileString = new String(is.readAllBytes()); @@ -79,7 +79,7 @@ public void testGenerateHashFromString() throws Exception { } @Test - public void testMessageDigestHash() throws Exception { + void testMessageDigestHash() throws Exception { try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(testFiles.get(0))) { if (is == null) throw new Exception("no test file available"); var digest = MessageDigest.getInstance("MD5"); diff --git a/packages/catalog-generator/src/test/resources/camel-4.9.0-tokenizer-schema.json b/packages/catalog-generator/src/test/resources/camel-4.9.0-tokenizer-schema.json new file mode 100644 index 00000000..b2f7f459 --- /dev/null +++ b/packages/catalog-generator/src/test/resources/camel-4.9.0-tokenizer-schema.json @@ -0,0 +1,337 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "properties": { + "tokenizer": { + "title": "Specialized tokenizer for AI applications", + "description": "Represents a Camel tokenizer for AI.", + "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 this EIP from the route during build time. Once an EIP has been disabled then it cannot be enabled later at runtime." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Sets the id of this node" + }, + "langChain4jCharacterTokenizer": {}, + "langChain4jLineTokenizer": {}, + "langChain4jParagraphTokenizer": {}, + "langChain4jSentenceTokenizer": {}, + "langChain4jWordTokenizer": {} + }, + "anyOf": [ + { + "oneOf": [ + { + "type": "object", + "required": [ + "langChain4jCharacterTokenizer" + ], + "properties": { + "langChain4jCharacterTokenizer": { + "$ref": "#/items/definitions/org.apache.camel.model.tokenizer.LangChain4jCharacterTokenizerDefinition" + } + } + }, + { + "not": { + "anyOf": [ + { + "required": [ + "langChain4jCharacterTokenizer" + ] + }, + { + "required": [ + "langChain4jLineTokenizer" + ] + }, + { + "required": [ + "langChain4jParagraphTokenizer" + ] + }, + { + "required": [ + "langChain4jSentenceTokenizer" + ] + }, + { + "required": [ + "langChain4jWordTokenizer" + ] + } + ] + } + }, + { + "type": "object", + "required": [ + "langChain4jLineTokenizer" + ], + "properties": { + "langChain4jLineTokenizer": { + "$ref": "#/items/definitions/org.apache.camel.model.tokenizer.LangChain4jTokenizerDefinition" + } + } + }, + { + "type": "object", + "required": [ + "langChain4jParagraphTokenizer" + ], + "properties": { + "langChain4jParagraphTokenizer": { + "$ref": "#/items/definitions/org.apache.camel.model.tokenizer.LangChain4jParagraphTokenizerDefinition" + } + } + }, + { + "type": "object", + "required": [ + "langChain4jSentenceTokenizer" + ], + "properties": { + "langChain4jSentenceTokenizer": { + "$ref": "#/items/definitions/org.apache.camel.model.tokenizer.LangChain4jSentenceTokenizerDefinition" + } + } + }, + { + "type": "object", + "required": [ + "langChain4jWordTokenizer" + ], + "properties": { + "langChain4jWordTokenizer": { + "$ref": "#/items/definitions/org.apache.camel.model.tokenizer.LangChain4jWordTokenizerDefinition" + } + } + } + ] + } + ] + } + }, + "definitions": { + "org.apache.camel.model.tokenizer.LangChain4jCharacterTokenizerDefinition": { + "title": "LangChain4J Tokenizer with character splitter", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this node" + }, + "maxOverlap": { + "type": "number", + "title": "Max Overlap", + "description": "Sets the maximum number of tokens that can overlap in each segment" + }, + "maxTokens": { + "type": "number", + "title": "Max Tokens", + "description": "Sets the maximum number of tokens on each segment" + }, + "tokenizerType": { + "type": "string", + "title": "Tokenizer Type", + "description": "Sets the tokenizer type", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + }, + "org.apache.camel.model.tokenizer.LangChain4jLineTokenizerDefinition": { + "title": "LangChain4J Tokenizer with line splitter", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this node" + }, + "maxOverlap": { + "type": "number", + "title": "Max Overlap", + "description": "Sets the maximum number of tokens that can overlap in each segment" + }, + "maxTokens": { + "type": "number", + "title": "Max Tokens", + "description": "Sets the maximum number of tokens on each segment" + }, + "tokenizerType": { + "type": "string", + "title": "Tokenizer Type", + "description": "Sets the tokenizer type", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + }, + "org.apache.camel.model.tokenizer.LangChain4jParagraphTokenizerDefinition": { + "title": "LangChain4J Tokenizer with paragraph splitter", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this node" + }, + "maxOverlap": { + "type": "number", + "title": "Max Overlap", + "description": "Sets the maximum number of tokens that can overlap in each segment" + }, + "maxTokens": { + "type": "number", + "title": "Max Tokens", + "description": "Sets the maximum number of tokens on each segment" + }, + "tokenizerType": { + "type": "string", + "title": "Tokenizer Type", + "description": "Sets the tokenizer type", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + }, + "org.apache.camel.model.tokenizer.LangChain4jSentenceTokenizerDefinition": { + "title": "LangChain4J Tokenizer with sentence splitter", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this node" + }, + "maxOverlap": { + "type": "number", + "title": "Max Overlap", + "description": "Sets the maximum number of tokens that can overlap in each segment" + }, + "maxTokens": { + "type": "number", + "title": "Max Tokens", + "description": "Sets the maximum number of tokens on each segment" + }, + "tokenizerType": { + "type": "string", + "title": "Tokenizer Type", + "description": "Sets the tokenizer type", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + }, + "org.apache.camel.model.tokenizer.LangChain4jTokenizerDefinition": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "maxOverlap": { + "type": "number" + }, + "maxTokens": { + "type": "number" + }, + "tokenizerType": { + "type": "string", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + }, + "org.apache.camel.model.tokenizer.LangChain4jWordTokenizerDefinition": { + "title": "LangChain4J Tokenizer with word splitter", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this node" + }, + "maxOverlap": { + "type": "number", + "title": "Max Overlap", + "description": "Sets the maximum number of tokens that can overlap in each segment" + }, + "maxTokens": { + "type": "number", + "title": "Max Tokens", + "description": "Sets the maximum number of tokens on each segment" + }, + "tokenizerType": { + "type": "string", + "title": "Tokenizer Type", + "description": "Sets the tokenizer type", + "enum": [ + "OPEN_AI", + "AZURE", + "QWEN" + ] + } + }, + "required": [ + "maxOverlap", + "maxTokens" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/producerConsumerConf.cy.ts b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/producerConsumerConf.cy.ts new file mode 100644 index 00000000..c6da5283 --- /dev/null +++ b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/producerConsumerConf.cy.ts @@ -0,0 +1,20 @@ +describe('Tests for producer/consumer sidebar config', () => { + beforeEach(() => { + cy.openHomePage(); + }); + + it('Check if producer/consumer properties are allowed or forbidden on route nodes', () => { + cy.uploadFixture('flows/camelRoute/consumerProducer.yaml'); + cy.openDesignPage(); + + cy.openStepConfigurationTab('amqp'); + cy.selectFormTab('All'); + cy.get('.pf-v5-c-expandable-section__toggle-text').contains('Consumer (advanced) properties').should('exist'); + cy.get('.pf-v5-c-expandable-section__toggle-text').contains('Producer (advanced) properties').should('not.exist'); + + cy.openStepConfigurationTab('activemq6'); + cy.selectFormTab('All'); + cy.get('.pf-v5-c-expandable-section__toggle-text').contains('Producer (advanced) properties').should('exist'); + cy.get('.pf-v5-c-expandable-section__toggle-text').contains('Consumer (advanced) properties').should('not.exist'); + }); +}); diff --git a/packages/ui-tests/cypress/fixtures/flows/camelRoute/consumerProducer.yaml b/packages/ui-tests/cypress/fixtures/flows/camelRoute/consumerProducer.yaml new file mode 100644 index 00000000..3b62a6e4 --- /dev/null +++ b/packages/ui-tests/cypress/fixtures/flows/camelRoute/consumerProducer.yaml @@ -0,0 +1,11 @@ +- route: + id: route-1921 + from: + id: from-2265 + uri: amqp + parameters: {} + steps: + - to: + id: to-9644 + uri: activemq6 + parameters: {} diff --git a/packages/ui-tests/stories/modal/ActionConfirmationModal.stories.tsx b/packages/ui-tests/stories/modal/ActionConfirmationModal.stories.tsx new file mode 100644 index 00000000..8fa88270 --- /dev/null +++ b/packages/ui-tests/stories/modal/ActionConfirmationModal.stories.tsx @@ -0,0 +1,66 @@ +import { + ActionConfirmationModalContextProvider, + ActionConfirmationModalContext, + ActionConfirmationButtonOption, +} from '@kaoto/kaoto/testing'; +import { Meta, StoryFn } from '@storybook/react'; +import { FunctionComponent, useContext, useState } from 'react'; +import { ButtonVariant } from '@patternfly/react-core'; + +export default { + title: 'Modal/ActionConfirmationModal', + component: ActionConfirmationModalContextProvider, +} as Meta; + +type TestComponentProps = { + title: string; + btnTitle?: string; + text?: string; + additionalModalText?: string; + buttonOptions?: Record; +}; + +const TestComponent: FunctionComponent = (props) => { + const [confirmationResult, setConfirmationResult] = useState(''); + const { actionConfirmation: deleteConfirmation } = useContext(ActionConfirmationModalContext)!; + const handleDelete = async () => { + const res = await deleteConfirmation(props); + setConfirmationResult(res); + }; + + return ( +

    + +
    +

    {confirmationResult}

    +

    + ); +}; + +const Template: StoryFn = (_args) => { + return ( + + + + + ); +}; + +export const ActionConfirmationModal = Template.bind({}); +ActionConfirmationModal.args = {}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 3b3b0e8f..85463ef4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,7 +52,7 @@ }, "dependencies": { "@dnd-kit/core": "^6.1.0", - "@kaoto-next/uniforms-patternfly": "^0.7.1", + "@kaoto-next/uniforms-patternfly": "^0.7.10", "@kaoto/xml-schema-ts": "workspace:*", "@kie-tools-core/editor": "0.32.0", "@kie-tools-core/notifications": "0.32.0", diff --git a/packages/ui/src/components/Catalog/Catalog.tsx b/packages/ui/src/components/Catalog/Catalog.tsx index 245c152e..a132f7a0 100644 --- a/packages/ui/src/components/Catalog/Catalog.tsx +++ b/packages/ui/src/components/Catalog/Catalog.tsx @@ -42,26 +42,27 @@ export const Catalog: FunctionComponent> = (prop /** Selected Providers */ const [selectedProviders, setSelectedProviders] = useState(providers); - /** Filter by selected group */ - const filteredTilesByGroup = useMemo(() => { + const filteredTiles = useMemo(() => { return filterTiles(props.tiles, { searchTerm, searchTags: filterTags, selectedProviders }); }, [filterTags, props.tiles, searchTerm, selectedProviders]); /** Set the tiles groups */ const tilesGroups = useMemo(() => { - return Object.entries(filteredTilesByGroup).map(([group, tiles]) => ({ name: group, count: tiles.length })); - }, [filteredTilesByGroup]); + const groups: Record = {}; + filteredTiles.forEach((tile) => { + if (!groups[tile.type]) { + groups[tile.type] = []; + } + groups[tile.type].push(tile); + }); + return Object.entries(groups).map(([group, tiles]) => ({ name: group, count: tiles.length })); + }, [filteredTiles]); const [activeGroups, setActiveGroups] = useState(tilesGroups.map((g) => g.name)); - const filteredTiles = useMemo(() => { - return Object.entries(filteredTilesByGroup).reduce((acc, [group, tiles]) => { - if (activeGroups.includes(group)) { - acc.push(...tiles); - } - return acc; - }, [] as ITile[]); - }, [activeGroups, filteredTilesByGroup]); + const filteredTilesByGroup = useMemo(() => { + return filteredTiles.filter((tile) => activeGroups.includes(tile.type)); + }, [activeGroups, filteredTiles]); const onFilterChange = useCallback( (_event: unknown, value = '') => { @@ -116,7 +117,7 @@ export const Catalog: FunctionComponent> = (prop /> { const options = { searchTerm: 'message' }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq], - [CatalogKind.Pattern]: [tilesMap.setBody, tilesMap.split], - [CatalogKind.Kamelet]: [tilesMap.slackSource], - }); + expect(result).toEqual([tilesMap.activemq, tilesMap.setBody, tilesMap.split, tilesMap.slackSource]); }); it('should filter tiles by provider', () => { const options = { selectedProviders: ['Red Hat'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.cron], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.cron]); }); it('should return tiles without provider when community is selected', () => { const options = { selectedProviders: ['Community'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast], - [CatalogKind.Pattern]: [tilesMap.setBody, tilesMap.split], - [CatalogKind.Kamelet]: [tilesMap.beerSource, tilesMap.slackSource], - }); + expect(result).toEqual([ + tilesMap.activemq, + tilesMap.cometd, + tilesMap.hazelcast, + tilesMap.setBody, + tilesMap.split, + tilesMap.beerSource, + tilesMap.slackSource, + ]); }); it('should filter tiles by a single tag', () => { const options = { searchTags: ['messaging'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast]); }); it('should filter tiles by multiple tags', () => { const options = { searchTags: ['messaging', 'clustering'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.hazelcast], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.hazelcast]); }); it('should filter tiles by search term and tags', () => { const options = { searchTerm: 'cr', searchTags: ['scheduling'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.cron], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.cron]); }); }); diff --git a/packages/ui/src/components/Catalog/filter-tiles.ts b/packages/ui/src/components/Catalog/filter-tiles.ts index f7c10d64..ee155f29 100644 --- a/packages/ui/src/components/Catalog/filter-tiles.ts +++ b/packages/ui/src/components/Catalog/filter-tiles.ts @@ -6,41 +6,57 @@ const checkThatArrayContainsAllTags = (tileTags: string[], searchTags: string[]) export const filterTiles = ( tiles: ITile[], options?: { searchTerm?: string; searchTags?: string[]; selectedProviders?: string[] }, -): Record => { +): ITile[] => { const { searchTerm = '', searchTags = [], selectedProviders = [] } = options ?? {}; const searchTermLowercase = searchTerm.toLowerCase(); - return tiles.reduce( - (acc, tile) => { - /** Filter by selected tags */ - const doesTagsMatches = searchTags.length ? checkThatArrayContainsAllTags(tile.tags, searchTags) : true; - - /** Filter by providers */ - let doesProviderMatch = true; - if (selectedProviders.length) { - doesProviderMatch = - tile.provider === undefined - ? selectedProviders.includes('Community') - : selectedProviders.includes(tile.provider); - } - - /** Determine whether the tile should be included in the filtered list */ - const shouldInclude = - doesTagsMatches && - doesProviderMatch && - (!searchTermLowercase || - tile.name.toLowerCase().includes(searchTermLowercase) || - tile.title.toLowerCase().includes(searchTermLowercase) || - tile.description?.toLowerCase().includes(searchTermLowercase) || - tile.tags.some((tag) => tag.toLowerCase().includes(searchTermLowercase))); - - acc[tile.type] = acc[tile.type] ?? []; - if (shouldInclude) { - acc[tile.type].push(tile); - } - - return acc; - }, - {} as Record, - ); + // Step 1: Score each tile based on how well it matches the search term + const scoredTiles = tiles.map((tile) => { + let score = 0; + + // Score based on name + const nameLower = tile.name.toLowerCase(); + if (nameLower.startsWith(searchTermLowercase)) { + score += 100; + } else if (nameLower.includes(searchTermLowercase)) { + score += 40; + } + + // Score based on title + if (tile.title?.toLowerCase().includes(searchTermLowercase)) { + score += 40; + } + + // Score based on description + if (tile.description?.toLowerCase().includes(searchTermLowercase)) { + score += 10; + } + + return { tile, score }; + }); + + // Step 2: Filter tiles based on score, tags, and providers + const filteredTiles = scoredTiles.filter(({ tile, score }) => { + // Exclude tiles with no match + if (score <= 0) return false; + + // Filter by selected tags + const doesTagsMatch = searchTags.length ? checkThatArrayContainsAllTags(tile.tags, searchTags) : true; + + // Filter by selected providers + let doesProviderMatch = true; + if (selectedProviders.length) { + doesProviderMatch = + tile.provider === undefined + ? selectedProviders.includes('Community') + : selectedProviders.includes(tile.provider); + } + + return doesTagsMatch && doesProviderMatch; + }); + + // Step 3: Sort the filtered tiles by score in descending order + const tilesResult: ITile[] = filteredTiles.sort((a, b) => b.score - a.score).map(({ tile }) => tile); + + return tilesResult; }; diff --git a/packages/ui/src/components/Form/CustomAutoField.test.tsx b/packages/ui/src/components/Form/CustomAutoField.test.tsx index 2e150780..4667754c 100644 --- a/packages/ui/src/components/Form/CustomAutoField.test.tsx +++ b/packages/ui/src/components/Form/CustomAutoField.test.tsx @@ -11,45 +11,11 @@ jest.mock('uniforms', () => { import { BoolField, DateField, ListField, RadioField, TextField } from '@kaoto-next/uniforms-patternfly'; import { AutoFieldProps } from 'uniforms'; import { CustomAutoField } from './CustomAutoField'; -import { OneOfField } from './OneOf/OneOfField'; import { CustomNestField } from './customField/CustomNestField'; import { DisabledField } from './customField/DisabledField'; import { TypeaheadField } from './customField/TypeaheadField'; describe('CustomAutoField', () => { - it('should return `OneOfField` if `props.oneOf` is an array with a length > 0', () => { - const props: AutoFieldProps = { - oneOf: [{ type: 'string' }], - name: 'test', - }; - - const result = CustomAutoField(props); - - expect(result).toBe(OneOfField); - }); - - it('should NOT return `OneOfField` if `props.oneOf` is an empty array', () => { - const props: AutoFieldProps = { - oneOf: [], - name: 'test', - }; - - const result = CustomAutoField(props); - - expect(result).not.toBe(OneOfField); - }); - - it('should NOT return `OneOfField` if `props.oneOf` is not an array', () => { - const props: AutoFieldProps = { - oneOf: undefined, - name: 'test', - }; - - const result = CustomAutoField(props); - - expect(result).not.toBe(OneOfField); - }); - it('should return `RadioField` if `props.options` & `props.checkboxes` are defined and `props.fieldType` is not `Array`', () => { const props: AutoFieldProps = { options: [], diff --git a/packages/ui/src/components/Form/CustomAutoField.tsx b/packages/ui/src/components/Form/CustomAutoField.tsx index 67ca24a6..2e33e55c 100644 --- a/packages/ui/src/components/Form/CustomAutoField.tsx +++ b/packages/ui/src/components/Form/CustomAutoField.tsx @@ -1,16 +1,15 @@ import { BoolField, DateField, ListField, RadioField, TextField } from '@kaoto-next/uniforms-patternfly'; import { createAutoField } from 'uniforms'; import { getValue } from '../../utils'; -import { OneOfField } from './OneOf/OneOfField'; import { BeanReferenceField } from './bean/BeanReferenceField'; +import { CustomLongTextField } from './customField/CustomLongTextField'; import { CustomNestField } from './customField/CustomNestField'; import { DisabledField } from './customField/DisabledField'; +import { PasswordField } from './customField/PasswordField'; import { TypeaheadField } from './customField/TypeaheadField'; import { ExpressionAwareNestField } from './expression/ExpressionAwareNestField'; import { ExpressionField } from './expression/ExpressionField'; import { PropertiesField } from './properties/PropertiesField'; -import { CustomLongTextField } from './customField/CustomLongTextField'; -import { PasswordField } from './customField/PasswordField'; // Name of the properties that should load CustomLongTextField const CustomLongTextProps = ['Expression', 'Description', 'Query']; @@ -20,10 +19,6 @@ const CustomLongTextProps = ['Expression', 'Description', 'Query']; * In case a field is not supported, it will render a DisabledField */ export const CustomAutoField = createAutoField((props) => { - if (Array.isArray(props.oneOf) && props.oneOf.length > 0) { - return OneOfField; - } - if (props.options) { return props.checkboxes && props.fieldType !== Array ? RadioField : TypeaheadField; } diff --git a/packages/ui/src/components/Form/CustomAutoFields.test.tsx b/packages/ui/src/components/Form/CustomAutoFields.test.tsx index 0aa5f113..3627527a 100644 --- a/packages/ui/src/components/Form/CustomAutoFields.test.tsx +++ b/packages/ui/src/components/Form/CustomAutoFields.test.tsx @@ -37,4 +37,54 @@ describe('CustomAutoFields', () => { expect(wrapper?.asFragment()).toMatchSnapshot(); }); + + it('should render the "oneOf" selector when needed', () => { + const mockSchema: KaotoSchemaDefinition['schema'] = { + title: 'Test', + type: 'object', + additionalProperties: false, + properties: { + id: { + title: 'ID', + type: 'string', + }, + }, + oneOf: [ + { + title: 'One', + type: 'object', + properties: { + timerName: { + title: 'Timer Name', + description: 'The name of the timer', + type: 'string', + }, + }, + }, + { + title: 'Two', + type: 'object', + properties: { + pattern: { + title: 'Pattern', + description: + 'Allows you to specify a custom Date pattern to use for setting the time option using URI syntax.', + type: 'string', + }, + }, + }, + ], + }; + + const wrapper = render( + + + + + , + ); + + const oneOfToggle = wrapper.queryByTestId('-oneof-toggle'); + expect(oneOfToggle).toBeInTheDocument(); + }); }); diff --git a/packages/ui/src/components/Form/CustomAutoFields.tsx b/packages/ui/src/components/Form/CustomAutoFields.tsx index 3626033e..1d25b645 100644 --- a/packages/ui/src/components/Form/CustomAutoFields.tsx +++ b/packages/ui/src/components/Form/CustomAutoFields.tsx @@ -1,14 +1,14 @@ import { AutoField } from '@kaoto-next/uniforms-patternfly'; +import { Card, CardBody } from '@patternfly/react-core'; import { ComponentType, createElement, useContext } from 'react'; import { useForm } from 'uniforms'; -import { KaotoSchemaDefinition } from '../../models'; -import { Card, CardBody } from '@patternfly/react-core'; -import { getFieldGroups } from '../../utils'; -import { CatalogKind } from '../../models'; +import { CatalogKind, KaotoSchemaDefinition } from '../../models'; import { CanvasFormTabsContext, FilteredFieldContext } from '../../providers'; +import { getFieldGroups } from '../../utils'; import './CustomAutoFields.scss'; import { CustomExpandableSection } from './customField/CustomExpandableSection'; import { NoFieldFound } from './NoFieldFound'; +import { OneOfField } from './OneOf/OneOfField'; export type AutoFieldsProps = { autoField?: ComponentType<{ name: string }>; @@ -28,11 +28,7 @@ export function CustomAutoFields({ const rootField = schema.getField(''); const { filteredFieldText, isGroupExpanded } = useContext(FilteredFieldContext); const canvasFormTabsContext = useContext(CanvasFormTabsContext); - - /** Special handling for oneOf schemas */ - if (Array.isArray((rootField as KaotoSchemaDefinition['schema']).oneOf)) { - return createElement(element, props, [createElement(autoField!, { key: '', name: '' })]); - } + const oneOf = (rootField as KaotoSchemaDefinition['schema']).oneOf; const cleanQueryTerm = filteredFieldText.replace(/\s/g, '').toLowerCase(); const actualFields = (fields ?? schema.getSubfields()).filter( @@ -78,6 +74,9 @@ export function CustomAutoFields({ ))} + + {/* Special handling for oneOf schemas */} + {Array.isArray(oneOf) && } , ); } diff --git a/packages/ui/src/components/Form/OneOf/OneOfField.test.tsx b/packages/ui/src/components/Form/OneOf/OneOfField.test.tsx index 8333936e..8f8addb9 100644 --- a/packages/ui/src/components/Form/OneOf/OneOfField.test.tsx +++ b/packages/ui/src/components/Form/OneOf/OneOfField.test.tsx @@ -20,7 +20,7 @@ describe('OneOfField', () => { }; it('should render', () => { - const wrapper = render(, { + const wrapper = render(, { wrapper: (props) => ( {props.children} @@ -32,7 +32,7 @@ describe('OneOfField', () => { }); it('should render correctly', () => { - const wrapper = render(, { + const wrapper = render(, { wrapper: (props) => ( {props.children} @@ -45,7 +45,7 @@ describe('OneOfField', () => { }); it('should render the appropriate schema when given a matching model', () => { - const wrapper = render(, { + const wrapper = render(, { wrapper: (props) => ( {props.children} @@ -60,7 +60,7 @@ describe('OneOfField', () => { }); it('should render a new selected schema', async () => { - const wrapper = render(, { + const wrapper = render(, { wrapper: (props) => ( {props.children} diff --git a/packages/ui/src/components/Form/OneOf/OneOfField.tsx b/packages/ui/src/components/Form/OneOf/OneOfField.tsx index 95c0a6b1..d84b06c4 100644 --- a/packages/ui/src/components/Form/OneOf/OneOfField.tsx +++ b/packages/ui/src/components/Form/OneOf/OneOfField.tsx @@ -1,6 +1,6 @@ -import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { GuaranteedProps, connectField } from 'uniforms'; +import { HTMLFieldProps, connectField } from 'uniforms'; import { useAppliedSchema, useSchemaBridgeContext } from '../../../hooks'; import { KaotoSchemaDefinition } from '../../../models'; import { SchemaBridgeProvider } from '../../../providers/schema-bridge.provider'; @@ -10,9 +10,16 @@ import { CustomAutoForm, CustomAutoFormRef } from '../CustomAutoForm'; import { SchemaService } from '../schema.service'; import { OneOfSchemaList } from './OneOfSchemaList'; -interface OneOfComponentProps extends GuaranteedProps { - oneOf: KaotoSchemaDefinition['schema'][]; -} +type OneOfComponentProps = HTMLFieldProps< + object, + HTMLDivElement, + { + properties?: Record; + helperText?: string; + itemProps?: object; + oneOf: KaotoSchemaDefinition['schema'][]; + } +>; const applyDefinitionsToSchema = ( schema?: KaotoSchemaDefinition['schema'], @@ -27,7 +34,7 @@ const applyDefinitionsToSchema = ( }); }; -const OneOfComponent: FunctionComponent = ({ name: propsName, oneOf, onChange }) => { +export const OneOfField = connectField(({ name: propsName, oneOf, onChange }: OneOfComponentProps) => { const formRef = useRef(null); const divRef = useRef(null); @@ -106,6 +113,4 @@ const OneOfComponent: FunctionComponent = ({ name: propsNam )} ); -}; - -export const OneOfField = connectField(OneOfComponent as unknown as Parameters[0]); +}); diff --git a/packages/ui/src/components/Form/OneOf/OneOfSchemaList.scss b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.scss new file mode 100644 index 00000000..dc4022f1 --- /dev/null +++ b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.scss @@ -0,0 +1,3 @@ +.oneof-toggle { + margin-bottom: 1rem; +} diff --git a/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx index e0dd6ecd..f95e1f53 100644 --- a/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx +++ b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx @@ -1,7 +1,4 @@ import { - Card, - CardBody, - CardTitle, Dropdown, DropdownItem, DropdownList, @@ -15,6 +12,7 @@ import { FunctionComponent, PropsWithChildren, Ref, useCallback, useEffect, useS import { OneOfSchemas } from '../../../utils/get-oneof-schema-list'; import { isDefined } from '../../../utils/is-defined'; import { SchemaService } from '../schema.service'; +import './OneOfSchemaList.scss'; interface OneOfComponentProps extends PropsWithChildren { name: string; @@ -50,6 +48,7 @@ export const OneOfSchemaList: FunctionComponent = ({ = ({ } return ( - - - - - {oneOfSchemas.map((schemaDef) => { - return ( - - {schemaDef.name} - - ); - })} - - - + <> + + + {oneOfSchemas.map((schemaDef) => { + return ( + + {schemaDef.name} + + ); + })} + + - {children} - + {children} + ); }; diff --git a/packages/ui/src/components/Form/OneOf/__snapshots__/OneOfField.test.tsx.snap b/packages/ui/src/components/Form/OneOf/__snapshots__/OneOfField.test.tsx.snap index 8ddab0b7..556199ff 100644 --- a/packages/ui/src/components/Form/OneOf/__snapshots__/OneOfField.test.tsx.snap +++ b/packages/ui/src/components/Form/OneOf/__snapshots__/OneOfField.test.tsx.snap @@ -7,74 +7,55 @@ exports[`OneOfField should render 1`] = ` data-testid="base-form" novalidate="" > - `; diff --git a/packages/ui/src/providers/action-confirmation-modal.provider.tsx b/packages/ui/src/providers/action-confirmation-modal.provider.tsx index 598a2b7f..2dcd5ad2 100644 --- a/packages/ui/src/providers/action-confirmation-modal.provider.tsx +++ b/packages/ui/src/providers/action-confirmation-modal.provider.tsx @@ -1,10 +1,9 @@ -import { Button, ButtonVariant, Modal, ModalVariant } from '@patternfly/react-core'; +import { Button, ButtonVariant, Modal, ModalVariant, Split, SplitItem } from '@patternfly/react-core'; import { FunctionComponent, PropsWithChildren, createContext, useCallback, useMemo, useRef, useState } from 'react'; -export const ACTION_INDEX_CANCEL = 0; -export const ACTION_INDEX_CONFIRM = 1; +export const ACTION_ID_CANCEL = 'cancel'; +export const ACTION_ID_CONFIRM = 'confirm'; export interface ActionConfirmationButtonOption { - index: number; buttonText: string; variant: ButtonVariant; isDanger?: boolean; @@ -14,9 +13,9 @@ interface ActionConfirmationModalContextValue { actionConfirmation: (options: { title?: string; text?: string; - buttonOptions?: ActionConfirmationButtonOption[]; + buttonOptions?: Record; additionalModalText?: string; - }) => Promise; + }) => Promise; } export const ActionConfirmationModalContext = createContext(undefined); @@ -29,20 +28,20 @@ export const ActionConfirmationModalContextProvider: FunctionComponent([]); - const [buttonOptions, setButtonOptions] = useState([]); + const [buttonOptions, setButtonOptions] = useState>({}); const actionConfirmationRef = useRef<{ - resolve: (index: number) => void; + resolve: (actionId: string) => void; reject: (error: unknown) => unknown; }>(); const handleCloseModal = useCallback(() => { setIsModalOpen(false); - actionConfirmationRef.current?.resolve(ACTION_INDEX_CANCEL); + actionConfirmationRef.current?.resolve(ACTION_ID_CANCEL); }, []); - const handleAction = useCallback((index: number) => { + const handleAction = useCallback((actionId: string) => { setIsModalOpen(false); - actionConfirmationRef.current?.resolve(index); + actionConfirmationRef.current?.resolve(actionId); }, []); const actionConfirmation = useCallback( @@ -51,10 +50,10 @@ export const ActionConfirmationModalContextProvider: FunctionComponent; } = {}, ) => { - const actionConfirmationPromise = new Promise((resolve, reject) => { + const actionConfirmationPromise = new Promise((resolve, reject) => { /** Set both resolve and reject functions to be used once the user choose an action */ actionConfirmationRef.current = { resolve, reject }; }); @@ -67,7 +66,7 @@ export const ActionConfirmationModalContextProvider: FunctionComponent + {...Object.entries(buttonOptions).map(([actionId, option]) => ( + + + + ))} + + + + + ); + return ( {props.children} @@ -94,27 +121,7 @@ export const ActionConfirmationModalContextProvider: FunctionComponent ( - - )), - , - ]} + footer={footer} > {textParagraphs.length === 1 ? textParagraphs[0] diff --git a/packages/ui/src/providers/action-confirmaton-modal.provider.test.tsx b/packages/ui/src/providers/action-confirmaton-modal.provider.test.tsx index b2b4ed64..34974d2c 100644 --- a/packages/ui/src/providers/action-confirmaton-modal.provider.test.tsx +++ b/packages/ui/src/providers/action-confirmaton-modal.provider.test.tsx @@ -1,13 +1,15 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { FunctionComponent, useContext } from 'react'; import { + ACTION_ID_CANCEL, + ACTION_ID_CONFIRM, ActionConfirmationButtonOption, ActionConfirmationModalContext, ActionConfirmationModalContextProvider, } from './action-confirmation-modal.provider'; import { ButtonVariant } from '@patternfly/react-core'; -let actionConfirmationResult: number | undefined; +let actionConfirmationResult: string | undefined; describe('ActionConfirmationModalProvider', () => { beforeEach(() => { @@ -28,7 +30,7 @@ describe('ActionConfirmationModalProvider', () => { fireEvent.click(confirmButton); // Wait for actionConfirmation promise to resolve - await waitFor(() => expect(actionConfirmationResult).toEqual(1)); + await waitFor(() => expect(actionConfirmationResult).toEqual(ACTION_ID_CONFIRM)); }); it('calls actionConfirmation with false when Cancel button is clicked', async () => { @@ -45,7 +47,7 @@ describe('ActionConfirmationModalProvider', () => { fireEvent.click(cancelButton); // Wait for actionConfirmation promise to resolve - await waitFor(() => expect(actionConfirmationResult).toEqual(0)); + await waitFor(() => expect(actionConfirmationResult).toEqual(ACTION_ID_CANCEL)); }); it('should allow consumers to update the modal title and text', () => { @@ -74,19 +76,17 @@ describe('ActionConfirmationModalProvider', () => { title="Custom title" text="Custom text" additionalModalText="Additional text is added in the modal description" - buttonOptions={[ - { - index: 1, + buttonOptions={{ + 'del-step-and-file': { buttonText: 'Delete the step, and delete the file(s)', variant: ButtonVariant.danger, }, - { - index: 2, + 'del-step-only': { buttonText: 'Delete the step, but keep the file(s)', variant: ButtonVariant.secondary, isDanger: true, }, - ]} + }} /> , ); @@ -98,12 +98,12 @@ describe('ActionConfirmationModalProvider', () => { const modalDialog = wrapper.getByRole('dialog'); expect(modalDialog.textContent).toContain('Additional text is added in the modal description'); act(() => { - const cancelButton = wrapper.getByTestId('action-confirmation-modal-btn-0'); + const cancelButton = wrapper.getByTestId('action-confirmation-modal-btn-cancel'); expect(cancelButton.textContent).toEqual('Cancel'); fireEvent.click(cancelButton); }); await waitFor(() => { - expect(actionConfirmationResult).toEqual(0); + expect(actionConfirmationResult).toEqual(ACTION_ID_CANCEL); }); act(() => { @@ -111,12 +111,12 @@ describe('ActionConfirmationModalProvider', () => { fireEvent.click(deleteButton); }); act(() => { - const deleteStepAndFileButton = wrapper.getByTestId('action-confirmation-modal-btn-1'); + const deleteStepAndFileButton = wrapper.getByTestId('action-confirmation-modal-btn-del-step-and-file'); expect(deleteStepAndFileButton.textContent).toEqual('Delete the step, and delete the file(s)'); fireEvent.click(deleteStepAndFileButton); }); await waitFor(() => { - expect(actionConfirmationResult).toEqual(1); + expect(actionConfirmationResult).toEqual('del-step-and-file'); }); act(() => { @@ -124,12 +124,12 @@ describe('ActionConfirmationModalProvider', () => { fireEvent.click(deleteButton); }); act(() => { - const deleteStepOnlyButton = wrapper.getByTestId('action-confirmation-modal-btn-2'); + const deleteStepOnlyButton = wrapper.getByTestId('action-confirmation-modal-btn-del-step-only'); expect(deleteStepOnlyButton.textContent).toEqual('Delete the step, but keep the file(s)'); fireEvent.click(deleteStepOnlyButton); }); await waitFor(() => { - expect(actionConfirmationResult).toEqual(2); + expect(actionConfirmationResult).toEqual('del-step-only'); }); }); }); @@ -138,7 +138,7 @@ interface TestComponentProps { title: string; text: string; additionalModalText?: string; - buttonOptions?: ActionConfirmationButtonOption[]; + buttonOptions?: Record; } const TestComponent: FunctionComponent = (props) => { diff --git a/yarn.lock b/yarn.lock index 15e80c41..0cf8ad54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2770,19 +2770,19 @@ __metadata: languageName: node linkType: hard -"@kaoto-next/uniforms-patternfly@npm:^0.7.1": - version: 0.7.1 - resolution: "@kaoto-next/uniforms-patternfly@npm:0.7.1" +"@kaoto-next/uniforms-patternfly@npm:^0.7.10": + version: 0.7.10 + resolution: "@kaoto-next/uniforms-patternfly@npm:0.7.10" dependencies: invariant: "npm:^2.2.4" lodash.clonedeep: "npm:^4.5.0" - uniforms: "npm:4.0.0-alpha.5" peerDependencies: "@patternfly/react-core": ^5.0.0 "@patternfly/react-icons": ^5.0.0 react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10/7b3fb24da2b383dcbd7c5fcff96570f213a1e380018d297b655a9ad4853cecfe9db6d6e16c333b6b0a6b4d76dd59deb1c76108a6e7264eaba1a4a9c3610d9550 + uniforms: 4.0.0-alpha.5 + checksum: 10/b6035b21974272c9d207475224326a839fe6aeec44a6f31eacbeb3728f986a2e0e2a1e968703da30d3da075b25503c77e1b381567595b76db82e00bee9fc8b69 languageName: node linkType: hard @@ -2859,7 +2859,7 @@ __metadata: "@babel/preset-typescript": "npm:^7.21.5" "@dnd-kit/core": "npm:^6.1.0" "@eslint/js": "npm:^9.10.0" - "@kaoto-next/uniforms-patternfly": "npm:^0.7.1" + "@kaoto-next/uniforms-patternfly": "npm:^0.7.10" "@kaoto/camel-catalog": "workspace:*" "@kaoto/xml-schema-ts": "workspace:*" "@kie-tools-core/editor": "npm:0.32.0"