From aa31c0d31c16ea9e1263f198fd314de349226d49 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:23:28 -0400 Subject: [PATCH] artifacts logic remove unused params translate artifact node add Jeff fixes expose monitoring icon for metric artifacts Artifact node fixes add groups logic to render collapsed group node styling remove groups logic, fix icon color remove PipelineTaskGroup remove unused TaskGroupTargetAnchor fix artifact node dimensions fix linting issues Update artifact node height Co-authored-by: Jeff Phillips remove unused padding values, use ArtifactTaskNodeProps remove directional condition, fix tests resolve linting issues remove unused vars revert es6 target --- frontend/package-lock.json | 9 +- frontend/package.json | 2 +- .../topology/usePipelineTaskTopology.ts | 5 +- .../concepts/topology/PipelineTaskEdge.tsx | 30 ++++ .../topology/PipelineVisualizationSurface.tsx | 11 +- frontend/src/concepts/topology/TaskEdge.tsx | 19 +- frontend/src/concepts/topology/const.ts | 2 +- .../topology/customNodes/ArtifactTaskNode.tsx | 163 ++++++++++++++++++ .../topology/customNodes/StandardTaskNode.tsx | 53 ++++-- frontend/src/concepts/topology/factories.ts | 14 +- frontend/src/concepts/topology/types.ts | 7 +- .../topology/useTopologyController.ts | 5 +- frontend/src/concepts/topology/utils.ts | 15 ++ 13 files changed, 290 insertions(+), 45 deletions(-) create mode 100644 frontend/src/concepts/topology/PipelineTaskEdge.tsx create mode 100644 frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 26fe31f943..e1b340230f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,7 @@ "@patternfly/react-styles": "^5.2.1", "@patternfly/react-table": "^5.2.1", "@patternfly/react-tokens": "^5.2.1", - "@patternfly/react-topology": "^5.1.0", + "@patternfly/react-topology": "^5.3.0-prerelease.5", "@patternfly/react-virtualized-extension": "^5.0.0", "@types/classnames": "^2.3.1", "axios": "^1.6.4", @@ -3720,9 +3720,9 @@ "integrity": "sha512-8GYz/jnJTGAWUJt5eRAW5dtyiHPKETeFJBPGHaUQnvi/t1ZAkoy8i4Kd/RlHsDC7ktiu813SKCmlzwBwldAHKg==" }, "node_modules/@patternfly/react-topology": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.1.0.tgz", - "integrity": "sha512-Qzu7GMxqCsRvQj4RF2AHOGSp0nPpVuDE2xpdAaj/yCKz0cqHhvrwpC4+qyVL3mlqIs5qb+Fxm2d81Do7YIx3ig==", + "version": "5.3.0-prerelease.5", + "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.3.0-prerelease.5.tgz", + "integrity": "sha512-u5Jr3B4nvv/7uqn+u4e7I3CZwFVXLX7uanQP08rhvauWBG2hAhQYwxFEGRCm0j+1LEQWd2dwXvauGQM5gL0Itw==", "dependencies": { "@patternfly/react-core": "^5.1.1", "@patternfly/react-icons": "^5.1.1", @@ -3733,7 +3733,6 @@ "@types/react-measure": "^2.0.6", "d3": "^7.8.0", "dagre": "0.8.2", - "lodash": "^4.17.19", "mobx": "^6.9.0", "mobx-react": "^7.6.0", "point-in-svg-path": "^1.0.1", diff --git a/frontend/package.json b/frontend/package.json index 2111bc4dea..ac15bdb206 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,7 +58,7 @@ "@patternfly/react-styles": "^5.2.1", "@patternfly/react-table": "^5.2.1", "@patternfly/react-tokens": "^5.2.1", - "@patternfly/react-topology": "^5.1.0", + "@patternfly/react-topology": "^5.3.0-prerelease.5", "@patternfly/react-virtualized-extension": "^5.0.0", "@types/classnames": "^2.3.1", "axios": "^1.6.4", diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index d2609934e0..0536c80cb1 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -1,6 +1,7 @@ import { PipelineRunKFv2, PipelineSpecVariable } from '~/concepts/pipelines/kfTypes'; import { createNode } from '~/concepts/topology'; import { PipelineNodeModelExpanded } from '~/concepts/topology/types'; +import { createArtifactNode } from '~/concepts/topology/utils'; import { composeArtifactType, parseComponentsForArtifactRelationship, @@ -64,10 +65,12 @@ export const usePipelineTaskTopology = ( const id = artifactId ?? artifactKey; nodes.push( - createNode({ + createArtifactNode({ id, label, + artifactType: data.schemaTitle, runAfter: [taskId], + status: translateStatusForNode(status?.state), }), ); diff --git a/frontend/src/concepts/topology/PipelineTaskEdge.tsx b/frontend/src/concepts/topology/PipelineTaskEdge.tsx new file mode 100644 index 0000000000..b81fb295a0 --- /dev/null +++ b/frontend/src/concepts/topology/PipelineTaskEdge.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { + DEFAULT_SPACER_NODE_TYPE, + GraphElement, + Edge, + EdgeTerminalType, + observer, + TaskEdge, +} from '@patternfly/react-topology'; + +interface PipelineTaskEdgeProps { + element: GraphElement; +} + +const PipelineTaskEdge: React.FC = ({ element, ...props }) => { + const edge = element as Edge; + return ( + + ); +}; + +export default observer(PipelineTaskEdge); diff --git a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx index 3c09647aa1..29bc56cff3 100644 --- a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx +++ b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx @@ -31,10 +31,13 @@ const PipelineVisualizationSurface: React.FC const controller = useVisualizationController(); const [error, setError] = React.useState(); React.useEffect(() => { - // PF Bug - // TODO: Pipeline Topology weirdly doesn't set a width and height on spacer nodes -- but they do when using finally spacer nodes - const spacerNodes = getSpacerNodes(nodes).map((s) => ({ ...s, width: 1, height: 1 })); - const renderNodes = [...spacerNodes, ...nodes]; + const spacerNodes = getSpacerNodes(nodes); + + // Dagre likes the root nodes to be first in the order + const renderNodes = [...spacerNodes, ...nodes].sort( + (a, b) => (a.runAfterTasks?.length ?? 0) - (b.runAfterTasks?.length ?? 0), + ); + // TODO: We can have a weird edge issue if the node is off by a few pixels vertically from the center const edges = getEdgesFromNodes(renderNodes); try { diff --git a/frontend/src/concepts/topology/TaskEdge.tsx b/frontend/src/concepts/topology/TaskEdge.tsx index 1523c3cf99..39022e4f6d 100644 --- a/frontend/src/concepts/topology/TaskEdge.tsx +++ b/frontend/src/concepts/topology/TaskEdge.tsx @@ -5,8 +5,8 @@ import { observer, Edge, integralShapePath, - DEFAULT_SPACER_NODE_TYPE, ConnectorArrow, + DagreLayoutOptions, } from '@patternfly/react-topology'; interface TaskEdgeProps { @@ -24,22 +24,21 @@ const TaskEdge: React.FunctionComponent = ({ const endPoint = element.getEndPoint(); const groupClassName = css(styles.topologyEdge, className); const startIndent: number = element.getData()?.indent || 0; + const verticalLayout = + (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions).rankdir === 'TB'; return ( - - {element.getTarget().getType() !== DEFAULT_SPACER_NODE_TYPE ? ( - - ) : null} + ); }; diff --git a/frontend/src/concepts/topology/const.ts b/frontend/src/concepts/topology/const.ts index deac7d9186..72073d2cdf 100644 --- a/frontend/src/concepts/topology/const.ts +++ b/frontend/src/concepts/topology/const.ts @@ -2,4 +2,4 @@ export const PIPELINE_LAYOUT = 'PipelineLayout'; export const PIPELINE_NODE_SEPARATION_VERTICAL = 100; export const NODE_WIDTH = 100; -export const NODE_HEIGHT = 30; +export const NODE_HEIGHT = 35; diff --git a/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx new file mode 100644 index 0000000000..68ee73c6a6 --- /dev/null +++ b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx @@ -0,0 +1,163 @@ +import React, { LegacyRef } from 'react'; +import { + TaskNode, + DEFAULT_WHEN_OFFSET, + Node, + WhenDecorator, + NodeModel, + WithSelectionProps, + observer, + useAnchor, + AnchorEnd, + getRunStatusModifier, + ScaleDetailsLevel, + useHover, + TaskNodeSourceAnchor, + TaskNodeTargetAnchor, + GraphElement, +} from '@patternfly/react-topology'; +import { ListIcon, MonitoringIcon } from '@patternfly/react-icons'; +import { TaskNodeProps } from '@patternfly/react-topology/dist/esm/pipelines/components/nodes/TaskNode'; +import { css } from '@patternfly/react-styles'; +import { StandardTaskNodeData } from '~/concepts/topology/types'; + +const ICON_PADDING = 8; + +type IconTaskNodeProps = { + element: Node; +} & WithSelectionProps; + +const IconTaskNode: React.FC = observer(({ element, selected, onSelect }) => { + const data = element.getData(); + const status = data?.status; + const bounds = element.getBounds(); + const iconSize = bounds.height - ICON_PADDING * 2; + + const runStatusModifier = status && getRunStatusModifier(status); + + useAnchor( + React.useCallback( + (node: Node) => new TaskNodeSourceAnchor(node, ScaleDetailsLevel.high, 0, true), + [], + ), + AnchorEnd.source, + ); + useAnchor( + React.useCallback( + (node: Node) => new TaskNodeTargetAnchor(node, 0, ScaleDetailsLevel.high, 0, true), + [], + ), + AnchorEnd.target, + ); + + return ( + + + + {data?.artifactType === 'system.Metrics' ? ( + + ) : ( + + )} + + + ); +}); + +type ArtifactTaskNodeInnerProps = WithSelectionProps & { + element: Node; +} & Omit & { element: Node }; + +const ArtifactTaskNodeInner: React.FC = observer( + ({ element, selected, onSelect, ...rest }) => { + const bounds = element.getBounds(); + const [isHover, hoverRef] = useHover(); + const detailsLevel = element.getGraph().getDetailsLevel(); + const data = element.getData(); + const scale = element.getGraph().getScale(); + const iconSize = 24; + const whenDecorator = data?.whenStatus ? ( + + ) : null; + const upScale = 1 / scale; + + return ( + } + > + {isHover || detailsLevel !== ScaleDetailsLevel.high ? ( + + + {whenDecorator} + + {!isHover && detailsLevel !== ScaleDetailsLevel.high ? ( + + + + {data?.artifactType === 'system.Metrics' ? : } + + + + ) : null} + + ) : ( + + )} + + ); + }, +); + +type ArtifactTaskNodeProps = { + element: GraphElement; +} & WithSelectionProps; + +const ArtifactTaskNode: React.FC = ({ element, ...rest }) => ( + +); + +export default ArtifactTaskNode; diff --git a/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx b/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx index 294d36a111..23ad94386f 100644 --- a/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx +++ b/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx @@ -1,30 +1,59 @@ import * as React from 'react'; import { - TaskNode, DEFAULT_WHEN_OFFSET, - Node, + DEFAULT_WHEN_SIZE, + GraphElement, + observer, + RunStatus, + ScaleDetailsLevel, + TaskNode, + useHover, WhenDecorator, - NodeModel, + WithContextMenuProps, WithSelectionProps, - observer, } from '@patternfly/react-topology'; -import { StandardTaskNodeData } from '~/concepts/topology/types'; -type DemoTaskNodeProps = WithSelectionProps & { - element: Node; -}; +type StandardTaskNodeProps = { + element: GraphElement; +} & WithContextMenuProps & + WithSelectionProps; -const StandardTaskNode: React.FC = ({ element, onSelect, selected }) => { +const StandardTaskNode: React.FunctionComponent = ({ + element, + onSelect, + selected, + ...rest +}) => { const data = element.getData(); + const [hover, hoverRef] = useHover(); + const detailsLevel = element.getGraph().getDetailsLevel(); const whenDecorator = data?.whenStatus ? ( ) : null; return ( - - {whenDecorator} - + }> + + {whenDecorator} + + ); }; diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index c6f836fd62..08f06a48a1 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -10,23 +10,23 @@ import { withSelection, } from '@patternfly/react-topology'; import StandardTaskNode from '~/concepts/topology/customNodes/StandardTaskNode'; -import TaskEdge from './TaskEdge'; -// Topology gap... their types have issues with Strict TS mode -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +import { ICON_TASK_NODE_TYPE } from './utils'; +import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; +import PipelineTaskEdge from './PipelineTaskEdge'; + export const pipelineComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { return withPanZoom()(GraphComponent); } switch (type) { case DEFAULT_TASK_NODE_TYPE: - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore return withSelection()(StandardTaskNode); + case ICON_TASK_NODE_TYPE: + return withSelection()(ArtifactTaskNode); case DEFAULT_SPACER_NODE_TYPE: return SpacerNode; case DEFAULT_EDGE_TYPE: - return TaskEdge; + return PipelineTaskEdge; default: return undefined; } diff --git a/frontend/src/concepts/topology/types.ts b/frontend/src/concepts/topology/types.ts index f2f9ad9b12..1c35f045b2 100644 --- a/frontend/src/concepts/topology/types.ts +++ b/frontend/src/concepts/topology/types.ts @@ -2,14 +2,17 @@ import { PipelineNodeModel, RunStatus, WhenStatus } from '@patternfly/react-topo export type NodeConstructDetails = { id: string; - label: string; + label?: string; + artifactType?: string; runAfter?: string[]; status?: RunStatus; + tasks?: string[]; }; export type StandardTaskNodeData = { - status: RunStatus; + status?: RunStatus; whenStatus?: WhenStatus; + artifactType?: string; }; export type PipelineNodeModelExpanded = PipelineNodeModel & { diff --git a/frontend/src/concepts/topology/useTopologyController.ts b/frontend/src/concepts/topology/useTopologyController.ts index 7a09df1758..35e0eb52ba 100644 --- a/frontend/src/concepts/topology/useTopologyController.ts +++ b/frontend/src/concepts/topology/useTopologyController.ts @@ -4,7 +4,7 @@ import { GRAPH_LAYOUT_END_EVENT, Layout, NODE_SEPARATION_HORIZONTAL, - PipelineDagreLayout, + PipelineDagreGroupsLayout, Visualization, } from '@patternfly/react-topology'; import { pipelineComponentFactory } from '~/concepts/topology/factories'; @@ -19,10 +19,11 @@ const useTopologyController = (graphId: string): Visualization | null => { visualizationController.registerComponentFactory(pipelineComponentFactory); visualizationController.registerLayoutFactory( (type: string, graph: Graph): Layout | undefined => - new PipelineDagreLayout(graph, { + new PipelineDagreGroupsLayout(graph, { nodesep: PIPELINE_NODE_SEPARATION_VERTICAL, ranksep: NODE_SEPARATION_HORIZONTAL, ignoreGroups: true, + rankdir: 'TB', }), ); visualizationController.fromModel( diff --git a/frontend/src/concepts/topology/utils.ts b/frontend/src/concepts/topology/utils.ts index e1a0b6f086..ace5d54849 100644 --- a/frontend/src/concepts/topology/utils.ts +++ b/frontend/src/concepts/topology/utils.ts @@ -5,6 +5,11 @@ import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; +export const ICON_TASK_NODE_TYPE = 'ICON_TASK_NODE'; + +export const ARTIFACT_NODE_WIDTH = 44; +export const ARTIFACT_NODE_HEIGHT = NODE_HEIGHT; + export const createNode = (details: NodeConstructDetails): PipelineNodeModelExpanded => ({ id: details.id, label: details.label, @@ -18,3 +23,13 @@ export const createNode = (details: NodeConstructDetails): PipelineNodeModelExpa } : undefined, }); + +export const createArtifactNode = (details: NodeConstructDetails): PipelineNodeModelExpanded => ({ + id: details.id, + label: `${details.label} (Type: ${details.artifactType?.slice(7)})`, + type: ICON_TASK_NODE_TYPE, + width: ARTIFACT_NODE_WIDTH, + height: ARTIFACT_NODE_HEIGHT, + runAfterTasks: details.runAfter, + data: { status: details.status ?? undefined, artifactType: details.artifactType }, +});