Skip to content

Commit

Permalink
[Feature] Custom action parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
mproch committed Apr 6, 2021
1 parent 6f22eaf commit f2ce653
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions ui/client/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 14 additions & 1 deletion ui/client/actions/nk/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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({
Expand Down Expand Up @@ -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,
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import JsonEditor from "./JsonEditor"
import DualParameterEditor from "./DualParameterEditor"

type ValuesType = Array<string>
type EditorProps = $TodoType
export type EditorProps = $TodoType

export type SimpleEditor<P extends EditorProps = EditorProps> = Editor<P> & {
switchableTo: (expressionObj: ExpressionObj, values?: ValuesType) => boolean,
Expand Down
74 changes: 74 additions & 0 deletions ui/client/components/modals/CustomActionDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<GenericModalDialog
confirm={confirm}
type={Dialogs.types.customAction}
init={init}
header={action?.name}
>
<div className="node-table">
{
(action?.parameters || []).map(param => {
const editorType = param.editor.type
const Editor = editors[editorType]
const fieldName = param.name
return (
<div className={"node-row"} key={param.name}>
<div className="node-label" title={fieldName}>{fieldName}:</div>
<Editor
editorConfig={param?.editor}
className={"node-value"}
validators={[]}
formatter={null}
expressionInfo={null}
onValueChange={setParam(fieldName)}
expressionObj={{language: ExpressionLang.String, expression: mapState[fieldName]}}
values={[]}
readOnly={false}
key={fieldName}
showSwitch={false}
showValidation={false}
variableTypes={{}}
/>
</div>
)
})
}
</div>

</GenericModalDialog>
)
}

export default CustomActionDialog
2 changes: 2 additions & 0 deletions ui/client/components/modals/Dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -18,6 +19,7 @@ export function AllDialogs(): JSX.Element {
<GenerateTestDataDialog/>
<CalculateCountsDialog/>
<CompareVersionsDialog/>
<CustomActionDialog/>
</>
)
}
Expand Down
2 changes: 2 additions & 0 deletions ui/client/components/modals/DialogsTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type DialogType = "INFO_MODAL"
| "GENERATE_TEST_DATA"
| "CALCULATE_COUNTS"
| "COMPARE_VERSIONS"
| "CUSTOM_ACTION"

export const dialogTypesMap: Record<string, DialogType> = {
infoModal: "INFO_MODAL",
Expand All @@ -12,4 +13,5 @@ export const dialogTypesMap: Record<string, DialogType> = {
generateTestData: "GENERATE_TEST_DATA",
calculateCounts: "CALCULATE_COUNTS",
compareVersions: "COMPARE_VERSIONS",
customAction: "CUSTOM_ACTION",
}
8 changes: 5 additions & 3 deletions ui/client/components/modals/GenericModalDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type OwnProps = {
header?: string,
confirm?: (close: () => void) => PromiseLike<void>,
type: DialogType,
init?: () => void,
init: () => void,
}

type State = {
Expand Down Expand Up @@ -74,8 +74,10 @@ class GenericModalDialog extends React.Component<Props, State> {
<div className={`espModal ${this.props.style || "confirmationModal"}`} data-testid="modal">
{this.props.header ?
(
<div className="modal-title modal-draggable-handle" style={{color: "white", backgroundColor: "#70C6CE"}}>
<span>{this.props.header}</span>
<div className="modalHeader modal-draggable-handle">
<div className="modal-title" style={{color: "white", backgroundColor: "#70C6CE"}}>
<span>{this.props.header}</span>
</div>
</div>
) :
null}
Expand Down
57 changes: 25 additions & 32 deletions ui/client/components/toolbars/status/buttons/CustomActionButton.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -21,30 +20,24 @@ export default function CustomActionButton(props: Props) {
const dispatch = useDispatch()
const {t} = useTranslation()

const icon = action.icon
? <img alt={`custom-action-${action.name}`} src={action.icon} />
: <DefaultIcon/>
const icon = action.icon ?
<img alt={`custom-action-${action.name}`} src={action.icon}/> :
<DefaultIcon/>

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 <ToolbarButton
name={action.name}
title={toolTip}
disabled={isDisabled}
icon={icon}
onClick={onClick}
/>
}
const toolTip = isDisabled ?
t("panels.actions.custom-action.tooltips.disabled", "Disabled for {{statusName}} status.", {statusName}) :
null

return (
<ToolbarButton
name={action.name}
title={toolTip}
disabled={isDisabled}
icon={icon}
onClick={() => dispatch(toggleCustomAction(action))}
/>
)
}
12 changes: 11 additions & 1 deletion ui/client/reducers/ui.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -21,6 +21,7 @@ export type UiState = {
action: (processId: ProcessId, comment: string) => void,
displayWarnings: boolean,
text: string,
customAction: CustomAction,
}>,
allModalsClosed: boolean,
isToolTipsHighlighted: boolean,
Expand Down Expand Up @@ -128,6 +129,15 @@ const uiReducer: Reducer<UiState> = (state = emptyUiState, action) => {
},
}
}
case "TOGGLE_CUSTOM_ACTION": {
return {
...state,
modalDialog: {
openDialog: dialogTypesMap.customAction,
customAction: action.customAction,
},
}
}

default:
return state
Expand Down
7 changes: 7 additions & 0 deletions ui/client/types/process.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -30,6 +31,12 @@ export type CustomAction = {
name: string,
allowedStateStatusNames: Array<string>,
icon: string | null,
parameters: Array<CustomActionParameter> | null,
}

export type CustomActionParameter = {
name: string,
editor: EditorProps,
}

export type ProcessDefinitionData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

}

0 comments on commit f2ce653

Please sign in to comment.