Skip to content

Commit

Permalink
feat(310): map task v1 visuals, supporting both old and new api (#357)
Browse files Browse the repository at this point in the history
* feat(310): map task v1 visuals, supporting both old and new api
* chore: add tests

Signed-off-by: Nastya Rusina <[email protected]>
  • Loading branch information
anrusina authored Apr 8, 2022
1 parent 9d1a5f8 commit 4bbc71f
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/components/Entities/EntityDetailsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const useStyles = makeStyles((theme: Theme) => ({
width: '100%',
},
headerText: {
margin: `0 ${theme.spacing(1)}px`,
margin: theme.spacing(0, 1),
},
headerTextContainer: {
display: 'flex',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TaskTemplate } from 'models/Task/types';
import { useTabState } from 'components/hooks/useTabState';
import { PanelSection } from 'components/common/PanelSection';
import { DumpJSON } from 'components/common/DumpJSON';
import { isMapTaskType } from 'models/Task/utils';
import { TaskExecutionsList } from '../../TaskExecutionsList/TaskExecutionsList';
import { NodeExecutionInputs } from './NodeExecutionInputs';
import { NodeExecutionOutputs } from './NodeExecutionOutputs';
Expand All @@ -17,6 +18,12 @@ const useStyles = makeStyles((theme) => {
},
tabs: {
borderBottom: `1px solid ${theme.palette.divider}`,
'& .MuiTabs-flexContainer': {
justifyContent: 'space-around',
},
},
tabItem: {
margin: theme.spacing(0, 1),
},
};
});
Expand Down Expand Up @@ -68,13 +75,15 @@ export const NodeExecutionTabs: React.FC<{
break;
}
}

const executionLabel = isMapTaskType(taskTemplate?.type) ? 'Map Execution' : 'Executions';
return (
<>
<Tabs {...tabState} className={styles.tabs}>
<Tab value={tabIds.executions} label="Executions" />
<Tab value={tabIds.inputs} label="Inputs" />
<Tab value={tabIds.outputs} label="Outputs" />
{!!taskTemplate && <Tab value={tabIds.task} label="Task" />}
<Tab className={styles.tabItem} value={tabIds.executions} label={executionLabel} />
<Tab className={styles.tabItem} value={tabIds.inputs} label="Inputs" />
<Tab className={styles.tabItem} value={tabIds.outputs} label="Outputs" />
{!!taskTemplate && <Tab className={styles.tabItem} value={tabIds.task} label="Task" />}
</Tabs>
<div className={styles.content}>{tabContent}</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export const formatSecondsToHmsFormat = (seconds: number) => {
return `${seconds}s`;
};

// narusina - check if exports are still needed
export const getOffsetColor = (isCachedValue: boolean[]) => {
const colors = isCachedValue.map((val) => (val === true ? CASHED_GREEN : TRANSPARENT));
return colors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import classnames from 'classnames';
import { PanelSection } from 'components/common/PanelSection';
import { useCommonStyles } from 'components/common/styles';
import { TaskExecutionPhase } from 'models/Execution/enums';
import { TaskExecution } from 'models/Execution/types';
import { MapTaskStatusInfo } from 'components/common/MapTaskExecutionsList/MapTaskStatusInfo';
import { TaskExecutionDetails } from './TaskExecutionDetails';
import { TaskExecutionError } from './TaskExecutionError';
import { TaskExecutionLogs } from './TaskExecutionLogs';
import { formatRetryAttempt, getGroupedLogs } from './utils';

const useStyles = makeStyles((theme: Theme) => ({
detailsLink: {
fontWeight: 'normal',
},
header: {
marginBottom: theme.spacing(1),
},
title: {
marginBottom: theme.spacing(1),
},
showDetailsButton: {
marginTop: theme.spacing(1),
},
section: {
marginBottom: theme.spacing(2),
},
}));

interface MapTaskExecutionsListItemProps {
taskExecution: TaskExecution;
showAttempts: boolean;
}

const RENDER_ORDER: TaskExecutionPhase[] = [
TaskExecutionPhase.UNDEFINED,
TaskExecutionPhase.INITIALIZING,
TaskExecutionPhase.WAITING_FOR_RESOURCES,
TaskExecutionPhase.QUEUED,
TaskExecutionPhase.RUNNING,
TaskExecutionPhase.SUCCEEDED,
TaskExecutionPhase.ABORTED,
TaskExecutionPhase.FAILED,
];

/** Renders an individual `TaskExecution` record as part of a list */
export const MapTaskExecutionsListItem: React.FC<MapTaskExecutionsListItemProps> = ({
taskExecution,
showAttempts,
}) => {
const commonStyles = useCommonStyles();
const styles = useStyles();

const { closure } = taskExecution;
const taskHasStarted = closure.phase >= TaskExecutionPhase.QUEUED;
const headerText = formatRetryAttempt(taskExecution.id.retryAttempt);
const logsInfo = getGroupedLogs(closure.metadata?.externalResources ?? []);

// Set UI elements in a proper rendering order
const logsSections: JSX.Element[] = [];
for (const key of RENDER_ORDER) {
const values = logsInfo.get(key);
if (values) {
logsSections.push(<MapTaskStatusInfo status={key} taskLogs={values} expanded={false} />);
}
}

return (
<PanelSection>
{/* Attempts header is ahown only if there is more than one attempt */}
{showAttempts ? (
<section className={styles.section}>
<header className={styles.header}>
<Typography variant="h6" className={classnames(styles.title, commonStyles.textWrapped)}>
{headerText}
</Typography>
</header>
</section>
) : null}
{/* Error info is shown only if there is an error present for this map task */}
{closure.error ? (
<section className={styles.section}>
<TaskExecutionError error={closure.error} />
</section>
) : null}

{/* If main map task has log attached - show it here */}
{closure.logs && closure.logs.length > 0 ? (
<section className={styles.section}>
<TaskExecutionLogs taskLogs={taskExecution.closure.logs || []} title="Task Log" />
</section>
) : null}
{/* child/array logs separated by subtasks phase */}
{logsSections}

{/* If map task is actively started - show 'started' and 'run time' details */}
{taskHasStarted && (
<section className={styles.section}>
<TaskExecutionDetails taskExecution={taskExecution} />
</section>
)}
</PanelSection>
);
};
29 changes: 20 additions & 9 deletions src/components/Executions/TaskExecutionsList/TaskExecutionLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import { Typography } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { Core } from 'flyteidl';
import { NewTargetLink } from 'components/common/NewTargetLink';
import { useCommonStyles } from 'components/common/styles';
import { TaskLog } from 'models/Common/types';
import { noLogsFoundString } from '../constants';

const useStyles = makeStyles((theme: Theme) => ({
Expand All @@ -13,34 +13,45 @@ const useStyles = makeStyles((theme: Theme) => ({
sectionHeader: {
marginTop: theme.spacing(1),
},
logName: {
fontWeight: 'lighter',
},
}));

const TaskLogList: React.FC<{ logs: TaskLog[] }> = ({ logs }) => {
export const TaskLogList: React.FC<{ logs: Core.ITaskLog[] }> = ({ logs }) => {
const styles = useStyles();
const commonStyles = useCommonStyles();
if (!(logs && logs.length > 0)) {
return <span className={commonStyles.hintText}>{noLogsFoundString}</span>;
}
return (
<>
{logs.map(({ name, uri }) => (
<NewTargetLink className={styles.logLink} key={name} external={true} href={uri}>
{name}
</NewTargetLink>
))}
{logs.map(({ name, uri }) =>
uri ? (
<NewTargetLink className={styles.logLink} key={name} external={true} href={uri}>
{name}
</NewTargetLink>
) : (
// If there is no url, show item a a name string only, as it's not really clickable
<div className={styles.logName}>{name}</div>
),
)}
</>
);
};

/** Renders log links from a `taskLogs`(aka taskExecution.closure.logs), if they exist.
* Otherwise renders a message indicating that no logs are available.
*/
export const TaskExecutionLogs: React.FC<{ taskLogs: TaskLog[] }> = ({ taskLogs }) => {
export const TaskExecutionLogs: React.FC<{ taskLogs: Core.ITaskLog[]; title?: string }> = ({
taskLogs,
title,
}) => {
const styles = useStyles();
return (
<section>
<header className={styles.sectionHeader}>
<Typography variant="h6">Logs</Typography>
<Typography variant="h6">{title ?? 'Logs'}</Typography>
</header>
<TaskLogList logs={taskLogs} />
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Protobuf } from 'flyteidl';
import { MessageFormat, ResourceType } from 'models/Common/types';
import { Protobuf, Event } from 'flyteidl';
import { MessageFormat, ResourceType, TaskLog } from 'models/Common/types';
import { TaskExecutionPhase } from 'models/Execution/enums';
import { TaskExecution } from 'models/Execution/types';

import * as Long from 'long';
import { TaskType } from 'models/Task/constants';

// we probably will create a new helper function in future, to make testing/storybooks closer to what we see in API Json responses
const getProtobufTimestampFromIsoTime = (isoDateTime: string): Protobuf.ITimestamp => {
Expand All @@ -14,6 +15,19 @@ const getProtobufTimestampFromIsoTime = (isoDateTime: string): Protobuf.ITimesta
return timestamp;
};

const getProtobufDurationFromString = (durationSec: string): Protobuf.Duration => {
const secondsInt = parseInt(durationSec, 10);
const duration = new Protobuf.Duration();
duration.seconds = Long.fromInt(secondsInt);
return duration;
};

export const MockTaskExceutionLog: TaskLog = {
uri: '#',
name: 'Cloudwatch Logs (User)',
messageFormat: MessageFormat.JSON,
};

export const MockPythonTaskExecution: TaskExecution = {
id: {
taskId: {
Expand All @@ -38,16 +52,79 @@ export const MockPythonTaskExecution: TaskExecution = {
outputUri:
's3://flyte-demo/metadata/propeller/flytesnacks-development-ogaayir2e3/athenaworkflowsexamplesayhello/data/0/outputs.pb',
phase: TaskExecutionPhase.SUCCEEDED,
logs: [
{
uri: 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-2#logEventViewer:group=/aws/containerinsights/flyte-demo-2/application;stream=var.log.containers.ogaayir2e3-ff65vi3y-0_flytesnacks-development_ogaayir2e3-ff65vi3y-0-380d210ccaac45a6e2314a155822b36a67e044914069d01323bc18832487ac4a.log',
name: 'Cloudwatch Logs (User)',
messageFormat: MessageFormat.JSON,
},
],
logs: [MockTaskExceutionLog],
createdAt: getProtobufTimestampFromIsoTime('2022-03-17T21:30:53.469624134Z'),
updatedAt: getProtobufTimestampFromIsoTime('2022-03-17T21:31:04.011303736Z'),
reason: 'task submitted to K8s',
taskType: 'python-task',
taskType: TaskType.PYTHON,
},
};

export const getMockMapTaskLogItem = (
phase: TaskExecutionPhase,
hasLogs: boolean,
index?: number,
retryAttempt?: number,
): Event.IExternalResourceInfo => {
const retryString = retryAttempt && retryAttempt > 0 ? `-${retryAttempt}` : '';
return {
externalId: `y286hpfvwh-n0-0-${index ?? 0}`,
index: index,
phase: phase,
retryAttempt: retryAttempt,
logs: hasLogs
? [
{
uri: '#',
name: `Kubernetes Logs #0-${index ?? 0}${retryString} (State)`,
messageFormat: MessageFormat.JSON,
},
]
: [],
};
};

export const MockMapTaskExecution: TaskExecution = {
id: {
taskId: {
resourceType: ResourceType.TASK,
project: 'flytesnacks',
domain: 'development',
name: 'flyte.workflows.example.mapper_a_mappable_task_0',
version: 'v2',
},
nodeExecutionId: {
nodeId: 'n0',
executionId: {
project: 'flytesnacks',
domain: 'development',
name: 'y286hpfvwh',
},
},
},
inputUri:
's3://my-s3-bucket/metadata/propeller/sandbox/flytesnacks-development-y286hpfvwh/n0/data/inputs.pb',
closure: {
outputUri:
's3://my-s3-bucket/metadata/propeller/sandbox/flytesnacks-development-y286hpfvwh/n0/data/0/outputs.pb',
phase: TaskExecutionPhase.SUCCEEDED,
startedAt: getProtobufTimestampFromIsoTime('2022-03-30T19:31:09.487343Z'),
duration: getProtobufDurationFromString('190.302384340s'),
createdAt: getProtobufTimestampFromIsoTime('2022-03-30T19:31:09.487343693Z'),
updatedAt: getProtobufTimestampFromIsoTime('2022-03-30T19:34:19.789727340Z'),
taskType: 'container_array',
metadata: {
generatedName: 'y286hpfvwh-n0-0',
externalResources: [
getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true),
getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, 1),
getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, 2),
getMockMapTaskLogItem(TaskExecutionPhase.FAILED, true, 3),
getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, 3, 1),
getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, 4),
getMockMapTaskLogItem(TaskExecutionPhase.FAILED, false, 5),
],
pluginIdentifier: 'k8s-array',
},
},
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as React from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { noExecutionsFoundString } from 'common/constants';
import { NonIdealState } from 'components/common/NonIdealState';
import { WaitForData } from 'components/common/WaitForData';
import { NodeExecution, TaskExecution } from 'models/Execution/types';
import * as React from 'react';
import { isMapTaskType } from 'models/Task/utils';
import { useTaskExecutions, useTaskExecutionsRefresher } from '../useTaskExecutions';
import { MapTaskExecutionsListItem } from './MapTaskExecutionListItem';
import { TaskExecutionsListItem } from './TaskExecutionsListItem';
import { getUniqueTaskExecutionName } from './utils';

Expand All @@ -31,14 +33,26 @@ export const TaskExecutionsListContent: React.FC<{
/>
);
}

return (
<>
{taskExecutions.map((taskExecution) => (
<TaskExecutionsListItem
key={getUniqueTaskExecutionName(taskExecution)}
taskExecution={taskExecution}
/>
))}
{taskExecutions.map((taskExecution) => {
const taskType = taskExecution.closure.taskType ?? undefined;
const useNewMapTaskView =
isMapTaskType(taskType) && taskExecution.closure.metadata?.externalResources;
return useNewMapTaskView ? (
<MapTaskExecutionsListItem
key={getUniqueTaskExecutionName(taskExecution)}
taskExecution={taskExecution}
showAttempts={taskExecutions.length > 1}
/>
) : (
<TaskExecutionsListItem
key={getUniqueTaskExecutionName(taskExecution)}
taskExecution={taskExecution}
/>
);
})}
</>
);
};
Expand Down
Loading

0 comments on commit 4bbc71f

Please sign in to comment.