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(310): map task v1 visuals, supporting both old and new api #357

Merged
merged 3 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
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: `0 ${theme.spacing(1)}px`,
anrusina marked this conversation as resolved.
Show resolved Hide resolved
},
};
});
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 RenderOrder: TaskExecutionPhase[] = [
anrusina marked this conversation as resolved.
Show resolved Hide resolved
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 RenderOrder) {
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