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 }, +});