diff --git a/designer/client/src/components/toolbars/status/ProcessInfo.tsx b/designer/client/src/components/toolbars/status/ProcessInfo.tsx index a5bf62e00a5..5d5e9b64090 100644 --- a/designer/client/src/components/toolbars/status/ProcessInfo.tsx +++ b/designer/client/src/components/toolbars/status/ProcessInfo.tsx @@ -3,7 +3,7 @@ import i18next from "i18next"; import { SwitchTransition } from "react-transition-group"; import { useSelector } from "react-redux"; import { RootState } from "../../../reducers"; -import { getScenario, getProcessUnsavedNewName, isProcessRenamed } from "../../../reducers/selectors/graph"; +import { getScenario, getProcessUnsavedNewName, isProcessRenamed, getProcessVersionId } from "../../../reducers/selectors/graph"; import { getProcessState } from "../../../reducers/selectors/scenarioState"; import { getCustomActions } from "../../../reducers/selectors/settings"; import { CssFade } from "../../CssFade"; @@ -28,6 +28,7 @@ const ProcessInfo = memo(({ id, buttonsVariant, children }: ToolbarPanelProps) = const unsavedNewName = useSelector((state: RootState) => getProcessUnsavedNewName(state)); const processState = useSelector((state: RootState) => getProcessState(state)); const customActions = useSelector((state: RootState) => getCustomActions(state)); + const versionId = useSelector(getProcessVersionId); const description = ProcessStateUtils.getStateDescription(scenario, processState); const transitionKey = ProcessStateUtils.getTransitionKey(scenario, processState); diff --git a/designer/client/src/components/toolbars/status/buttons/CustomActionButton.tsx b/designer/client/src/components/toolbars/status/buttons/CustomActionButton.tsx index ec7080d5e43..e2d80c2363b 100644 --- a/designer/client/src/components/toolbars/status/buttons/CustomActionButton.tsx +++ b/designer/client/src/components/toolbars/status/buttons/CustomActionButton.tsx @@ -1,4 +1,4 @@ -import React, { ComponentType } from "react"; +import React, {ComponentType, useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import DefaultIcon from "../../../../assets/img/toolbarButtons/custom_action.svg"; import { CustomAction } from "../../../../types"; @@ -9,6 +9,9 @@ import { ToolbarButton } from "../../../toolbarComponents/toolbarButtons"; import { ToolbarButtonProps } from "../../types"; import UrlIcon from "../../../UrlIcon"; import { FallbackProps } from "react-error-boundary"; +import {useSelector} from "react-redux"; +import {getProcessVersionId} from "../../../../reducers/selectors/graph"; +import {resolveCustomActionDisplayability} from "../../../../helpers/customActionDisplayabilityResolver"; type CustomActionProps = { action: CustomAction; @@ -28,7 +31,9 @@ export default function CustomActionButton(props: CustomActionProps) { ); const statusName = processStatus?.name; - const available = !disabled && action.allowedStateStatusNames.includes(statusName); + const available = !disabled && + action.allowedStateStatusNames.includes(statusName) && + resolveCustomActionDisplayability(action.displayPolicy); const toolTip = available ? null diff --git a/designer/client/src/helpers/customActionDisplayabilityResolver.ts b/designer/client/src/helpers/customActionDisplayabilityResolver.ts new file mode 100644 index 00000000000..b9e94ac75ba --- /dev/null +++ b/designer/client/src/helpers/customActionDisplayabilityResolver.ts @@ -0,0 +1,19 @@ +import {CustomActionDisplayPolicy} from "../types"; +import {useSelector} from "react-redux"; +import {getProcessName, getProcessVersionId, getScenario} from "../reducers/selectors/graph"; +import {ActionType} from "../components/Process/types"; + +export function resolveCustomActionDisplayability(displayPolicy: CustomActionDisplayPolicy){ + const processVersionId = useSelector(getProcessVersionId); + const scenario = useSelector(getScenario); + + switch(displayPolicy.type) { + case "UICustomActionDisplaySimplePolicy": + const { version, operator, expr } = displayPolicy; + return false; + case "UICustomActionDisplayConditionalPolicy": + return false; + default: + return false; + } +} diff --git a/designer/client/src/types/scenarioGraph.ts b/designer/client/src/types/scenarioGraph.ts index 619c5a3147a..c8e5543bf1e 100644 --- a/designer/client/src/types/scenarioGraph.ts +++ b/designer/client/src/types/scenarioGraph.ts @@ -25,9 +25,40 @@ export type ProcessAdditionalFields = { metaDataType: string; }; +export type StatusExpr = { + type: 'UIStatusExpr'; + status: string; +}; + +export type NodeExpr = { + type: 'UINodeExpr'; + node: string; +}; + +type CustomActionDisplayPolicyExpr = StatusExpr | NodeExpr; + +// CustomActionDisplayPolicy ADT +export type CustomActionDisplaySimplePolicy = { + type: 'UICustomActionDisplaySimplePolicy'; + version: number; + operator: string; + expr: CustomActionDisplayPolicyExpr; +}; + +export type CustomActionDisplayConditionalPolicy = { + type: 'UICustomActionDisplayConditionalPolicy'; + condition: string; + operands: CustomActionDisplayPolicy[]; +}; + +export type CustomActionDisplayPolicy = + | CustomActionDisplaySimplePolicy + | CustomActionDisplayConditionalPolicy; + export type CustomAction = { name: string; allowedStateStatusNames: Array; + displayPolicy?: CustomActionDisplayPolicy; icon?: string; parameters?: Array; }; diff --git a/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala b/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala index 1259a32a193..2ff9418bd87 100644 --- a/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala +++ b/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala @@ -3,15 +3,22 @@ package pl.touk.nussknacker.restmodel import io.circe.generic.JsonCodec import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} import io.circe.{Decoder, Encoder} -import pl.touk.nussknacker.engine.api.component.ComponentType.ComponentType import pl.touk.nussknacker.engine.api.component.{ComponentGroupName, ComponentId} import pl.touk.nussknacker.engine.api.definition.ParameterEditor -import pl.touk.nussknacker.engine.api.deployment.CustomAction +import pl.touk.nussknacker.engine.api.deployment.{ + CustomAction, + CustomActionDisplayConditionalPolicy, + CustomActionDisplayPolicy, + CustomActionDisplaySimplePolicy, + NodeExpr, + StatusExpr +} import pl.touk.nussknacker.engine.api.typed.typing.TypingResult import pl.touk.nussknacker.engine.graph.EdgeType import pl.touk.nussknacker.engine.graph.evaluatedparam.{Parameter => NodeParameter} import pl.touk.nussknacker.engine.graph.expression.Expression import pl.touk.nussknacker.engine.graph.node.NodeData +import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec} import java.net.URI @@ -135,19 +142,58 @@ package object definition { def apply(action: CustomAction): UICustomAction = UICustomAction( name = action.name, allowedStateStatusNames = action.allowedStateStatusNames, + displayPolicy = action.displayPolicy.map(UICustomActionDisplayPolicy.fromCustomActionDisplayPolicy), icon = action.icon, parameters = action.parameters.map(p => UICustomActionParameter(p.name, p.editor)) ) } + implicit val configuration: Configuration = Configuration.default.withDefaults.withDiscriminator("type") + + @JsonCodec final case class UICustomActionParameter(name: String, editor: ParameterEditor) + + @ConfiguredJsonCodec sealed trait UICustomActionDisplayPolicyExpr + + @JsonCodec case class UIStatusExpr(status: String) extends UICustomActionDisplayPolicyExpr + @JsonCodec case class UINodeExpr(node: String) extends UICustomActionDisplayPolicyExpr + + @ConfiguredJsonCodec sealed trait UICustomActionDisplayPolicy + + @JsonCodec case class UICustomActionDisplaySimplePolicy( + version: Long, + operator: String, + expr: UICustomActionDisplayPolicyExpr + ) extends UICustomActionDisplayPolicy + + object UICustomActionDisplayPolicy { + + def fromCustomActionDisplayPolicy(displayPolicy: CustomActionDisplayPolicy): UICustomActionDisplayPolicy = + displayPolicy match { + case CustomActionDisplaySimplePolicy(version, operator, NodeExpr(node)) => + UICustomActionDisplaySimplePolicy(version, operator, UINodeExpr(node)) + + case CustomActionDisplaySimplePolicy(version, operator, StatusExpr(status)) => + UICustomActionDisplaySimplePolicy(version, operator, UIStatusExpr(status)) + + case CustomActionDisplayConditionalPolicy(condition, operands) => + val uiOperands = operands.map(fromCustomActionDisplayPolicy) + UICustomActionDisplayConditionalPolicy(condition, uiOperands) + } + + } + + @JsonCodec case class UICustomActionDisplayConditionalPolicy( + condition: String, + operands: List[UICustomActionDisplayPolicy] + ) extends UICustomActionDisplayPolicy + @JsonCodec final case class UICustomAction( name: String, allowedStateStatusNames: List[String], + displayPolicy: Option[UICustomActionDisplayPolicy], icon: Option[URI], parameters: List[UICustomActionParameter] ) - @JsonCodec final case class UICustomActionParameter(name: String, editor: ParameterEditor) - } diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala index 9aa78902b31..cb42ce294c9 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala @@ -1,9 +1,11 @@ package pl.touk.nussknacker.engine.api.deployment import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomParameterValidationError import pl.touk.nussknacker.engine.api.definition.ParameterEditor import pl.touk.nussknacker.engine.deployment.User +import scala.util.{Failure, Success, Try} import java.net.URI /* @@ -22,6 +24,7 @@ case class CustomAction( name: String, // We cannot use "engine.api.deployment.StateStatus" because it can be implemented as a class containing nonconstant attributes allowedStateStatusNames: List[String], + displayPolicy: Option[CustomActionDisplayPolicy] = None, parameters: List[CustomActionParameter] = Nil, icon: Option[URI] = None ) @@ -32,3 +35,62 @@ case class CustomActionParameter(name: String, editor: ParameterEditor) case class CustomActionRequest(name: String, processVersion: ProcessVersion, user: User, params: Map[String, String]) case class CustomActionResult(req: CustomActionRequest, msg: String) + +sealed trait CustomActionDisplayPolicy +sealed trait CustomActionDisplayPolicyExpr +case class StatusExpr(status: String) extends CustomActionDisplayPolicyExpr +case class NodeExpr(node: String) extends CustomActionDisplayPolicyExpr +case class CustomActionDisplaySimplePolicy(version: Long, operator: String, expr: CustomActionDisplayPolicyExpr) + extends CustomActionDisplayPolicy +case class CustomActionDisplayConditionalPolicy(condition: String, operands: List[CustomActionDisplayPolicy]) + extends CustomActionDisplayPolicy +class CustomActionDisplayPolicyError(msg: String) extends IllegalArgumentException(msg) + +object CustomActionDisplayPolicy { + + class CustomActionDisplayPolicyBuilder private ( + private val version: Option[Long] = None, + private val operator: Option[String] = None, + private val expr: Option[CustomActionDisplayPolicyExpr] = None, + private val condition: Option[String] = None, + private val operands: List[CustomActionDisplayPolicy] = List() + ) { + def withVersion(version: Long): CustomActionDisplayPolicyBuilder = + new CustomActionDisplayPolicyBuilder(version = Some(version), operator, expr, condition, operands) + + def withOperator(operator: String): CustomActionDisplayPolicyBuilder = + if (operator == "is" || operator == "contains") { + new CustomActionDisplayPolicyBuilder(version, operator = Some(operator), expr, condition, operands) + } else { + throw new CustomActionDisplayPolicyError("Operator must be one of ['is', 'contains']") + } + + def withExpr(expr: CustomActionDisplayPolicyExpr): CustomActionDisplayPolicyBuilder = + new CustomActionDisplayPolicyBuilder(version, operator, expr = Some(expr), condition, operands) + + def withCondition(condition: String): CustomActionDisplayPolicyBuilder = + if (condition == "OR" || condition == "AND") { + new CustomActionDisplayPolicyBuilder(version, operator, expr, condition = Some(condition), operands) + } else { + throw new CustomActionDisplayPolicyError("Operator must be one of ['OR', 'AND']") + } + + def withOperands(operands: CustomActionDisplayPolicy*): CustomActionDisplayPolicyBuilder = + new CustomActionDisplayPolicyBuilder(version, operator, expr, condition, operands = operands.toList) + + def buildSimplePolicy(): CustomActionDisplaySimplePolicy = + CustomActionDisplaySimplePolicy( + version.getOrElse(throw new CustomActionDisplayPolicyError("version not set")), + operator.getOrElse(throw new CustomActionDisplayPolicyError("operator not set")), + expr.getOrElse(throw new CustomActionDisplayPolicyError("expr not set")) + ) + + def buildConditionalPolicy(): CustomActionDisplayConditionalPolicy = + CustomActionDisplayConditionalPolicy( + condition.getOrElse(throw new CustomActionDisplayPolicyError("condition not set")), + if (operands.nonEmpty) operands else throw new CustomActionDisplayPolicyError("operands list is empty") + ) + + } + +}