From f2ce653a9d2f857bdc9f68dd598cef9628670bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciek=20Pr=C3=B3chniak?= Date: Tue, 16 Mar 2021 08:00:17 +0100 Subject: [PATCH] [Feature] Custom action parameters --- .../engine/api/deployment/CustomAction.scala | 6 +- ui/client/actions/actionTypes.ts | 1 + ui/client/actions/nk/modal.ts | 15 +++- .../node-modal/editors/expression/Editor.ts | 2 +- .../components/modals/CustomActionDialog.tsx | 74 +++++++++++++++++++ ui/client/components/modals/Dialogs.tsx | 2 + ui/client/components/modals/DialogsTypes.tsx | 2 + .../components/modals/GenericModalDialog.tsx | 8 +- .../status/buttons/CustomActionButton.tsx | 57 +++++++------- ui/client/reducers/ui.ts | 12 ++- ui/client/types/process.ts | 7 ++ .../restmodel/definition/package.scala | 14 +++- 12 files changed, 157 insertions(+), 43 deletions(-) create mode 100644 ui/client/components/modals/CustomActionDialog.tsx diff --git a/engine/api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala b/engine/api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala index c2ab399940b..8b92d8a39f3 100644 --- a/engine/api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala +++ b/engine/api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/CustomAction.scala @@ -1,6 +1,7 @@ package pl.touk.nussknacker.engine.api.deployment import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.definition.{Parameter, ParameterEditor} import java.net.URI @@ -10,7 +11,6 @@ CustomActions purpose is to allow non standard process management actions (like FIXME: 1. Additional validations on action invoke, like checking if process definition is valid -2. Handle CustomActionRequest#params Things to consider in future changes: 1. Allowing for attaching comments to custom actions, similarly to stop/deploy comments. @@ -20,8 +20,12 @@ Things to consider in future changes: 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], + parameters: List[CustomActionParameter] = Nil, icon: Option[URI] = None) +//TODO: validators? +case class CustomActionParameter(name: String, editor: ParameterEditor) + case class CustomActionRequest(name: String, processVersion: ProcessVersion, user: User, diff --git a/ui/client/actions/actionTypes.ts b/ui/client/actions/actionTypes.ts index 64d29004cf3..71e3aa68e60 100644 --- a/ui/client/actions/actionTypes.ts +++ b/ui/client/actions/actionTypes.ts @@ -54,6 +54,7 @@ export type ActionTypes = | "UPDATE_IMPORTED_PROCESS" | "CLEAR_PROCESS" | "TOGGLE_PROCESS_ACTION_MODAL" + | "TOGGLE_CUSTOM_ACTION" | "DISPLAY_PROCESS_COUNTS" | "HIDE_RUN_PROCESS_DETAILS" | "DISPLAY_TEST_RESULTS_DETAILS" diff --git a/ui/client/actions/nk/modal.ts b/ui/client/actions/nk/modal.ts index 26292a5a6ff..a167d7a56c0 100644 --- a/ui/client/actions/nk/modal.ts +++ b/ui/client/actions/nk/modal.ts @@ -5,7 +5,7 @@ import NodeUtils from "../../components/graph/NodeUtils" import {DialogType} from "../../components/modals/DialogsTypes" import history from "../../history" import {ThunkAction} from "../reduxTypes" -import {Edge, NodeType} from "../../types" +import {CustomAction, Edge, NodeType} from "../../types" import {EventInfo, reportEvent} from "./reportEvent" export type DisplayModalNodeDetailsAction = { @@ -27,6 +27,11 @@ export type ToggleInfoModalAction = { text: string, } +export type ToggleCustomActionAction = { + type: "TOGGLE_CUSTOM_ACTION", + customAction: CustomAction, +} + export function displayModalNodeDetails(node: NodeType, readonly: boolean, eventInfo: EventInfo): ThunkAction { return (dispatch) => { history.replace({ @@ -88,3 +93,11 @@ export function toggleInfoModal(openDialog: DialogType, text: string): ToggleInf text: text, } } + +export function toggleCustomAction(customAction: CustomAction): ToggleCustomActionAction { + return { + type: "TOGGLE_CUSTOM_ACTION", + customAction: customAction, + } +} + diff --git a/ui/client/components/graph/node-modal/editors/expression/Editor.ts b/ui/client/components/graph/node-modal/editors/expression/Editor.ts index 6c7b37afb48..36aaf108289 100644 --- a/ui/client/components/graph/node-modal/editors/expression/Editor.ts +++ b/ui/client/components/graph/node-modal/editors/expression/Editor.ts @@ -24,7 +24,7 @@ import JsonEditor from "./JsonEditor" import DualParameterEditor from "./DualParameterEditor" type ValuesType = Array -type EditorProps = $TodoType +export type EditorProps = $TodoType export type SimpleEditor

= Editor

& { switchableTo: (expressionObj: ExpressionObj, values?: ValuesType) => boolean, diff --git a/ui/client/components/modals/CustomActionDialog.tsx b/ui/client/components/modals/CustomActionDialog.tsx new file mode 100644 index 00000000000..fef4e5c8ae5 --- /dev/null +++ b/ui/client/components/modals/CustomActionDialog.tsx @@ -0,0 +1,74 @@ +import React, {useCallback, useState} from "react" +import {useDispatch, useSelector} from "react-redux" +import {getProcessId} from "../../reducers/selectors/graph" +import {loadProcessState} from "../../actions/nk" +import GenericModalDialog from "./GenericModalDialog" +import Dialogs from "./Dialogs" +import HttpService from "../../http/HttpService" +import {getModalDialog} from "../../reducers/selectors/ui" +import {editors} from "../graph/node-modal/editors/expression/Editor" +import {ExpressionLang} from "../graph/node-modal/editors/expression/types" + +function CustomActionDialog(): JSX.Element { + + const processId = useSelector(getProcessId) + const dispatch = useDispatch() + const action = useSelector(getModalDialog).customAction + + const empty = (action?.parameters || []).reduce((obj, param) => ({...obj, [param.name]: ""}), {}) + + const [mapState, setState] = useState(empty) + + //initial state, we want to compute it only once + const init = useCallback(() => setState({}), []) + + const confirm = useCallback(async () => { + await dispatch(HttpService + .customAction(processId, action.name, mapState) + .finally(() => dispatch(loadProcessState(processId)))) + }, [processId, action, mapState]) + + const setParam = (name: string) => (value: any) => setState(current => ({...current, [name]: value})) + + return ( + +

+ { + (action?.parameters || []).map(param => { + const editorType = param.editor.type + const Editor = editors[editorType] + const fieldName = param.name + return ( +
+
{fieldName}:
+ +
+ ) + }) + } +
+ + + ) +} + +export default CustomActionDialog diff --git a/ui/client/components/modals/Dialogs.tsx b/ui/client/components/modals/Dialogs.tsx index 31c0fa9e10e..f97570d31bc 100644 --- a/ui/client/components/modals/Dialogs.tsx +++ b/ui/client/components/modals/Dialogs.tsx @@ -7,6 +7,7 @@ import GenerateTestDataDialog from "./GenerateTestDataDialog" import InfoModal from "./InfoModal" import ProcessActionDialog from "./ProcessActionDialog" import SaveProcessDialog from "./SaveProcessDialog" +import CustomActionDialog from "./CustomActionDialog" export function AllDialogs(): JSX.Element { return ( @@ -18,6 +19,7 @@ export function AllDialogs(): JSX.Element { + ) } diff --git a/ui/client/components/modals/DialogsTypes.tsx b/ui/client/components/modals/DialogsTypes.tsx index e4574830f29..31203b62051 100644 --- a/ui/client/components/modals/DialogsTypes.tsx +++ b/ui/client/components/modals/DialogsTypes.tsx @@ -4,6 +4,7 @@ export type DialogType = "INFO_MODAL" | "GENERATE_TEST_DATA" | "CALCULATE_COUNTS" | "COMPARE_VERSIONS" + | "CUSTOM_ACTION" export const dialogTypesMap: Record = { infoModal: "INFO_MODAL", @@ -12,4 +13,5 @@ export const dialogTypesMap: Record = { generateTestData: "GENERATE_TEST_DATA", calculateCounts: "CALCULATE_COUNTS", compareVersions: "COMPARE_VERSIONS", + customAction: "CUSTOM_ACTION", } diff --git a/ui/client/components/modals/GenericModalDialog.tsx b/ui/client/components/modals/GenericModalDialog.tsx index 1e90b3e6d82..95b96503ae1 100644 --- a/ui/client/components/modals/GenericModalDialog.tsx +++ b/ui/client/components/modals/GenericModalDialog.tsx @@ -17,7 +17,7 @@ type OwnProps = { header?: string, confirm?: (close: () => void) => PromiseLike, type: DialogType, - init?: () => void, + init: () => void, } type State = { @@ -74,8 +74,10 @@ class GenericModalDialog extends React.Component {
{this.props.header ? ( -
- {this.props.header} +
+
+ {this.props.header} +
) : null} diff --git a/ui/client/components/toolbars/status/buttons/CustomActionButton.tsx b/ui/client/components/toolbars/status/buttons/CustomActionButton.tsx index 33e6c4d7f74..363342aeabd 100644 --- a/ui/client/components/toolbars/status/buttons/CustomActionButton.tsx +++ b/ui/client/components/toolbars/status/buttons/CustomActionButton.tsx @@ -1,17 +1,16 @@ import React from "react" -import ToolbarButton from "../../../toolbarComponents/ToolbarButton"; -import {ReactComponent as DefaultIcon} from "../../../../assets/img/toolbarButtons/custom_action.svg"; -import {loadProcessState} from "../../../../actions/nk"; -import HttpService from "../../../../http/HttpService"; -import {useDispatch} from "react-redux"; -import {StatusType} from "../../../Process/types"; -import {useTranslation} from "react-i18next"; -import {CustomAction} from "../../../../types"; +import ToolbarButton from "../../../toolbarComponents/ToolbarButton" +import {ReactComponent as DefaultIcon} from "../../../../assets/img/toolbarButtons/custom_action.svg" +import {toggleCustomAction} from "../../../../actions/nk" +import {useDispatch} from "react-redux" +import {StatusType} from "../../../Process/types" +import {useTranslation} from "react-i18next" +import {CustomAction} from "../../../../types" type Props = { action: CustomAction, processId: string, - processStatus: StatusType | null + processStatus: StatusType | null, } export default function CustomActionButton(props: Props) { @@ -21,30 +20,24 @@ export default function CustomActionButton(props: Props) { const dispatch = useDispatch() const {t} = useTranslation() - const icon = action.icon - ? {`custom-action-${action.name}`} - : + const icon = action.icon ? + {`custom-action-${action.name}`} : + const statusName = processStatus?.name const isDisabled = !action.allowedStateStatusNames.includes(statusName) - const toolTip = isDisabled - ? t("panels.actions.custom-action.tooltips.disabled", "Disabled for {{statusName}} status.", {statusName}) - : null - - // TODO: handle additional params - const onClick = () => { - confirm(`Do you want to run ${action.name}`) - && dispatch(HttpService - .customAction(processId, action.name, {}) - .finally(() => dispatch(loadProcessState(processId)))) - } - - return -} \ No newline at end of file + const toolTip = isDisabled ? + t("panels.actions.custom-action.tooltips.disabled", "Disabled for {{statusName}} status.", {statusName}) : + null + + return ( + dispatch(toggleCustomAction(action))} + /> + ) +} diff --git a/ui/client/reducers/ui.ts b/ui/client/reducers/ui.ts index cfccef8e0f5..e814900c4b3 100644 --- a/ui/client/reducers/ui.ts +++ b/ui/client/reducers/ui.ts @@ -1,5 +1,5 @@ import {DialogType, dialogTypesMap} from "../components/modals/DialogsTypes" -import {ProcessId} from "../types" +import {CustomAction, ProcessId} from "../types" import {Reducer} from "../actions/reduxTypes" import {mergeReducers} from "./mergeReducers" @@ -21,6 +21,7 @@ export type UiState = { action: (processId: ProcessId, comment: string) => void, displayWarnings: boolean, text: string, + customAction: CustomAction, }>, allModalsClosed: boolean, isToolTipsHighlighted: boolean, @@ -128,6 +129,15 @@ const uiReducer: Reducer = (state = emptyUiState, action) => { }, } } + case "TOGGLE_CUSTOM_ACTION": { + return { + ...state, + modalDialog: { + openDialog: dialogTypesMap.customAction, + customAction: action.customAction, + }, + } + } default: return state diff --git a/ui/client/types/process.ts b/ui/client/types/process.ts index 1fd62232093..cc86706e17e 100644 --- a/ui/client/types/process.ts +++ b/ui/client/types/process.ts @@ -1,6 +1,7 @@ import {Edge} from "./edge" import {NodeType} from "./node" import {ValidationResult} from "./validation" +import {EditorProps} from "../components/graph/node-modal/editors/expression/Editor" export type Process = { id: string, @@ -30,6 +31,12 @@ export type CustomAction = { name: string, allowedStateStatusNames: Array, icon: string | null, + parameters: Array | null, +} + +export type CustomActionParameter = { + name: string, + editor: EditorProps, } export type ProcessDefinitionData = { diff --git a/ui/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala b/ui/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala index 985fb052c60..419a01d2f0b 100644 --- a/ui/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala +++ b/ui/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala @@ -4,7 +4,7 @@ import io.circe.Decoder import io.circe.generic.JsonCodec import io.circe.generic.semiauto.deriveDecoder import pl.touk.nussknacker.engine.api.definition.{MandatoryParameterValidator, ParameterEditor, ParameterValidator} -import pl.touk.nussknacker.engine.api.deployment.{CustomAction, StateStatus} +import pl.touk.nussknacker.engine.api.deployment.CustomAction import pl.touk.nussknacker.engine.api.process.SingleNodeConfig import pl.touk.nussknacker.engine.api.typed.typing.TypingResult import pl.touk.nussknacker.engine.definition.TypeInfos.MethodInfo @@ -85,10 +85,16 @@ package object definition { import pl.touk.nussknacker.restmodel.codecs.URICodecs.{uriDecoder, uriEncoder} def apply(action: CustomAction): UICustomAction = UICustomAction( - name = action.name, allowedStateStatusNames = action.allowedStateStatusNames, icon = action.icon + name = action.name, allowedStateStatusNames = action.allowedStateStatusNames, icon = action.icon, parameters = + action.parameters.map(p => UICustomActionParameter(p.name, p.editor)) ) } - @JsonCodec - case class UICustomAction(name: String, allowedStateStatusNames: List[String], icon: Option[URI]) + + @JsonCodec case class UICustomAction(name: String, + allowedStateStatusNames: List[String], + icon: Option[URI], + parameters: List[UICustomActionParameter]) + + @JsonCodec case class UICustomActionParameter(name: String, editor: ParameterEditor) }