diff --git a/packages/composites/ui-atoms/src/Icons/RerunIcon/index.tsx b/packages/composites/ui-atoms/src/Icons/RerunIcon/index.tsx new file mode 100644 index 000000000..048345443 --- /dev/null +++ b/packages/composites/ui-atoms/src/Icons/RerunIcon/index.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +interface IconProps { + size?: number; + className?: string; + onClick?: () => void; +} + +export const RerunIcon = (props: IconProps): JSX.Element => { + const { size = 18, className, onClick } = props; + return ( + + + + ); +}; diff --git a/packages/composites/ui-atoms/src/Icons/index.tsx b/packages/composites/ui-atoms/src/Icons/index.tsx index 5b32e816c..0cbe71ead 100644 --- a/packages/composites/ui-atoms/src/Icons/index.tsx +++ b/packages/composites/ui-atoms/src/Icons/index.tsx @@ -1,2 +1,3 @@ export { FlyteLogo } from './FlyteLogo'; export { InfoIcon } from './InfoIcon'; +export { RerunIcon } from './RerunIcon'; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx new file mode 100644 index 000000000..e5251378f --- /dev/null +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx @@ -0,0 +1,70 @@ +import { Button } from '@material-ui/core'; +import * as React from 'react'; +import { ResourceIdentifier, Identifier, Variable } from 'models/Common/types'; +import { getTask } from 'models/Task/api'; +import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; +import { NodeExecutionIdentifier } from 'models/Execution/types'; +import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; +import { literalsToLiteralValueMap } from 'components/Launch/LaunchForm/utils'; +import { TaskInitialLaunchParameters } from 'components/Launch/LaunchForm/types'; +import { NodeExecutionDetails } from '../types'; +import t from './strings'; + +interface ExecutionDetailsActionsProps { + className?: string; + details: NodeExecutionDetails; + nodeExecutionId: NodeExecutionIdentifier; +} + +export const ExecutionDetailsActions = (props: ExecutionDetailsActionsProps): JSX.Element => { + const { className, details, nodeExecutionId } = props; + + const [showLaunchForm, setShowLaunchForm] = React.useState(false); + const [taskInputsTypes, setTaskInputsTypes] = React.useState< + Record | undefined + >(); + + const executionData = useNodeExecutionData(nodeExecutionId); + + const id = details.taskTemplate?.id as ResourceIdentifier | undefined; + + React.useEffect(() => { + const fetchTask = async () => { + const task = await getTask(id as Identifier); + setTaskInputsTypes(task.closure.compiledTask.template?.interface?.inputs?.variables); + }; + if (id) fetchTask(); + }, [id]); + + if (!id) { + return <>; + } + + const literals = executionData.value.fullInputs?.literals; + + const initialParameters: TaskInitialLaunchParameters = { + values: literals && taskInputsTypes && literalsToLiteralValueMap(literals, taskInputsTypes), + taskId: id as Identifier | undefined, + }; + + const rerunOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowLaunchForm(true); + }; + + return ( + <> +
+ +
+ + + ); +}; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index f0575986b..ee96d2c34 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -41,6 +41,7 @@ import { getTaskExecutionDetailReasons } from './utils'; import { ExpandableMonospaceText } from '../../common/ExpandableMonospaceText'; import { fetchWorkflowExecution } from '../useWorkflowExecution'; import { NodeExecutionTabs } from './NodeExecutionTabs'; +import { ExecutionDetailsActions } from './ExecutionDetailsActions'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; @@ -93,6 +94,11 @@ const useStyles = makeStyles((theme: Theme) => { marginTop: theme.spacing(2), paddingTop: theme.spacing(2), }, + actionsContainer: { + borderTop: `1px solid ${theme.palette.divider}`, + marginTop: theme.spacing(2), + paddingTop: theme.spacing(2), + }, nodeTypeContent: { minWidth: theme.spacing(9), }, @@ -395,6 +401,13 @@ export const NodeExecutionDetailsPanelContent: React.FC {statusContent} {!dag && detailsContent} + {details && ( + + )} {dag ? : tabsContent} diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx new file mode 100644 index 000000000..4595ae70e --- /dev/null +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx @@ -0,0 +1,8 @@ +import { createLocalizedString } from '@flyteconsole/locale'; + +const str = { + rerun: 'RERUN', +}; + +export { patternKey } from '@flyteconsole/locale'; +export default createLocalizedString(str); diff --git a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx new file mode 100644 index 000000000..62ef88c35 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx @@ -0,0 +1,101 @@ +import { IconButton, Tooltip } from '@material-ui/core'; +import { NodeExecution } from 'models/Execution/types'; +import * as React from 'react'; +import InputsAndOutputsIcon from '@material-ui/icons/Tv'; +import { RerunIcon } from '@flyteconsole/ui-atoms'; +import { Identifier, ResourceIdentifier, Variable } from 'models/Common/types'; +import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; +import { getTask } from 'models/Task/api'; +import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; +import { TaskInitialLaunchParameters } from 'components/Launch/LaunchForm/types'; +import { literalsToLiteralValueMap } from 'components/Launch/LaunchForm/utils'; +import { NodeExecutionsTableState } from './types'; +import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; +import { NodeExecutionDetails } from '../types'; +import t from './strings'; + +interface NodeExecutionActionsProps { + execution: NodeExecution; + state: NodeExecutionsTableState; +} + +export const NodeExecutionActions = (props: NodeExecutionActionsProps): JSX.Element => { + const { execution, state } = props; + + const detailsContext = useNodeExecutionContext(); + const [showLaunchForm, setShowLaunchForm] = React.useState(false); + const [nodeExecutionDetails, setNodeExecutionDetails] = React.useState< + NodeExecutionDetails | undefined + >(); + const [taskInputsTypes, setTaskInputsTypes] = React.useState< + Record | undefined + >(); + + const executionData = useNodeExecutionData(execution.id); + const literals = executionData.value.fullInputs?.literals; + const id = nodeExecutionDetails?.taskTemplate?.id as ResourceIdentifier; + + React.useEffect(() => { + detailsContext.getNodeExecutionDetails(execution).then((res) => { + setNodeExecutionDetails(res); + }); + }); + + React.useEffect(() => { + const fetchTask = async () => { + const task = await getTask(id as Identifier); + setTaskInputsTypes(task.closure.compiledTask.template?.interface?.inputs?.variables); + }; + if (id) fetchTask(); + }, [id]); + + // open the side panel for selected execution's detail + const inputsAndOutputsIconOnClick = (e: React.MouseEvent) => { + // prevent the parent row body onClick event trigger + e.stopPropagation(); + // use null in case if there is no execution provided - when it is null will close panel + state.setSelectedExecution(execution?.id ?? null); + }; + + const rerunIconOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowLaunchForm(true); + }; + + const renderRerunAction = () => { + if (!id) { + return <>; + } + + const initialParameters: TaskInitialLaunchParameters = { + values: literals && taskInputsTypes && literalsToLiteralValueMap(literals, taskInputsTypes), + taskId: id as Identifier | undefined, + }; + return ( + <> + + + + + + + + ); + }; + + return ( +
+ + + + + + {renderRerunAction()} +
+ ); +}; diff --git a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx index e03d00d47..5a9925b62 100644 --- a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -10,6 +10,7 @@ import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; import { getNodeExecutionTimingMS } from '../utils'; +import { NodeExecutionActions } from './NodeExecutionActions'; import { SelectNodeExecutionLink } from './SelectNodeExecutionLink'; import { useColumnStyles } from './styles'; import { NodeExecutionCellRendererData, NodeExecutionColumnDefinition } from './types'; @@ -201,11 +202,11 @@ export function generateColumns( }, { cellRenderer: ({ execution, state }) => ( - + ), className: styles.columnLogs, - key: 'logs', - label: 'logs', + key: 'actions', + label: '', }, ]; } diff --git a/packages/zapp/console/src/components/Executions/Tables/strings.tsx b/packages/zapp/console/src/components/Executions/Tables/strings.tsx new file mode 100644 index 000000000..402c312c7 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/Tables/strings.tsx @@ -0,0 +1,9 @@ +import { createLocalizedString } from '@flyteconsole/locale'; + +const str = { + inputsAndOutputsTooltip: 'View Inputs & Outpus', + rerunTooltip: 'Rerun', +}; + +export { patternKey } from '@flyteconsole/locale'; +export default createLocalizedString(str); diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormDialog.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormDialog.tsx new file mode 100644 index 000000000..1c3a25716 --- /dev/null +++ b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormDialog.tsx @@ -0,0 +1,51 @@ +import { Dialog } from '@material-ui/core'; +import * as React from 'react'; +import { LaunchForm } from 'components/Launch/LaunchForm/LaunchForm'; +import { ResourceIdentifier, ResourceType } from 'models/Common/types'; +import { + TaskInitialLaunchParameters, + WorkflowInitialLaunchParameters, +} from 'components/Launch/LaunchForm/types'; + +interface LaunchFormDialogProps { + id: ResourceIdentifier; + initialParameters: TaskInitialLaunchParameters | WorkflowInitialLaunchParameters; + showLaunchForm: boolean; + setShowLaunchForm: React.Dispatch>; +} + +function getLaunchProps(id: ResourceIdentifier) { + if (id.resourceType === ResourceType.TASK) { + return { taskId: id }; + } else if (id.resourceType === ResourceType.WORKFLOW) { + return { workflowId: id }; + } + throw new Error('Unknown Resource Type'); +} + +export const LaunchFormDialog = (props: LaunchFormDialogProps): JSX.Element => { + const { id, initialParameters, showLaunchForm, setShowLaunchForm } = props; + + const onCancelLaunch = () => setShowLaunchForm(false); + + // prevent child onclick event in the dialog triggers parent onclick event + const dialogOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + + + + ); +}; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts index 2b8befe5d..fa59ec600 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -6,6 +6,7 @@ import { LaunchPlan } from 'models/Launch/types'; import { Task } from 'models/Task/types'; import { Workflow } from 'models/Workflow/types'; import * as moment from 'moment'; +import { LiteralValueMap } from 'components/Launch/LaunchForm/types'; import { simpleTypeToInputType, typeLabels } from './constants'; import { inputToLiteral } from './inputHelpers/inputHelpers'; import { typeIsSupported } from './inputHelpers/utils'; @@ -190,3 +191,22 @@ export function isEnterInputsState(state: BaseInterpretedLaunchState): boolean { LaunchState.SUBMIT_SUCCEEDED, ].some(state.matches); } + +export function literalsToLiteralValueMap( + literals: { + [k: string]: Core.ILiteral; + }, + nameToTypeMap: Record, +): LiteralValueMap { + const literalValueMap: LiteralValueMap = new Map(); + + for (var i = 0; i < Object.keys(literals).length; i++) { + const name = Object.keys(literals)[i]; + const type = nameToTypeMap[name].type; + const typeDefinition = getInputDefintionForLiteralType(type); + const inputKey = createInputCacheKey(name, typeDefinition); + literalValueMap.set(inputKey, literals[Object.keys(literals)[i]]); + } + + return literalValueMap; +}