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

fix: update timeline view to show dynamic wf internals on first render #562

Merged
merged 4 commits into from
Aug 17, 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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import { WaitForQuery } from 'components/common/WaitForQuery';
import { DataError } from 'components/Errors/DataError';
import { useTabState } from 'components/hooks/useTabState';
import { secondaryBackgroundColor } from 'components/Theme/constants';
import { Execution, NodeExecution } from 'models/Execution/types';
import { Execution, ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types';
import { useContext, useEffect, useMemo, useState } from 'react';
import { keyBy } from 'lodash';
import { isMapTaskV1 } from 'models/Task/utils';
import { useQueryClient } from 'react-query';
import { LargeLoadingSpinner } from 'components/common/LoadingSpinner';
import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails';
import { NodeExecutionsRequestConfigContext } from '../contexts';
import { NodeExecutionsByIdContext, NodeExecutionsRequestConfigContext } from '../contexts';
import { ExecutionFilters } from '../ExecutionFilters';
import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState';
import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable';
import { tabs } from './constants';
import { ExecutionChildrenLoader } from './ExecutionChildrenLoader';
import { useExecutionNodeViewsState } from './useExecutionNodeViewsState';
import { ExecutionNodesTimeline } from './Timeline';
import { fetchTaskExecutionList } from '../taskExecutionQueries';
import { getGroupedLogs } from '../TaskExecutionsList/utils';
import { useAllTreeNodeExecutionGroupsQuery } from '../nodeExecutionQueries';
import { ExecutionWorkflowGraph } from './ExecutionWorkflowGraph';

const useStyles = makeStyles((theme: Theme) => ({
filters: {
Expand All @@ -31,8 +39,15 @@ const useStyles = makeStyles((theme: Theme) => ({
background: secondaryBackgroundColor,
paddingLeft: theme.spacing(3.5),
},
loading: {
margin: 'auto',
},
}));

interface WorkflowNodeExecution extends NodeExecution {
logsByPhase?: LogsByPhase;
}

export interface ExecutionNodeViewsProps {
execution: Execution;
}
Expand All @@ -43,11 +58,22 @@ export const ExecutionNodeViews: React.FC<ExecutionNodeViewsProps> = ({ executio
const styles = useStyles();
const filterState = useNodeExecutionFiltersState();
const tabState = useTabState(tabs, defaultTab);
const queryClient = useQueryClient();
const requestConfig = useContext(NodeExecutionsRequestConfigContext);

const {
closure: { abortMetadata },
closure: { abortMetadata, workflowId },
} = execution;

const [nodeExecutions, setNodeExecutions] = useState<NodeExecution[]>([]);
const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState<
WorkflowNodeExecution[]
>([]);

const nodeExecutionsById = useMemo(() => {
return keyBy(nodeExecutionsWithResources, 'scopedId');
}, [nodeExecutionsWithResources]);

/* We want to maintain the filter selection when switching away from the Nodes
tab and back, but do not want to filter the nodes when viewing the graph. So,
we will only pass filters to the execution state when on the nodes tab. */
Expand All @@ -58,6 +84,61 @@ export const ExecutionNodeViews: React.FC<ExecutionNodeViewsProps> = ({ executio
appliedFilters,
);

useEffect(() => {
let isCurrent = true;
async function fetchData(baseNodeExecutions, queryClient) {
const newValue = await Promise.all(
baseNodeExecutions.map(async (baseNodeExecution) => {
const taskExecutions = await fetchTaskExecutionList(queryClient, baseNodeExecution.id);

const useNewMapTaskView = taskExecutions.every((taskExecution) => {
const {
closure: { taskType, metadata, eventVersion = 0 },
} = taskExecution;
return isMapTaskV1(
eventVersion,
metadata?.externalResources?.length ?? 0,
taskType ?? undefined,
);
});
const externalResources: ExternalResource[] = taskExecutions
.map((taskExecution) => taskExecution.closure.metadata?.externalResources)
.flat()
.filter((resource): resource is ExternalResource => !!resource);

const logsByPhase: LogsByPhase = getGroupedLogs(externalResources);

return {
...baseNodeExecution,
...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }),
};
}),
);

if (isCurrent) {
setNodeExecutionsWithResources(newValue);
}
}

if (nodeExecutions.length > 0) {
fetchData(nodeExecutions, queryClient);
}
return () => {
isCurrent = false;
};
}, [nodeExecutions]);

const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery(
nodeExecutionsQuery.data ?? [],
requestConfig,
);

useEffect(() => {
if (!childGroupsQuery.isLoading && childGroupsQuery.data) {
setNodeExecutions(childGroupsQuery.data);
}
}, [childGroupsQuery.data]);

const renderNodeExecutionsTable = (nodeExecutions: NodeExecution[]) => (
<NodeExecutionsRequestConfigContext.Provider value={nodeExecutionsRequestConfig}>
<NodeExecutionsTable
Expand All @@ -67,49 +148,67 @@ export const ExecutionNodeViews: React.FC<ExecutionNodeViewsProps> = ({ executio
</NodeExecutionsRequestConfigContext.Provider>
);

const renderExecutionLoader = (nodeExecutions: NodeExecution[]) => {
const renderExecutionChildrenLoader = () =>
nodeExecutions.length > 0 ? <ExecutionWorkflowGraph workflowId={workflowId} /> : null;

const renderExecutionLoader = () => {
return (
<ExecutionChildrenLoader
nodeExecutions={nodeExecutions}
workflowId={execution.closure.workflowId}
/>
<WaitForQuery errorComponent={DataError} query={childGroupsQuery}>
{renderExecutionChildrenLoader}
</WaitForQuery>
);
};

const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => (
<ExecutionNodesTimeline nodeExecutions={nodeExecutions} />
const renderExecutionsTimeline = () => (
<WaitForQuery
errorComponent={DataError}
query={childGroupsQuery}
loadingComponent={TimelineLoading}
>
{() => <ExecutionNodesTimeline />}
</WaitForQuery>
);

const TimelineLoading = () => {
return (
<div className={styles.loading}>
<LargeLoadingSpinner />
</div>
);
};

return (
<>
<Tabs className={styles.tabs} {...tabState}>
<Tab value={tabs.nodes.id} label={tabs.nodes.label} />
<Tab value={tabs.graph.id} label={tabs.graph.label} />
<Tab value={tabs.timeline.id} label={tabs.timeline.label} />
</Tabs>
<NodeExecutionDetailsContextProvider workflowId={execution.closure.workflowId}>
<div className={styles.nodesContainer}>
{tabState.value === tabs.nodes.id && (
<>
<div className={styles.filters}>
<ExecutionFilters {...filterState} />
</div>
<NodeExecutionDetailsContextProvider workflowId={workflowId}>
<NodeExecutionsByIdContext.Provider value={nodeExecutionsById}>
<div className={styles.nodesContainer}>
{tabState.value === tabs.nodes.id && (
<>
<div className={styles.filters}>
<ExecutionFilters {...filterState} />
</div>
<WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
{renderNodeExecutionsTable}
</WaitForQuery>
</>
)}
{tabState.value === tabs.graph.id && (
<WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
{renderExecutionLoader}
</WaitForQuery>
)}
{tabState.value === tabs.timeline.id && (
<WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
{renderNodeExecutionsTable}
{renderExecutionsTimeline}
</WaitForQuery>
</>
)}
{tabState.value === tabs.graph.id && (
<WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
{renderExecutionLoader}
</WaitForQuery>
)}
{tabState.value === tabs.timeline.id && (
<WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
{renderExecutionsTimeline}
</WaitForQuery>
)}
</div>
)}
</div>
</NodeExecutionsByIdContext.Provider>
</NodeExecutionDetailsContextProvider>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,27 @@ import { WaitForQuery } from 'components/common/WaitForQuery';
import { DataError } from 'components/Errors/DataError';
import { makeWorkflowQuery } from 'components/Workflow/workflowQueries';
import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph';
import { keyBy } from 'lodash';
import { TaskExecutionPhase } from 'models/Execution/enums';
import { ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types';
import { endNodeId, startNodeId } from 'models/Node/constants';
import { isMapTaskV1 } from 'models/Task/utils';
import { Workflow, WorkflowId } from 'models/Workflow/types';
import * as React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { NodeExecutionsContext } from '../contexts';
import { fetchTaskExecutionList } from '../taskExecutionQueries';
import { getGroupedLogs } from '../TaskExecutionsList/utils';
import { NodeExecutionsByIdContext } from '../contexts';
import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent';

export interface ExecutionWorkflowGraphProps {
nodeExecutions: NodeExecution[];
workflowId: WorkflowId;
}

interface WorkflowNodeExecution extends NodeExecution {
logsByPhase?: LogsByPhase;
}

/** Wraps a WorkflowGraph, customizing it to also show execution statuses */
export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
nodeExecutions,
workflowId,
}) => {
export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({ workflowId }) => {
const queryClient = useQueryClient();
const workflowQuery = useQuery<Workflow, Error>(makeWorkflowQuery(queryClient, workflowId));

const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState<
WorkflowNodeExecution[]
>([]);
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const nodeExecutionsById = useContext(NodeExecutionsByIdContext);

const nodeExecutionsById = useMemo(
() => keyBy(nodeExecutionsWithResources, 'scopedId'),
[nodeExecutionsWithResources],
);
// Note: flytegraph allows multiple selection, but we only support showing
// a single item in the details panel
const selectedExecution = selectedNodes.length
Expand All @@ -57,49 +38,6 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
const [selectedPhase, setSelectedPhase] = useState<TaskExecutionPhase | undefined>(undefined);
const [isDetailsTabClosed, setIsDetailsTabClosed] = useState<boolean>(!selectedExecution);

useEffect(() => {
let isCurrent = true;
async function fetchData(nodeExecutions, queryClient) {
const newValue = await Promise.all(
nodeExecutions.map(async (nodeExecution) => {
const taskExecutions = await fetchTaskExecutionList(queryClient, nodeExecution.id);

const useNewMapTaskView = taskExecutions.every((taskExecution) => {
const {
closure: { taskType, metadata, eventVersion = 0 },
} = taskExecution;
return isMapTaskV1(
eventVersion,
metadata?.externalResources?.length ?? 0,
taskType ?? undefined,
);
});
const externalResources: ExternalResource[] = taskExecutions
.map((taskExecution) => taskExecution.closure.metadata?.externalResources)
.flat()
.filter((resource): resource is ExternalResource => !!resource);

const logsByPhase: LogsByPhase = getGroupedLogs(externalResources);

return {
...nodeExecution,
...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }),
};
}),
);

if (isCurrent) {
setNodeExecutionsWithResources(newValue);
}
}

fetchData(nodeExecutions, queryClient);

return () => {
isCurrent = false;
};
}, [nodeExecutions]);

useEffect(() => {
setIsDetailsTabClosed(!selectedExecution);
}, [selectedExecution]);
Expand All @@ -126,18 +64,15 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
selectedPhase={selectedPhase}
onPhaseSelectionChanged={setSelectedPhase}
isDetailsTabClosed={isDetailsTabClosed}
nodeExecutionsById={nodeExecutionsById}
workflow={workflow}
/>
);

return (
<>
<NodeExecutionsContext.Provider value={nodeExecutionsById}>
<WaitForQuery errorComponent={DataError} query={workflowQuery}>
{renderGraph}
</WaitForQuery>
</NodeExecutionsContext.Provider>
<WaitForQuery errorComponent={DataError} query={workflowQuery}>
{renderGraph}
</WaitForQuery>
<DetailsPanel open={!!selectedExecution} onClose={onCloseDetailsPanel}>
{selectedExecution && (
<NodeExecutionDetailsPanelContent
Expand Down
Loading