Skip to content

Commit

Permalink
fix: update timeline view to show dynamic wf internals on first render (
Browse files Browse the repository at this point in the history
#562)

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

Signed-off-by: Olga Nad <[email protected]>

* fix: update tests and clean up code

Signed-off-by: Olga Nad <[email protected]>

* fix: test

Signed-off-by: Olga Nad <[email protected]>

Signed-off-by: Olga Nad <[email protected]>
  • Loading branch information
olga-union authored Aug 17, 2022
1 parent 1f89e2b commit 9b10e5f
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 324 deletions.

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

0 comments on commit 9b10e5f

Please sign in to comment.