Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Nu-5490] Expand Custom Action with display policy #5491

Closed
10 changes: 9 additions & 1 deletion designer/client/src/components/toolbars/status/ProcessInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ 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";
Expand All @@ -28,6 +33,9 @@ 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);

console.log(versionId);
philemone marked this conversation as resolved.
Show resolved Hide resolved

const description = ProcessStateUtils.getStateDescription(scenario, processState);
const transitionKey = ProcessStateUtils.getTransitionKey(scenario, processState);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -18,6 +21,7 @@ type CustomActionProps = {

export default function CustomActionButton(props: CustomActionProps) {
const { action, processStatus, disabled } = props;
const [isAvailable, setIsAvailable] = useState<boolean>(false);

const { t } = useTranslation();

Expand All @@ -27,10 +31,18 @@ export default function CustomActionButton(props: CustomActionProps) {
<DefaultIcon />
);

useEffect(() => {
const resolveDisplayability = async () => {
const res = await resolveCustomActionDisplayability(action.displayPolicy);
setIsAvailable(!disabled &&
action.allowedStateStatusNames.includes(statusName) && res);
}
resolveDisplayability();
}, []);

const statusName = processStatus?.name;
const available = !disabled && action.allowedStateStatusNames.includes(statusName);

const toolTip = available
const toolTip = isAvailable
? null
: t("panels.actions.custom-action.tooltips.disabled", "Disabled for {{statusName}} status.", { statusName });

Expand All @@ -39,7 +51,7 @@ export default function CustomActionButton(props: CustomActionProps) {
<ToolbarButton
name={action.name}
title={toolTip}
disabled={!available}
disabled={!isAvailable}
icon={icon}
onClick={() =>
open<CustomAction>({
Expand Down
23 changes: 23 additions & 0 deletions designer/client/src/helpers/customActionDisplayabilityResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {CustomActionDisplayPolicy} from "../types";
import {useSelector} from "react-redux";
import {getProcessName, getProcessVersionId} from "../reducers/selectors/graph";
import HttpService from "../http/HttpService";

export async function resolveCustomActionDisplayability(displayPolicy: CustomActionDisplayPolicy){
const processName = useSelector(getProcessName);
const processVersionId = useSelector(getProcessVersionId);

switch(displayPolicy) {
case CustomActionDisplayPolicy.CurrentlyViewedProcessVersionIsDeployed:
try {
const response = await HttpService.fetchLastDeployedVersionId(processName);
const data = response.data;
return data.versionId === processVersionId;
} catch (error) {
console.error("Error while fetching last deployed version ID:", error);
return false;
}
default:
return false;
}
}
8 changes: 8 additions & 0 deletions designer/client/src/http/HttpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export interface AppBuildInfo {
processingType: any;
}

export type LastDeployedVersionId = {
versionId: number | null;
}

export type ComponentActionType = {
id: string;
title: string;
Expand Down Expand Up @@ -268,6 +272,10 @@ class HttpService {
.then((res) => res.data.filter(({ actionType }) => actionType === "DEPLOY").map(({ performedAt }) => performedAt));
}

fetchLastDeployedVersionId(processName: string) {
return api.get<LastDeployedVersionId>(`/processes/${encodeURIComponent(processName)}/lastDeployedVersionId`);
}

deploy(processName: string, comment?: string): Promise<{ isSuccess: boolean }> {
return api
.post(`/processManagement/deploy/${encodeURIComponent(processName)}`, comment)
Expand Down
5 changes: 5 additions & 0 deletions designer/client/src/types/scenarioGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ export type ProcessAdditionalFields = {
metaDataType: string;
};

export enum CustomActionDisplayPolicy {
CurrentlyViewedProcessVersionIsDeployed
}

export type CustomAction = {
name: string;
allowedStateStatusNames: Array<string>;
displayPolicy?: CustomActionDisplayPolicy;
icon?: string;
parameters?: Array<CustomActionParameter>;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package pl.touk.nussknacker.restmodel

import io.circe.generic.JsonCodec
import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder}
import io.circe.generic.extras.semiauto.{
deriveConfiguredDecoder,
deriveConfiguredEncoder,
deriveEnumerationDecoder,
deriveEnumerationEncoder
}
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
import pl.touk.nussknacker.engine.api.deployment.{CurrentlyViewedProcessVersionIsDeployed, CustomAction}
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}
Expand Down Expand Up @@ -135,6 +141,9 @@ package object definition {
def apply(action: CustomAction): UICustomAction = UICustomAction(
name = action.name,
allowedStateStatusNames = action.allowedStateStatusNames,
displayPolicy = action.displayPolicy.map { case CurrentlyViewedProcessVersionIsDeployed =>
UICustomActionDisplayPolicy.CurrentlyViewedProcessVersionIsDeployed
},
icon = action.icon,
parameters = action.parameters.map(p => UICustomActionParameter(p.name, p.editor))
)
Expand All @@ -144,10 +153,23 @@ package object definition {
@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)

sealed trait UICustomActionDisplayPolicy

object UICustomActionDisplayPolicy {
case object CurrentlyViewedProcessVersionIsDeployed extends UICustomActionDisplayPolicy

implicit val customActionDisplayPolicyEncoder: Encoder[UICustomActionDisplayPolicy] =
deriveEnumerationEncoder[UICustomActionDisplayPolicy]

implicit val customActionDisplayPolicyDecoder: Decoder[UICustomActionDisplayPolicy] =
deriveEnumerationDecoder[UICustomActionDisplayPolicy]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ class ProcessesResources(
processService.getProcessActions(processId.id)
}
}
} ~ path("processes" / ProcessNameSegment / "lastDeployedVersionId") { processName =>
processId(processName) { processId =>
complete {
processService.getLastDeployedVersionId(processId.id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use results of processService.getLatestProcessWithDetails or processService.getProcessWithDetails instead of adding new endpoint? See ScenarioWithDetails.lastDeployedAction, there is some info on current and last deployed version.

}
}
} ~ path("processes" / ProcessNameSegment) { processName =>
processId(processName) { processId =>
(delete & canWrite(processId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ object ProcessService {
case class ValidateAndResolve(includeValidationNodeResults: Boolean = true) extends ValidationMode
}

@JsonCodec case class LastDeployedVersionId(versionId: Option[Long])
}

trait ProcessService {
Expand Down Expand Up @@ -134,6 +135,7 @@ trait ProcessService {

def getProcessActions(id: ProcessId): Future[List[ProcessAction]]

def getLastDeployedVersionId(id: ProcessId): Future[LastDeployedVersionId]
}

/**
Expand Down Expand Up @@ -497,4 +499,19 @@ class DBProcessService(
throw ProcessValidationError("Scenario category not found.")
}

override def getLastDeployedVersionId(id: ProcessId): Future[LastDeployedVersionId] = {
// actions are sorted by timestamp in DESC order, thanks to that the optimalization below works
val maybeLastProcessActionsDB = processActionRepository.getFinishedProcessActions(id, None).map(_.headOption)
val lastDeployedVersionIdDB = maybeLastProcessActionsDB
.map(_.flatMap {
case ProcessAction(_, _, processVersionId, _, _, _, ProcessActionType.Deploy, _, _, _, _, _) =>
Some(processVersionId.value)
case _ =>
None
})
.map(LastDeployedVersionId.apply)

dbioRunner.runInTransaction(lastDeployedVersionIdDB)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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
)
Expand All @@ -32,3 +33,8 @@ 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

// TODO: Add more
case object CurrentlyViewedProcessVersionIsDeployed extends CustomActionDisplayPolicy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it is good approach. Those policies seem to be instance-specific, but each NU instance can be configured with different custom actions, different policies.

What happens when I want to add my own custom policy? E.g I want a button that is enabled when scenario diagram contains specified node. Do I need to hardcode it here and in designer/client/src/helpers/customActionDisplayabilityResolver.ts ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answering to your very last question, the answer is yes. If such hardcoded solution cannot be taken into account, the natural improvement is to create some configuration language (implemented using yaml, json, whatever), send it thru' the backend and teach frontend to read it. And of course we should not then use selaed traits.

Loading