diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala index fa244d926..8a44119c7 100644 --- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala +++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala @@ -18,43 +18,75 @@ package za.co.absa.enceladus.dao.rest import org.apache.commons.lang.exception.ExceptionUtils import org.slf4j.LoggerFactory import org.springframework.web.client.{ResourceAccessException, RestClientException} +import za.co.absa.enceladus.dao.rest.CrossHostApiCaller.logger import za.co.absa.enceladus.dao.{DaoException, RetryableException} +import scala.annotation.tailrec import scala.util.{Failure, Random, Try} -protected object CrossHostApiCaller { +object CrossHostApiCaller { - def apply(apiBaseUrls: List[String]): CrossHostApiCaller = { - new CrossHostApiCaller(apiBaseUrls, Random.nextInt(apiBaseUrls.size)) + private val logger = LoggerFactory.getLogger(classOf[CrossHostApiCaller]) + + final val DefaultUrlsRetryCount: Int = 0 + + private def createInstance(apiBaseUrls: Seq[String], urlsRetryCount: Int, startWith: Option[Int]): CrossHostApiCaller = { + val maxTryCount: Int = (if (urlsRetryCount < 0) { + logger.warn(s"Urls retry count cannot be negative ($urlsRetryCount). Using default number of retries instead ($DefaultUrlsRetryCount).") //scalastyle:ignore maxLineLength + DefaultUrlsRetryCount + } else { + urlsRetryCount + }) + 1 + val currentHostIndex = startWith.getOrElse(Random.nextInt(Math.max(apiBaseUrls.size, 1))) + new CrossHostApiCaller(apiBaseUrls.toVector, maxTryCount, currentHostIndex) } + def apply(apiBaseUrls: Seq[String], urlsRetryCount: Int = DefaultUrlsRetryCount, startWith: Option[Int] = None): CrossHostApiCaller = { + createInstance(apiBaseUrls, urlsRetryCount, startWith) + } } -protected class CrossHostApiCaller(apiBaseUrls: List[String], var currentHostIndex: Int) extends ApiCaller { - private val logger = LoggerFactory.getLogger(this.getClass) +protected class CrossHostApiCaller private(apiBaseUrls: Vector[String], maxTryCount: Int, private var currentHostIndex: Int) + extends ApiCaller { + + def baseUrlsCount: Int = apiBaseUrls.size + + def currentBaseUrl: String = apiBaseUrls(currentHostIndex) + + def nextBaseUrl(): String = { + currentHostIndex = (currentHostIndex + 1) % baseUrlsCount + currentBaseUrl + } - private val maxAttempts = apiBaseUrls.size - 1 def call[T](fn: String => T): T = { + def logFailure(error: Throwable, url: String, attemptNumber: Int, nextUrl: Option[String]): Unit = { + val rootCause = ExceptionUtils.getRootCauseMessage(error) + val switching = nextUrl.map(s => s", switching host to $s").getOrElse("") + logger.warn(s"Request failed on host $url (attempt $attemptNumber of $maxTryCount)$switching - $rootCause") + } - def attempt(index: Int, attemptCount: Int = 0): Try[T] = { - currentHostIndex = index - val currentBaseUrl = apiBaseUrls(index) - Try { - fn(currentBaseUrl) + @tailrec + def attempt(url: String, attemptNumber: Int, urlsTried: Int): Try[T] = { + val result =Try { + fn(url) }.recoverWith { case e @ (_: ResourceAccessException | _: RestClientException) => Failure(DaoException("Server non-responsive", e)) - }.recoverWith { - case e: RetryableException if attemptCount < maxAttempts => - val nextIndex = (index + 1) % apiBaseUrls.size - val nextBaseUrl = apiBaseUrls(nextIndex) - val rootCause = ExceptionUtils.getRootCauseMessage(e) - logger.warn(s"Request failed on host $currentBaseUrl, switching host to $nextBaseUrl - $rootCause") - attempt(nextIndex, attemptCount + 1) + } + //using match instead of recoverWith to make the function @tailrec + result match { + case Failure(e: RetryableException) if attemptNumber < maxTryCount => + logFailure(e, url, attemptNumber, None) + attempt(url, attemptNumber + 1, urlsTried) + case Failure(e: RetryableException) if urlsTried < baseUrlsCount => + val nextUrl = nextBaseUrl() + logFailure(e, url, attemptNumber, Option(nextUrl)) + attempt(nextUrl, 1, urlsTried + 1) + case _ => result } } - attempt(currentHostIndex).get + attempt(currentBaseUrl,1, 1).get } } diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala index a4787d203..719af163f 100644 --- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala +++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala @@ -29,7 +29,7 @@ object MenasConnectionStringParser { .replaceAll("/$", "") .replaceAll("/api$", "") ) - .toSet + .distinct .toList } diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala index 51bd04ecf..5edde52d5 100644 --- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala +++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala @@ -16,16 +16,33 @@ package za.co.absa.enceladus.dao.rest import za.co.absa.enceladus.dao.auth.MenasCredentials +import za.co.absa.enceladus.dao.rest.RestDaoFactory.AvailabilitySetup.{Fallback, AvailabilitySetup, RoundRobin} object RestDaoFactory { + object AvailabilitySetup extends Enumeration { + final type AvailabilitySetup = Value + + final val RoundRobin = Value("roundrobin") + final val Fallback = Value("fallback") + } + + final val DefaultAvailabilitySetup: AvailabilitySetup = RoundRobin + private val restTemplate = RestTemplateSingleton.instance - def getInstance(authCredentials: MenasCredentials, apiBaseUrls: List[String]): MenasRestDAO = { - val apiCaller = CrossHostApiCaller(apiBaseUrls) + def getInstance(authCredentials: MenasCredentials, + apiBaseUrls: List[String], + urlsRetryCount: Option[Int] = None, + menasSetup: AvailabilitySetup = DefaultAvailabilitySetup): MenasRestDAO = { + val startsWith = if (menasSetup == Fallback) { + Option(0) + } else { + None + } + val apiCaller = CrossHostApiCaller(apiBaseUrls, urlsRetryCount.getOrElse(CrossHostApiCaller.DefaultUrlsRetryCount), startsWith) val authClient = AuthClient(authCredentials, apiCaller) val restClient = new RestClient(authClient, restTemplate) new MenasRestDAO(apiCaller, restClient) } - } diff --git a/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala index 67301e22b..29ae6fc0b 100644 --- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala +++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala @@ -17,6 +17,7 @@ package za.co.absa.enceladus.dao.rest import org.mockito.Mockito import org.springframework.web.client.ResourceAccessException +import za.co.absa.enceladus.dao.rest.CrossHostApiCaller.DefaultUrlsRetryCount import za.co.absa.enceladus.dao.{DaoException, UnauthorizedException} class CrossHostApiCallerSuite extends BaseTestSuite { @@ -27,12 +28,23 @@ class CrossHostApiCallerSuite extends BaseTestSuite { Mockito.reset(restClient) } + "CrossHostApiCaller" should { + "cycle through urls" in { + val crossHostApiCaller = CrossHostApiCaller(Vector("a", "b", "c", "d"), DefaultUrlsRetryCount, startWith = Some(1)) + crossHostApiCaller.nextBaseUrl() should be("c") + crossHostApiCaller.nextBaseUrl() should be("d") + crossHostApiCaller.nextBaseUrl() should be("a") + crossHostApiCaller.nextBaseUrl() should be("b") + crossHostApiCaller.nextBaseUrl() should be("c") + } + } + "CrossHostApiCaller::call" should { "return the result of the first successful call" when { "there are no failures" in { Mockito.when(restClient.sendGet[String]("a")).thenReturn("success") - val result = new CrossHostApiCaller(List("a", "b", "c"), 0).call { str => + val result = CrossHostApiCaller(Vector("a", "b", "c"), DefaultUrlsRetryCount, startWith = Some(0)).call { str => restClient.sendGet[String](str) } @@ -42,16 +54,33 @@ class CrossHostApiCallerSuite extends BaseTestSuite { "only some calls fail with a retryable exception" in { Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A")) - Mockito.when(restClient.sendGet[String]("b")).thenReturn("success") + Mockito.when(restClient.sendGet[String]("b")) + .thenThrow(DaoException("Something went wrong B")) + .thenReturn("success") - val result = new CrossHostApiCaller(List("a", "b", "c"), 0).call { str => + val result = CrossHostApiCaller(Vector("a", "b", "c"), 2, Some(0)).call { str => + restClient.sendGet[String](str) + } + + result should be("success") + Mockito.verify(restClient, Mockito.times(3)).sendGet[String]("a") + Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("b") + Mockito.verify(restClient, Mockito.never()).sendGet[String]("c") + } + + "despite retry count is negative" in { + Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A")) + Mockito.when(restClient.sendGet[String]("b")).thenThrow(DaoException("Something went wrong B")) + Mockito.when(restClient.sendGet[String]("c")).thenReturn("success") + + val result = CrossHostApiCaller(Vector("a", "b", "c"), -2, Some(0)).call { str => restClient.sendGet[String](str) } result should be("success") Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("a") Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("b") - Mockito.verify(restClient, Mockito.never()).sendGet[String]("c") + Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("c") } } @@ -62,7 +91,7 @@ class CrossHostApiCallerSuite extends BaseTestSuite { Mockito.when(restClient.sendGet[String]("c")).thenThrow(DaoException("Something went wrong C")) val exception = intercept[DaoException] { - new CrossHostApiCaller(List("a", "b", "c"), 0).call { str => + CrossHostApiCaller(Vector("a", "b", "c"), 0, Some(0)).call { str => restClient.sendGet[String](str) } } @@ -73,12 +102,29 @@ class CrossHostApiCallerSuite extends BaseTestSuite { Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("c") } + "all calls fail with a retryable exception over multiple attempts" in { + Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A")) + Mockito.when(restClient.sendGet[String]("b")).thenThrow(DaoException("Something went wrong B")) + Mockito.when(restClient.sendGet[String]("c")).thenThrow(DaoException("Something went wrong C")) + + val exception = intercept[DaoException] { + CrossHostApiCaller(Vector("a", "b", "c"), 1, Some(0)).call { str => + restClient.sendGet[String](str) + } + } + + exception.getMessage should be("Something went wrong C") + Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("a") + Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("b") + Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("c") + } + "any call fails with a non-retryable exception" in { Mockito.when(restClient.sendGet[String]("a")).thenThrow(new ResourceAccessException("Something went wrong A")) Mockito.when(restClient.sendGet[String]("b")).thenThrow(UnauthorizedException("Wrong credentials")) val exception = intercept[UnauthorizedException] { - new CrossHostApiCaller(List("a", "b", "c"), 0).call { str => + CrossHostApiCaller(Vector("a", "b", "c"), 0, Some(0)).call { str => restClient.sendGet[String](str) } } @@ -89,6 +135,17 @@ class CrossHostApiCallerSuite extends BaseTestSuite { Mockito.verify(restClient, Mockito.never()).sendGet[String]("c") } } + + "fail on not having Urls" when { + "none are provided" in { + val exception = intercept[IndexOutOfBoundsException] { + CrossHostApiCaller(Vector()).call { str => + restClient.sendGet[String](str) + } + } + exception.getMessage should be ("0") + } + } } } diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala index 6c41f7bec..fce399bff 100644 --- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala +++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala @@ -202,6 +202,24 @@ class MenasConnectionStringParserSuite extends BaseTestSuite { exception.getMessage should be("Malformed Menas connection string") } } - } + "keep the order of urls" when { + val expectedList = List( + "http://host1:8080/menas", + "http://host2:9000/menas", + "http://host3:8080/menas", + "http://host4:9000/menas", + "http://localhost:8080/menas", + "http://localhost:8090/menas" + ) + "they are full fledged urls separated by semicolon" in { + val result = MenasConnectionStringParser.parse("http://host1:8080/menas;http://host2:9000/menas;http://host3:8080/menas;http://host4:9000/menas;http://localhost:8080/menas;http://localhost:8090/menas") + result should be(expectedList) + } + "varied hosts separated by comma within one url" in { + val result = MenasConnectionStringParser.parse("http://host1:8080,host2:9000,host3:8080,host4:9000,localhost:8080,localhost:8090/menas") + result should be(expectedList) + } + } + } } diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala index 5d546150e..e1f2042d2 100644 --- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala +++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala @@ -15,12 +15,15 @@ package za.co.absa.enceladus.dao.rest +import org.mockito.MockitoSugar.withObjectMocked +import org.mockito.{ArgumentMatchersSugar, Mockito} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import za.co.absa.enceladus.dao.UnauthorizedException import za.co.absa.enceladus.dao.auth.{InvalidMenasCredentials, MenasKerberosCredentials, MenasPlainCredentials} +import za.co.absa.enceladus.dao.rest.RestDaoFactory.AvailabilitySetup -class RestDaoFactorySuite extends AnyWordSpec with Matchers { +class RestDaoFactorySuite extends AnyWordSpec with Matchers with ArgumentMatchersSugar { private val menasApiBaseUrls = List("http://localhost:8080/menas/api") @@ -47,6 +50,44 @@ class RestDaoFactorySuite extends AnyWordSpec with Matchers { exception.getMessage should be("No Menas credentials provided") } } + "properly adjusts the starting URL based on the setup type " when { + val fooCrossHostApiCaller = CrossHostApiCaller(Seq.empty) + val plainCredentials = MenasPlainCredentials("user", "changeme") + "when it's round-robin" in { + withObjectMocked[CrossHostApiCaller.type] { + Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller) + val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls) + getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient]) + Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply( + menasApiBaseUrls, + CrossHostApiCaller.DefaultUrlsRetryCount, + None) + } + } + "when it's fallback" in { + withObjectMocked[CrossHostApiCaller.type] { + Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller) + val plainCredentials = MenasPlainCredentials("user", "changeme") + val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls, None, AvailabilitySetup.Fallback) + getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient]) + Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply( + menasApiBaseUrls, + CrossHostApiCaller.DefaultUrlsRetryCount, + Option(0)) + } + } + "when the setup type is not specified" in { + withObjectMocked[CrossHostApiCaller.type] { + Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller) + val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls) + getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient]) + Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply( + menasApiBaseUrls, + CrossHostApiCaller.DefaultUrlsRetryCount, + None) + } + } + } } private def getAuthClient(restClient: RestClient): AuthClient = { @@ -54,5 +95,5 @@ class RestDaoFactorySuite extends AnyWordSpec with Matchers { field.setAccessible(true) field.get(restClient).asInstanceOf[AuthClient] } - } + diff --git a/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala b/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala index 5d8a0379a..793070d47 100644 --- a/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala +++ b/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala @@ -56,7 +56,7 @@ class DataFrameFilterSuite extends AnyFunSuite { assert(filterExpr2.semanticEquals(expected)) } - test("Three filters joined with an and condidion") { + test("Three filters joined with an and condition") { val f1 = DiffersFilter("column1", "v1") val f2 = DiffersFilter("column2", "v2") val f3 = DiffersFilter("column3", "v3") diff --git a/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js b/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js index 53a97f6a2..9d640688a 100644 --- a/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js +++ b/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js @@ -29,6 +29,9 @@ class ConformanceRuleDialog { this._ruleForms = new ConformanceRuleFormRepository(this); this._rules = this._ruleForms.all; + this._schemaService = new SchemaService(this.model, eventBus); + this.filterEdit = new FilterEdit(sap.ui.getCore(), "MappingConformanceRule--", this._schemaService); + this.model.setProperty("/rules", this.rules); this.model.setProperty("/dataTypes", this._ruleForms.byType("CastingConformanceRule").dataTypes); } @@ -105,6 +108,13 @@ class ConformanceRuleDialog { let newRule = $.extend(true, {}, this.model.getProperty("/newRule")); this.beforeSubmitChanges(newRule); this.resetRuleValidation(); + + newRule.hasValidFilter = true; // default for non-MappingConformanceRules + if (newRule._t === "MappingConformanceRule") { + const validFilter = this.filterEdit.validateFilterData(); + newRule.hasValidFilter = validFilter; + } + if (this.ruleForms.byType(newRule._t).isValid(newRule, this.controller._transitiveSchemas, currentDataset.conformance)) { if (this.model.getProperty("/newRule/isEdit")) { this.updateRule(currentDataset, newRule); @@ -166,6 +176,10 @@ class ConformanceRuleDialog { const model = new sap.ui.model.json.JSONModel(mappingTableSchema); model.setSizeLimit(5000); this._dialog.setModel(model, "mappingTableSchema"); + + const colNames = FilterEdit.extractFieldNamesInDepth(mappingTableSchema.fields); + const columnNamesModel = new sap.ui.model.json.JSONModel({columnNames: colNames}); + this._dialog.setModel(columnNamesModel, "suggestedColumns"); } }); const datasetSchema = this._dialog.getModel("schema").oData; @@ -285,6 +299,8 @@ class ConformanceRuleDialog { } if (currentRule._t === "MappingConformanceRule") { + const filterModel = new sap.ui.model.json.JSONModel(); + if (!currentRule.isEdit) { newRule.newJoinConditions = []; newRule.newOutputColumns = []; @@ -310,9 +326,16 @@ class ConformanceRuleDialog { newRule.newJoinConditions = aNewJoinConditions; newRule.newOutputColumns = aNewOutputColumns; + + const filters = [FilterTreeUtils.addNiceNamesToFilterData(newRule.mappingTableFilter)]; + filterModel.setProperty("/editingFilters", filters); + } this.mappingTableService.getAllVersions(newRule.mappingTable, sap.ui.getCore().byId("mappingTableVersionSelect")); this.selectMappingTableVersion(newRule.mappingTable, newRule.mappingTableVersion); + + this._dialog.setModel(filterModel, "filterEdit"); // filter editing has its own named model ("filterEdit") + this.filterEdit.bindFilterEditControls(this._dialog); } if (!newRule.isEdit && newRule.order === undefined) { @@ -339,6 +362,19 @@ class ConformanceRuleDialog { }); delete newRule.newOutputColumns; delete newRule.joinConditions; + + const updatedFilters = this._dialog.getModel("filterEdit").getProperty("/editingFilters"); + if (updatedFilters) { + if (updatedFilters.length > 1) { + console.error(`Multiple root filters found, aborting: ${JSON.stringify(updatedFilters)}`); + sap.m.MessageToast.show("Invalid filter update found (multiple roots), no filter update done"); + } else { + const cleanedFilter = FilterTreeUtils.removeDeletedNodesFromFilterData(updatedFilters[0]); + const updatedFilter = FilterTreeUtils.removeNiceNamesFromFilterData(cleanedFilter); + const schemaFilledFilter = this.filterEdit.applyValueTypesFromSchema(updatedFilter); + newRule.mappingTableFilter = schemaFilledFilter + } + } } } @@ -372,6 +408,11 @@ class ConformanceRuleDialog { resetRuleValidation() { const newRule = this.model.getProperty("/newRule"); this.ruleForms.byType(newRule._t).reset(); + + if (newRule._t === "MappingConformanceRule") { + this.filterEdit.resetFilterValidation(); + } + } } diff --git a/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js b/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js index 891ce97b6..b27121f4a 100644 --- a/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js +++ b/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js @@ -162,20 +162,7 @@ class CastingConformanceRuleForm extends ConformanceRuleForm { } get dataTypes() { - return [ - {type: "boolean"}, - {type: "byte"}, - {type: "short"}, - {type: "integer"}, - {type: "long"}, - {type: "float"}, - {type: "double"}, - {type: "decimal(38,18)"}, - {type: "string"}, - {type: "date"}, - {type: "timestamp"}, - {type: "binary"} - ] + return DataTypeUtils.dataTypesAsTypes; } get outputDataTypeControl() { @@ -382,7 +369,8 @@ class MappingConformanceRuleForm extends ConformanceRuleForm { isCorrectlyConfigured(rule) { return this.hasValidInputColumn(rule.targetAttribute) & this.hasValidOutputColumns(rule) - & this.hasValidJoinConditions(rule.newJoinConditions); + & this.hasValidJoinConditions(rule.newJoinConditions) + & rule.hasValidFilter; } hasValidJoinConditions(fieldValue = []) { diff --git a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml index 33fd46501..8ce064040 100644 --- a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml +++ b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml @@ -17,4 +17,6 @@ +