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

feat: rerun task action in execution page #488

Merged
merged 8 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/composites/ui-atoms/src/Icons/RerunIcon/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 13 11"
fill="none"
onClick={onClick}
>
<path
d="M4.8 1.4H8.8V0L12.6 2.2L8.8 4.4V3H4.8C3.04 3 1.6 4.44 1.6 6.2C1.6 7.96 3.04 9.4 4.8 9.4H8.8V11H4.8C2.16 11 0 8.84 0 6.2C0 3.56 2.16 1.4 4.8 1.4Z"
fill="#666666"
/>
</svg>
);
};
1 change: 1 addition & 0 deletions packages/composites/ui-atoms/src/Icons/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { FlyteLogo } from './FlyteLogo';
export { InfoIcon } from './InfoIcon';
export { RerunIcon } from './RerunIcon';
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [taskInputsTypes, setTaskInputsTypes] = React.useState<
Record<string, Variable> | undefined
>();

const executionData = useNodeExecutionData(nodeExecutionId);

const id = details.taskTemplate?.id as ResourceIdentifier | undefined;
anrusina marked this conversation as resolved.
Show resolved Hide resolved

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<HTMLElement>) => {
e.stopPropagation();
setShowLaunchForm(true);
};

return (
<>
<div className={className}>
<Button variant="outlined" color="primary" onClick={rerunOnClick}>
{t('rerun')}
</Button>
</div>
<LaunchFormDialog
id={id}
initialParameters={initialParameters}
showLaunchForm={showLaunchForm}
setShowLaunchForm={setShowLaunchForm}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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),
},
Expand Down Expand Up @@ -395,6 +401,13 @@ export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProp
</Typography>
{statusContent}
{!dag && detailsContent}
{details && (
<ExecutionDetailsActions
className={styles.actionsContainer}
details={details}
nodeExecutionId={nodeExecutionId}
/>
)}
</div>
</header>
{dag ? <WorkflowTabs nodeId={nodeExecutionId.nodeId} dagData={dag} /> : tabsContent}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createLocalizedString } from '@flyteconsole/locale';

const str = {
rerun: 'RERUN',
};

export { patternKey } from '@flyteconsole/locale';
export default createLocalizedString(str);
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [nodeExecutionDetails, setNodeExecutionDetails] = React.useState<
NodeExecutionDetails | undefined
>();
const [taskInputsTypes, setTaskInputsTypes] = React.useState<
Record<string, Variable> | 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<HTMLElement>) => {
// 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<HTMLElement>) => {
e.stopPropagation();
setShowLaunchForm(true);
};

const renderRerunAction = () => {
if (!id) {
return <></>;
}

const initialParameters: TaskInitialLaunchParameters = {
values: literals && taskInputsTypes && literalsToLiteralValueMap(literals, taskInputsTypes),
taskId: id as Identifier | undefined,
};
return (
<>
<Tooltip title={t('rerunTooltip')}>
<IconButton onClick={rerunIconOnClick}>
<RerunIcon />
</IconButton>
</Tooltip>
<LaunchFormDialog
id={id}
initialParameters={initialParameters}
showLaunchForm={showLaunchForm}
setShowLaunchForm={setShowLaunchForm}
/>
</>
);
};

return (
<div>
<Tooltip title={t('inputsAndOutputsTooltip')}>
<IconButton onClick={inputsAndOutputsIconOnClick}>
<InputsAndOutputsIcon />
</IconButton>
</Tooltip>
{renderRerunAction()}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -201,11 +202,11 @@ export function generateColumns(
},
{
cellRenderer: ({ execution, state }) => (
<SelectNodeExecutionLink execution={execution} linkText="View Logs" state={state} />
<NodeExecutionActions execution={execution} state={state} />
),
className: styles.columnLogs,
key: 'logs',
label: 'logs',
key: 'actions',
label: '',
},
];
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<boolean>>;
}

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<HTMLElement>) => {
e.stopPropagation();
};

return (
<Dialog
scroll="paper"
maxWidth="sm"
fullWidth={true}
open={showLaunchForm}
onClick={dialogOnClick}
>
<LaunchForm
initialParameters={initialParameters}
onClose={onCancelLaunch}
{...getLaunchProps(id)}
/>
</Dialog>
);
};
20 changes: 20 additions & 0 deletions packages/zapp/console/src/components/Launch/LaunchForm/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, Variable>,
): LiteralValueMap {
const literalValueMap: LiteralValueMap = new Map<string, Core.ILiteral>();

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;
}