From cca633e1b597b5a4d22ba30f19d75eaf6fa3a1ff Mon Sep 17 00:00:00 2001 From: csirius <85753828+csirius@users.noreply.github.com> Date: Wed, 15 Dec 2021 17:39:51 -0500 Subject: [PATCH] feat: update node list view and add executions bar chart (#252) * feat: update node list view and add executions bar chart Signed-off-by: csirius <85753828+csirius@users.noreply.github.com> * fix: unit testing coverage Signed-off-by: csirius <85753828+csirius@users.noreply.github.com> * fix: remove unused vars Signed-off-by: csirius <85753828+csirius@users.noreply.github.com> * fix: show 100 executions for bar chart Signed-off-by: csirius <85753828+csirius@users.noreply.github.com> * fix: navigate to execution detail when bar chart item is clicked Signed-off-by: csirius <85753828+csirius@users.noreply.github.com> --- src/components/Entities/EntityExecutions.tsx | 2 +- .../Entities/EntityExecutionsBarChart.tsx | 13 ++-- src/components/Executions/Tables/constants.ts | 1 + .../Tables/nodeExecutionColumns.tsx | 47 +++++++++----- src/components/Executions/Tables/styles.ts | 6 +- .../Tables/test/NodeExecutionsTable.test.tsx | 21 +++++-- src/components/Project/ProjectExecutions.tsx | 62 ++++++++++++++++++- src/components/common/BarChart.tsx | 2 +- 8 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/components/Entities/EntityExecutions.tsx b/src/components/Entities/EntityExecutions.tsx index d4012388b..443f63c8f 100644 --- a/src/components/Entities/EntityExecutions.tsx +++ b/src/components/Entities/EntityExecutions.tsx @@ -45,7 +45,7 @@ export const EntityExecutions: React.FC = ({ const baseFilters = React.useMemo( () => executionFilterGenerator[resourceType](id), - [id] + [id, resourceType] ); const executions = useWorkflowExecutions( diff --git a/src/components/Entities/EntityExecutionsBarChart.tsx b/src/components/Entities/EntityExecutionsBarChart.tsx index 497ae38ed..446224453 100644 --- a/src/components/Entities/EntityExecutionsBarChart.tsx +++ b/src/components/Entities/EntityExecutionsBarChart.tsx @@ -9,10 +9,8 @@ import { useWorkflowExecutionFiltersState } from 'components/Executions/filters/ import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; import { SortDirection } from 'models/AdminEntity/types'; import { ResourceIdentifier } from 'models/Common/types'; -import { Execution, WorkflowExecutionIdentifier } from 'models/Execution/types'; +import { Execution } from 'models/Execution/types'; import { executionSortFields } from 'models/Execution/constants'; -import { Routes } from 'routes/routes'; -import { history } from 'routes/history'; import { executionFilterGenerator } from './generators'; import { getWorkflowExecutionPhaseConstants, @@ -36,7 +34,10 @@ export interface EntityExecutionsBarChartProps { chartIds: string[]; } -const getExecutionTimeData = (executions: Execution[], fillSize = 100) => { +export const getExecutionTimeData = ( + executions: Execution[], + fillSize = 100 +) => { const newExecutions = [...executions].reverse().map(execution => { const duration = getWorkflowExecutionTimingMS(execution)?.duration || 1; return { @@ -66,14 +67,14 @@ const getExecutionTimeData = (executions: Execution[], fillSize = 100) => { } return new Array(fillSize - newExecutions.length) .fill(0) - .map(_ => ({ + .map(() => ({ value: 1, color: '#e5e5e5' })) .concat(newExecutions); }; -const getStartExecutionTime = (executions: Execution[]) => { +export const getStartExecutionTime = (executions: Execution[]) => { if (executions.length === 0) { return ''; } diff --git a/src/components/Executions/Tables/constants.ts b/src/components/Executions/Tables/constants.ts index 8493a5658..097d18d67 100644 --- a/src/components/Executions/Tables/constants.ts +++ b/src/components/Executions/Tables/constants.ts @@ -11,6 +11,7 @@ export const nodeExecutionsTableColumnWidths = { duration: 100, logs: 225, type: 144, + nodeId: 144, name: 380, phase: 150, startedAt: 200 diff --git a/src/components/Executions/Tables/nodeExecutionColumns.tsx b/src/components/Executions/Tables/nodeExecutionColumns.tsx index d6b3d9020..56a331a4b 100644 --- a/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -31,28 +31,25 @@ const NodeExecutionName: React.FC = ({ const detailsQuery = useNodeExecutionDetails(execution); const commonStyles = useCommonStyles(); const styles = useColumnStyles(); - const nodeId = execution.id.nodeId; const isSelected = state.selectedExecution != null && isEqual(execution.id, state.selectedExecution); - const renderReadableName = ({ - displayId, - displayName - }: NodeExecutionDetails) => { + const renderReadableName = ({ displayName }: NodeExecutionDetails) => { + const truncatedName = displayName?.split('.').pop() || ''; const readableName = isSelected ? ( - {displayId || nodeId} + {truncatedName} ) : ( ); @@ -75,12 +72,24 @@ const NodeExecutionName: React.FC = ({ ); }; +const NodeExecutionDisplayId: React.FC = ({ + execution +}) => { + const detailsQuery = useNodeExecutionDetails(execution); + const extractDisplayId = ({ displayId }: NodeExecutionDetails) => + displayId || execution.id.nodeId; + return {extractDisplayId}; +}; + const NodeExecutionDisplayType: React.FC = ({ execution }) => { const detailsQuery = useNodeExecutionDetails(execution); - const extractDisplayType = ({ displayType }: NodeExecutionDetails) => - displayType; + const extractDisplayType = ({ displayType }: NodeExecutionDetails) => ( + + {displayType || execution.id.nodeId} + + ); return ( {extractDisplayType} ); @@ -108,7 +117,19 @@ export function generateColumns( cellRenderer: props => , className: styles.columnName, key: 'name', - label: 'node' + label: 'task name' + }, + { + cellRenderer: props => , + className: styles.columnNodeId, + key: 'nodeId', + label: 'node id' + }, + { + cellRenderer: props => , + className: styles.columnType, + key: 'type', + label: 'type' }, { cellRenderer: ({ @@ -133,12 +154,6 @@ export function generateColumns( key: 'phase', label: 'status' }, - { - cellRenderer: props => , - className: styles.columnType, - key: 'type', - label: 'type' - }, { cellRenderer: ({ execution: { closure } }) => { const { startedAt } = closure; diff --git a/src/components/Executions/Tables/styles.ts b/src/components/Executions/Tables/styles.ts index 666205596..14136ea99 100644 --- a/src/components/Executions/Tables/styles.ts +++ b/src/components/Executions/Tables/styles.ts @@ -138,8 +138,12 @@ export const useColumnStyles = makeStyles((theme: Theme) => ({ marginLeft: theme.spacing(nameColumnLeftMarginGridWidth) } }, + columnNodeId: { + flexBasis: nodeExecutionsTableColumnWidths.nodeId + }, columnType: { - flexBasis: nodeExecutionsTableColumnWidths.type + flexBasis: nodeExecutionsTableColumnWidths.type, + textTransform: 'capitalize' }, columnStatus: { display: 'flex', diff --git a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx index e6cea3072..4a5cf2e9d 100644 --- a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -63,9 +63,13 @@ describe('NodeExecutionsTable', () => { const shouldUpdateFn = (nodeExecutions: NodeExecution[]) => nodeExecutions.some(ne => !nodeExecutionIsTerminal(ne)); - const selectNode = async (container: HTMLElement, nodeId: string) => { + const selectNode = async ( + container: HTMLElement, + truncatedName: string, + nodeId: string + ) => { const nodeNameAnchor = await waitFor(() => - getByText(container, nodeId) + getByText(container, truncatedName) ); fireEvent.click(nodeNameAnchor); // Wait for Details Panel to render and then for the nodeId header @@ -500,10 +504,13 @@ describe('NodeExecutionsTable', () => { it('should render updated state if selected nodeExecution object changes', async () => { nodeExecution.closure.phase = NodeExecutionPhase.RUNNING; updateNodeExecutions([nodeExecution]); + const truncatedName = + fixture.tasks.python.id.name.split('.').pop() || ''; // Render table, click first node const { container } = renderTable(); const detailsPanel = await selectNode( container, + truncatedName, nodeExecution.id.nodeId ); expect(getByText(detailsPanel, 'Running')).toBeInTheDocument(); @@ -535,8 +542,14 @@ describe('NodeExecutionsTable', () => { dynamicTaskNameEl, 'listitem' ); - await expandParentNode(dynamicRowEl); - await selectNode(container, childNodeExecution.id.nodeId); + const parentNodeEl = await expandParentNode(dynamicRowEl); + const truncatedName = + fixture.tasks.python.id.name.split('.').pop() || ''; + await selectNode( + parentNodeEl[0], + truncatedName, + childNodeExecution.id.nodeId + ); // Wait for Details Panel to render and then for the nodeId header const detailsPanel = await waitFor(() => diff --git a/src/components/Project/ProjectExecutions.tsx b/src/components/Project/ProjectExecutions.tsx index 5e195eab9..9328555c9 100644 --- a/src/components/Project/ProjectExecutions.tsx +++ b/src/components/Project/ProjectExecutions.tsx @@ -1,4 +1,5 @@ -import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles, Theme } from '@material-ui/core/styles'; import { getCacheKey } from 'components/Cache/utils'; import { ErrorBoundary } from 'components/common/ErrorBoundary'; import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; @@ -13,12 +14,35 @@ import { Execution } from 'models/Execution/types'; import * as React from 'react'; import { useInfiniteQuery } from 'react-query'; import { failedToLoadExecutionsString } from './constants'; +import { BarChart } from 'components/common/BarChart'; +import { + getExecutionTimeData, + getStartExecutionTime +} from 'components/Entities/EntityExecutionsBarChart'; +import classNames from 'classnames'; +import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; +import { WaitForData } from 'components/common/WaitForData'; +import { history } from 'routes/history'; +import { Routes } from 'routes/routes'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme: Theme) => ({ container: { display: 'flex', flex: '1 1 auto', flexDirection: 'column' + }, + header: { + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(1), + borderBottom: `1px solid ${theme.palette.divider}` + }, + marginTop: { + marginTop: theme.spacing(2) + }, + chartContainer: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(3), + paddingTop: theme.spacing(1) } })); export interface ProjectExecutionsProps { @@ -73,6 +97,19 @@ export const ProjectExecutions: React.FC = ({ [query.data?.pages] ); + const handleBarChartItemClick = React.useCallback(item => { + history.push(Routes.ExecutionDetails.makeUrl(item.metadata)); + }, []); + + const last100Executions = useWorkflowExecutions( + { domain, project }, + { + sort: defaultSort, + filter: filtersState.appliedFilters, + limit: 100 + } + ); + const fetch = React.useCallback(() => query.fetchNextPage(), [query]); const content = query.isLoadingError ? ( @@ -99,6 +136,27 @@ export const ProjectExecutions: React.FC = ({ if (filtersState.filters[4].status === 'LOADED') { return (
+ + Last 100 Executions in the Project + +
+ + + +
+ + All Executions in the Project + {content}
diff --git a/src/components/common/BarChart.tsx b/src/components/common/BarChart.tsx index e7cc956c4..b156d032e 100644 --- a/src/components/common/BarChart.tsx +++ b/src/components/common/BarChart.tsx @@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) => ({ flex: 1, display: 'flex', flexDirection: 'column', + justifyContent: 'flex-end', alignItems: 'center', '&:last-child': { marginRight: 0 @@ -35,7 +36,6 @@ const useStyles = makeStyles((theme: Theme) => ({ }, itemBar: { borderRadius: 2, - flex: 1, marginRight: theme.spacing(0.25), minHeight: theme.spacing(0.75), cursor: 'pointer',