Skip to content

Commit

Permalink
separate presentation from business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadius committed Feb 17, 2025
1 parent dea343a commit 064fa83
Show file tree
Hide file tree
Showing 53 changed files with 534 additions and 499 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ trait DeploymentManager extends AutoCloseable {
* from the cache or not. If you use any kind of cache in your DM implementation please wrap result data
* with WithDataFreshnessStatus.cached(data) in opposite situation use WithDataFreshnessStatus.fresh(data)
*/
def getProcessStates(name: ProcessName)(
def getScenarioDeploymentsStatuses(scenarioName: ProcessName)(
implicit freshnessPolicy: DataFreshnessPolicy
): Future[WithDataFreshnessStatus[List[StatusDetails]]]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package pl.touk.nussknacker.engine.api.deployment

import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ProcessStatus
import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ScenarioStatusWithScenarioContext
import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName

import java.net.URI
Expand All @@ -21,32 +21,42 @@ import java.net.URI
*/
class OverridingProcessStateDefinitionManager(
delegate: ProcessStateDefinitionManager,
statusActionsPF: PartialFunction[ProcessStatus, List[ScenarioActionName]] = PartialFunction.empty,
statusActionsPF: PartialFunction[ScenarioStatusWithScenarioContext, List[ScenarioActionName]] =
PartialFunction.empty,
statusIconsPF: PartialFunction[StateStatus, URI] = PartialFunction.empty,
statusTooltipsPF: PartialFunction[StateStatus, String] = PartialFunction.empty,
statusDescriptionsPF: PartialFunction[StateStatus, String] = PartialFunction.empty,
customStateDefinitions: Map[StatusName, StateDefinitionDetails] = Map.empty,
customVisibleActions: Option[List[ScenarioActionName]] = None,
customActionTooltips: Option[ProcessStatus => Map[ScenarioActionName, String]] = None,
customActionTooltips: Option[ScenarioStatusWithScenarioContext => Map[ScenarioActionName, String]] = None,
) extends ProcessStateDefinitionManager {

override def visibleActions: List[ScenarioActionName] =
customVisibleActions.getOrElse(delegate.visibleActions)
override def visibleActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName] =
customVisibleActions.getOrElse(delegate.visibleActions(input))

override def statusActions(processStatus: ProcessStatus): List[ScenarioActionName] =
statusActionsPF.applyOrElse(processStatus, delegate.statusActions)
override def statusActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName] =
statusActionsPF.applyOrElse(input, delegate.statusActions)

override def actionTooltips(processStatus: ProcessStatus): Map[ScenarioActionName, String] =
customActionTooltips.map(_(processStatus)).getOrElse(delegate.actionTooltips(processStatus))
override def actionTooltips(input: ScenarioStatusWithScenarioContext): Map[ScenarioActionName, String] =
customActionTooltips.map(_(input)).getOrElse(delegate.actionTooltips(input))

override def statusIcon(stateStatus: StateStatus): URI =
statusIconsPF.orElse(stateDefinitionsPF(_.icon)).applyOrElse(stateStatus, delegate.statusIcon)
override def statusIcon(input: ScenarioStatusWithScenarioContext): URI =
statusIconsPF
.orElse(stateDefinitionsPF(_.icon))
.lift(input.status)
.getOrElse(delegate.statusIcon(input))

override def statusTooltip(stateStatus: StateStatus): String =
statusTooltipsPF.orElse(stateDefinitionsPF(_.tooltip)).applyOrElse(stateStatus, delegate.statusTooltip)
override def statusTooltip(input: ScenarioStatusWithScenarioContext): String =
statusTooltipsPF
.orElse(stateDefinitionsPF(_.tooltip))
.lift(input.status)
.getOrElse(delegate.statusTooltip(input))

override def statusDescription(stateStatus: StateStatus): String =
statusDescriptionsPF.orElse(stateDefinitionsPF(_.description)).applyOrElse(stateStatus, delegate.statusDescription)
override def statusDescription(input: ScenarioStatusWithScenarioContext): String =
statusDescriptionsPF
.orElse(stateDefinitionsPF(_.description))
.lift(input.status)
.getOrElse(delegate.statusDescription(input))

override def stateDefinitions: Map[StatusName, StateDefinitionDetails] =
delegate.stateDefinitions ++ customStateDefinitions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package pl.touk.nussknacker.engine.api.deployment

import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.{ProcessStatus, defaultVisibleActions}
import io.circe.Json
import pl.touk.nussknacker.engine.api.ProcessVersion
import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.{
ScenarioStatusPresentationDetails,
ScenarioStatusWithScenarioContext,
defaultVisibleActions
}
import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName
import pl.touk.nussknacker.engine.api.process.VersionId
import pl.touk.nussknacker.engine.deployment.{DeploymentId, ExternalDeploymentId}

import java.net.URI

//@TODO: In future clean up it.
// TODO: Some cleanups such as rename to sth close to presentation
/**
* Used to specify status definitions (for filtering and scenario status visualization) and status transitions (actions).
*/
Expand All @@ -29,54 +36,44 @@ trait ProcessStateDefinitionManager {
* Override those methods to customize varying state properties or custom visualizations,
* e.g. handle schedule date in [[PeriodicProcessStateDefinitionManager]]
*/
def statusTooltip(stateStatus: StateStatus): String =
stateDefinitions(stateStatus.name).tooltip
def statusTooltip(input: ScenarioStatusWithScenarioContext): String =
stateDefinitions(input.status.name).tooltip

def statusDescription(input: ScenarioStatusWithScenarioContext): String =
stateDefinitions(input.status.name).description

def statusDescription(stateStatus: StateStatus): String =
stateDefinitions(stateStatus.name).description
def statusIcon(input: ScenarioStatusWithScenarioContext): URI =
statusIcon(input.status)

def statusIcon(stateStatus: StateStatus): URI =
stateDefinitions(stateStatus.name).icon
private[nussknacker] def statusIcon(status: StateStatus): URI =
stateDefinitions(status.name).icon

/**
* Actions that are applicable to scenario in general. They may be available only in particular states, as defined by `def statusActions`
*/
def visibleActions: List[ScenarioActionName] = defaultVisibleActions
def visibleActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName] = defaultVisibleActions

/**
* Custom tooltips for actions
*/
def actionTooltips(processStatus: ProcessStatus): Map[ScenarioActionName, String] = Map.empty
def actionTooltips(input: ScenarioStatusWithScenarioContext): Map[ScenarioActionName, String] = Map.empty

/**
* Allowed transitions between states.
*/
def statusActions(processStatus: ProcessStatus): List[ScenarioActionName]
def statusActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName]

/**
* Enhances raw [[StateStatus]] with scenario properties, including deployment info.
* Returns presentations details of status
*/
// FIXME abr: extract other class without most of fields from ProcessState
def processState(
statusDetails: StatusDetails,
latestVersionId: VersionId,
deployedVersionId: Option[VersionId],
currentlyPresentedVersionId: Option[VersionId],
): ProcessState = {
val status = ProcessStatus(statusDetails.status, latestVersionId, deployedVersionId, currentlyPresentedVersionId)
ProcessState(
statusDetails.externalDeploymentId,
statusDetails.status,
statusDetails.version,
visibleActions,
statusActions(status),
actionTooltips(status),
statusIcon(statusDetails.status),
statusTooltip(statusDetails.status),
statusDescription(statusDetails.status),
statusDetails.startTime,
statusDetails.attributes,
statusDetails.errors
def statusPresentation(input: ScenarioStatusWithScenarioContext): ScenarioStatusPresentationDetails = {
ScenarioStatusPresentationDetails(
visibleActions(input),
statusActions(input),
actionTooltips(input),
statusIcon(input),
statusTooltip(input),
statusDescription(input),
)
}

Expand All @@ -87,15 +84,37 @@ object ProcessStateDefinitionManager {
/**
* ProcessStatus contains status of the scenario, it is used as argument of ProcessStateDefinitionManager methods
*
* @param stateStatus current scenario state
* @param statusDetails current scenario state
* @param latestVersionId latest saved versionId for the scenario
* @param deployedVersionId currently deployed versionId of the scenario
*/
final case class ProcessStatus(
stateStatus: StateStatus,
final case class ScenarioStatusWithScenarioContext(
private val statusDetails: StatusDetails,
latestVersionId: VersionId,
deployedVersionId: Option[VersionId],
currentlyPresentedVersionId: Option[VersionId],
) {
def status: StateStatus = statusDetails.status
def deploymentId: Option[DeploymentId] = statusDetails.deploymentId
def externalDeploymentId: Option[ExternalDeploymentId] = statusDetails.externalDeploymentId
def version: Option[ProcessVersion] = statusDetails.version
def startTime: Option[Long] = statusDetails.startTime
def attributes: Option[Json] = statusDetails.attributes
def errors: List[String] = statusDetails.errors

def withStatus(newStatus: StateStatus): ScenarioStatusWithScenarioContext =
copy(statusDetails = statusDetails.copy(status = newStatus))

}

final case class ScenarioStatusPresentationDetails(
visibleActions: List[ScenarioActionName],
// This one is not exactly a part of presentation but for now we keep in this class
allowedActions: List[ScenarioActionName],
actionTooltips: Map[ScenarioActionName, String],
icon: URI,
tooltip: String,
description: String
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pl.touk.nussknacker.engine.api.deployment

import io.circe.Json
import pl.touk.nussknacker.engine.api.ProcessVersion
import pl.touk.nussknacker.engine.deployment.{DeploymentId, ExternalDeploymentId}

case class StatusDetails(
status: StateStatus,
deploymentId: Option[DeploymentId],
externalDeploymentId: Option[ExternalDeploymentId] = None,
version: Option[ProcessVersion] = None,
startTime: Option[Long] = None,
attributes: Option[Json] = None,
errors: List[String] = List.empty
) {
def externalDeploymentIdUnsafe: ExternalDeploymentId =
externalDeploymentId.getOrElse(throw new IllegalStateException(s"externalDeploymentId is missing"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ class CachingProcessStateDeploymentManager(
.expireAfterWrite(java.time.Duration.ofMillis(cacheTTL.toMillis))
.buildAsync[ProcessName, List[StatusDetails]]

override def getProcessStates(
name: ProcessName
override def getScenarioDeploymentsStatuses(
scenarioName: ProcessName
)(implicit freshnessPolicy: DataFreshnessPolicy): Future[WithDataFreshnessStatus[List[StatusDetails]]] = {
def fetchAndUpdateCache(): Future[WithDataFreshnessStatus[List[StatusDetails]]] = {
val resultFuture = delegate.getProcessStates(name)
cache.put(name, resultFuture.map(_.value).toJava.toCompletableFuture)
val resultFuture = delegate.getScenarioDeploymentsStatuses(scenarioName)
cache.put(scenarioName, resultFuture.map(_.value).toJava.toCompletableFuture)
resultFuture
}

freshnessPolicy match {
case DataFreshnessPolicy.Fresh =>
fetchAndUpdateCache()
case DataFreshnessPolicy.CanBeCached =>
Option(cache.getIfPresent(name))
Option(cache.getIfPresent(scenarioName))
.map(_.toScala.map(WithDataFreshnessStatus.cached))
.getOrElse(
fetchAndUpdateCache()
Expand Down Expand Up @@ -72,7 +72,7 @@ object CachingProcessStateDeploymentManager extends LazyLogging {
)
}
.getOrElse {
logger.debug(s"Skipping ProcessState caching for DeploymentManager: $delegate")
logger.debug(s"Skipping state caching for DeploymentManager: $delegate")
delegate
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package pl.touk.nussknacker.engine.api.deployment.simple

import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ProcessStatus
import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ScenarioStatusWithScenarioContext
import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName.DefaultActions
import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName
import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.ProblemStateStatus._
import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.{ProblemStateStatus, statusActionsPF}
import pl.touk.nussknacker.engine.api.deployment.{
ProcessState,
ProcessStateDefinitionManager,
ScenarioActionName,
StateDefinitionDetails,
StateStatus,
StatusDetails
StateStatus
}
import pl.touk.nussknacker.engine.api.process.VersionId

/**
* Base [[ProcessStateDefinitionManager]] with basic state definitions and state transitions.
Expand All @@ -22,23 +18,24 @@ import pl.touk.nussknacker.engine.api.process.VersionId
*/
object SimpleProcessStateDefinitionManager extends ProcessStateDefinitionManager {

override def statusActions(processStatus: ProcessStatus): List[ScenarioActionName] =
statusActionsPF.applyOrElse(processStatus, (_: ProcessStatus) => DefaultActions)
override def statusActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName] =
statusActionsPF.lift(input.status).getOrElse(DefaultActions)

override def statusDescription(stateStatus: StateStatus): String = stateStatus match {
override def statusDescription(input: ScenarioStatusWithScenarioContext): String = statusDescription(input.status)

private[nussknacker] def statusDescription(status: StateStatus): String = status match {
case _ @ProblemStateStatus(message, _) => message
case _ => SimpleStateStatus.definitions(stateStatus.name).description
case _ => SimpleStateStatus.definitions(status.name).description
}

override def statusTooltip(stateStatus: StateStatus): String = stateStatus match {
override def statusTooltip(input: ScenarioStatusWithScenarioContext): String = statusTooltip(input.status)

private[nussknacker] def statusTooltip(status: StateStatus): String = status match {
case _ @ProblemStateStatus(message, _) => message
case _ => SimpleStateStatus.definitions(stateStatus.name).tooltip
case _ => SimpleStateStatus.definitions(status.name).tooltip
}

override def stateDefinitions: Map[StatusName, StateDefinitionDetails] =
SimpleStateStatus.definitions

def errorFailedToGet(versionId: VersionId): ProcessState =
processState(StatusDetails(FailedToGet, None), versionId, None, None)

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package pl.touk.nussknacker.engine.api.deployment.simple

import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ProcessStatus
import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName
import pl.touk.nussknacker.engine.api.deployment._
import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.ProblemStateStatus.defaultActions
import pl.touk.nussknacker.engine.api.process.VersionId

import java.net.URI

// FIXME abr separate core statuses and DM statuses - the same for presentation
object SimpleStateStatus {

def fromDeploymentStatus(deploymentStatus: DeploymentStatus): StateStatus = {
Expand Down Expand Up @@ -92,7 +92,7 @@ object SimpleStateStatus {
status
)

val statusActionsPF: PartialFunction[ProcessStatus, List[ScenarioActionName]] = _.stateStatus match {
val statusActionsPF: PartialFunction[StateStatus, List[ScenarioActionName]] = {
case SimpleStateStatus.NotDeployed =>
List(ScenarioActionName.Deploy, ScenarioActionName.Archive, ScenarioActionName.Rename)
case SimpleStateStatus.DuringDeploy => List(ScenarioActionName.Deploy, ScenarioActionName.Cancel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ class DeploymentManagerStub(implicit ec: ExecutionContext) extends BaseDeploymen
notImplemented
}

override def getProcessStates(
name: ProcessName
override def getScenarioDeploymentsStatuses(
scenarioName: ProcessName
)(implicit freshnessPolicy: DataFreshnessPolicy): Future[WithDataFreshnessStatus[List[StatusDetails]]] = {
Future.successful(
WithDataFreshnessStatus.fresh(scenarioStatusMap.get(name).map(StatusDetails(_, None)).toList)
WithDataFreshnessStatus.fresh(scenarioStatusMap.get(scenarioName).map(StatusDetails(_, None)).toList)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package pl.touk.nussknacker.engine.api.deployment

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ProcessStatus
import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ScenarioStatusWithScenarioContext
import pl.touk.nussknacker.engine.api.deployment.StateDefinitionDetails.UnknownIcon
import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName
import pl.touk.nussknacker.engine.api.process.VersionId

class OverridingProcessStateDefinitionManagerTest extends AnyFunSuite with Matchers {

Expand All @@ -28,7 +29,7 @@ class OverridingProcessStateDefinitionManagerTest extends AnyFunSuite with Match
)
)

override def statusActions(processStatus: ProcessStatus): List[ScenarioActionName] = Nil
override def statusActions(input: ScenarioStatusWithScenarioContext): List[ScenarioActionName] = Nil
}

test("should combine delegate state definitions with custom overrides") {
Expand Down Expand Up @@ -58,10 +59,13 @@ class OverridingProcessStateDefinitionManagerTest extends AnyFunSuite with Match
definitionsMap(CustomState.name).description shouldBe "Custom description"
definitionsMap(CustomStateThatOverrides.name).description shouldBe "Custom description that overrides"

def toInput(status: StateStatus) =
ScenarioStatusWithScenarioContext(StatusDetails(status, None), VersionId(1), None, None)

// Description assigned to a scenario, with custom calculations
manager.statusDescription(DefaultState) shouldBe "Calculated description for default, e.g. schedule date"
manager.statusDescription(CustomState) shouldBe "Calculated description for custom, e.g. schedule date"
manager.statusDescription(CustomStateThatOverrides) shouldBe "Custom description that overrides"
manager.statusDescription(toInput(DefaultState)) shouldBe "Calculated description for default, e.g. schedule date"
manager.statusDescription(toInput(CustomState)) shouldBe "Calculated description for custom, e.g. schedule date"
manager.statusDescription(toInput(CustomStateThatOverrides)) shouldBe "Custom description that overrides"
}

}
Loading

0 comments on commit 064fa83

Please sign in to comment.