diff --git a/build.sbt b/build.sbt index cb026edc15a..388e6e75faa 100644 --- a/build.sbt +++ b/build.sbt @@ -1,15 +1,15 @@ import com.typesafe.sbt.packager.SettingsHelper import com.typesafe.sbt.packager.docker.DockerPlugin.autoImport.dockerUsername import pl.project13.scala.sbt.JmhPlugin -import pl.project13.scala.sbt.JmhPlugin._ -import sbt.Keys._ -import sbt._ +import pl.project13.scala.sbt.JmhPlugin.* +import sbt.* +import sbt.Keys.* import sbtassembly.AssemblyPlugin.autoImport.assembly import sbtassembly.MergeStrategy -import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ +import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations.* import scala.language.postfixOps -import scala.sys.process._ +import scala.sys.process.* import scala.util.Try import scala.xml.Elem import scala.xml.transform.{RewriteRule, RuleTransformer} @@ -288,7 +288,7 @@ val flinkCommonsTextV = "1.10.0" val flinkCommonsIOV = "2.15.1" val avroV = "1.11.4" //we should use max(version used by confluent, version acceptable by flink), https://docs.confluent.io/platform/current/installation/versions-interoperability.html - confluent version reference -val kafkaV = "3.6.2" +val kafkaV = "3.8.1" //TODO: Spring 5.3 has some problem with handling our PrimitiveOrWrappersPropertyAccessor val springV = "5.2.23.RELEASE" val scalaTestV = "3.2.18" @@ -321,7 +321,7 @@ val dropWizardV = "5.0.0-rc15" val scalaCollectionsCompatV = "2.12.0" val testContainersScalaV = "0.41.4" val testContainersJavaV = "1.20.1" -val nettyV = "4.1.113.Final" +val nettyV = "4.1.115.Final" val nettyReactiveStreamsV = "2.0.12" val akkaV = "2.6.20" @@ -330,8 +330,8 @@ val akkaManagementV = "1.1.4" val akkaHttpCirceV = "1.39.2" val slickV = "3.4.1" // 3.5 drops Scala 2.12 val slickPgV = "0.21.1" // 0.22.2 uses Slick 3.5 -val hikariCpV = "5.1.0" -val hsqldbV = "2.7.3" +val hikariCpV = "6.2.1" +val hsqldbV = "2.7.4" val postgresV = "42.7.4" // Flway 10 requires Java 17 val flywayV = "9.22.3" @@ -354,6 +354,7 @@ val findBugsV = "3.0.2" val enumeratumV = "1.7.4" val ujsonV = "4.0.1" val igniteV = "2.10.0" +val retryV = "0.3.6" // depending on scala version one of this jar lays in Flink lib dir def flinkLibScalaDeps(scalaVersion: String, configurations: Option[String] = None) = forScalaVersion(scalaVersion) { @@ -617,7 +618,7 @@ lazy val flinkDeploymentManager = (project in flink("management")) ExclusionRule("com.esotericsoftware", "kryo-shaded"), ), "org.apache.flink" % "flink-statebackend-rocksdb" % flinkV % flinkScope, - "com.softwaremill.retry" %% "retry" % "0.3.6", + "com.softwaremill.retry" %% "retry" % retryV, "org.wiremock" % "wiremock" % wireMockV % Test, "org.scalatestplus" %% "mockito-5-10" % scalaTestPlusV % Test, ) ++ flinkLibScalaDeps(scalaVersion.value, Some(flinkScope)) @@ -810,7 +811,16 @@ lazy val flinkExecutor = (project in flink("executor")) ) }.toList, ) - .dependsOn(flinkComponentsUtils, scenarioCompiler, flinkExtensionsApi, flinkTestUtils % Test) + .dependsOn( + flinkComponentsUtils, + flinkExtensionsApi, + scenarioCompiler, + // Various components uses one of library in stack: sttp -> async-http-client -> netty + // Different versions of netty which is on the bottom of this stack causes NoClassDefFoundError. + // To overcome this problem and reduce size of model jar bundle, we add http utils as a compile time dependency. + httpUtils, + flinkTestUtils % Test + ) lazy val scenarioCompiler = (project in file("scenario-compiler")) .settings(commonSettings) @@ -1019,11 +1029,12 @@ lazy val kafkaTestUtils = (project in utils("kafka-test-utils")) name := "nussknacker-kafka-test-utils", libraryDependencies ++= { Seq( - "org.apache.kafka" %% "kafka" % kafkaV excludeAll ( + "org.apache.kafka" %% "kafka" % kafkaV excludeAll ( ExclusionRule("log4j", "log4j"), ExclusionRule("org.slf4j", "slf4j-log4j12") ), - "org.slf4j" % "log4j-over-slf4j" % slf4jV + "org.slf4j" % "log4j-over-slf4j" % slf4jV, + "com.softwaremill.retry" %% "retry" % retryV ) } ) @@ -1351,7 +1362,15 @@ lazy val liteEngineRuntime = (project in lite("runtime")) ) }, ) - .dependsOn(liteComponentsApi, scenarioCompiler, testUtils % Test) + .dependsOn( + liteComponentsApi, + scenarioCompiler, + // Various components uses one of library in stack: sttp -> async-http-client -> netty + // Different versions of netty which is on the bottom of this stack causes NoClassDefFoundError. + // To overcome this problem and reduce size of model jar bundle, we add http utils as a compile time dependency. + httpUtils, + testUtils % Test + ) lazy val liteEngineKafkaIntegrationTest: Project = (project in lite("integration-test")) .configs(IntegrationTest) @@ -1499,11 +1518,12 @@ lazy val liteK8sDeploymentManager = (project in lite("k8sDeploymentManager")) libraryDependencies ++= { Seq( // From version 4.0.0 onwards, skuber uses pekko instead of akka, so we need to migrate to pekko first - "io.github.hagay3" %% "skuber" % "3.2" exclude ("commons-logging", "commons-logging"), - "com.github.julien-truffaut" %% "monocle-core" % monocleV, - "com.github.julien-truffaut" %% "monocle-macro" % monocleV, - "com.typesafe.akka" %% "akka-slf4j" % akkaV % Test, - "org.wiremock" % "wiremock" % wireMockV % Test, + "io.github.hagay3" %% "skuber" % "3.2" exclude ("commons-logging", "commons-logging"), + "com.github.julien-truffaut" %% "monocle-core" % monocleV, + "com.github.julien-truffaut" %% "monocle-macro" % monocleV, + "com.typesafe.akka" %% "akka-slf4j" % akkaV % Test, + "org.wiremock" % "wiremock" % wireMockV % Test, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-future" % sttpV % Test, ) }, buildAndImportRuntimeImageToK3d := { @@ -1537,20 +1557,20 @@ lazy val componentsApi = (project in file("components-api")) name := "nussknacker-components-api", libraryDependencies ++= { Seq( - "org.apache.commons" % "commons-text" % flinkCommonsTextV, - "org.typelevel" %% "cats-core" % catsV, - "com.beachape" %% "enumeratum" % enumeratumV, - "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingV, - "com.typesafe" % "config" % configV, - "org.semver4j" % "semver4j" % "5.4.0", - "javax.validation" % "validation-api" % javaxValidationApiV, - "org.scala-lang.modules" %% "scala-collection-compat" % scalaCollectionsCompatV, - "com.iheart" %% "ficus" % ficusV, - "org.springframework" % "spring-core" % springV, - "org.springframework" % "spring-expression" % springV % Test, - "com.google.code.findbugs" % "jsr305" % findBugsV, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-future" % sttpV, - "org.scalatestplus" %% s"scalacheck-$scalaCheckVshort" % scalaTestPlusV % Test + "org.apache.commons" % "commons-text" % flinkCommonsTextV, + "org.typelevel" %% "cats-core" % catsV, + "com.beachape" %% "enumeratum" % enumeratumV, + "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingV, + "com.typesafe" % "config" % configV, + "org.semver4j" % "semver4j" % "5.4.0", + "javax.validation" % "validation-api" % javaxValidationApiV, + "org.scala-lang.modules" %% "scala-collection-compat" % scalaCollectionsCompatV, + "com.iheart" %% "ficus" % ficusV, + "org.springframework" % "spring-core" % springV, + "org.springframework" % "spring-expression" % springV % Test, + "com.google.code.findbugs" % "jsr305" % findBugsV, + "com.softwaremill.sttp.client3" %% "core" % sttpV, + "org.scalatestplus" %% s"scalacheck-$scalaCheckVshort" % scalaTestPlusV % Test ) } ) @@ -1676,15 +1696,20 @@ lazy val processReports = (project in file("designer/processReports")) ) .dependsOn(httpUtils, commonUtils, testUtils % "it,test") +// This dependency is delivered by flink-executor and lite-runtime to ensure the same version of libraries in stack: +// sttp -> async-http-client -> netty. Different versions of netty in model classpath causes NoClassDefFoundError. +// Also, thanks to this approach we reduce size of model jar bundle. lazy val httpUtils = (project in utils("http-utils")) .settings(commonSettings) .settings( name := "nussknacker-http-utils", libraryDependencies ++= { Seq( - "com.softwaremill.sttp.client3" %% "core" % sttpV, - "com.softwaremill.sttp.client3" %% "json-common" % sttpV, - "com.softwaremill.sttp.client3" %% "circe" % sttpV, + "com.softwaremill.sttp.client3" %% "core" % sttpV, + "com.softwaremill.sttp.client3" %% "json-common" % sttpV, + "com.softwaremill.sttp.client3" %% "circe" % sttpV, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-future" % sttpV, + "io.netty" % "netty-transport-native-epoll" % nettyV, ) } ) @@ -1702,19 +1727,18 @@ lazy val openapiComponents = (project in component("openapi")) .settings( name := "nussknacker-openapi", libraryDependencies ++= Seq( - "io.swagger.core.v3" % "swagger-integration" % swaggerIntegrationV excludeAll ( + "io.swagger.core.v3" % "swagger-integration" % swaggerIntegrationV excludeAll ( ExclusionRule(organization = "jakarta.activation"), ExclusionRule(organization = "jakarta.validation") ), - "io.netty" % "netty-transport-native-epoll" % nettyV, - "org.apache.flink" % "flink-streaming-java" % flinkV % Provided, - "org.scalatest" %% "scalatest" % scalaTestV % "it,test" + "org.apache.flink" % "flink-streaming-java" % flinkV % Provided, + "org.scalatest" %% "scalatest" % scalaTestV % "it,test" ), ) .dependsOn( componentsUtils % Provided, jsonUtils % Provided, - httpUtils, + httpUtils % Provided, requestResponseComponentsUtils % "it,test", flinkComponentsTestkit % "it,test" ) @@ -1889,6 +1913,18 @@ lazy val listenerApi = (project in file("designer/listener-api")) ) .dependsOn(extensionsApi) +lazy val configLoaderApi = (project in file("designer/config-loader-api")) + .settings(commonSettings) + .settings( + name := "nussknacker-config-loader-api", + libraryDependencies ++= { + Seq( + "org.typelevel" %% "cats-effect" % catsEffectV + ) + } + ) + .dependsOn(extensionsApi) + lazy val deploymentManagerApi = (project in file("designer/deployment-manager-api")) .settings(commonSettings) .settings( @@ -1961,21 +1997,21 @@ lazy val designer = (project in file("designer/server")) assembly / assemblyMergeStrategy := designerMergeStrategy, libraryDependencies ++= { Seq( - "com.typesafe.akka" %% "akka-http" % akkaHttpV, - "com.typesafe.akka" %% "akka-slf4j" % akkaV, - "com.typesafe.akka" %% "akka-stream" % akkaV, - "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV % Test, - "com.typesafe.akka" %% "akka-testkit" % akkaV % Test, - "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceV, - "com.softwaremill.sttp.client3" %% "akka-http-backend" % sttpV, - "ch.qos.logback" % "logback-core" % logbackV, - "ch.qos.logback" % "logback-classic" % logbackV, - "ch.qos.logback.contrib" % "logback-json-classic" % logbackJsonV, - "ch.qos.logback.contrib" % "logback-jackson" % logbackJsonV, - "com.fasterxml.jackson.core" % "jackson-databind" % jacksonV, - "org.slf4j" % "log4j-over-slf4j" % slf4jV, - "com.carrotsearch" % "java-sizeof" % "0.0.5", - "org.typelevel" %% "case-insensitive" % "1.4.0", + "com.typesafe.akka" %% "akka-http" % akkaHttpV, + "com.typesafe.akka" %% "akka-slf4j" % akkaV, + "com.typesafe.akka" %% "akka-stream" % akkaV, + "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV % Test, + "com.typesafe.akka" %% "akka-testkit" % akkaV % Test, + "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceV, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % sttpV, + "ch.qos.logback" % "logback-core" % logbackV, + "ch.qos.logback" % "logback-classic" % logbackV, + "ch.qos.logback.contrib" % "logback-json-classic" % logbackJsonV, + "ch.qos.logback.contrib" % "logback-jackson" % logbackJsonV, + "com.fasterxml.jackson.core" % "jackson-databind" % jacksonV, + "org.slf4j" % "log4j-over-slf4j" % slf4jV, + "com.carrotsearch" % "java-sizeof" % "0.0.5", + "org.typelevel" %% "case-insensitive" % "1.4.0", // It's needed by flinkDeploymentManager which has disabled includingScala "org.scala-lang" % "scala-compiler" % scalaVersion.value, @@ -2029,6 +2065,7 @@ lazy val designer = (project in file("designer/server")) deploymentManagerApi, restmodel, listenerApi, + configLoaderApi, defaultHelpers % Test, testUtils % Test, flinkTestUtils % Test, @@ -2169,6 +2206,7 @@ lazy val modules = List[ProjectReference]( httpUtils, restmodel, listenerApi, + configLoaderApi, deploymentManagerApi, designer, sqlComponents, diff --git a/common-api/src/main/scala/pl/touk/nussknacker/engine/api/parameter/ParameterName.scala b/common-api/src/main/scala/pl/touk/nussknacker/engine/api/parameter/ParameterName.scala index 72076f2fb67..532d9453763 100644 --- a/common-api/src/main/scala/pl/touk/nussknacker/engine/api/parameter/ParameterName.scala +++ b/common-api/src/main/scala/pl/touk/nussknacker/engine/api/parameter/ParameterName.scala @@ -1,5 +1,16 @@ package pl.touk.nussknacker.engine.api.parameter +import io.circe.generic.extras.semiauto.{deriveUnwrappedDecoder, deriveUnwrappedEncoder} +import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} + final case class ParameterName(value: String) { def withBranchId(branchId: String): ParameterName = ParameterName(s"$value for branch $branchId") } + +object ParameterName { + implicit val encoder: Encoder[ParameterName] = deriveUnwrappedEncoder + implicit val decoder: Decoder[ParameterName] = deriveUnwrappedDecoder + + implicit val keyEncoder: KeyEncoder[ParameterName] = KeyEncoder.encodeKeyString.contramap(_.value) + implicit val keyDecoder: KeyDecoder[ParameterName] = KeyDecoder.decodeKeyString.map(ParameterName(_)) +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/TemplateEvaluationResult.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/TemplateEvaluationResult.scala new file mode 100644 index 00000000000..0353b003bea --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/TemplateEvaluationResult.scala @@ -0,0 +1,15 @@ +package pl.touk.nussknacker.engine.api + +case class TemplateEvaluationResult(renderedParts: List[TemplateRenderedPart]) { + def renderedTemplate: String = renderedParts.map(_.value).mkString("") +} + +sealed trait TemplateRenderedPart { + def value: String +} + +object TemplateRenderedPart { + case class RenderedLiteral(value: String) extends TemplateRenderedPart + + case class RenderedSubExpression(value: String) extends TemplateRenderedPart +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/AdditionalUIConfigProvider.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/AdditionalUIConfigProvider.scala index 7be6b67a99f..6e38258c60f 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/AdditionalUIConfigProvider.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/AdditionalUIConfigProvider.scala @@ -1,5 +1,6 @@ package pl.touk.nussknacker.engine.api.component +import io.circe.generic.JsonCodec import pl.touk.nussknacker.engine.api.definition.FixedExpressionValue import pl.touk.nussknacker.engine.api.parameter.{ ParameterName, @@ -24,6 +25,7 @@ object AdditionalUIConfigProvider { val empty = new DefaultAdditionalUIConfigProvider(Map.empty, Map.empty) } +@JsonCodec case class ComponentAdditionalConfig( parameterConfigs: Map[ParameterName, ParameterAdditionalUIConfig], icon: Option[String] = None, @@ -32,6 +34,7 @@ case class ComponentAdditionalConfig( disabled: Boolean = false ) +@JsonCodec case class ParameterAdditionalUIConfig( required: Boolean, initialValue: Option[FixedExpressionValue], diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/DesignerWideComponentId.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/DesignerWideComponentId.scala index d6a35fdd628..99b0ddcbec4 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/DesignerWideComponentId.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/component/DesignerWideComponentId.scala @@ -1,7 +1,7 @@ package pl.touk.nussknacker.engine.api.component import io.circe.generic.extras.semiauto.{deriveUnwrappedDecoder, deriveUnwrappedEncoder} -import io.circe.{Decoder, Encoder} +import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} // TODO This class is used as a work around for the problem that the components are duplicated across processing types. // We plan to get rid of this. After that, we could replace usages of this class by usage of ComponentId @@ -14,6 +14,10 @@ object DesignerWideComponentId { implicit val encoder: Encoder[DesignerWideComponentId] = deriveUnwrappedEncoder implicit val decoder: Decoder[DesignerWideComponentId] = deriveUnwrappedDecoder + implicit val keyEncoder: KeyEncoder[DesignerWideComponentId] = KeyEncoder.encodeKeyString.contramap(_.value) + implicit val keyDecoder: KeyDecoder[DesignerWideComponentId] = + KeyDecoder.decodeKeyString.map(DesignerWideComponentId(_)) + def apply(value: String): DesignerWideComponentId = new DesignerWideComponentId(value.toLowerCase) def forBuiltInComponent(componentId: ComponentId): DesignerWideComponentId = { diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala new file mode 100644 index 00000000000..8e4cec7ee42 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -0,0 +1,251 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.Validated._ +import cats.data.{NonEmptyList, Validated, ValidatedNel} +import cats.implicits.{catsSyntaxValidatedId, _} +import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.typing._ + +/** + * This class determine whether we can assign one type to another type - that is if its the same class, a subclass or can be converted to another type. We provide two modes of conversion - + * 1. Loose conversion is based on the fact that TypingResults are + * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). It is basically how SpEL + * can convert things. Like CommonSupertypeFinder it's in the spirit of "Be type safe as much as possible, but also provide some helpful + * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". + * 2. Strict conversion checks whether we can convert to a wider type. Eg only widening numerical types + * are allowed ( Int -> Long). For other types it should work the same as a loose conversion. + * + */ +object AssignabilityDeterminer { + + private val javaMapClass = classOf[java.util.Map[_, _]] + private val javaListClass = classOf[java.util.List[_]] + private val arrayOfAnyRefClass = classOf[Array[AnyRef]] + + /** + * This method checks if `givenType` can by subclass of `superclassCandidate` + * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` + */ + def isAssignableLoose(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, LooseConversionChecker) + + def isAssignableStrict(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, StrictConversionChecker) + + private def isAssignable(from: TypingResult, to: TypingResult, conversionChecker: ConversionChecker) = { + (from, to) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => isNullAsignableTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + isAnyOfAssignableToAnyOf(NonEmptyList.one(given), superclass.possibleTypes, conversionChecker) + case (given: TypedUnion, superclass: SingleTypingResult) => + isAnyOfAssignableToAnyOf(given.possibleTypes, NonEmptyList.one(superclass), conversionChecker) + case (given: SingleTypingResult, superclass: SingleTypingResult) => + isSingleAssignableToSingle(given, superclass, conversionChecker) + case (given: TypedUnion, superclass: TypedUnion) => + isAnyOfAssignableToAnyOf(given.possibleTypes, superclass.possibleTypes, conversionChecker) + } + } + + private def isNullAsignableTo(to: TypingResult): ValidatedNel[String, Unit] = to match { + // TODO: Null should not be subclass of typed map that has all values assigned. + case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel + case _ => ().validNel + } + + private def isSingleAssignableToSingle( + from: SingleTypingResult, + to: SingleTypingResult, + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + val objTypeRestriction = isSingleAssignableToTypedClass(from, to.runtimeObjType, conversionChecker) + val typedObjectRestrictions = (_: Unit) => + to match { + case superclass: TypedObjectTypingResult => + val givenTypeFields = from match { + case given: TypedObjectTypingResult => given.fields + case _ => Map.empty[String, TypingResult] + } + + superclass.fields.toList + .map { case (name, typ) => + givenTypeFields.get(name) match { + case None => + s"Field '$name' is lacking".invalidNel + case Some(givenFieldType) => + condNel( + isAssignable(givenFieldType, typ, conversionChecker).isValid, + (), + s"Field '$name' is of the wrong type. Expected: ${givenFieldType.display}, actual: ${typ.display}" + ) + } + } + .foldLeft(().validNel[String])(_.combine(_)) + case _ => + ().validNel + } + val dictRestriction = (_: Unit) => { + (from, to) match { + case (given: TypedDict, superclass: TypedDict) => + condNel( + given.dictId == superclass.dictId, + (), + "The type and the superclass candidate are Dicts with unequal IDs" + ) + case (_: TypedDict, _) => + "The type is a Dict but the superclass candidate not".invalidNel + case (_, _: TypedDict) => + "The superclass candidate is a Dict but the type not".invalidNel + case _ => + ().validNel + } + } + val taggedValueRestriction = (_: Unit) => { + (from, to) match { + case (givenTaggedValue: TypedTaggedValue, superclassTaggedValue: TypedTaggedValue) => + condNel( + givenTaggedValue.tag == superclassTaggedValue.tag, + (), + s"Tagged values have unequal tags: ${givenTaggedValue.tag} and ${superclassTaggedValue.tag}" + ) + case (_: TypedTaggedValue, _) => ().validNel + case (_, _: TypedTaggedValue) => + s"The type is not a tagged value".invalidNel + case _ => ().validNel + } + } + // Type like Integer can be subclass of Integer{5}, because Integer could + // possibly have value of 5, that would make it subclass of Integer{5}. + // This allows us to supply unknown Integer to function that requires + // Integer{5}. + val dataValueRestriction = (_: Unit) => { + (from, to) match { + case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) + if givenValue == candidateValue => + ().validNel + case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) => + s"Types with value have different values: $givenValue and $candidateValue".invalidNel + case _ => ().validNel + } + } + objTypeRestriction andThen + (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) + } + + private def isSingleAssignableToTypedClass( + from: SingleTypingResult, + to: TypedClass, + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { + def canBeSubOrSuperclass(givenClassParam: TypingResult, superclassParam: TypingResult) = + condNel( + isAssignable(givenClassParam, superclassParam, conversionChecker).isValid || + isAssignable(superclassParam, givenClassParam, conversionChecker).isValid, + (), + f"None of ${givenClassParam.display} and ${superclassParam.display} is a subclass of another" + ) + + (givenClass, superclassCandidate) match { + case (TypedClass(_, givenElementParam :: Nil), TypedClass(superclass, superclassParam :: Nil)) + // Array are invariant but we have built-in conversion between array types - this check should be moved outside this class when we move away canBeConvertedTo as well + if javaListClass.isAssignableFrom(superclass) || arrayOfAnyRefClass.isAssignableFrom(superclass) => + isAssignable(givenElementParam, superclassParam, conversionChecker) + case ( + TypedClass(_, givenKeyParam :: givenValueParam :: Nil), + TypedClass(superclass, superclassKeyParam :: superclassValueParam :: Nil) + ) if javaMapClass.isAssignableFrom(superclass) => + // Map's key generic param is invariant. We can't just check givenKeyParam == superclassKeyParam because of Unknown type which is a kind of wildcard + condNel( + isAssignable(givenKeyParam, superclassKeyParam, conversionChecker).isValid && + isAssignable(superclassKeyParam, givenKeyParam, conversionChecker).isValid, + (), + s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" + ) andThen (_ => isAssignable(givenValueParam, superclassValueParam, conversionChecker)) + case _ => + // for unknown types we are lax - the generic type may be co- contra- or in-variant - and we don't want to + // return validation errors in this case. It's better to accept to much than too little + condNel( + superclassCandidate.params.zip(givenClass.params).forall { case (superclassParam, givenClassParam) => + canBeSubOrSuperclass(givenClassParam, superclassParam).isValid + }, + (), + s"Wrong type parameters" + ) + } + } + val givenClass = from.runtimeObjType + + val equalClassesOrCanAssign = + condNel( + givenClass == to, + (), + f"${givenClass.display} and ${to.display} are not the same" + ) orElse + isAssignable(givenClass.klass, to.klass) + + val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, to)) + canBeSubclass orElse conversionChecker.isConvertable(from, to) + } + + private def isAnyOfAssignableToAnyOf( + from: NonEmptyList[SingleTypingResult], + to: NonEmptyList[SingleTypingResult], + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against + // e.g. (String | Int).isAnyOfAssignableToAnyOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. + // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here + // "double exists" looks like a good tradeoff + condNel( + from.exists(given => to.exists(isSingleAssignableToSingle(given, _, conversionChecker).isValid)), + (), + s"""None of the following types: + |${from.map(" - " + _.display).toList.mkString(",\n")} + |can be a subclass of any of: + |${to.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin + ) + } + + // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... + private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = + condNel(ClassUtils.isAssignable(from, to, true), (), s"$to is not assignable from $from") + + // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) + private sealed trait ConversionChecker { + + def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] + + } + + private object StrictConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = + s"${from.runtimeObjType.display} cannot be strictly converted to ${to.display}" + condNel(TypeConversionHandler.canBeStrictlyConvertedTo(from, to), (), errMsgPrefix) + } + + } + + private object LooseConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = s"${from.runtimeObjType.display} cannot be converted to ${to.display}" + condNel(TypeConversionHandler.canBeLooselyConvertedTo(from, to), (), errMsgPrefix) + } + + } + +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala deleted file mode 100644 index 9545ab30dd6..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ /dev/null @@ -1,214 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import cats.data.Validated._ -import cats.data.{NonEmptyList, ValidatedNel} -import cats.implicits.{catsSyntaxValidatedId, _} -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.typing._ - -/** - * This class determine if type can be subclass of other type. It basically based on fact that TypingResults are - * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). - * - * This class, like CommonSupertypeFinder is in spirit of "Be type safety as much as possible, but also provide some helpful - * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". - * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. - */ -trait CanBeSubclassDeterminer { - - private val javaMapClass = classOf[java.util.Map[_, _]] - private val javaListClass = classOf[java.util.List[_]] - private val arrayOfAnyRefClass = classOf[Array[AnyRef]] - - /** - * This method checks if `givenType` can by subclass of `superclassCandidate` - * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` - */ - def canBeSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeSubclassOf(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeSubclassOf(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeSubclassOf(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeSubclassOf(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => canBeSubclassOf(given.possibleTypes, superclass.possibleTypes) - } - } - - private def canNullBeSubclassOf(result: TypingResult): ValidatedNel[String, Unit] = result match { - // TODO: Null should not be subclass of typed map that has all values assigned. - case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel - case _ => ().validNel - } - - protected def singleCanBeSubclassOf( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val objTypeRestriction = classCanBeSubclassOf(givenType, superclassCandidate.runtimeObjType) - val typedObjectRestrictions = (_: Unit) => - superclassCandidate match { - case superclass: TypedObjectTypingResult => - val givenTypeFields = givenType match { - case given: TypedObjectTypingResult => given.fields - case _ => Map.empty[String, TypingResult] - } - - superclass.fields.toList - .map { case (name, typ) => - givenTypeFields.get(name) match { - case None => - s"Field '$name' is lacking".invalidNel - case Some(givenFieldType) => - condNel( - canBeSubclassOf(givenFieldType, typ).isValid, - (), - s"Field '$name' is of the wrong type. Expected: ${givenFieldType.display}, actual: ${typ.display}" - ) - } - } - .foldLeft(().validNel[String])(_.combine(_)) - case _ => - ().validNel - } - val dictRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (given: TypedDict, superclass: TypedDict) => - condNel( - given.dictId == superclass.dictId, - (), - "The type and the superclass candidate are Dicts with unequal IDs" - ) - case (_: TypedDict, _) => - "The type is a Dict but the superclass candidate not".invalidNel - case (_, _: TypedDict) => - "The superclass candidate is a Dict but the type not".invalidNel - case _ => - ().validNel - } - } - val taggedValueRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (givenTaggedValue: TypedTaggedValue, superclassTaggedValue: TypedTaggedValue) => - condNel( - givenTaggedValue.tag == superclassTaggedValue.tag, - (), - s"Tagged values have unequal tags: ${givenTaggedValue.tag} and ${superclassTaggedValue.tag}" - ) - case (_: TypedTaggedValue, _) => ().validNel - case (_, _: TypedTaggedValue) => - s"The type is not a tagged value".invalidNel - case _ => ().validNel - } - } - // Type like Integer can be subclass of Integer{5}, because Integer could - // possibly have value of 5, that would make it subclass of Integer{5}. - // This allows us to supply unknown Integer to function that requires - // Integer{5}. - val dataValueRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) - if givenValue == candidateValue => - ().validNel - case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) => - s"Types with value have different values: $givenValue and $candidateValue".invalidNel - case _ => ().validNel - } - } - objTypeRestriction andThen - (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) - } - - protected def classCanBeSubclassOf( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - - val equalClassesOrCanAssign = - condNel( - givenClass == superclassCandidate, - (), - f"${givenClass.display} and ${superclassCandidate.display} are not the same" - ) orElse - isAssignable(givenClass.klass, superclassCandidate.klass) - - val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, superclassCandidate)) - canBeSubclass orElse canBeConvertedTo(givenType, superclassCandidate) - } - - private def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { - def canBeSubOrSuperclass(givenClassParam: TypingResult, superclassParam: TypingResult) = - condNel( - canBeSubclassOf(givenClassParam, superclassParam).isValid || - canBeSubclassOf(superclassParam, givenClassParam).isValid, - (), - f"None of ${givenClassParam.display} and ${superclassParam.display} is a subclass of another" - ) - - (givenClass, superclassCandidate) match { - case (TypedClass(_, givenElementParam :: Nil), TypedClass(superclass, superclassParam :: Nil)) - // Array are invariant but we have built-in conversion between array types - this check should be moved outside this class when we move away canBeConvertedTo as well - if javaListClass.isAssignableFrom(superclass) || arrayOfAnyRefClass.isAssignableFrom(superclass) => - canBeSubclassOf(givenElementParam, superclassParam) - case ( - TypedClass(_, givenKeyParam :: givenValueParam :: Nil), - TypedClass(superclass, superclassKeyParam :: superclassValueParam :: Nil) - ) if javaMapClass.isAssignableFrom(superclass) => - // Map's key generic param is invariant. We can't just check givenKeyParam == superclassKeyParam because of Unknown type which is a kind of wildcard - condNel( - canBeSubclassOf(givenKeyParam, superclassKeyParam).isValid && - canBeSubclassOf(superclassKeyParam, givenKeyParam).isValid, - (), - s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" - ) andThen (_ => canBeSubclassOf(givenValueParam, superclassValueParam)) - case _ => - // for unknown types we are lax - the generic type may be co- contra- or in-variant - and we don't want to - // return validation errors in this case. It's better to accept to much than too little - condNel( - superclassCandidate.params.zip(givenClass.params).forall { case (superclassParam, givenClassParam) => - canBeSubOrSuperclass(givenClassParam, superclassParam).isValid - }, - (), - s"Wrong type parameters" - ) - } - } - - private def canBeSubclassOf( - givenTypes: NonEmptyList[SingleTypingResult], - superclassCandidates: NonEmptyList[SingleTypingResult] - ): ValidatedNel[String, Unit] = { - // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against - // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. - // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here - // "double exists" looks like a good tradeoff - condNel( - givenTypes.exists(given => superclassCandidates.exists(singleCanBeSubclassOf(given, _).isValid)), - (), - s"""None of the following types: - |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} - |can be a subclass of any of: - |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin - ) - } - - // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) - private def canBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" - condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) - } - - // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = - condNel(ClassUtils.isAssignable(from, to, true), (), s"$to is not assignable from $from") -} - -object CanBeSubclassDeterminer extends CanBeSubclassDeterminer diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala index fa37bc54a40..647f169c393 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala @@ -15,9 +15,9 @@ object NumberTypeUtils { else if (typ == Typed[java.lang.Double]) java.lang.Double.valueOf(0) else if (typ == Typed[java.math.BigDecimal]) java.math.BigDecimal.ZERO // in case of some unions - else if (typ.canBeSubclassOf(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) + else if (typ.canBeConvertedTo(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) // double is quite safe - it can be converted to any Number - else if (typ.canBeSubclassOf(Typed[Number])) java.lang.Double.valueOf(0) + else if (typ.canBeConvertedTo(Typed[Number])) java.lang.Double.valueOf(0) else throw new IllegalArgumentException(s"Not expected type: ${typ.display}, should be Number") } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index cc91f255239..e5629241152 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -3,8 +3,10 @@ package pl.touk.nussknacker.engine.api.typed import org.apache.commons.lang3.{ClassUtils, LocaleUtils} import org.springframework.util.StringUtils import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypedObjectWithValue} +import java.math.BigInteger import java.nio.charset.Charset import java.time._ import java.time.chrono.{ChronoLocalDate, ChronoLocalDateTime} @@ -20,8 +22,8 @@ object TypeConversionHandler { /** * java.math.BigDecimal is quite often returned as a wrapper for all kind of numbers (floating and without floating point). - * Given to this we cannot to be sure if conversion is safe or not based on type (without scale knowledge). - * So we have two options: enforce user to convert to some type without floating point (e.g. BigInteger) or be loose in this point. + * Given to this we cannot be sure if conversion is safe or not based on type (without scale knowledge). + * So we have two options: force user to convert to some type without floating point (e.g. BigInteger) or be loose in this point. * Be default we will be loose. */ // TODO: Add feature flag: strictBigDecimalChecking (default false?) @@ -35,8 +37,8 @@ object TypeConversionHandler { cl } - def canConvert(value: String, superclassCandidate: TypedClass): Boolean = { - ClassUtils.isAssignable(superclassCandidate.klass, klass, true) && Try( + def canConvert(value: String, to: TypedClass): Boolean = { + ClassUtils.isAssignable(to.klass, klass, true) && Try( convert(value) ).isSuccess } @@ -63,17 +65,33 @@ object TypeConversionHandler { StringConversion[ChronoLocalDateTime[_]](LocalDateTime.parse) ) - def canBeConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = { - handleNumberConversions(givenType.runtimeObjType, superclassCandidate) || - handleStringToValueClassConversions(givenType, superclassCandidate) + def canBeLooselyConvertedTo(from: SingleTypingResult, to: TypedClass): Boolean = + canBeConvertedToAux(from, to) + + def canBeStrictlyConvertedTo(from: SingleTypingResult, to: TypedClass): Boolean = + canBeConvertedToAux(from, to, strict = true) + + private def canBeConvertedToAux(from: SingleTypingResult, to: TypedClass, strict: Boolean = false) = { + handleStringToValueClassConversions(from, to) || + handleNumberConversion(from.runtimeObjType, to, strict) + } + + private def handleNumberConversion(from: SingleTypingResult, to: TypedClass, strict: Boolean) = { + val boxedGivenClass = ClassUtils.primitiveToWrapper(from.runtimeObjType.klass) + val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(to.klass) + + if (strict) + handleStrictNumberConversions(boxedGivenClass, boxedSuperclassCandidate) + else + handleLooseNumberConversion(boxedGivenClass, boxedSuperclassCandidate) } // See org.springframework.core.convert.support.NumberToNumberConverterFactory - private def handleNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { - val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) - val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) + private def handleLooseNumberConversion( + boxedGivenClass: Class[_], + boxedSuperclassCandidate: Class[_] + ): Boolean = { // We can't check precision here so we need to be loose here - // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) if (NumberTypesPromotionStrategy .isFloatingNumber(boxedSuperclassCandidate) || boxedSuperclassCandidate == classOf[java.math.BigDecimal]) { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) @@ -84,13 +102,25 @@ object TypeConversionHandler { } } + private def handleStrictNumberConversions(givenClass: Class[_], to: Class[_]): Boolean = { + (givenClass, to) match { + case (bigInteger, t) + if (bigInteger == classOf[BigInteger] && (t == classOf[BigDecimal] || t == classOf[BigInteger])) => + true + case (f, t) if (AllNumbers.contains(f) && AllNumbers.contains(t)) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) + case _ => false + + } + } + private def handleStringToValueClassConversions( - givenType: SingleTypingResult, - superclassCandidate: TypedClass + from: SingleTypingResult, + to: TypedClass ): Boolean = - givenType match { + from match { case TypedObjectWithValue(_, str: String) => - stringConversions.exists(_.canConvert(str, superclassCandidate)) + stringConversions.exists(_.canConvert(str, to)) case _ => false } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index ca5cbde9597..1aff3f9a890 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -27,15 +27,18 @@ object typing { // TODO: Rename to Typed, maybe NuType? sealed trait TypingResult { - // TODO: We should split this method into two or three methods: - // - Simple, strictly checking subclassing similar to isAssignable, where we don't do heuristics like - // Any can be subclass of Int, or for Union of Int and String can be subclass of Int - // - The one with heuristics considering limitations of our tool like poor support for generics, lack - // of casting allowing things described above - // - The one that allow things above + SPeL conversions like any Number to any Number conversion, - // String to LocalDate etc. This one should be accessible only for context where SPeL is used - final def canBeSubclassOf(typingResult: TypingResult): Boolean = - CanBeSubclassDeterminer.canBeSubclassOf(this, typingResult).isValid + /** + * Checks if there exists a conversion to a given typingResult, with possible loss of precision, e.g. long to int. + * If you need to retain conversion precision, use canBeStrictlyConvertedTo + */ + final def canBeConvertedTo(typingResult: TypingResult): Boolean = + AssignabilityDeterminer.isAssignableLoose(this, typingResult).isValid + + /** + * Checks if the conversion to a given typingResult can be made without loss of precision + */ + final def canBeStrictlyConvertedTo(typingResult: TypingResult): Boolean = + AssignabilityDeterminer.isAssignableStrict(this, typingResult).isValid def valueOpt: Option[Any] @@ -466,7 +469,9 @@ object typing { case class CastTypedValue[T: TypeTag]() { def unapply(typingResult: TypingResult): Option[TypingResultTypedValue[T]] = { - Option(typingResult).filter(_.canBeSubclassOf(Typed.fromDetailedType[T])).map(new TypingResultTypedValue(_)) + Option(typingResult) + .filter(_.canBeConvertedTo(Typed.fromDetailedType[T])) + .map(new TypingResultTypedValue(_)) } } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala new file mode 100644 index 00000000000..82d90e679f2 --- /dev/null +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala @@ -0,0 +1,67 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.NonEmptyList +import cats.data.Validated.{Invalid, Valid} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks.forAll +import org.scalatest.prop.Tables.Table +import pl.touk.nussknacker.engine.api.typed.typing.Typed + +class AssignabilityDeterminerSpec extends AnyFunSuite with Matchers { + + val wideningConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (Typed[Int], Typed[Int], Valid(()), Valid(())), + (Typed[Int], Typed[Double], Valid(()), Valid(())), + (Typed[List[Int]], Typed[List[Int]], Valid(()), Valid(())), + (Typed[List[Int]], Typed[List[Any]], Valid(()), Valid(())), + (Typed[Map[String, Int]], Typed[Map[String, Int]], Valid(()), Valid(())), + (Typed[Map[String, Int]], Typed[Map[Any, Any]], Valid(()), Valid(())) + ) + + test("isAssignableStrict should pass for widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, expectedStrict, _) => + val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) + result shouldBe expectedStrict + } + } + + test("isAssignableLoose should pass for widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, _, expectedLoose) => + val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) + result shouldBe expectedLoose + } + } + + val narrowingConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (Typed[Long], Typed[Int], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[Long], Typed[Short], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[Double], Typed[Float], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[BigDecimal], Typed[Double], Invalid(NonEmptyList.of("")), Valid(())) + ) + + test("isAssignableStrict should fail for narrowing numerical cases") { + forAll(narrowingConversionCases) { (sourceType, targetType, expectedStrict, _) => + val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) + result match { + case Valid(_) if expectedStrict.isValid => succeed + case Invalid(_) if expectedStrict.isInvalid => succeed + case _ => fail(s"Unexpected result: $result for types $sourceType -> $targetType") + } + } + } + + test("isAssignableLoose should pass for narrowing cases") { + forAll(narrowingConversionCases) { (sourceType, targetType, _, expectedLoose) => + val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) + result match { + case Valid(_) if expectedLoose.isValid => succeed + case Invalid(_) if expectedLoose.isInvalid => succeed + case _ => fail(s"Unexpected result: $result for types $sourceType -> $targetType") + } + } + } + +} diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala index 6e8e4ce6708..b1d98cd537d 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala @@ -60,20 +60,20 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w } test("should type empty list") { - Typed.fromInstance(Nil).canBeSubclassOf(Typed(classOf[List[_]])) shouldBe true - Typed.fromInstance(Nil.asJava).canBeSubclassOf(Typed(classOf[java.util.List[_]])) shouldBe true + Typed.fromInstance(Nil).canBeConvertedTo(Typed(classOf[List[_]])) shouldBe true + Typed.fromInstance(Nil.asJava).canBeConvertedTo(Typed(classOf[java.util.List[_]])) shouldBe true } test("should type lists and return union of types coming from all elements") { def checkTypingResult(obj: Any, klass: Class[_], paramTypingResult: TypingResult): Unit = { val typingResult = Typed.fromInstance(obj) - typingResult.canBeSubclassOf(Typed(klass)) shouldBe true + typingResult.canBeConvertedTo(Typed(klass)) shouldBe true typingResult.withoutValue .asInstanceOf[TypedClass] .params .loneElement - .canBeSubclassOf(paramTypingResult) shouldBe true + .canBeConvertedTo(paramTypingResult) shouldBe true } def checkNotASubclassOfOtherParamTypingResult(obj: Any, otherParamTypingResult: TypingResult): Unit = { @@ -82,7 +82,7 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w .asInstanceOf[TypedClass] .params .loneElement - .canBeSubclassOf(otherParamTypingResult) shouldBe false + .canBeConvertedTo(otherParamTypingResult) shouldBe false } val listOfSimpleObjects = List[Any](1.1, 2) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala index 5483df1a9a8..22d0836fd1b 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala @@ -5,6 +5,7 @@ import cats.data.NonEmptyList import org.scalatest.{Inside, OptionValues} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.typed.AssignabilityDeterminer.isAssignableLoose import pl.touk.nussknacker.engine.api.typed.typing._ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with OptionValues with Inside { @@ -13,11 +14,11 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio private def list(arg: TypingResult) = Typed.genericTypeClass[java.util.List[_]](List(arg)) - import CanBeSubclassDeterminer.canBeSubclassOf + import AssignabilityDeterminer.isAssignable - test("determine if can be subclass for typed object") { + test("determine if can be subclass for simple typed objects") { - canBeSubclassOf( + isAssignableLoose( typeMap( "field1" -> Typed[String], "field2" -> Typed[Int], @@ -37,8 +38,10 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio "Field 'field4' is lacking" ) .invalid + } - canBeSubclassOf( + test("determine if can be subclass for map of typed objects") { + isAssignableLoose( typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))), typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe NonEmptyList @@ -49,30 +52,30 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio } test("determine if can be subclass for class") { - canBeSubclassOf(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe + isAssignableLoose(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe "Set[BigDecimal] cannot be converted to Set[String]".invalidNel } test("determine if can be subclass for tagged value") { - canBeSubclassOf( + isAssignableLoose( Typed.tagged(Typed.typedClass[String], "tag1"), Typed.tagged(Typed.typedClass[String], "tag2") ) shouldBe "Tagged values have unequal tags: tag1 and tag2".invalidNel - canBeSubclassOf(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe + isAssignableLoose(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe "The type is not a tagged value".invalidNel } test("determine if can be subclass for object with value") { - canBeSubclassOf(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe + isAssignableLoose(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe "Types with value have different values: 2 and 3".invalidNel } test("determine if can be subclass for null") { - canBeSubclassOf(Typed[String], TypedNull) shouldBe + isAssignableLoose(Typed[String], TypedNull) shouldBe "No type can be subclass of Null".invalidNel - canBeSubclassOf(TypedNull, Typed.fromInstance(1)) shouldBe + isAssignableLoose(TypedNull, Typed.fromInstance(1)) shouldBe "Null cannot be subclass of type with value".invalidNel } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala index ec38ad9cf2a..1581edf488c 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala @@ -32,33 +32,33 @@ class TypingResultSpec test("determine if can be subclass for typed object") { - typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf( + typeMap("field1" -> Typed[String]).canBeConvertedTo( typeMap("field1" -> Typed[String], "field2" -> Typed[Int]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[Number]) ) shouldBe true - typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeSubclassOf( + typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe true - typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeSubclassOf( + typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe false - typeMap("field1" -> Typed[String]).canBeSubclassOf(Typed[java.util.Map[_, _]]) shouldBe true + typeMap("field1" -> Typed[String]).canBeConvertedTo(Typed[java.util.Map[_, _]]) shouldBe true - Typed[java.util.Map[_, _]].canBeSubclassOf(typeMap("field1" -> Typed[String])) shouldBe false + Typed[java.util.Map[_, _]].canBeConvertedTo(typeMap("field1" -> Typed[String])) shouldBe false } test("extract Unknown value type when no super matching supertype found among all fields of Record") { @@ -76,72 +76,78 @@ class TypingResultSpec } test("determine if can be subclass for typed unions") { - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Typed[Int]) shouldBe true - Typed[Int].canBeSubclassOf(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Typed(Typed[Long], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Typed(Typed[Long], Typed[Int])) shouldBe true } test("determine if can be subclass for unknown") { - Unknown.canBeSubclassOf(Typed[Int]) shouldBe true - Typed[Int].canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(typeMap("field1" -> Typed[String])) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeConvertedTo(typeMap("field1" -> Typed[String])) shouldBe true + typeMap("field1" -> Typed[String]).canBeConvertedTo(Unknown) shouldBe true } test("determine if can be subclass for class") { Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true Typed .fromDetailedType[java.util.List[Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[_, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true // For arrays it might be tricky - Typed.fromDetailedType[Array[BigDecimal]].canBeSubclassOf(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true - Typed.fromDetailedType[Array[BigDecimal]].canBeSubclassOf(Typed.fromDetailedType[Array[Number]]) shouldBe true - Typed.fromDetailedType[Array[Number]].canBeSubclassOf(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false + Typed + .fromDetailedType[Array[BigDecimal]] + .canBeConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true + Typed + .fromDetailedType[Array[BigDecimal]] + .canBeConvertedTo(Typed.fromDetailedType[Array[Number]]) shouldBe true + Typed + .fromDetailedType[Array[Number]] + .canBeConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false } test("determine if numbers can be converted") { - Typed[Int].canBeSubclassOf(Typed[Long]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[Int]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[Double]) shouldBe true - Typed[Double].canBeSubclassOf(Typed[Long]) shouldBe false - Typed[java.math.BigDecimal].canBeSubclassOf(Typed[Long]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[java.math.BigDecimal]) shouldBe true + Typed[Int].canBeConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[Double]) shouldBe true + Typed[Double].canBeConvertedTo(Typed[Long]) shouldBe false + Typed[java.math.BigDecimal].canBeConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[java.math.BigDecimal]) shouldBe true } test("find common supertype for simple types") { @@ -297,22 +303,22 @@ class TypingResultSpec test("determine if can be subclass for tagged value") { Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true + .canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false + .canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false - Typed.tagged(Typed.typedClass[String], "tag1").canBeSubclassOf(Typed.typedClass[String]) shouldBe true - Typed.typedClass[String].canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false + .canBeConvertedTo(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false + Typed.tagged(Typed.typedClass[String], "tag1").canBeConvertedTo(Typed.typedClass[String]) shouldBe true + Typed.typedClass[String].canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false } test("determine if can be subclass for null") { - TypedNull.canBeSubclassOf(Typed[Int]) shouldBe true - TypedNull.canBeSubclassOf(Typed.fromInstance(4)) shouldBe false - TypedNull.canBeSubclassOf(TypedNull) shouldBe true - Typed[String].canBeSubclassOf(TypedNull) shouldBe false + TypedNull.canBeConvertedTo(Typed[Int]) shouldBe true + TypedNull.canBeConvertedTo(Typed.fromInstance(4)) shouldBe false + TypedNull.canBeConvertedTo(TypedNull) shouldBe true + Typed[String].canBeConvertedTo(TypedNull) shouldBe false } test("should deeply extract typ parameters") { @@ -331,20 +337,20 @@ class TypingResultSpec } test("determine if can be subclass for object with value") { - Typed.fromInstance(45).canBeSubclassOf(Typed.typedClass[Long]) shouldBe true - Typed.fromInstance(29).canBeSubclassOf(Typed.typedClass[String]) shouldBe false - Typed.fromInstance(78).canBeSubclassOf(Typed.fromInstance(78)) shouldBe true - Typed.fromInstance(12).canBeSubclassOf(Typed.fromInstance(15)) shouldBe false - Typed.fromInstance(41).canBeSubclassOf(Typed.fromInstance("t")) shouldBe false - Typed.typedClass[String].canBeSubclassOf(Typed.fromInstance("t")) shouldBe true + Typed.fromInstance(45).canBeConvertedTo(Typed.typedClass[Long]) shouldBe true + Typed.fromInstance(29).canBeConvertedTo(Typed.typedClass[String]) shouldBe false + Typed.fromInstance(78).canBeConvertedTo(Typed.fromInstance(78)) shouldBe true + Typed.fromInstance(12).canBeConvertedTo(Typed.fromInstance(15)) shouldBe false + Typed.fromInstance(41).canBeConvertedTo(Typed.fromInstance("t")) shouldBe false + Typed.typedClass[String].canBeConvertedTo(Typed.fromInstance("t")) shouldBe true } test("determine if can be subclass for object with value - use conversion") { - Typed.fromInstance("2007-12-03").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe true - Typed.fromInstance("2007-12-03T10:15:30").canBeSubclassOf(Typed.typedClass[LocalDateTime]) shouldBe true + Typed.fromInstance("2007-12-03").canBeConvertedTo(Typed.typedClass[LocalDate]) shouldBe true + Typed.fromInstance("2007-12-03T10:15:30").canBeConvertedTo(Typed.typedClass[LocalDateTime]) shouldBe true - Typed.fromInstance("2007-12-03-qwerty").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe false - Typed.fromInstance("2007-12-03").canBeSubclassOf(Typed.typedClass[Currency]) shouldBe false + Typed.fromInstance("2007-12-03-qwerty").canBeConvertedTo(Typed.typedClass[LocalDate]) shouldBe false + Typed.fromInstance("2007-12-03").canBeConvertedTo(Typed.typedClass[Currency]) shouldBe false } test("determinate if can be superclass for objects with value") { @@ -443,7 +449,7 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeSubclassOf(input) shouldBe true + input.canBeConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { superType shouldEqual input @@ -457,11 +463,11 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeSubclassOf(input) shouldBe true + input.canBeConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { // We generate combinations of types co we can only check if input type is a subclass of super type - input.canBeSubclassOf(superType) + input.canBeConvertedTo(superType) } } } @@ -477,12 +483,12 @@ class TypingResultSpec logger.trace(s"Checking supertype of: ${first.display} and ${second.display}") withClue(s"Input: ${first.display}; ${second.display};") { - first.canBeSubclassOf(first) shouldBe true - second.canBeSubclassOf(second) shouldBe true + first.canBeConvertedTo(first) shouldBe true + second.canBeConvertedTo(second) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(first, second) withClue(s"Supertype: ${superType.display};") { - first.canBeSubclassOf(superType) - second.canBeSubclassOf(superType) + first.canBeConvertedTo(superType) + second.canBeConvertedTo(superType) } } } diff --git a/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenAPIServiceSpec.scala b/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenAPIServiceSpec.scala index cbc7cbe770b..2d9b6631609 100644 --- a/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenAPIServiceSpec.scala +++ b/components/openapi/src/it/scala/pl/touk/nussknacker/openapi/functional/OpenAPIServiceSpec.scala @@ -17,7 +17,7 @@ import pl.touk.nussknacker.engine.util.service.EagerServiceWithStaticParametersA import pl.touk.nussknacker.http.backend.FixedAsyncHttpClientBackendProvider import pl.touk.nussknacker.openapi.enrichers.{SwaggerEnricherCreator, SwaggerEnrichers} import pl.touk.nussknacker.openapi.parser.SwaggerParser -import pl.touk.nussknacker.openapi.{ApiKeyConfig, OpenAPIServicesConfig} +import pl.touk.nussknacker.openapi.{ApiKeySecret, OpenAPIServicesConfig, SecurityConfig, SecuritySchemeName} import pl.touk.nussknacker.test.PatientScalaFutures import java.net.URL @@ -44,10 +44,10 @@ class OpenAPIServiceSpec val client = new DefaultAsyncHttpClient() try { new StubService().withCustomerService { port => - val securities = Map("apikey" -> ApiKeyConfig("TODO")) + val secretBySchemeName = Map(SecuritySchemeName("apikey") -> ApiKeySecret("TODO")) val config = OpenAPIServicesConfig( new URL("http://foo"), - security = Some(securities), + security = secretBySchemeName, rootUrl = Some(new URL(s"http://localhost:$port")) ) val services = SwaggerParser.parse(definition, config).collect { case Valid(service) => diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIComponentProvider.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIComponentProvider.scala index 86c3de9f4d5..1d84f8314af 100644 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIComponentProvider.scala +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIComponentProvider.scala @@ -10,7 +10,7 @@ import pl.touk.nussknacker.engine.api.CirceUtil import pl.touk.nussknacker.engine.api.component.{ComponentDefinition, ComponentProvider, NussknackerVersion} import pl.touk.nussknacker.engine.api.process.ProcessObjectDependencies import pl.touk.nussknacker.engine.util.config.ConfigEnrichments._ -import pl.touk.nussknacker.openapi.OpenAPIsConfig._ +import pl.touk.nussknacker.openapi.OpenAPIServicesConfig._ import pl.touk.nussknacker.openapi.discovery.SwaggerOpenApiDefinitionDiscovery import pl.touk.nussknacker.openapi.enrichers.{SwaggerEnricherCreator, SwaggerEnrichers} import pl.touk.nussknacker.openapi.parser.ServiceParseError diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfig.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfig.scala new file mode 100644 index 00000000000..9f88df8152f --- /dev/null +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfig.scala @@ -0,0 +1,82 @@ +package pl.touk.nussknacker.openapi + +import com.typesafe.config.Config +import io.swagger.v3.oas.models.PathItem.HttpMethod +import net.ceedubs.ficus.readers.{ArbitraryTypeReader, ValueReader} +import pl.touk.nussknacker.http.backend.{DefaultHttpClientConfig, HttpClientConfig} +import sttp.model.StatusCode + +import java.net.URL +import scala.util.matching.Regex + +final case class OpenAPIServicesConfig( + url: URL, + // by default we allow only GET, as enrichers should be idempotent and not change data + allowedMethods: List[String] = List(HttpMethod.GET.name()), + codesToInterpretAsEmpty: List[Int] = List(StatusCode.NotFound.code), + namePattern: Regex = ".*".r, + rootUrl: Option[URL] = None, + // For backward compatibility it is called security. We should probably rename it and bundle together with secret + private val security: Map[SecuritySchemeName, Secret] = Map.empty, + private val secret: Option[Secret] = None, + httpClientConfig: HttpClientConfig = DefaultHttpClientConfig() +) { + def securityConfig: SecurityConfig = + new SecurityConfig(secretBySchemeName = security, commonSecretForAnyScheme = secret) +} + +final class SecurityConfig( + secretBySchemeName: Map[SecuritySchemeName, Secret], + commonSecretForAnyScheme: Option[Secret] +) { + + def secret(schemeName: SecuritySchemeName): Option[Secret] = + secretBySchemeName.get(schemeName) orElse commonSecretForAnyScheme + +} + +object SecurityConfig { + def empty: SecurityConfig = new SecurityConfig(Map.empty, None) +} + +final case class SecuritySchemeName(value: String) + +sealed trait Secret + +final case class ApiKeySecret(apiKeyValue: String) extends Secret + +object OpenAPIServicesConfig { + + import net.ceedubs.ficus.Ficus._ + import pl.touk.nussknacker.engine.util.config.ConfigEnrichments._ + import HttpClientConfig._ + + implicit val securitySchemeNameVR: ValueReader[SecuritySchemeName] = + ValueReader[String].map(SecuritySchemeName(_)) + + implicit val regexReader: ValueReader[Regex] = (config: Config, path: String) => new Regex(config.getString(path)) + + implicit val apiKeyVR: ValueReader[ApiKeySecret] = ValueReader.relative { conf => + ApiKeySecret( + apiKeyValue = conf.as[String]("apiKeyValue") + ) + } + + implicit val secretVR: ValueReader[Secret] = ValueReader.relative { conf => + conf.as[String]("type") match { + case "apiKey" => conf.rootAs[ApiKeySecret] + case typ => throw new Exception(s"Not supported swagger security type '$typ' in the configuration") + } + } + + implicit val secretBySchemeNameVR: ValueReader[Map[SecuritySchemeName, Secret]] = + ValueReader[Map[String, Secret]].map { secretBySchemeName => + secretBySchemeName.map { case (schemeNameString, secret) => + SecuritySchemeName(schemeNameString) -> secret + } + } + + implicit val openAPIServicesConfigVR: ValueReader[OpenAPIServicesConfig] = + ArbitraryTypeReader.arbitraryTypeValueReader[OpenAPIServicesConfig] + +} diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIsConfig.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIsConfig.scala deleted file mode 100644 index 82d3cb553f1..00000000000 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/OpenAPIsConfig.scala +++ /dev/null @@ -1,50 +0,0 @@ -package pl.touk.nussknacker.openapi - -import com.typesafe.config.Config -import io.swagger.v3.oas.models.PathItem.HttpMethod -import net.ceedubs.ficus.readers.{ArbitraryTypeReader, ValueReader} -import pl.touk.nussknacker.http.backend.{DefaultHttpClientConfig, HttpClientConfig} -import sttp.model.StatusCode - -import java.net.URL -import scala.util.matching.Regex - -final case class OpenAPIServicesConfig( - url: URL, - // by default we allow only GET, as enrichers should be idempotent and not change data - allowedMethods: List[String] = List(HttpMethod.GET.name()), - codesToInterpretAsEmpty: List[Int] = List(StatusCode.NotFound.code), - namePattern: Regex = ".*".r, - rootUrl: Option[URL] = None, - security: Option[Map[String, OpenAPISecurityConfig]] = None, - httpClientConfig: HttpClientConfig = DefaultHttpClientConfig() -) - -sealed trait OpenAPISecurityConfig - -final case class ApiKeyConfig(apiKeyValue: String) extends OpenAPISecurityConfig - -object OpenAPIsConfig { - - import net.ceedubs.ficus.Ficus._ - import pl.touk.nussknacker.engine.util.config.ConfigEnrichments._ - - implicit val openAPIServicesConfigVR: ValueReader[OpenAPIServicesConfig] = - ArbitraryTypeReader.arbitraryTypeValueReader[OpenAPIServicesConfig] - - implicit val regexReader: ValueReader[Regex] = (config: Config, path: String) => new Regex(config.getString(path)) - - implicit val openAPISecurityConfigVR: ValueReader[OpenAPISecurityConfig] = ValueReader.relative(conf => { - conf.as[String]("type") match { - case "apiKey" => conf.rootAs[ApiKeyConfig] - case typ => throw new Exception(s"Not supported swagger security type '$typ' in the configuration") - } - }) - - implicit val apiKeyConfigVR: ValueReader[ApiKeyConfig] = ValueReader.relative(conf => { - ApiKeyConfig( - apiKeyValue = conf.as[String]("apiKeyValue") - ) - }) - -} diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/ParseToSwaggerService.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/ParseToSwaggerService.scala index 546f764f4c1..040083c82f7 100644 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/ParseToSwaggerService.scala +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/ParseToSwaggerService.scala @@ -102,8 +102,7 @@ private[parser] class ParseToSwaggerService(openapi: OpenAPI, openAPIsConfig: Op Option(operation.getSecurity).orElse(Option(openapi.getSecurity)).map(_.asScala.toList).getOrElse(Nil) val securitySchemes = Option(openapi.getComponents).flatMap(c => Option(c.getSecuritySchemes)).map(_.asScala.toMap) - val securities = openAPIsConfig.security.getOrElse(Map.empty) - SecuritiesParser.parseSwaggerSecurities(securityRequirements, securitySchemes, securities) + SecuritiesParser.parseOperationSecurities(securityRequirements, securitySchemes, openAPIsConfig.securityConfig) } private def prepareParameters(operation: Operation): ValidationResult[List[SwaggerParameter]] = { diff --git a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/SecuritiesParser.scala b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/SecuritiesParser.scala index 148ddca2fd5..7a0e2a52699 100644 --- a/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/SecuritiesParser.scala +++ b/components/openapi/src/main/scala/pl/touk/nussknacker/openapi/parser/SecuritiesParser.scala @@ -7,11 +7,13 @@ import io.swagger.v3.oas.models.security.{SecurityRequirement, SecurityScheme} import pl.touk.nussknacker.engine.api.util.ReflectUtils import pl.touk.nussknacker.openapi.parser.ParseToSwaggerService.ValidationResult import pl.touk.nussknacker.openapi.{ - ApiKeyConfig, ApiKeyInCookie, ApiKeyInHeader, ApiKeyInQuery, - OpenAPISecurityConfig, + ApiKeySecret, + Secret, + SecurityConfig, + SecuritySchemeName, SwaggerSecurity } @@ -21,24 +23,24 @@ private[parser] object SecuritiesParser extends LazyLogging { import cats.syntax.apply._ - def parseSwaggerSecurities( - securityRequirements: List[SecurityRequirement], - securitySchemes: Option[Map[String, SecurityScheme]], - securitiesConfigs: Map[String, OpenAPISecurityConfig] + def parseOperationSecurities( + securityRequirementsDefinition: List[SecurityRequirement], + securitySchemesDefinition: Option[Map[String, SecurityScheme]], + securityConfig: SecurityConfig ): ValidationResult[List[SwaggerSecurity]] = - securityRequirements match { + securityRequirementsDefinition match { case Nil => Nil.validNel case _ => - securitySchemes match { + securitySchemesDefinition match { case None => "There is no security scheme definition in the openAPI definition".invalidNel case Some(securitySchemes) => { // finds the first security requirement that can be met by the config - securityRequirements.view + securityRequirementsDefinition.view .map { securityRequirement => matchSecuritiesForRequiredSchemes( securityRequirement.asScala.keys.toList, securitySchemes, - securitiesConfigs + securityConfig ) } .foldLeft("No security requirement can be met because:".invalidNel[List[SwaggerSecurity]])(_.findValid(_)) @@ -48,55 +50,59 @@ private[parser] object SecuritiesParser extends LazyLogging { } } - def matchSecuritiesForRequiredSchemes( + private def matchSecuritiesForRequiredSchemes( requiredSchemesNames: List[String], securitySchemes: Map[String, SecurityScheme], - securitiesConfigs: Map[String, OpenAPISecurityConfig] + securitiesConfig: SecurityConfig ): ValidationResult[List[SwaggerSecurity]] = - requiredSchemesNames - .map { implicit schemeName: String => - { - val securityScheme: ValidationResult[SecurityScheme] = Validated.fromOption( - securitySchemes.get(schemeName), - NonEmptyList.of(s"""there is no security scheme definition for scheme name "$schemeName"""") - ) - val securityConfig: ValidationResult[OpenAPISecurityConfig] = Validated.fromOption( - securitiesConfigs.get(schemeName), - NonEmptyList.of(s"""there is no security config for scheme name "$schemeName"""") - ) + requiredSchemesNames.map { schemeName => + { + val validatedSecurityScheme: ValidationResult[SecurityScheme] = Validated.fromOption( + securitySchemes.get(schemeName), + NonEmptyList.of(s"""there is no security scheme definition for scheme name "$schemeName"""") + ) + val validatedSecuritySecretConfigured: ValidationResult[Secret] = Validated.fromOption( + securitiesConfig.secret(SecuritySchemeName(schemeName)), + NonEmptyList.of(s"""there is no security secret configured for scheme name "$schemeName"""") + ) - (securityScheme, securityConfig).tupled.andThen(t => getSecurityFromSchemeAndConfig(t._1, t._2)) - } + (validatedSecurityScheme, validatedSecuritySecretConfigured) + .mapN { case (securityScheme, configuredSecret) => + getSecurityFromSchemeAndSecret(securityScheme, configuredSecret) + } + .andThen(identity) } - .foldLeft[ValidationResult[List[SwaggerSecurity]]](Nil.validNel)(_.combine(_)) + }.sequence - def getSecurityFromSchemeAndConfig(securityScheme: SecurityScheme, securityConfig: OpenAPISecurityConfig)( - implicit schemeName: String - ): ValidationResult[List[SwaggerSecurity]] = { + private def getSecurityFromSchemeAndSecret( + securityScheme: SecurityScheme, + secret: Secret + ): ValidationResult[SwaggerSecurity] = { import SecurityScheme.Type._ - (securityScheme.getType, securityConfig) match { - case (APIKEY, apiKeyConfig: ApiKeyConfig) => - getApiKeySecurity(securityScheme, apiKeyConfig) + (securityScheme.getType, secret) match { + case (APIKEY, apiKeySecret: ApiKeySecret) => + getApiKeySecurity(securityScheme, apiKeySecret).validNel case (otherType: SecurityScheme.Type, _) => { - val securityConfigClassName = ReflectUtils.simpleNameWithoutSuffix(securityConfig.getClass) - s"Security type $otherType is not supported yet or ($otherType, $securityConfigClassName) is a mismatch security scheme type and security config pair".invalidNel + val secretClassName = ReflectUtils.simpleNameWithoutSuffix(secret.getClass) + s"Security type $otherType is not supported yet or ($otherType, $secretClassName) is a mismatch security scheme type and security config pair".invalidNel } } } - def getApiKeySecurity(securityScheme: SecurityScheme, apiKeyConfig: ApiKeyConfig)( - implicit schemeName: String - ): ValidationResult[List[SwaggerSecurity]] = { + private def getApiKeySecurity( + securityScheme: SecurityScheme, + apiKeySecret: ApiKeySecret + ): SwaggerSecurity = { val name = securityScheme.getName - val key = apiKeyConfig.apiKeyValue + val key = apiKeySecret.apiKeyValue import SecurityScheme.In._ securityScheme.getIn match { case QUERY => - (ApiKeyInQuery(name, key) :: Nil).validNel + ApiKeyInQuery(name, key) case HEADER => - (ApiKeyInHeader(name, key) :: Nil).validNel + ApiKeyInHeader(name, key) case COOKIE => - (ApiKeyInCookie(name, key) :: Nil).validNel + ApiKeyInCookie(name, key) } } diff --git a/components/openapi/src/test/resources/swagger/multiple-schemes-for-single-operation.yml b/components/openapi/src/test/resources/swagger/multiple-schemes-for-single-operation.yml new file mode 100644 index 00000000000..50828f46a18 --- /dev/null +++ b/components/openapi/src/test/resources/swagger/multiple-schemes-for-single-operation.yml @@ -0,0 +1,36 @@ +openapi: "3.1.0" +info: + title: Simple API overview + version: 2.0.0 +servers: + - url: http://dummy.io +paths: + /: + get: + security: + - headerConfig: [] + - queryConfig: [] + - cookieConfig: [] + responses: + '200': + description: "-" + content: + application/json: + schema: + type: object + operationId: root +components: + schemas: {} + securitySchemes: + headerConfig: + type: apiKey + name: keyHeader + in: header + queryConfig: + type: apiKey + name: keyParam + in: query + cookieConfig: + type: apiKey + name: keyCookie + in: cookie diff --git a/components/openapi/src/test/resources/swagger/service-security.yml b/components/openapi/src/test/resources/swagger/service-security.yml index b2641b3c917..db96a36d113 100644 --- a/components/openapi/src/test/resources/swagger/service-security.yml +++ b/components/openapi/src/test/resources/swagger/service-security.yml @@ -16,7 +16,7 @@ paths: application/json: schema: type: object - operationId: header + operationId: headerOperationId /queryPath: get: security: @@ -28,7 +28,7 @@ paths: application/json: schema: type: object - operationId: query + operationId: queryOperationId /cookiePath: get: security: @@ -40,7 +40,7 @@ paths: application/json: schema: type: object - operationId: cookie + operationId: cookieOperationId components: schemas: {} securitySchemes: @@ -55,4 +55,4 @@ components: cookieConfig: type: apiKey name: keyCookie - in: cookie \ No newline at end of file + in: cookie diff --git a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/BaseOpenAPITest.scala b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/BaseOpenAPITest.scala index 88c76368c57..e4392b42363 100644 --- a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/BaseOpenAPITest.scala +++ b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/BaseOpenAPITest.scala @@ -4,12 +4,12 @@ import cats.data.Validated import cats.data.Validated.{Invalid, Valid} import org.apache.commons.io.IOUtils import pl.touk.nussknacker.engine.api.process.ComponentUseCase -import pl.touk.nussknacker.engine.api.{Context, ContextId, JobData, MetaData, ProcessVersion, StreamMetaData} +import pl.touk.nussknacker.engine.api._ import pl.touk.nussknacker.engine.util.runtimecontext.TestEngineRuntimeContext import pl.touk.nussknacker.engine.util.service.EagerServiceWithStaticParametersAndReturnType import pl.touk.nussknacker.openapi.enrichers.{SwaggerEnricherCreator, SwaggerEnrichers} import pl.touk.nussknacker.openapi.parser.{ServiceParseError, SwaggerParser} -import sttp.client3.testing.SttpBackendStub +import sttp.client3.SttpBackend import java.net.URL import java.nio.charset.StandardCharsets @@ -47,7 +47,7 @@ trait BaseOpenAPITest { protected def parseToEnrichers( resource: String, - backend: SttpBackendStub[Future, Any], + backend: SttpBackend[Future, Any], config: OpenAPIServicesConfig = baseConfig ): Map[ServiceName, EagerServiceWithStaticParametersAndReturnType] = { val services = parseServicesFromResourceUnsafe(resource, config) diff --git a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfigTest.scala b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfigTest.scala new file mode 100644 index 00000000000..6262fd013a3 --- /dev/null +++ b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIServicesConfigTest.scala @@ -0,0 +1,73 @@ +package pl.touk.nussknacker.openapi + +import com.typesafe.config.ConfigFactory +import org.scalatest.OptionValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class OpenAPIServicesConfigTest extends AnyFunSuite with Matchers with OptionValues { + + import net.ceedubs.ficus.Ficus._ + import OpenAPIServicesConfig._ + + test("should parse apikey secret for each scheme") { + val config = ConfigFactory.parseString("""url: "http://foo" + |security { + | apikeySecurityScheme { + | type: "apiKey" + | apiKeyValue: "34534asfdasf" + | } + | apikeySecurityScheme2 { + | type: "apiKey" + | apiKeyValue: "123" + | } + |}""".stripMargin) + + val parsedConfig = config.as[OpenAPIServicesConfig] + parsedConfig.securityConfig + .secret(SecuritySchemeName("apikeySecurityScheme")) + .value shouldEqual ApiKeySecret(apiKeyValue = "34534asfdasf") + parsedConfig.securityConfig + .secret(SecuritySchemeName("apikeySecurityScheme2")) + .value shouldEqual ApiKeySecret(apiKeyValue = "123") + } + + test("should parse common apikey secret for any scheme") { + val config = ConfigFactory.parseString("""url: "http://foo" + |secret { + | type: "apiKey" + | apiKeyValue: "34534asfdasf" + |}""".stripMargin) + + val parsedConfig = config.as[OpenAPIServicesConfig] + parsedConfig.securityConfig + .secret(SecuritySchemeName("someScheme")) + .value shouldEqual ApiKeySecret(apiKeyValue = "34534asfdasf") + parsedConfig.securityConfig + .secret(SecuritySchemeName("someOtherScheme")) + .value shouldEqual ApiKeySecret(apiKeyValue = "34534asfdasf") + } + + test("should parse combined apikey secret for each scheme and common apikey secret for any scheme") { + val config = ConfigFactory.parseString("""url: "http://foo" + |security { + | someScheme { + | type: "apiKey" + | apiKeyValue: "123" + | } + |} + |secret { + | type: "apiKey" + | apiKeyValue: "234" + |}""".stripMargin) + + val parsedConfig = config.as[OpenAPIServicesConfig] + parsedConfig.securityConfig + .secret(SecuritySchemeName("someScheme")) + .value shouldEqual ApiKeySecret(apiKeyValue = "123") + parsedConfig.securityConfig + .secret(SecuritySchemeName("someOtherScheme")) + .value shouldEqual ApiKeySecret(apiKeyValue = "234") + } + +} diff --git a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIsConfigTest.scala b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIsConfigTest.scala deleted file mode 100644 index a698eff89f2..00000000000 --- a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/OpenAPIsConfigTest.scala +++ /dev/null @@ -1,26 +0,0 @@ -package pl.touk.nussknacker.openapi - -import com.typesafe.config.ConfigFactory -import org.scalatest.OptionValues -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class OpenAPIsConfigTest extends AnyFunSuite with Matchers with OptionValues { - - import net.ceedubs.ficus.Ficus._ - import OpenAPIsConfig._ - - test("should parse apikey security") { - val config = ConfigFactory.parseString("""url: "http://foo" - |security { - | apikeySecuritySchema { - | type: "apiKey" - | apiKeyValue: "34534asfdasf" - | } - |}""".stripMargin) - - val parsedConfig = config.as[OpenAPIServicesConfig] - parsedConfig.security.value.get("apikeySecuritySchema").value shouldEqual ApiKeyConfig(apiKeyValue = "34534asfdasf") - } - -} diff --git a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/SecurityTest.scala b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/SecurityTest.scala index 28765d28616..e9de5f99285 100644 --- a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/SecurityTest.scala +++ b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/SecurityTest.scala @@ -3,89 +3,162 @@ package pl.touk.nussknacker.openapi import com.typesafe.scalalogging.LazyLogging import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, BeforeAndAfterAll} +import org.scalatest.{Assertion, BeforeAndAfterAll, LoneElement, TryValues} import pl.touk.nussknacker.engine.api.ContextId import pl.touk.nussknacker.engine.api.test.EmptyInvocationCollector.Instance import pl.touk.nussknacker.engine.api.typed.TypedMap import pl.touk.nussknacker.test.PatientScalaFutures import sttp.client3.testing.SttpBackendStub -import sttp.client3.{Request, Response} +import sttp.client3.{Request, Response, SttpBackend} import sttp.model.{HeaderNames, StatusCode} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future +import scala.util.Try class SecurityTest extends AnyFunSuite with BeforeAndAfterAll with Matchers + with TryValues + with LoneElement with LazyLogging with PatientScalaFutures with BaseOpenAPITest { - sealed case class Config( - path: String, - securityName: String, - serviceName: String, - key: String, - assertion: Request[_, _] => Assertion - ) + private class StubbedOperationLogic( + val operationId: String, + private val path: List[String], + val securitySchemeName: SecuritySchemeName, + val expectedSecret: ApiKeySecret, + val checkSecret: (Request[_, _], ApiKeySecret) => Assertion + ) { + + def handleMatchingRequest(request: Request[_, _]): Option[Try[Assertion]] = + Option(request).filter(requestMatches).map(_ => Try(checkSecret(request, expectedSecret))) - private val configs = List[Config]( - Config( - "headerPath", - "headerConfig", - "header", - "h1", - _.headers.find(_.name == "keyHeader").map(_.value) shouldBe Some("h1") + private def requestMatches(request: Request[_, _]) = { + request.uri.path == path + } + + } + + private val stubbedSecretCheckingLogics = List[StubbedOperationLogic]( + new StubbedOperationLogic( + "headerOperationId", + "headerPath" :: Nil, + SecuritySchemeName("headerConfig"), + ApiKeySecret("h1"), + (req, expectedSecret) => + req.headers.find(_.name == "keyHeader").map(_.value) shouldBe Some(expectedSecret.apiKeyValue) + ), + new StubbedOperationLogic( + "queryOperationId", + "queryPath" :: Nil, + SecuritySchemeName("queryConfig"), + ApiKeySecret("q1"), + (req, expectedSecret) => req.uri.params.get("keyParam") shouldBe Some(expectedSecret.apiKeyValue) ), - Config("queryPath", "queryConfig", "query", "q1", _.uri.params.get("keyParam") shouldBe Some("q1")), - Config( - "cookiePath", - "cookieConfig", - "cookie", - "c1", - _.headers.find(_.name == HeaderNames.Cookie).map(_.value) shouldBe Some("keyCookie=c1") + new StubbedOperationLogic( + "cookieOperationId", + "cookiePath" :: Nil, + SecuritySchemeName("cookieConfig"), + ApiKeySecret("c1"), + (req, expectedSecret) => + req.headers.find(_.name == HeaderNames.Cookie).map(_.value) shouldBe Some( + s"keyCookie=${expectedSecret.apiKeyValue}" + ) ), ) - test("service returns customers") { - val backend = SttpBackendStub.asynchronousFuture - .whenRequestMatches { request => - val pathMatches = configs.find(_.path == request.uri.path.head) - pathMatches.foreach(_.assertion(request)) - pathMatches.isDefined + private val definitionMatchingStubbedLogic = "service-security.yml" + + val backend: SttpBackend[Future, Any] = SttpBackendStub.asynchronousFuture.whenAnyRequest + .thenRespondF { request => + Future { + val operationsLogicResults = + stubbedSecretCheckingLogics + .flatMap(logic => logic.handleMatchingRequest(request).map(logic.operationId -> _)) + .toMap + operationsLogicResults.loneElement._2.success.value + Response("{}", StatusCode.Ok) } - .thenRespond(Response("{}", StatusCode.Ok)) + } - val withCorrectConfig = - enrichersForSecurityConfig(backend, configs.map(c => c.securityName -> ApiKeyConfig(c.key)).toMap) - configs.foreach { config => - withClue(config.serviceName) { + test("secret configured for each scheme in definition") { + val enricherWithCorrectConfig = + parseToEnrichers( + definitionMatchingStubbedLogic, + backend, + baseConfig.copy( + security = stubbedSecretCheckingLogics.map(c => c.securitySchemeName -> c.expectedSecret).toMap, + ) + ) + stubbedSecretCheckingLogics.foreach { logic => + withClue(logic.operationId) { implicit val contextId: ContextId = ContextId("1") - withCorrectConfig(ServiceName(config.serviceName)) + enricherWithCorrectConfig(ServiceName(logic.operationId)) .invoke(Map.empty) .futureValue shouldBe TypedMap(Map.empty) } } - val withBadConfig = - enrichersForSecurityConfig(backend, configs.map(c => c.securityName -> ApiKeyConfig("bla")).toMap) - configs.foreach { config => - withClue(config.serviceName) { + val enricherWithBadConfig = + parseToEnrichers( + definitionMatchingStubbedLogic, + backend, + baseConfig.copy( + security = stubbedSecretCheckingLogics.map(c => c.securitySchemeName -> ApiKeySecret("bla")).toMap, + ) + ) + stubbedSecretCheckingLogics.foreach { logic => + withClue(logic.operationId) { intercept[Exception] { implicit val contextId: ContextId = ContextId("1") - withBadConfig(ServiceName(config.serviceName)).invoke(Map.empty).futureValue + enricherWithBadConfig(ServiceName(logic.operationId)).invoke(Map.empty).futureValue } } } } - private def enrichersForSecurityConfig( - backend: SttpBackendStub[Future, Any], - securities: Map[String, ApiKeyConfig] - ) = { - parseToEnrichers("service-security.yml", backend, baseConfig.copy(security = Some(securities))) + test("common secret configured for any scheme") { + stubbedSecretCheckingLogics.foreach { config => + withClue(config.operationId) { + val enricherWithSingleSecurityConfig = parseToEnrichers( + definitionMatchingStubbedLogic, + backend, + baseConfig.copy(secret = Some(config.expectedSecret)) + ) + implicit val contextId: ContextId = ContextId("1") + enricherWithSingleSecurityConfig(ServiceName(config.operationId)) + .invoke(Map.empty) + .futureValue shouldBe TypedMap(Map.empty) + } + } + } + + test("common secret configured for any scheme with one operation handling multiple security schemes") { + val secretMatchesEveryScheme = ApiKeySecret("single-secret") + val backend = SttpBackendStub.asynchronousFuture.whenAnyRequest + .thenRespondF { request => + Future { + val operationsLogicResults = + stubbedSecretCheckingLogics + .map(logic => logic.operationId -> Try(logic.checkSecret(request, secretMatchesEveryScheme))) + .toMap + operationsLogicResults.filter(_._2.isSuccess) should have size 1 + Response("{}", StatusCode.Ok) + } + } + val enricherWithSingleSecurityConfig = parseToEnrichers( + "multiple-schemes-for-single-operation.yml", + backend, + baseConfig.copy(secret = Some(secretMatchesEveryScheme)) + ) + implicit val contextId: ContextId = ContextId("1") + enricherWithSingleSecurityConfig(ServiceName("root")) + .invoke(Map.empty) + .futureValue shouldBe TypedMap(Map.empty) } } diff --git a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/parser/SwaggerParserTest.scala b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/parser/SwaggerParserTest.scala index 52149521331..0c9adf8bd94 100644 --- a/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/parser/SwaggerParserTest.scala +++ b/components/openapi/src/test/scala/pl/touk/nussknacker/openapi/parser/SwaggerParserTest.scala @@ -107,7 +107,7 @@ class SwaggerParserTest extends AnyFunSuite with BaseOpenAPITest with Matchers { errorsFor("noResponseType") shouldBe List("No response with application/json or */* media types found") errorsFor("unhandledSecurity") shouldBe List( - "No security requirement can be met because: there is no security config for scheme name \"headerConfig\"" + "No security requirement can be met because: there is no security secret configured for scheme name \"headerConfig\"" ) errorsFor("unhandledFormat") shouldBe List("Type 'number' in format 'decimal' is not supported") diff --git a/components/sql/src/main/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricher.scala b/components/sql/src/main/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricher.scala index 9b4f525235e..457ff3a92ae 100644 --- a/components/sql/src/main/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricher.scala +++ b/components/sql/src/main/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricher.scala @@ -41,10 +41,7 @@ object DatabaseQueryEnricher { final val queryParamName: ParameterName = ParameterName("Query") - final val queryParamDeclaration = - ParameterDeclaration - .mandatory[String](queryParamName) - .withCreator(modify = _.copy(editor = Some(SqlParameterEditor))) + final val queryParam = Parameter[String](queryParamName).copy(editor = Some(SqlParameterEditor)) final val resultStrategyParamName: ParameterName = ParameterName("Result strategy") @@ -132,7 +129,7 @@ class DatabaseQueryEnricher(val dbPoolConfig: DBPoolConfig, val dbMetaDataProvid ): ContextTransformationDefinition = { case TransformationStep(Nil, _) => NextParameters(parameters = resultStrategyParamDeclaration.createParameter() :: - queryParamDeclaration.createParameter() :: + queryParam :: cacheTTLParamDeclaration.createParameter() :: Nil ) } @@ -142,14 +139,15 @@ class DatabaseQueryEnricher(val dbPoolConfig: DBPoolConfig, val dbMetaDataProvid ): ContextTransformationDefinition = { case TransformationStep( (`resultStrategyParamName`, DefinedEagerParameter(strategyName: String, _)) :: - (`queryParamName`, DefinedEagerParameter(query: String, _)) :: + (`queryParamName`, DefinedEagerParameter(query: TemplateEvaluationResult, _)) :: (`cacheTTLParamName`, _) :: Nil, None ) => - if (query.isEmpty) { + val renderedQuery = query.renderedTemplate + if (renderedQuery.isEmpty) { FinalResults(context, errors = CustomNodeError("Query is missing", Some(queryParamName)) :: Nil, state = None) } else { - parseQuery(context, dependencies, strategyName, query) + parseQuery(context, dependencies, strategyName, renderedQuery) } } diff --git a/components/sql/src/test/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricherValidationTest.scala b/components/sql/src/test/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricherValidationTest.scala index 1b7c63d4766..62a2873475a 100644 --- a/components/sql/src/test/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricherValidationTest.scala +++ b/components/sql/src/test/scala/pl/touk/nussknacker/sql/service/DatabaseQueryEnricherValidationTest.scala @@ -1,10 +1,11 @@ package pl.touk.nussknacker.sql.service +import pl.touk.nussknacker.engine.api.TemplateRenderedPart.RenderedLiteral import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomNodeError import pl.touk.nussknacker.engine.api.context.transformation.{DefinedEagerParameter, OutputVariableNameValue} import pl.touk.nussknacker.engine.api.context.{OutputVar, ValidationContext} import pl.touk.nussknacker.engine.api.typed.typing.{Typed, Unknown} -import pl.touk.nussknacker.engine.api.NodeId +import pl.touk.nussknacker.engine.api.{NodeId, TemplateEvaluationResult} import pl.touk.nussknacker.sql.db.query.{ResultSetStrategy, SingleResultStrategy} import pl.touk.nussknacker.sql.db.schema.MetaDataProviderFactory import pl.touk.nussknacker.sql.utils.BaseHsqlQueryEnricherTest @@ -32,8 +33,10 @@ class DatabaseQueryEnricherValidationTest extends BaseHsqlQueryEnricherTest { service.TransformationStep( List( DatabaseQueryEnricher.resultStrategyParamName -> eagerValueParameter(SingleResultStrategy.name), - DatabaseQueryEnricher.queryParamName -> eagerValueParameter("select from"), - DatabaseQueryEnricher.cacheTTLParamName -> eagerValueParameter(Duration.ofMinutes(1)), + DatabaseQueryEnricher.queryParamName -> eagerValueParameter( + TemplateEvaluationResult(List(RenderedLiteral("select from"))) + ), + DatabaseQueryEnricher.cacheTTLParamName -> eagerValueParameter(Duration.ofMinutes(1)), ), None ) @@ -62,8 +65,10 @@ class DatabaseQueryEnricherValidationTest extends BaseHsqlQueryEnricherTest { service.TransformationStep( List( DatabaseQueryEnricher.resultStrategyParamName -> eagerValueParameter(ResultSetStrategy.name), - DatabaseQueryEnricher.queryParamName -> eagerValueParameter("select * from persons"), - DatabaseQueryEnricher.cacheTTLParamName -> eagerValueParameter(Duration.ofMinutes(1)), + DatabaseQueryEnricher.queryParamName -> eagerValueParameter( + TemplateEvaluationResult(List(RenderedLiteral("select * from persons"))) + ), + DatabaseQueryEnricher.cacheTTLParamName -> eagerValueParameter(Duration.ofMinutes(1)), ), None ) diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png index 2faa0fce27b..4e0a37340f9 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by processing mode #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by processing mode #1.png index 9fbb630e47d..2369f45b904 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by processing mode #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by processing mode #1.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png index 8f79298458d..6d203e6054a 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png index a7f073a9601..6e56d9e4090 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png index 5862bd56258..31cf1a2867f 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png index 70f95efa447..5bfe842d262 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png index e5cc8a1b1b0..1b9e57a924c 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png index 821872447cf..06ac67c0a84 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png index 79f4a5c188a..338d51142ac 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png index 5584ba90ae7..dc24b2f8870 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png index 8bfa0387e8b..933137660d6 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Fragment should allow adding input parameters and display used fragment graph in modal #4.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Fragment should allow adding input parameters and display used fragment graph in modal #4.png index b6f7e64d25a..2e0af0a8267 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Fragment should allow adding input parameters and display used fragment graph in modal #4.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Fragment should allow adding input parameters and display used fragment graph in modal #4.png differ diff --git a/designer/client/cypress/e2e/components.cy.ts b/designer/client/cypress/e2e/components.cy.ts index be96c802b1c..88587a19555 100644 --- a/designer/client/cypress/e2e/components.cy.ts +++ b/designer/client/cypress/e2e/components.cy.ts @@ -247,7 +247,7 @@ describe("Components list", () => { }); // Sort by processing mode - cy.get("[role='columnheader'][aria-label='Processing modes']").dblclick({ force: true }); + cy.get("[role='columnheader'][data-field='allowedProcessingModes']").dblclick({ force: true }); cy.get("#app-container>main").matchImage(); }); diff --git a/designer/client/cypress/e2e/connectionError.cy.ts b/designer/client/cypress/e2e/connectionError.cy.ts index 5e0aef6aca9..26f45a48907 100644 --- a/designer/client/cypress/e2e/connectionError.cy.ts +++ b/designer/client/cypress/e2e/connectionError.cy.ts @@ -59,13 +59,13 @@ describe("Connection error", () => { cy.get("[model-id='filter']").dblclick(); cy.wait("@validation"); - cy.intercept("/api/notifications", { statusCode: 502 }); + cy.intercept("/api/notifications?scenarioName=*", { statusCode: 502 }); cy.contains(/Backend connection issue/).should("be.visible"); cy.get("body").matchImage({ screenshotConfig, }); - cy.intercept("/api/notifications", (req) => { + cy.intercept("/api/notifications?scenarioName=*", (req) => { req.continue(); }); @@ -82,12 +82,12 @@ describe("Connection error", () => { const statusIntervalTick = 10000; const visibleStatusToastMessageBeforeConnectionError = () => { - cy.intercept("/api/processes/*/status", { statusCode: 502 }); + cy.intercept("/api/processes/*/status?currentlyPresentedVersionId=*", { statusCode: 502 }); // Check if the status toast message is not visible after the backend connection issue. We need to speed up interval to not wait 12s for a request cy.contains(/Cannot fetch status/).should("be.visible"); - cy.intercept("/api/processes/*/status", (req) => { + cy.intercept("/api/processes/*/status?currentlyPresentedVersionId=*", (req) => { req.continue(); }); }; diff --git a/designer/client/cypress/e2e/description.cy.ts b/designer/client/cypress/e2e/description.cy.ts index ff4c3632fa0..8c597ca1550 100644 --- a/designer/client/cypress/e2e/description.cy.ts +++ b/designer/client/cypress/e2e/description.cy.ts @@ -22,9 +22,7 @@ describe("Description", () => { it("should display markdown", () => { cy.get(`[title="toggle description view"]`).should("not.exist"); - cy.contains(/^properties$/i) - .should("be.enabled") - .dblclick(); + cy.contains(/^properties$/i).click(); cy.get("[data-testid=window]").should("be.visible").as("window"); cy.get("[data-testid=window]").contains("Description").next().find(".ace_editor").should("be.visible").click("center") diff --git a/designer/client/cypress/e2e/fragment.cy.ts b/designer/client/cypress/e2e/fragment.cy.ts index 7545c06fcf9..5a9cf09f0e7 100644 --- a/designer/client/cypress/e2e/fragment.cy.ts +++ b/designer/client/cypress/e2e/fragment.cy.ts @@ -158,6 +158,11 @@ describe("Fragment", () => { .click(); cy.get("[id$='option-1']").click({ force: true }); + // Provide String Fixed value inputMode + cy.get("@window").contains("+").click(); + cy.get("[data-testid='fieldsRow:8']").find("[placeholder='Field name']").type("generic_type"); + cy.get("[data-testid='fieldsRow:8']").contains("String").click().type("List[String]{enter}"); + cy.get("@window") .contains(/^apply$/i) .click(); @@ -281,7 +286,7 @@ describe("Fragment", () => { cy.get("[model-id=input]").should("be.visible").trigger("dblclick"); cy.get("[data-testid=window]").should("be.visible").as("window"); cy.get("@window").contains("+").click(); - cy.get("[data-testid='fieldsRow:8']").find("[placeholder='Field name']").type("test5"); + cy.get("[data-testid='fieldsRow:9']").find("[placeholder='Field name']").type("test5"); cy.get("@window") .contains(/^apply$/i) .click(); diff --git a/designer/client/docker-compose.yml b/designer/client/docker-compose.yml index 9c959007965..95c5739190d 100644 --- a/designer/client/docker-compose.yml +++ b/designer/client/docker-compose.yml @@ -20,7 +20,7 @@ services: redpanda: container_name: cypress_e2e_redpanda - image: vectorized/redpanda:v23.3.12 + image: redpandadata/redpanda:v23.3.12 command: - redpanda start - --smp 1 diff --git a/designer/client/package-lock.json b/designer/client/package-lock.json index 6bc67642915..3dcb992fd53 100644 --- a/designer/client/package-lock.json +++ b/designer/client/package-lock.json @@ -18,10 +18,11 @@ "@hello-pangea/dnd": "16.6.0", "@juggle/resize-observer": "3.3.1", "@loadable/component": "5.15.2", - "@mui/icons-material": "5.15.7", - "@mui/lab": "5.0.0-alpha.165", - "@mui/material": "5.15.7", - "@touk/federated-component": "1.0.0", + "@mui/icons-material": "5.16.7", + "@mui/lab": "5.0.0-alpha.173", + "@mui/material": "5.16.7", + "@mui/system": "5.16.7", + "@touk/federated-component": "1.1.0", "@touk/window-manager": "1.9.1", "ace-builds": "1.34.2", "axios": "1.7.5", @@ -165,7 +166,7 @@ "babel-loader": "8.2.3", "babel-plugin-istanbul": "6.1.1", "chalk": "4.1.2", - "chevrotain": "11.0.3", + "chevrotain": "10.5.0", "color": "4.2.0", "copy-webpack-plugin": "11.0.0", "crypto-browserify": "3.12.0", @@ -2244,9 +2245,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2307,42 +2308,36 @@ "dev": true }, "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", "dev": true, "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" } }, "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", "dev": true, "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" } }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "dev": true - }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", "dev": true }, "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", "dev": true }, "node_modules/@colors/colors": { @@ -2629,17 +2624,27 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/cache/node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@emotion/cache/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -2668,9 +2673,9 @@ } }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", @@ -2734,21 +2739,26 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "node_modules/@emotion/styled": { "version": "11.11.5", @@ -2773,9 +2783,9 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -2786,9 +2796,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.1", @@ -3252,28 +3262,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.2.tgz", - "integrity": "sha512-xymkSSowKdGqo0SRr2Mp4czH5A8o2Pum35PAD0ftb3gCcPacWzwhvtUeUqmVXm9EVtm2hThD/lRrFNcahMOaSQ==", + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -3281,9 +3294,10 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" }, "node_modules/@fontsource/inter": { "version": "5.0.16", @@ -4464,14 +4478,15 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.34", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz", - "integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.7", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", "clsx": "^2.1.0", "prop-types": "^15.8.1" @@ -4495,26 +4510,29 @@ } }, "node_modules/@mui/base/node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.9.tgz", - "integrity": "sha512-CSDpVevGaxsvMkiYBZ8ztki1z/eT0mM2MqUT21eCRiMz3DU4zQw5rXG5ML/yTuJF9Z2Wv9SliIeaRAuSR/9Nig==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.15.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.7.tgz", - "integrity": "sha512-EDAc8TVJGIA/imAvR3u4nANl2W5h3QeHieu2gK7Ypez/nIA55p08tHjf8UrMXEpxCAvfZO6piY9S9uaxETdicA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -4537,15 +4555,16 @@ } }, "node_modules/@mui/lab": { - "version": "5.0.0-alpha.165", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.165.tgz", - "integrity": "sha512-8/zJStT10nh9yrAzLOPTICGhpf5YiGp/JpM0bdTP7u5AE+YT+X2u6QwMxuCrVeW8/WVLAPFg0vtzyfgPcN5T7g==", + "version": "5.0.0-alpha.173", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", + "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.36", - "@mui/system": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.16.5", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.5", "clsx": "^2.1.0", "prop-types": "^15.8.1" }, @@ -4576,37 +4595,6 @@ } } }, - "node_modules/@mui/lab/node_modules/@mui/base": { - "version": "5.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz", - "integrity": "sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/lab/node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -4616,21 +4604,22 @@ } }, "node_modules/@mui/material": { - "version": "5.15.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz", - "integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.34", - "@mui/core-downloads-tracker": "^5.15.7", - "@mui/system": "^5.15.7", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.7", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", - "csstype": "^3.1.2", + "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.2.0", + "react-is": "^18.3.1", "react-transition-group": "^4.4.5" }, "engines": { @@ -4676,17 +4665,19 @@ } }, "node_modules/@mui/material/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.9.tgz", - "integrity": "sha512-/aMJlDOxOTAXyp4F2rIukW1O0anodAMCkv1DfBh/z9vaKHY3bd5fFf42wmP+0GRmwMinC5aWPpNfHXOED1fEtg==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.9", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" }, "engines": { @@ -4707,9 +4698,10 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.9.tgz", - "integrity": "sha512-NRKtYkL5PZDH7dEmaLEIiipd3mxNnQSO+Yo8rFNBNptY8wzQnQ+VjayTq39qH7Sast5cwHKYFusUrQyD+SS4Og==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -4738,15 +4730,16 @@ } }, "node_modules/@mui/system": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.9.tgz", - "integrity": "sha512-SxkaaZ8jsnIJ77bBXttfG//LUf6nTfOcaOuIgItqfHv60ZCQy/Hu7moaob35kBb+guxVJnoSZ+7vQJrA/E7pKg==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.9", - "@mui/styled-engine": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -4777,19 +4770,20 @@ } }, "node_modules/@mui/system/node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@mui/types": { - "version": "7.2.13", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -4798,14 +4792,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.9.tgz", - "integrity": "sha512-yDYfr61bCYUz1QtwvpqYy/3687Z8/nS4zv7lv/ih/6ZFGMl1iolEvxRmR84v2lOYxlds+kq1IVYbXxDKh8Z9sg==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" @@ -4824,10 +4820,18 @@ } } }, + "node_modules/@mui/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/@mui/utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", @@ -6109,10 +6113,9 @@ } }, "node_modules/@touk/federated-component": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@touk/federated-component/-/federated-component-1.0.0.tgz", - "integrity": "sha512-ibliSr5T1pbM8S8M0NqUGJlBQJSmBeDTuO1fcPRQ97XO+Ai+9tQ00qUaCr0tI43cdTXLlkunXW+K5IlM4Krx+Q==", - "hasInstallScript": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@touk/federated-component/-/federated-component-1.1.0.tgz", + "integrity": "sha512-pYHL8d4RWxONtW3+PJT/1AzaS9cmtyKLd9VXhEM+dIL0DMIe2IkvB12W/yZUBeB1mW8bWlG/vpthL0lD8yV44Q==", "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" @@ -6528,9 +6531,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz", - "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -6711,9 +6714,9 @@ } }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -9554,17 +9557,17 @@ } }, "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", "dev": true, "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" } }, "node_modules/chokidar": { @@ -23406,6 +23409,12 @@ "node": ">=0.10.0" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", @@ -30044,9 +30053,9 @@ } }, "@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -30095,42 +30104,36 @@ "dev": true }, "@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", "dev": true, "requires": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" } }, "@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", "dev": true, "requires": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" } }, - "@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "dev": true - }, "@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", "dev": true }, "@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", "dev": true }, "@colors/colors": { @@ -30375,17 +30378,27 @@ } }, "@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "requires": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" }, "dependencies": { + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -30416,9 +30429,9 @@ } }, "@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "@emotion/is-prop-valid": { "version": "1.2.2", @@ -30462,21 +30475,28 @@ } }, "@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "requires": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" + }, + "dependencies": { + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + } } }, "@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "@emotion/styled": { "version": "11.11.5", @@ -30492,9 +30512,9 @@ } }, "@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -30502,9 +30522,9 @@ "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" }, "@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" }, "@emotion/weak-memoize": { "version": "0.3.1", @@ -30738,34 +30758,34 @@ } }, "@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", "requires": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.8" } }, "@floating-ui/dom": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.2.tgz", - "integrity": "sha512-xymkSSowKdGqo0SRr2Mp4czH5A8o2Pum35PAD0ftb3gCcPacWzwhvtUeUqmVXm9EVtm2hThD/lRrFNcahMOaSQ==", + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", "requires": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" } }, "@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "requires": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.0.0" } }, "@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "@fontsource/inter": { "version": "5.0.16", @@ -31664,67 +31684,53 @@ } }, "@mui/base": { - "version": "5.0.0-beta.34", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz", - "integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", "requires": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.7", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", "clsx": "^2.1.0", "prop-types": "^15.8.1" }, "dependencies": { "clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" } } }, "@mui/core-downloads-tracker": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.9.tgz", - "integrity": "sha512-CSDpVevGaxsvMkiYBZ8ztki1z/eT0mM2MqUT21eCRiMz3DU4zQw5rXG5ML/yTuJF9Z2Wv9SliIeaRAuSR/9Nig==" + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==" }, "@mui/icons-material": { - "version": "5.15.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.7.tgz", - "integrity": "sha512-EDAc8TVJGIA/imAvR3u4nANl2W5h3QeHieu2gK7Ypez/nIA55p08tHjf8UrMXEpxCAvfZO6piY9S9uaxETdicA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", "requires": { "@babel/runtime": "^7.23.9" } }, "@mui/lab": { - "version": "5.0.0-alpha.165", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.165.tgz", - "integrity": "sha512-8/zJStT10nh9yrAzLOPTICGhpf5YiGp/JpM0bdTP7u5AE+YT+X2u6QwMxuCrVeW8/WVLAPFg0vtzyfgPcN5T7g==", + "version": "5.0.0-alpha.173", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", + "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", "requires": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.36", - "@mui/system": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.16.5", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.5", "clsx": "^2.1.0", "prop-types": "^15.8.1" }, "dependencies": { - "@mui/base": { - "version": "5.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz", - "integrity": "sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==", - "requires": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - } - }, "clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -31733,21 +31739,21 @@ } }, "@mui/material": { - "version": "5.15.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz", - "integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", "requires": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.34", - "@mui/core-downloads-tracker": "^5.15.7", - "@mui/system": "^5.15.7", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.7", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", - "csstype": "^3.1.2", + "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.2.0", + "react-is": "^18.3.1", "react-transition-group": "^4.4.5" }, "dependencies": { @@ -31765,26 +31771,26 @@ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" } } }, "@mui/private-theming": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.9.tgz", - "integrity": "sha512-/aMJlDOxOTAXyp4F2rIukW1O0anodAMCkv1DfBh/z9vaKHY3bd5fFf42wmP+0GRmwMinC5aWPpNfHXOED1fEtg==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", "requires": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.9", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" } }, "@mui/styled-engine": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.9.tgz", - "integrity": "sha512-NRKtYkL5PZDH7dEmaLEIiipd3mxNnQSO+Yo8rFNBNptY8wzQnQ+VjayTq39qH7Sast5cwHKYFusUrQyD+SS4Og==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", "requires": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -31793,47 +31799,54 @@ } }, "@mui/system": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.9.tgz", - "integrity": "sha512-SxkaaZ8jsnIJ77bBXttfG//LUf6nTfOcaOuIgItqfHv60ZCQy/Hu7moaob35kBb+guxVJnoSZ+7vQJrA/E7pKg==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", "requires": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.9", - "@mui/styled-engine": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "dependencies": { "clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" } } }, "@mui/types": { - "version": "7.2.13", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==" + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==" }, "@mui/utils": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.9.tgz", - "integrity": "sha512-yDYfr61bCYUz1QtwvpqYy/3687Z8/nS4zv7lv/ih/6ZFGMl1iolEvxRmR84v2lOYxlds+kq1IVYbXxDKh8Z9sg==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "requires": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "dependencies": { + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" } } }, @@ -32692,9 +32705,9 @@ "dev": true }, "@touk/federated-component": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@touk/federated-component/-/federated-component-1.0.0.tgz", - "integrity": "sha512-ibliSr5T1pbM8S8M0NqUGJlBQJSmBeDTuO1fcPRQ97XO+Ai+9tQ00qUaCr0tI43cdTXLlkunXW+K5IlM4Krx+Q==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@touk/federated-component/-/federated-component-1.1.0.tgz", + "integrity": "sha512-pYHL8d4RWxONtW3+PJT/1AzaS9cmtyKLd9VXhEM+dIL0DMIe2IkvB12W/yZUBeB1mW8bWlG/vpthL0lD8yV44Q==" }, "@touk/federated-types": { "version": "1.1.0", @@ -33081,9 +33094,9 @@ } }, "@types/jest": { - "version": "29.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz", - "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -33250,9 +33263,9 @@ } }, "@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" }, "@types/qs": { "version": "6.9.7", @@ -35452,17 +35465,17 @@ } }, "chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", "dev": true, "requires": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" } }, "chokidar": { @@ -45811,6 +45824,12 @@ } } }, + "regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true + }, "regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", diff --git a/designer/client/package.json b/designer/client/package.json index 1ed26f678d2..fe9d05ce7c8 100644 --- a/designer/client/package.json +++ b/designer/client/package.json @@ -11,10 +11,11 @@ "@hello-pangea/dnd": "16.6.0", "@juggle/resize-observer": "3.3.1", "@loadable/component": "5.15.2", - "@mui/icons-material": "5.15.7", - "@mui/lab": "5.0.0-alpha.165", - "@mui/material": "5.15.7", - "@touk/federated-component": "1.0.0", + "@mui/icons-material": "5.16.7", + "@mui/lab": "5.0.0-alpha.173", + "@mui/material": "5.16.7", + "@mui/system": "5.16.7", + "@touk/federated-component": "1.1.0", "@touk/window-manager": "1.9.1", "ace-builds": "1.34.2", "axios": "1.7.5", @@ -158,7 +159,7 @@ "babel-loader": "8.2.3", "babel-plugin-istanbul": "6.1.1", "chalk": "4.1.2", - "chevrotain": "11.0.3", + "chevrotain": "10.5.0", "color": "4.2.0", "copy-webpack-plugin": "11.0.0", "crypto-browserify": "3.12.0", diff --git a/designer/client/src/actions/actionTypes.ts b/designer/client/src/actions/actionTypes.ts index eb4d106d699..306aaea65fc 100644 --- a/designer/client/src/actions/actionTypes.ts +++ b/designer/client/src/actions/actionTypes.ts @@ -39,4 +39,5 @@ export type ActionTypes = | "PROCESS_VERSIONS_LOADED" | "UPDATE_BACKEND_NOTIFICATIONS" | "MARK_BACKEND_NOTIFICATION_READ" - | "ARCHIVED"; + | "ARCHIVED" + | "EDIT_PROPERTIES"; diff --git a/designer/client/src/actions/nk/calculateProcessAfterChange.ts b/designer/client/src/actions/nk/calculateProcessAfterChange.ts index b7bc80c9b43..f47595679b0 100644 --- a/designer/client/src/actions/nk/calculateProcessAfterChange.ts +++ b/designer/client/src/actions/nk/calculateProcessAfterChange.ts @@ -1,44 +1,16 @@ -import NodeUtils from "../../components/graph/NodeUtils"; -import { fetchProcessDefinition } from "./processDefinitionData"; import { getProcessDefinitionData } from "../../reducers/selectors/settings"; import { mapProcessWithNewNode, replaceNodeOutputEdges } from "../../components/graph/utils/graphUtils"; -import { alignFragmentWithSchema } from "../../components/graph/utils/fragmentSchemaAligner"; -import { Edge, NodeType, ScenarioGraph, ProcessDefinitionData, ScenarioGraphWithName } from "../../types"; +import { Edge, NodeType, ScenarioGraphWithName } from "../../types"; import { ThunkAction } from "../reduxTypes"; import { Scenario } from "../../components/Process/types"; -function alignFragmentsNodeWithSchema(scenarioGraph: ScenarioGraph, processDefinitionData: ProcessDefinitionData): ScenarioGraph { - return { - ...scenarioGraph, - nodes: scenarioGraph.nodes.map((node) => { - return node.type === "FragmentInput" ? alignFragmentWithSchema(processDefinitionData, node) : node; - }), - }; -} - export function calculateProcessAfterChange( scenario: Scenario, before: NodeType, after: NodeType, outputEdges: Edge[], ): ThunkAction> { - return async (dispatch, getState) => { - if (NodeUtils.nodeIsProperties(after)) { - const processDefinitionData = await dispatch(fetchProcessDefinition(scenario.processingType, scenario.isFragment)); - const processWithNewFragmentSchema = alignFragmentsNodeWithSchema(scenario.scenarioGraph, processDefinitionData); - // TODO: We shouldn't keep scenario name in properties.id - it is a top-level scenario property - if (after.id !== before.id) { - dispatch({ type: "PROCESS_RENAME", name: after.id }); - } - - const { id, ...properties } = after; - - return { - processName: after.id, - scenarioGraph: { ...processWithNewFragmentSchema, properties }, - }; - } - + return async (_, getState) => { let changedProcess = scenario.scenarioGraph; if (outputEdges) { const processDefinitionData = getProcessDefinitionData(getState()); @@ -54,7 +26,7 @@ export function calculateProcessAfterChange( } return { - processName: scenario.scenarioGraph.properties.id || scenario.name, + processName: scenario.name, scenarioGraph: mapProcessWithNewNode(changedProcess, before, after), }; }; diff --git a/designer/client/src/actions/nk/editNode.ts b/designer/client/src/actions/nk/editNode.ts index b765adbaf42..1886f8e48c1 100644 --- a/designer/client/src/actions/nk/editNode.ts +++ b/designer/client/src/actions/nk/editNode.ts @@ -12,10 +12,6 @@ export type EditNodeAction = { validationResult: ValidationResult; scenarioGraphAfterChange: ScenarioGraph; }; -export type RenameProcessAction = { - type: "PROCESS_RENAME"; - name: string; -}; export type EditScenarioLabels = { type: "EDIT_LABELS"; diff --git a/designer/client/src/actions/nk/editProperties.ts b/designer/client/src/actions/nk/editProperties.ts new file mode 100644 index 00000000000..31bc51193d0 --- /dev/null +++ b/designer/client/src/actions/nk/editProperties.ts @@ -0,0 +1,58 @@ +import { ProcessDefinitionData, PropertiesType, ScenarioGraph, ScenarioGraphWithName, ValidationResult } from "../../types"; +import { alignFragmentWithSchema } from "../../components/graph/utils/fragmentSchemaAligner"; +import { fetchProcessDefinition } from "./processDefinitionData"; +import { Scenario } from "../../components/Process/types"; +import HttpService from "../../http/HttpService"; +import { ThunkAction } from "../reduxTypes"; + +type EditPropertiesAction = { + type: "EDIT_PROPERTIES"; + validationResult: ValidationResult; + scenarioGraphAfterChange: ScenarioGraph; +}; + +type RenameProcessAction = { + type: "PROCESS_RENAME"; + name: string; +}; + +export type PropertiesActions = EditPropertiesAction | RenameProcessAction; + +// TODO: We synchronize fragment changes with a scenario in case of properties changes. We need to find a better way to hande it +function alignFragmentsNodeWithSchema(scenarioGraph: ScenarioGraph, processDefinitionData: ProcessDefinitionData): ScenarioGraph { + return { + ...scenarioGraph, + nodes: scenarioGraph.nodes.map((node) => { + return node.type === "FragmentInput" ? alignFragmentWithSchema(processDefinitionData, node) : node; + }), + }; +} + +const calculateProperties = (scenario: Scenario, changedProperties: PropertiesType): ThunkAction> => { + return async (dispatch) => { + const processDefinitionData = await dispatch(fetchProcessDefinition(scenario.processingType, scenario.isFragment)); + const processWithNewFragmentSchema = alignFragmentsNodeWithSchema(scenario.scenarioGraph, processDefinitionData); + + if (scenario.name !== changedProperties.name) { + dispatch({ type: "PROCESS_RENAME", name: changedProperties.name }); + } + + return { + processName: changedProperties.name, + scenarioGraph: { ...processWithNewFragmentSchema, properties: changedProperties }, + }; + }; +}; + +export function editProperties(scenario: Scenario, changedProperties: PropertiesType): ThunkAction { + return async (dispatch) => { + const { processName, scenarioGraph } = await dispatch(calculateProperties(scenario, changedProperties)); + const response = await HttpService.validateProcess(scenario.name, processName, scenarioGraph); + + dispatch({ + type: "EDIT_PROPERTIES", + validationResult: response.data, + scenarioGraphAfterChange: scenarioGraph, + }); + }; +} diff --git a/designer/client/src/actions/nk/index.ts b/designer/client/src/actions/nk/index.ts index a4d340942a7..178928c7709 100644 --- a/designer/client/src/actions/nk/index.ts +++ b/designer/client/src/actions/nk/index.ts @@ -12,3 +12,4 @@ export * from "./ui/layout"; export * from "./zoom"; export * from "./nodeDetails"; export * from "./loadProcessToolbarsConfiguration"; +export * from "./editProperties"; diff --git a/designer/client/src/actions/nk/node.ts b/designer/client/src/actions/nk/node.ts index 73b2aa56276..96d875710f0 100644 --- a/designer/client/src/actions/nk/node.ts +++ b/designer/client/src/actions/nk/node.ts @@ -7,7 +7,7 @@ import { getScenarioGraph } from "../../reducers/selectors/graph"; import { getProcessDefinitionData } from "../../reducers/selectors/settings"; import { Edge, EdgeType, NodeId, NodeType, ProcessDefinitionData, ValidationResult } from "../../types"; import { ThunkAction } from "../reduxTypes"; -import { EditNodeAction, EditScenarioLabels, RenameProcessAction } from "./editNode"; +import { EditNodeAction, EditScenarioLabels } from "./editNode"; import { layoutChanged, NodePosition, Position } from "./ui/layout"; export type NodesWithPositions = { node: NodeType; position: Position }[]; @@ -171,5 +171,4 @@ export type NodeActions = | NodesWithEdgesAddedAction | ValidationResultAction | EditNodeAction - | RenameProcessAction | EditScenarioLabels; diff --git a/designer/client/src/actions/nk/nodeDetails.ts b/designer/client/src/actions/nk/nodeDetails.ts index 1d39f6fda79..7fba33fb713 100644 --- a/designer/client/src/actions/nk/nodeDetails.ts +++ b/designer/client/src/actions/nk/nodeDetails.ts @@ -57,11 +57,7 @@ const validate = debounce( validationRequestData: ValidationRequest, callback: (nodeId: NodeId, data?: ValidationData | void) => void, ) => { - const validate = (node: NodeType) => - NodeUtils.nodeIsProperties(node) - ? //NOTE: we don't validationRequestData contains processProperties, but they are refreshed only on modal open - HttpService.validateProperties(processName, { additionalFields: node.additionalFields, name: node.id }) - : HttpService.validateNode(processName, { ...validationRequestData, nodeData: node }); + const validate = (node: NodeType) => HttpService.validateNode(processName, { ...validationRequestData, nodeData: node }); const nodeId = validationRequestData.nodeData.id; const nodeWithChangedName = applyIdFromFakeName(validationRequestData.nodeData); diff --git a/designer/client/src/actions/nk/process.ts b/designer/client/src/actions/nk/process.ts index 8c6572086b9..d7209ee2de0 100644 --- a/designer/client/src/actions/nk/process.ts +++ b/designer/client/src/actions/nk/process.ts @@ -26,9 +26,9 @@ export function fetchProcessToDisplay(processName: ProcessName, versionId?: Proc }; } -export function loadProcessState(processName: ProcessName): ThunkAction { +export function loadProcessState(processName: ProcessName, processVersionId: number): ThunkAction { return (dispatch) => - HttpService.fetchProcessState(processName).then(({ data }) => + HttpService.fetchProcessState(processName, processVersionId).then(({ data }) => dispatch({ type: "PROCESS_STATE_LOADED", processState: data, diff --git a/designer/client/src/actions/reduxTypes.ts b/designer/client/src/actions/reduxTypes.ts index 070a73798fc..57f2b266b80 100644 --- a/designer/client/src/actions/reduxTypes.ts +++ b/designer/client/src/actions/reduxTypes.ts @@ -2,7 +2,7 @@ import { AnyAction, Reducer as ReduxReducer } from "redux"; import { ThunkAction as TA, ThunkDispatch as TD } from "redux-thunk"; import { ActionTypes } from "./actionTypes"; -import { CountsActions, NodeActions, ScenarioActions, SelectionActions, NodeDetailsActions } from "./nk"; +import { CountsActions, NodeActions, ScenarioActions, SelectionActions, NodeDetailsActions, PropertiesActions } from "./nk"; import { UserSettingsActions } from "./nk/userSettings"; import { UiActions } from "./nk/ui/uiActions"; import { SettingsActions } from "./settingsActions"; @@ -25,7 +25,8 @@ type TypedAction = | NotificationActions | DisplayTestResultsDetailsAction | CountsActions - | ScenarioActions; + | ScenarioActions + | PropertiesActions; interface UntypedAction extends AnyAction { type: Exclude; diff --git a/engine/flink/management/periodic/src/main/resources/web/static/assets/custom-actions/batch-instant.svg b/designer/client/src/assets/img/toolbarButtons/run-off-schedule.svg similarity index 100% rename from engine/flink/management/periodic/src/main/resources/web/static/assets/custom-actions/batch-instant.svg rename to designer/client/src/assets/img/toolbarButtons/run-off-schedule.svg diff --git a/designer/client/src/assets/json/nodeAttributes.json b/designer/client/src/assets/json/nodeAttributes.json index 8d1cc9aa5b7..2e18a54a2ff 100644 --- a/designer/client/src/assets/json/nodeAttributes.json +++ b/designer/client/src/assets/json/nodeAttributes.json @@ -38,9 +38,6 @@ "Aggregate": { "name": "Aggregate" }, - "Properties": { - "name": "Properties" - }, "CustomNode": { "name": "CustomNode" }, diff --git a/designer/client/src/components/ComponentDragPreview.tsx b/designer/client/src/components/ComponentDragPreview.tsx index 26411b58a53..1028f91776b 100644 --- a/designer/client/src/components/ComponentDragPreview.tsx +++ b/designer/client/src/components/ComponentDragPreview.tsx @@ -49,6 +49,10 @@ export const ComponentDragPreview = forwardRef nu willChange: "transform", }); + if (!node) { + return null; + } + return createPortal(
state?.allowedActions.includes(PredefinedActionName.Archive); + public canSeeRunOffSchedule = (state: ProcessStateType): boolean => state?.visibleActions.includes(PredefinedActionName.RunOffSchedule); + + public canRunOffSchedule = (state: ProcessStateType): boolean => state?.allowedActions.includes(PredefinedActionName.RunOffSchedule); + getStateDescription({ isArchived, isFragment }: Scenario, processState: ProcessStateType): string { if (isArchived) { return isFragment ? descriptionFragmentArchived() : descriptionProcessArchived(); @@ -60,6 +64,10 @@ class ProcessStateUtils { } return `${name}-${processState?.icon || state?.icon || unknownIcon}`; } + + getActionCustomTooltip(processState: ProcessStateType, actionName: ActionName): string | undefined { + return processState?.actionTooltips[actionName] || undefined; + } } export default new ProcessStateUtils(); diff --git a/designer/client/src/components/Process/types.ts b/designer/client/src/components/Process/types.ts index 36ddc9dd2e8..cfb7d590d65 100644 --- a/designer/client/src/components/Process/types.ts +++ b/designer/client/src/components/Process/types.ts @@ -9,6 +9,7 @@ export enum PredefinedActionName { Archive = "ARCHIVE", UnArchive = "UNARCHIVE", Pause = "PAUSE", + RunOffSchedule = "RUN_OFF_SCHEDULE", } export type ActionName = string; @@ -69,7 +70,9 @@ export type ProcessName = Scenario["name"]; export type ProcessStateType = { status: StatusType; externalDeploymentId?: string; + visibleActions: Array; allowedActions: Array; + actionTooltips: Record; icon: string; tooltip: string; description: string; diff --git a/designer/client/src/components/RemoteComponent.tsx b/designer/client/src/components/RemoteComponent.tsx index 6c28c975a2b..9f6c7a2b19a 100644 --- a/designer/client/src/components/RemoteComponent.tsx +++ b/designer/client/src/components/RemoteComponent.tsx @@ -1,8 +1,9 @@ -import React from "react"; -import LoaderSpinner from "./spinner/Spinner"; import { FederatedComponent, FederatedComponentProps, getFederatedComponentLoader } from "@touk/federated-component"; -import { NuThemeProvider } from "../containers/theme/nuThemeProvider"; +import React, { useMemo } from "react"; import SystemUtils from "../common/SystemUtils"; +import { NuThemeProvider } from "../containers/theme/nuThemeProvider"; +import { useWindows, WindowKind } from "../windowManager"; +import LoaderSpinner from "./spinner/Spinner"; export const loadExternalReactModule = getFederatedComponentLoader({ Wrapper: NuThemeProvider }); export const loadExternalReactModuleWithAuth = getFederatedComponentLoader({ @@ -13,6 +14,38 @@ export const loadExternalReactModuleWithAuth = getFederatedComponentLoader({ window["loadExternalReactModule"] = loadExternalReactModule; window["loadExternalReactModuleWithAuth"] = loadExternalReactModuleWithAuth; -export const RemoteComponent =

>(props: FederatedComponentProps

) => ( - {...props} fallback={} buildHash={__BUILD_HASH__} /> -); +function PlainRemoteComponentRender

, T = unknown>( + props: FederatedComponentProps

, + ref: React.ForwardedRef, +) { + return ref={ref} {...props} fallback={} buildHash={__BUILD_HASH__} />; +} + +export const PlainRemoteComponent = React.forwardRef(PlainRemoteComponentRender) as

, T = unknown>( + props: FederatedComponentProps

& React.RefAttributes, +) => React.ReactElement; + +export type RemoteToolbarContentProps = { + openRemoteModuleWindow:

>(props: P & { url?: string; title?: string }) => void; +}; + +function RemoteComponentRender

, T = unknown>(props: FederatedComponentProps

, ref: React.ForwardedRef) { + const { open } = useWindows(); + const sharedContext = useMemo( + () => ({ + openRemoteModuleWindow: ({ title, ...props }) => + open({ + kind: WindowKind.remote, + title, + meta: props, + }), + }), + [open], + ); + + return ref={ref} {...sharedContext} {...props} />; +} + +export const RemoteComponent = React.forwardRef(RemoteComponentRender) as

, T = unknown>( + props: FederatedComponentProps

& React.RefAttributes, +) => React.ReactElement; diff --git a/designer/client/src/components/RemoteModuleDialog.tsx b/designer/client/src/components/RemoteModuleDialog.tsx new file mode 100644 index 00000000000..4d44d822dab --- /dev/null +++ b/designer/client/src/components/RemoteModuleDialog.tsx @@ -0,0 +1,61 @@ +import { ModuleUrl } from "@touk/federated-component"; +import { WindowContentProps } from "@touk/window-manager"; +import type { FooterButtonProps } from "@touk/window-manager/cjs/components/window/footer"; +import React, { useCallback, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { WindowContent, WindowKind } from "../windowManager"; +import { LoadingButtonTypes } from "../windowManager/LoadingButton"; +import { RemoteComponent } from "./RemoteComponent"; + +export type RemoteModuleDialogProps = NonNullable; +export type RemoteModuleDialogRef = NonNullable<{ + closeAction?: () => Promise; + adjustButtons: (buttons: { closeButton: FooterButtonProps; confirmButton: FooterButtonProps }) => FooterButtonProps[]; +}>; + +export function RemoteModuleDialog

>({ + close, + ...props +}: WindowContentProps): JSX.Element { + const { + data: { meta: passProps }, + } = props; + + const ref = useRef(); + + const closeAction = useCallback(async () => { + await Promise.all([ref.current?.closeAction?.()]); + close(); + }, [close]); + + const { t } = useTranslation(); + + const closeButton = useMemo( + () => ({ + title: t("dialog.button.cancel", "cancel"), + action: closeAction, + className: LoadingButtonTypes.secondaryButton, + }), + [closeAction, t], + ); + + const confirmButton = useMemo( + () => ({ + title: t("dialog.button.ok", "OK"), + action: closeAction, + }), + [closeAction, t], + ); + + return ( + + ref={ref} {...passProps} /> + + ); +} + +export default RemoteModuleDialog; diff --git a/designer/client/src/components/graph/EspNode/element.ts b/designer/client/src/components/graph/EspNode/element.ts index 4b281fd068d..682f583d852 100644 --- a/designer/client/src/components/graph/EspNode/element.ts +++ b/designer/client/src/components/graph/EspNode/element.ts @@ -146,7 +146,7 @@ export function makeElement(processDefinitionData: ProcessDefinitionData, theme: opacity: node.isDisabled ? 0.5 : 1, }, iconBackground: { - fill: theme.palette.custom.getNodeStyles(node).fill, + fill: theme.palette.custom.getNodeStyles(node.type).fill, opacity: node.isDisabled ? 0.5 : 1, }, icon: { diff --git a/designer/client/src/components/graph/Graph.tsx b/designer/client/src/components/graph/Graph.tsx index 21894b75694..5f861458287 100644 --- a/designer/client/src/components/graph/Graph.tsx +++ b/designer/client/src/components/graph/Graph.tsx @@ -429,8 +429,7 @@ export class Graph extends React.Component { addNode(node: NodeType, position: Position): void { if (this.props.isFragment === true) return; - const canAddNode = - this.props.capabilities.editFrontend && NodeUtils.isNode(node) && NodeUtils.isAvailable(node, this.props.processDefinitionData); + const canAddNode = this.props.capabilities.editFrontend && NodeUtils.isAvailable(node, this.props.processDefinitionData); if (canAddNode) { this.props.nodeAdded(node, position); diff --git a/designer/client/src/components/graph/NodeUtils.ts b/designer/client/src/components/graph/NodeUtils.ts index 2d50aa6defd..267eb2a31a4 100644 --- a/designer/client/src/components/graph/NodeUtils.ts +++ b/designer/client/src/components/graph/NodeUtils.ts @@ -1,55 +1,25 @@ /* eslint-disable i18next/no-literal-string */ -import { has, isEmpty, isEqual, uniqBy } from "lodash"; +import { isEqual, uniqBy } from "lodash"; import ProcessUtils from "../../common/ProcessUtils"; -import { - Edge, - EdgeKind, - EdgeType, - FragmentNodeType, - NodeId, - NodeType, - ProcessDefinitionData, - PropertiesType, - ScenarioGraph, - UINodeType, -} from "../../types"; -import { UnknownRecord } from "../../types/common"; +import { Edge, EdgeKind, EdgeType, FragmentNodeType, NodeId, NodeType, ProcessDefinitionData, ScenarioGraph } from "../../types"; import { createEdge } from "../../reducers/graph/utils"; import { Scenario } from "../Process/types"; class NodeUtils { - isNode = (obj: UnknownRecord): obj is NodeType => { - return !isEmpty(obj) && has(obj, "id") && has(obj, "type"); - }; - - nodeType = (node: UINodeType) => { - return node?.type ? node.type : "Properties"; - }; - - nodeIsProperties = (node: UINodeType): node is PropertiesType => { - const type = node && this.nodeType(node); - return type === "Properties"; - }; - - nodeIsFragment = (node: UINodeType): node is FragmentNodeType => { - return this.nodeType(node) === "FragmentInput"; - }; - - isPlainNode = (node: UINodeType) => { - return !isEmpty(node) && !this.nodeIsProperties(node); + nodeIsFragment = (node: NodeType): node is FragmentNodeType => { + return node.type === "FragmentInput"; }; nodeIsJoin = (node: NodeType): boolean => { - return node && this.nodeType(node) === "Join"; + return node && node.type === "Join"; }; nodesFromScenarioGraph = (scenarioGraph: ScenarioGraph): NodeType[] => scenarioGraph.nodes || []; edgesFromScenarioGraph = (scenarioGraph: ScenarioGraph) => scenarioGraph.edges || []; - // For sake of consistency with other nodes, name must be renamed to id - getProcessPropertiesNode = ({ name, scenarioGraph: { properties } }: Scenario, unsavedName?: string) => ({ - id: name || unsavedName, + getProcessProperties = ({ name, scenarioGraph: { properties } }: Scenario, unsavedName?: string) => ({ + name: name || unsavedName, ...properties, }); diff --git a/designer/client/src/components/graph/PanZoomPlugin.ts b/designer/client/src/components/graph/PanZoomPlugin.ts index c1bfc314530..57952bb7c25 100644 --- a/designer/client/src/components/graph/PanZoomPlugin.ts +++ b/designer/client/src/components/graph/PanZoomPlugin.ts @@ -1,11 +1,11 @@ /* @refresh reset */ import { dia, g } from "jointjs"; import { throttle } from "lodash"; +import { isVisualTesting } from "../../devHelpers"; import { GlobalCursor } from "./GlobalCursor"; import { select, Selection } from "d3-selection"; import { D3ZoomEvent, zoom, ZoomBehavior, ZoomedElementBaseType, zoomIdentity, ZoomTransform } from "d3-zoom"; import { rafThrottle } from "./rafThrottle"; -import { isVisualTesting } from "../toolbarSettings/DEV_TOOLBARS"; function isModified(event: MouseEvent | TouchEvent) { return event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; diff --git a/designer/client/src/components/graph/SelectionContextProvider.tsx b/designer/client/src/components/graph/SelectionContextProvider.tsx index e221a6a9b23..5e09087322f 100644 --- a/designer/client/src/components/graph/SelectionContextProvider.tsx +++ b/designer/client/src/components/graph/SelectionContextProvider.tsx @@ -56,11 +56,7 @@ function useClipboardParse() { return useCallback( (text) => { const selection = tryParseOrNull(text); - const isValid = - selection?.edges && - selection?.nodes?.every( - (node) => NodeUtils.isNode(node) && NodeUtils.isPlainNode(node) && NodeUtils.isAvailable(node, processDefinitionData), - ); + const isValid = selection?.edges && selection?.nodes?.every((node) => NodeUtils.isAvailable(node, processDefinitionData)); return isValid ? selection : null; }, [processDefinitionData], diff --git a/designer/client/src/components/graph/node-modal/DescriptionField.tsx b/designer/client/src/components/graph/node-modal/DescriptionField.tsx index 78f1d17276f..3bb9a8e1635 100644 --- a/designer/client/src/components/graph/node-modal/DescriptionField.tsx +++ b/designer/client/src/components/graph/node-modal/DescriptionField.tsx @@ -2,13 +2,13 @@ import { NodeField } from "./NodeField"; import { FieldType } from "./editors/field/Field"; import React from "react"; -import { NodeType, NodeValidationError, UINodeType } from "../../../types"; +import { NodeType, NodeValidationError, NodeOrPropertiesType } from "../../../types"; interface DescriptionFieldProps { autoFocus?: boolean; defaultValue?: string; isEditMode?: boolean; - node: UINodeType; + node: NodeOrPropertiesType; readonly?: boolean; renderFieldLabel: (paramName: string) => React.ReactNode; setProperty: (property: K, newValue: NodeType[K], defaultValue?: NodeType[K]) => void; diff --git a/designer/client/src/components/graph/node-modal/DescriptionOnlyContent.tsx b/designer/client/src/components/graph/node-modal/DescriptionOnlyContent.tsx index 842b7acc865..2eea0eb33c1 100644 --- a/designer/client/src/components/graph/node-modal/DescriptionOnlyContent.tsx +++ b/designer/client/src/components/graph/node-modal/DescriptionOnlyContent.tsx @@ -5,18 +5,18 @@ import { DescriptionView } from "../../../containers/DescriptionView"; import { FieldType } from "./editors/field/Field"; import { rowAceEditor } from "./NodeDetailsContent/NodeTableStyled"; import { NodeField } from "./NodeField"; -import { NodeTypeDetailsContentProps, useNodeTypeDetailsContentLogic } from "./NodeTypeDetailsContent"; +import { NodeType, PropertiesType } from "../../../types"; -type DescriptionOnlyContentProps = Pick & { +type DescriptionOnlyContentProps = { + onChange: (property: K, newValue: NodeType[K], defaultValue?: NodeType[K]) => void; + properties: PropertiesType; fieldPath: string; preview?: boolean; }; -export function DescriptionOnlyContent({ fieldPath, preview, node, onChange }: DescriptionOnlyContentProps) { - const { setProperty } = useNodeTypeDetailsContentLogic({ node, onChange }); - +export function DescriptionOnlyContent({ fieldPath, preview, properties, onChange }: DescriptionOnlyContentProps) { if (preview) { - return {get(node, fieldPath)}; + return {get(properties, fieldPath)}; } return ( @@ -31,8 +31,8 @@ export function DescriptionOnlyContent({ fieldPath, preview, node, onChange }: D null} - setProperty={setProperty} - node={node} + setProperty={onChange} + node={properties} isEditMode={true} showValidation={false} readonly={false} diff --git a/designer/client/src/components/graph/node-modal/IdField.tsx b/designer/client/src/components/graph/node-modal/IdField.tsx index c2a0578521f..3e5dc4a2c83 100644 --- a/designer/client/src/components/graph/node-modal/IdField.tsx +++ b/designer/client/src/components/graph/node-modal/IdField.tsx @@ -2,7 +2,7 @@ import { extendErrors, getValidationErrorsForField, uniqueScenarioValueValidator import Field, { FieldType } from "./editors/field/Field"; import React, { useMemo, useState } from "react"; import { useDiffMark } from "./PathsToMark"; -import { NodeType, NodeValidationError, UINodeType } from "../../../types"; +import { NodeType, NodeValidationError, NodeOrPropertiesType } from "../../../types"; import { useSelector } from "react-redux"; import { getProcessNodesIds } from "../../../reducers/selectors/graph"; import NodeUtils from "../NodeUtils"; @@ -11,7 +11,7 @@ import { nodeInput, nodeInputWithError } from "./NodeDetailsContent/NodeTableSty interface IdFieldProps { isEditMode?: boolean; - node: UINodeType; + node: NodeOrPropertiesType; renderFieldLabel: (paramName: string) => React.ReactNode; setProperty?: (property: K, newValue: NodeType[K], defaultValue?: NodeType[K]) => void; showValidation?: boolean; @@ -37,7 +37,7 @@ export function IdField({ isEditMode, node, renderFieldLabel, setProperty, showV const value = useMemo(() => node[FAKE_NAME_PROP_NAME] ?? node[propName], [node]); const marked = useMemo(() => isMarked(FAKE_NAME_PROP_NAME) || isMarked(propName), [isMarked]); - const isUniqueValueValidator = !NodeUtils.nodeIsProperties(node) && uniqueScenarioValueValidator(otherNodes); + const isUniqueValueValidator = uniqueScenarioValueValidator(otherNodes); const fieldErrors = getValidationErrorsForField( isUniqueValueValidator ? extendErrors(errors, value, FAKE_NAME_PROP_NAME, [isUniqueValueValidator]) : errors, diff --git a/designer/client/src/components/graph/node-modal/NodeAdditionalInfoBox.tsx b/designer/client/src/components/graph/node-modal/NodeAdditionalInfoBox.tsx index 56c851d14df..6448c89561d 100644 --- a/designer/client/src/components/graph/node-modal/NodeAdditionalInfoBox.tsx +++ b/designer/client/src/components/graph/node-modal/NodeAdditionalInfoBox.tsx @@ -1,14 +1,17 @@ import React, { useCallback, useEffect, useState } from "react"; -import HttpService from "../../../http/HttpService"; import { useDebounce } from "use-debounce"; -import { NodeType } from "../../../types"; +import { NodeOrPropertiesType } from "../../../types"; import { useSelector } from "react-redux"; import { getProcessName } from "./NodeDetailsContent/selectors"; -import NodeUtils from "../NodeUtils"; import { MarkdownStyled } from "./MarkdownStyled"; interface Props { - node: NodeType; + node: NodeOrPropertiesType; + handleGetAdditionalInfo: ( + processName: string, + node: NodeOrPropertiesType, + controller: AbortController, + ) => Promise; } //Types should match implementations of AdditionalInfo on Backend! @@ -20,7 +23,7 @@ interface MarkdownAdditionalInfo { } export default function NodeAdditionalInfoBox(props: Props): JSX.Element { - const { node } = props; + const { node, handleGetAdditionalInfo } = props; const processName = useSelector(getProcessName); const [additionalInfo, setAdditionalInfo] = useState(null); @@ -29,23 +32,21 @@ export default function NodeAdditionalInfoBox(props: Props): JSX.Element { //we don't wat to query BE on each key pressed (we send node parameters to get additional data) const [debouncedNode] = useDebounce(node, 1000); - const getAdditionalInfo = useCallback((processName: string, debouncedNode: NodeType) => { - const controller = new AbortController(); - const fetch = (node: NodeType) => - NodeUtils.nodeIsProperties(node) - ? HttpService.getPropertiesAdditionalInfo(processName, node, controller) - : HttpService.getNodeAdditionalInfo(processName, node, controller); - - fetch(debouncedNode).then((data) => { - // signal should cancel request, but for some reason it doesn't in dev - if (!controller.signal.aborted && data) { - setAdditionalInfo(data); - } - }); - return () => { - controller.abort(); - }; - }, []); + const getAdditionalInfo = useCallback( + (processName: string, debouncedNode: NodeOrPropertiesType) => { + const controller = new AbortController(); + handleGetAdditionalInfo(processName, debouncedNode, controller).then((data) => { + // signal should cancel request, but for some reason it doesn't in dev + if (!controller.signal.aborted && data) { + setAdditionalInfo(data); + } + }); + return () => { + controller.abort(); + }; + }, + [handleGetAdditionalInfo], + ); useEffect(() => { if (processName) { diff --git a/designer/client/src/components/graph/node-modal/NodeDetailsContent.tsx b/designer/client/src/components/graph/node-modal/NodeDetailsContent.tsx index 387b7f5305e..211f9965642 100644 --- a/designer/client/src/components/graph/node-modal/NodeDetailsContent.tsx +++ b/designer/client/src/components/graph/node-modal/NodeDetailsContent.tsx @@ -11,6 +11,7 @@ import { TestResultsWrapper } from "./TestResultsWrapper"; import { NodeTypeDetailsContent } from "./NodeTypeDetailsContent"; import { DebugNodeInspector } from "./NodeDetailsContent/DebugNodeInspector"; import { useUserSettings } from "../../../common/userSettings"; +import HttpService from "../../../http/HttpService"; export const NodeDetailsContent = ({ node, @@ -47,7 +48,7 @@ export const NodeDetailsContent = ({ showSwitch={showSwitch} /> - + {userSettings["debug.nodesAsJson"] && } ); diff --git a/designer/client/src/components/graph/node-modal/NodeField.tsx b/designer/client/src/components/graph/node-modal/NodeField.tsx index 26695d020ab..f2860b5af0b 100644 --- a/designer/client/src/components/graph/node-modal/NodeField.tsx +++ b/designer/client/src/components/graph/node-modal/NodeField.tsx @@ -3,7 +3,7 @@ import { getValidationErrorsForField } from "./editors/Validators"; import { get, isEmpty } from "lodash"; import React from "react"; import { useDiffMark } from "./PathsToMark"; -import { NodeType, NodeValidationError, UINodeType } from "../../../types"; +import { NodeType, NodeValidationError, NodeOrPropertiesType } from "../../../types"; import { nodeInput, nodeInputWithError } from "./NodeDetailsContent/NodeTableStyled"; import { cx } from "@emotion/css"; @@ -14,7 +14,7 @@ type NodeFieldProps = { fieldName: N; fieldType: FieldType; isEditMode?: boolean; - node: UINodeType; + node: NodeOrPropertiesType; readonly?: boolean; renderFieldLabel: (paramName: string) => React.ReactNode; setProperty: (property: K, newValue: NodeType[K], defaultValue?: NodeType[K]) => void; diff --git a/designer/client/src/components/graph/node-modal/NodeTypeDetailsContent.tsx b/designer/client/src/components/graph/node-modal/NodeTypeDetailsContent.tsx index ffee5d55076..639ab4f3ef5 100644 --- a/designer/client/src/components/graph/node-modal/NodeTypeDetailsContent.tsx +++ b/designer/client/src/components/graph/node-modal/NodeTypeDetailsContent.tsx @@ -4,7 +4,6 @@ import { useDispatch, useSelector } from "react-redux"; import { nodeDetailsClosed, nodeDetailsOpened, validateNodeData } from "../../../actions/nk"; import { getProcessDefinitionData } from "../../../reducers/selectors/settings"; import { Edge, NodeType, NodeValidationError, PropertiesType } from "../../../types"; -import NodeUtils from "../NodeUtils"; import { CustomNode } from "./customNode"; import { EnricherProcessor } from "./enricherProcessor"; import { ParamFieldLabel } from "./FieldLabel"; @@ -24,7 +23,6 @@ import { } from "./NodeDetailsContent/selectors"; import { generateUUIDs } from "./nodeUtils"; import { adjustParameters } from "./ParametersUtils"; -import { Properties } from "./properties"; import { Sink } from "./sink"; import { Source } from "./source"; import { Split } from "./split"; @@ -179,7 +177,7 @@ export function NodeTypeDetailsContent({ errors, showSwitch, ...props }: NodeTyp showValidation, } = useNodeTypeDetailsContentLogic(props); - switch (NodeUtils.nodeType(node)) { + switch (node.type) { case "Source": return ( ); - case "Properties": - return ( - - ); default: return ( , definitions: UIParam }; const parametersPath = (node) => { - switch (NodeUtils.nodeType(node)) { + switch (node.type) { case "CustomNode": return `parameters`; case "Join": diff --git a/designer/client/src/components/graph/node-modal/aggregate/aggMapLikeParser.tsx b/designer/client/src/components/graph/node-modal/aggregate/aggMapLikeParser.tsx index 793b26c7d9b..0adebac9a3b 100644 --- a/designer/client/src/components/graph/node-modal/aggregate/aggMapLikeParser.tsx +++ b/designer/client/src/components/graph/node-modal/aggregate/aggMapLikeParser.tsx @@ -1,28 +1,33 @@ import { createToken, EmbeddedActionsParser, Lexer } from "chevrotain"; +import { withLogs } from "../../../../devHelpers"; + +const LCurly = createToken({ name: "LCurly", pattern: /\{/, push_mode: "inside" }); +const RCurly = createToken({ name: "RCurly", pattern: /}/, pop_mode: true }); const CollectionOpen = createToken({ name: "CollectionOpen", pattern: /#COLLECTION.join\(\{/, + push_mode: "inside", }); const CollectionClose = createToken({ name: "CollectionClose", pattern: /}, "|"\)/, -}); -const ListOpen = createToken({ - name: "ListOpen", - pattern: /{/, + pop_mode: true, }); const ListClose = createToken({ name: "ListClose", pattern: /}\.toString/, + pop_mode: true, }); const MapOpen = createToken({ name: "MapOpen", - pattern: /(#AGG\.map\()?{/, + pattern: /#AGG\.map\(\{/, + push_mode: "inside", }); const MapClose = createToken({ name: "MapClose", - pattern: /}(\))?/, + pattern: /}\)/, + pop_mode: true, }); const Comma = createToken({ name: "Comma", @@ -34,8 +39,23 @@ const Colon = createToken({ }); const Identifier = createToken({ name: "Identifier", - pattern: /([a-zA-Z]\w*|"[^"]+")/, + pattern: /([a-zA-Z0-9]([.\s]?\w+)*)/, +}); + +const SingleQuoted = createToken({ + name: "SingleQuoted", + pattern: /'[^']*'/, +}); +const DoubleQuoted = createToken({ + name: "DoubleQuoted", + pattern: /"[^"]*"/, }); +const Wrapped = createToken({ + name: "Wrapped", + pattern: /\{[^}]*}/, + pop_mode: true, +}); + const Number = createToken({ name: "Number", pattern: /\d+/, @@ -50,113 +70,167 @@ const WhiteSpace = createToken({ group: Lexer.SKIPPED, }); -const aggMapTokens = [ - CollectionClose, - CollectionOpen, - ListClose, - ListOpen, - MapClose, - MapOpen, - Comma, - Spel, - Identifier, - Number, - Colon, - WhiteSpace, -]; - -export const AggMapLikeLexer = new Lexer(aggMapTokens); +const aggMapTokens = { + modes: { + outside: [ + SingleQuoted, + DoubleQuoted, + CollectionOpen, + MapOpen, + CollectionClose, + MapClose, + ListClose, + LCurly, + RCurly, + Comma, + Colon, + WhiteSpace, + Spel, + Identifier, + Number, + ], + inside: [ + Wrapped, + SingleQuoted, + DoubleQuoted, + CollectionClose, + MapClose, + ListClose, + RCurly, + Comma, + Colon, + WhiteSpace, + Spel, + Identifier, + Number, + ], + }, + defaultMode: "outside", +}; export class AggMapLikeParser extends EmbeddedActionsParser { - constructor() { - super(aggMapTokens, { recoveryEnabled: true }); - - this.performSelfAnalysis(); - } - - object = this.RULE("object", () => { + private lexer: Lexer; + private quoted = this.RULE("quoted", () => { + return this.OR([{ ALT: () => this.CONSUME(SingleQuoted) }, { ALT: () => this.CONSUME(DoubleQuoted) }]); + }); + private collectionItem = this.RULE("collectionItem", () => { + return this.OR([ + { ALT: () => this.CONSUME(Wrapped).image }, + { ALT: () => this.SUBRULE(this.quoted).image }, + { ALT: () => this.CONSUME(Spel).image?.trim() }, + { ALT: () => this.CONSUME(Identifier).image?.trim() }, + { ALT: () => this.CONSUME(Number).image }, + ]); + }); + private objectItem = this.RULE("objectItem", () => { const obj = {}; + const lit = this.OR([ + { ALT: () => this.CONSUME(Identifier).image }, + { + ALT: () => { + const { image } = this.SUBRULE(this.quoted); + return image?.substring?.(1, image.length - 1); + }, + }, + ]); + const colon = this.CONSUME(Colon); + const value = this.SUBRULE(this.collectionItem); - this.OR([{ ALT: () => this.CONSUME(MapOpen) }, { ALT: () => this.CONSUME(ListOpen) }]); + if (colon.isInsertedInRecovery) { + return null; + } + obj[lit] = value; + return obj; + }); + private mapItems = this.RULE("mapItems", () => { + const obj = {}; this.MANY_SEP({ SEP: Comma, DEF: () => { Object.assign(obj, this.SUBRULE(this.objectItem)); }, }); - this.CONSUME(MapClose); - return obj; }); + private aggMap = this.RULE("aggMap", () => { + const opening = this.CONSUME(MapOpen); + const obj = this.SUBRULE(this.mapItems); + this.CONSUME(MapClose); - objectItem = this.RULE("objectItem", () => { - const obj = {}; - - const lit = this.CONSUME(Identifier); - this.CONSUME(Colon); - - const value = this.OR([ - { ALT: () => this.CONSUME(Spel) }, - { ALT: () => this.CONSUME2(Identifier) }, - { ALT: () => this.CONSUME(Number) }, - ]); + if (!opening.image || opening.isInsertedInRecovery) { + return null; + } - let key: string; + return obj; + }); + private plainMap = this.RULE("plainMap", () => { + const opening = this.CONSUME(LCurly); + const obj = this.SUBRULE(this.mapItems); + this.CONSUME(RCurly); - if (!lit.isInsertedInRecovery) { - key = lit.image.replaceAll(/"/g, ""); - obj[key] = value.image; + if (!opening.image || opening.isInsertedInRecovery) { + return null; } return obj; }); - - collection = this.RULE("collection", () => { + private object = this.RULE("object", () => { + return this.OR([{ ALT: () => this.SUBRULE(this.aggMap) }, { ALT: () => this.SUBRULE(this.plainMap) }]); + }); + private collection = this.RULE("collection", () => { const arr = []; - - this.OR([ - { - ALT: () => { - this.CONSUME(CollectionOpen); - this.AT_LEAST_ONE_SEP({ - SEP: Comma, - DEF: () => { - const item = this.SUBRULE(this.collectionItem); - if (!item) return; - arr.push(item); - }, - }); - this.CONSUME(CollectionClose); - }, + this.CONSUME(CollectionOpen); + this.MANY_SEP({ + SEP: Comma, + DEF: () => { + const item = this.SUBRULE(this.collectionItem); + if (!item) return; + arr.push(item); }, - { - ALT: () => { - this.CONSUME(ListOpen); - this.AT_LEAST_ONE_SEP2({ - SEP: Comma, - DEF: () => { - const item = this.SUBRULE2(this.collectionItem); - if (!item) return; - arr.push(item); - }, - }); - this.CONSUME(ListClose); - }, + }); + this.CONSUME(CollectionClose); + return arr; + }); + private list = this.RULE("list", () => { + const arr = []; + this.CONSUME(LCurly); + this.MANY_SEP({ + SEP: Comma, + DEF: () => { + const item = this.SUBRULE(this.collectionItem); + if (!item) return; + arr.push(item); }, - ]); - + }); + this.CONSUME(ListClose); return arr; }); + private groupBy = this.RULE("groupBy", () => { + return this.OR([{ ALT: () => this.SUBRULE(this.collection) }, { ALT: () => this.SUBRULE(this.list) }]); + }); + private fullText = ""; - collectionItem = this.RULE("collectionItem", () => { - const value = this.OR([ - { ALT: () => this.CONSUME(Spel) }, - { ALT: () => this.CONSUME(Identifier) }, - { ALT: () => this.CONSUME(Number) }, - ]); + constructor(tokens = aggMapTokens) { + super(tokens, { recoveryEnabled: true }); + this.lexer = new Lexer(tokens); + this.parseObject = withLogs(this.parseObject.bind(this)); + this.parseList = withLogs(this.parseList.bind(this)); + this.performSelfAnalysis(); + } - if (!value.isInsertedInRecovery) { - return value.image; - } - }); + parseList(input: string): Array { + this.tokenizeInput(input); + return this.groupBy() || null; + } + + parseObject(input: string): Record { + this.tokenizeInput(input); + return this.object() || null; + } + + private tokenizeInput(input: string) { + const lexResult = this.lexer.tokenize(input); + this.fullText = input; + this.input = lexResult.tokens; + } } diff --git a/designer/client/src/components/graph/node-modal/aggregate/parser.test.ts b/designer/client/src/components/graph/node-modal/aggregate/parser.test.ts new file mode 100644 index 00000000000..5705cdc19bd --- /dev/null +++ b/designer/client/src/components/graph/node-modal/aggregate/parser.test.ts @@ -0,0 +1,60 @@ +import { AggMapLikeParser } from "./aggMapLikeParser"; + +describe("AggMapLikeParser", () => { + let parser: AggMapLikeParser; + + beforeEach(() => { + parser = new AggMapLikeParser(); + }); + + it.each([ + [`aaa`, null], + [`"aaa"`, null], + [`{`, null], + [`{}`, null], + [`{123}`, null], + [`{}.toString`, []], + [`#COLLECTION.join({}, "|")`, []], + [`{123}.toString`, ["123"]], + [`{ 123 }.toString`, ["123"]], + [`{123,456}.toString`, ["123", "456"]], + [`{ 123, 456 }.toString`, ["123", "456"]], + [`{ 123, aaa, 456 }.toString`, ["123", "aaa", "456"]], + [`{ 123, aaa.bbb }.toString`, ["123", "aaa.bbb"]], + [`{ 123, aaa_bbb }.toString`, ["123", "aaa_bbb"]], + [`{ 123, aaa bbb, ccc }.toString`, ["123", "aaa bbb", "ccc"]], + [`{ 123, "aaa bbb" }.toString`, ["123", `"aaa bbb"`]], + [`{ 123, 'aaa bbb' }.toString`, ["123", `'aaa bbb'`]], + [`{ 123, #aaa.bbb }.toString`, ["123", `#aaa.bbb`]], + [`{ 123, 123.bbb }.toString`, ["123", `123.bbb`]], + [`{ 123, 123.456.bbb }.toString`, ["123", `123.456.bbb`]], + [`{ 123, "{ 123, 456 }" }.toString`, ["123", `"{ 123, 456 }"`]], + [`{ 123, { 123, "456" } }.toString`, ["123", `{ 123, "456" }`]], + [`{ 123, { aaa: 123, bbb: "456" } }.toString`, ["123", `{ aaa: 123, bbb: "456" }`]], + [`#COLLECTION.join({ 123, 456 }, "|")`, ["123", "456"]], + ])("should parse list: %s => %s", (input, output) => { + expect(parser.parseList(input)).toEqual(output); + }); + + it.each([ + [`aaa`, null], + [`"aaa"`, null], + [`{}`, {}], + [`#AGG.map({})`, {}], + [`{aaa:123}`, { aaa: "123" }], + [`#AGG.map({aaa:123})`, { aaa: "123" }], + [`{ aaa: 123, bbb: "456" }`, { aaa: "123", bbb: `"456"` }], + [`{ a: 123, b c: 123 }`, { a: "123", "b c": "123" }], + [`{ aaa: 1 2 3 }`, { aaa: "1 2 3" }], + [`{ "a a a": 123 }`, { "a a a": "123" }], + [`{ aaa: #aaa.bbb }`, { aaa: "#aaa.bbb" }], + [`{ aaa: aaa.bbb }`, { aaa: "aaa.bbb" }], + [`{ aaa: "aaa bbb" }`, { aaa: `"aaa bbb"` }], + [`{ aaa: {} }`, { aaa: `{}` }], + [`{ aaa: { bbb: 123, ccc: "456" } }`, { aaa: `{ bbb: 123, ccc: "456" }` }], + [`{ aaa: { 123, "456" } }`, { aaa: `{ 123, "456" }` }], + [`{ aaa: "{ 123, '456' }" }`, { aaa: `"{ 123, '456' }"` }], + ])("should parse map: %s => %s", (input, output) => { + expect(parser.parseObject(input)).toEqual(output); + }); +}); diff --git a/designer/client/src/components/graph/node-modal/aggregate/useAggParamsSerializer.tsx b/designer/client/src/components/graph/node-modal/aggregate/useAggParamsSerializer.tsx index 3a154e7b7aa..44dc4b49a50 100644 --- a/designer/client/src/components/graph/node-modal/aggregate/useAggParamsSerializer.tsx +++ b/designer/client/src/components/graph/node-modal/aggregate/useAggParamsSerializer.tsx @@ -1,6 +1,6 @@ import { padStart } from "lodash"; import { useCallback, useMemo } from "react"; -import { AggMapLikeLexer, AggMapLikeParser } from "./aggMapLikeParser"; +import { AggMapLikeParser } from "./aggMapLikeParser"; export function useAggParamsSerializer(): [ (text: string) => Record, @@ -8,14 +8,7 @@ export function useAggParamsSerializer(): [ ] { const parser = useMemo(() => new AggMapLikeParser(), []); - const deserialize = useCallback( - (text: string): Record => { - const lexingResult = AggMapLikeLexer.tokenize(text); - parser.input = lexingResult.tokens; - return parser.object() || null; - }, - [parser], - ); + const deserialize = useCallback((input: string) => parser.parseObject(input), [parser]); const serialize = useCallback((paramName: string, map: Record): string => { const entries = Object.entries(map || {}).map(([key, value]) => { @@ -40,14 +33,7 @@ export function useAggParamsSerializer(): [ export function useGroupByParamsSerializer(): [(text: string) => string[], (paramName: string, arr: string[]) => string] { const parser = useMemo(() => new AggMapLikeParser(), []); - const deserialize = useCallback( - (text: string): string[] => { - const lexingResult = AggMapLikeLexer.tokenize(text); - parser.input = lexingResult.tokens; - return parser.collection() || null; - }, - [parser], - ); + const deserialize = useCallback((input: string) => parser.parseList(input), [parser]); const serialize = useCallback((paramName: string, arr: string[]): string => { const entries = arr.map((value) => { diff --git a/designer/client/src/components/graph/node-modal/editors/expression/AceWithSettings.tsx b/designer/client/src/components/graph/node-modal/editors/expression/AceWithSettings.tsx index 51d7b19a96a..c59f8f99743 100644 --- a/designer/client/src/components/graph/node-modal/editors/expression/AceWithSettings.tsx +++ b/designer/client/src/components/graph/node-modal/editors/expression/AceWithSettings.tsx @@ -43,6 +43,7 @@ export default forwardRef(function AceWithSettings( const scrollToView = throttle( () => { + if (!editor.isFocused()) return; // before setting cursor position ensure all position calculations are actual editor?.renderer.updateFull(true); const activeElement = editor.container.querySelector(".ace_cursor") || document.activeElement; diff --git a/designer/client/src/components/graph/node-modal/editors/expression/JsonEditor.tsx b/designer/client/src/components/graph/node-modal/editors/expression/JsonEditor.tsx index 8d6cc4d0392..dee9edd5840 100644 --- a/designer/client/src/components/graph/node-modal/editors/expression/JsonEditor.tsx +++ b/designer/client/src/components/graph/node-modal/editors/expression/JsonEditor.tsx @@ -52,6 +52,7 @@ export const JsonEditor: SimpleEditor = ({ sx={{ position: "relative" }} > + definitionData?.classes?.map((type) => ({ + value: type.refClazzName as SupportedType, + label: ProcessUtils.humanReadableType(type), + })), + [definitionData?.classes], + ); + + const orderedTypeOptions = useMemo(() => orderBy(typeOptions, (item) => [item.label, item.value], ["asc"]), [typeOptions]); + + const defaultTypeOption = useMemo(() => find(typeOptions, { label: "String" }) || head(typeOptions), [typeOptions]); + return { + orderedTypeOptions, + defaultTypeOption, + }; +} + export const Table = ({ expressionObj, onValueChange, className, fieldErrors }: EditorProps) => { const tableDateContext = useTableState(expressionObj); const [{ rows, columns }, dispatch, rawExpression] = tableDateContext; @@ -97,7 +121,7 @@ export const Table = ({ expressionObj, onValueChange, className, fieldErrors }: } }, [expressionObj.expression, onValueChange, rawExpression]); - const { defaultTypeOption, orderedTypeOptions } = useTypeOptions(); + const { defaultTypeOption, orderedTypeOptions } = useTableEditorTypeOptions(); const supportedTypes = useMemo(() => orderedTypeOptions.filter(({ value }) => SUPPORTED_TYPES.includes(value)), [orderedTypeOptions]); useEffect(() => { diff --git a/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx b/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx index 54f66ba61ee..c0aa32d74b1 100644 --- a/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx +++ b/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx @@ -14,13 +14,13 @@ interface Props extends Omit, "readOnly isEditMode?: boolean; } -export function useTypeOptions() { +export function useFragmentInputDefinitionTypeOptions() { const definitionData = useSelector(getProcessDefinitionData); + const typeOptions = useMemo( () => definitionData?.classes?.map((type) => ({ - // TODO: Instead of using type assertion type, set refClazzName as a union of available clazzNames - value: type.refClazzName as Value, + value: type.display as string, label: ProcessUtils.humanReadableType(type), })), [definitionData?.classes], @@ -40,7 +40,7 @@ export default function FragmentInputDefinition(props: Props): JSX.Element { const { node, setProperty, isEditMode, showValidation } = passProps; const readOnly = !isEditMode; - const { orderedTypeOptions, defaultTypeOption } = useTypeOptions(); + const { orderedTypeOptions, defaultTypeOption } = useFragmentInputDefinitionTypeOptions(); const addField = useCallback(() => { addElement("parameters", getDefaultFields(defaultTypeOption.value)); diff --git a/designer/client/src/components/graph/node-modal/fragment-input-definition/TypeSelect.tsx b/designer/client/src/components/graph/node-modal/fragment-input-definition/TypeSelect.tsx index 0d2611674c7..45576a3c765 100644 --- a/designer/client/src/components/graph/node-modal/fragment-input-definition/TypeSelect.tsx +++ b/designer/client/src/components/graph/node-modal/fragment-input-definition/TypeSelect.tsx @@ -1,5 +1,5 @@ import React, { HTMLProps, useCallback, useState } from "react"; -import Select from "react-select"; +import CreatableSelect from "react-select/creatable"; import { NodeValue } from "../node"; import { selectStyled } from "../../../../stylesheets/SelectStyled"; import { useTheme } from "@mui/material"; @@ -61,7 +61,7 @@ export function TypeSelect({ return ( -