From ad35b180d0a84fbb82e8a0466e79d26ed6c535bd 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 01/18] groups 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 add support for nested pipeline groups, wip run --fix add status to artifact nodes and fix hovered/selected icon color delete IconTaskNode remove custom-topology-components.css use forEach revert tsconfig to es5 --- .../topology/usePipelineTaskTopology.ts | 7 +- .../concepts/topology/PipelineTaskGroup.tsx | 69 +++++++++++++++++++ .../topology/TaskGroupSourceAnchor.tsx | 16 +++++ .../topology/TaskGroupTargetAnchor.tsx | 23 +++++++ .../topology/customNodes/IconSourceAnchor.tsx | 19 +++++ frontend/src/concepts/topology/factories.ts | 26 +++++++ frontend/src/concepts/topology/types.ts | 1 + .../topology/useTopologyController.ts | 5 +- frontend/src/concepts/topology/utils.ts | 23 +++++++ 9 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 frontend/src/concepts/topology/PipelineTaskGroup.tsx create mode 100644 frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx create mode 100644 frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx create mode 100644 frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index 11b8506480..cdc38e27c6 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -1,7 +1,7 @@ import { PipelineRunKFv2, PipelineSpecVariable, TaskKF } from '~/concepts/pipelines/kfTypes'; import { createNode } from '~/concepts/topology'; -import { PipelineNodeModelExpanded } from '~/concepts/topology/types'; -import { createArtifactNode } from '~/concepts/topology/utils'; +import { NodeConstructDetails, PipelineNodeModelExpanded } from '~/concepts/topology/types'; +import { createArtifactNode, createGroupNode } from '~/concepts/topology/utils'; import { composeArtifactType, parseComponentsForArtifactRelationship, @@ -112,10 +112,11 @@ export const usePipelineTaskTopology = ( } nodes.push( - createNode({ + createGroupNode({ id: taskId, label: taskName, runAfter, + tasks: tasks.map((i) => i.id), status: translateStatusForNode(status?.state), }), ); diff --git a/frontend/src/concepts/topology/PipelineTaskGroup.tsx b/frontend/src/concepts/topology/PipelineTaskGroup.tsx new file mode 100644 index 0000000000..9e4573e90e --- /dev/null +++ b/frontend/src/concepts/topology/PipelineTaskGroup.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { + AnchorEnd, + DefaultTaskGroup, + GraphElement, + isNode, + LabelPosition, + Node, + useAnchor, + WithContextMenuProps, + WithSelectionProps, + ShapeProps, + WithDragNodeProps, + EdgeCreationTypes, +} from '@patternfly/react-topology'; +import TaskGroupSourceAnchor from './TaskGroupSourceAnchor'; +import TaskGroupTargetAnchor from './TaskGroupTargetAnchor'; + +type PipelineTaskGroupProps = { + element: GraphElement; + collapsible?: boolean; + collapsedWidth?: number; + collapsedHeight?: number; + onCollapseChange?: (group: Node, collapsed: boolean) => void; + getCollapsedShape?: (node: Node) => React.FunctionComponent; + collapsedShadowOffset?: number; // defaults to 10 +} & WithContextMenuProps & + WithDragNodeProps & + WithSelectionProps; + +export const DEFAULT_TASK_WIDTH = 180; +export const DEFAULT_TASK_HEIGHT = 32; + +const getEdgeCreationTypes = (): EdgeCreationTypes => ({ + edgeType: 'edge', + spacerEdgeType: 'edge', +}); + +const PipelineTaskGroup: React.FunctionComponent = ({ + element, + ...rest +}) => { + useAnchor( + React.useCallback((node: Node) => new TaskGroupSourceAnchor(node), []), + AnchorEnd.source, + ); + useAnchor( + React.useCallback((node: Node) => new TaskGroupTargetAnchor(node), []), + AnchorEnd.target, + ); + if (!isNode(element)) { + return null; + } + return ( + + ); +}; + +export default observer(PipelineTaskGroup); diff --git a/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx b/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx new file mode 100644 index 0000000000..6a7585c368 --- /dev/null +++ b/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx @@ -0,0 +1,16 @@ +import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; + +export default class TaskGroupSourceAnchor extends AbstractAnchor { + constructor(owner: E) { + super(owner); + } + + getLocation(): Point { + return this.getReferencePoint(); + } + + getReferencePoint(): Point { + const bounds = this.owner.getBounds(); + return new Point(bounds.right(), bounds.y + bounds.height / 2); + } +} diff --git a/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx b/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx new file mode 100644 index 0000000000..c349cf72c7 --- /dev/null +++ b/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx @@ -0,0 +1,23 @@ +import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; + +export default class TaskGroupTargetAnchor extends AbstractAnchor { + private vertical = false; + + constructor(owner: E, vertical = false) { + super(owner); + this.vertical = vertical; + } + + getLocation(): Point { + return this.getReferencePoint(); + } + + getReferencePoint(): Point { + const bounds = this.owner.getBounds(); + + if (this.vertical) { + return new Point(bounds.x + bounds.width / 2, bounds.y); + } + return new Point(bounds.x, bounds.y + bounds.height / 2); + } +} diff --git a/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx b/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx new file mode 100644 index 0000000000..bc1780759b --- /dev/null +++ b/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx @@ -0,0 +1,19 @@ +import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; + +export default class IconSourceAnchor extends AbstractAnchor { + private size: number; + + constructor(owner: E, size: number) { + super(owner); + this.size = size; + } + + getLocation(): Point { + return this.getReferencePoint(); + } + + getReferencePoint(): Point { + const bounds = this.owner.getBounds(); + return new Point(bounds.x + this.size, bounds.y + bounds.height / 2); + } +} diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index 08f06a48a1..952b216c48 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -8,10 +8,12 @@ import { SpacerNode, withPanZoom, withSelection, + TaskEdge as PFTaskEdge, } from '@patternfly/react-topology'; import StandardTaskNode from '~/concepts/topology/customNodes/StandardTaskNode'; import { ICON_TASK_NODE_TYPE } from './utils'; import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; +import PipelineTaskGroup from './PipelineTaskGroup'; import PipelineTaskEdge from './PipelineTaskEdge'; export const pipelineComponentFactory: ComponentFactory = (kind, type) => { @@ -31,3 +33,27 @@ export const pipelineComponentFactory: ComponentFactory = (kind, type) => { return undefined; } }; + +export const pipelineGroupsComponentFactory = (kind: ModelKind, type: string) => { + if (kind === ModelKind.graph) { + return withPanZoom()(GraphComponent); + } + switch (type) { + case 'Execution': + return withSelection()(PipelineTaskGroup); + case 'Task': + return withSelection()(StandardTaskNode); + case ICON_TASK_NODE_TYPE: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return withSelection()(ArtifactTaskNode); + case DEFAULT_SPACER_NODE_TYPE: + return SpacerNode; + case 'edge': + return PipelineTaskEdge; + default: + return undefined; + } +}; + +export default pipelineGroupsComponentFactory; diff --git a/frontend/src/concepts/topology/types.ts b/frontend/src/concepts/topology/types.ts index 1c35f045b2..88db4c1ba4 100644 --- a/frontend/src/concepts/topology/types.ts +++ b/frontend/src/concepts/topology/types.ts @@ -1,4 +1,5 @@ import { PipelineNodeModel, RunStatus, WhenStatus } from '@patternfly/react-topology'; +import React from 'react'; export type NodeConstructDetails = { id: string; diff --git a/frontend/src/concepts/topology/useTopologyController.ts b/frontend/src/concepts/topology/useTopologyController.ts index 35e0eb52ba..3dde01bdf8 100644 --- a/frontend/src/concepts/topology/useTopologyController.ts +++ b/frontend/src/concepts/topology/useTopologyController.ts @@ -7,7 +7,9 @@ import { PipelineDagreGroupsLayout, Visualization, } from '@patternfly/react-topology'; -import { pipelineComponentFactory } from '~/concepts/topology/factories'; +import pipelineGroupsComponentFactory, { + pipelineComponentFactory, +} from '~/concepts/topology/factories'; import { PIPELINE_LAYOUT, PIPELINE_NODE_SEPARATION_VERTICAL } from './const'; const useTopologyController = (graphId: string): Visualization | null => { @@ -17,6 +19,7 @@ const useTopologyController = (graphId: string): Visualization | null => { const visualizationController = new Visualization(); visualizationController.setFitToScreenOnLayout(true); visualizationController.registerComponentFactory(pipelineComponentFactory); + visualizationController.registerComponentFactory(pipelineGroupsComponentFactory); visualizationController.registerLayoutFactory( (type: string, graph: Graph): Layout | undefined => new PipelineDagreGroupsLayout(graph, { diff --git a/frontend/src/concepts/topology/utils.ts b/frontend/src/concepts/topology/utils.ts index ace5d54849..cfacd790e2 100644 --- a/frontend/src/concepts/topology/utils.ts +++ b/frontend/src/concepts/topology/utils.ts @@ -10,6 +10,9 @@ export const ICON_TASK_NODE_TYPE = 'ICON_TASK_NODE'; export const ARTIFACT_NODE_WIDTH = 44; export const ARTIFACT_NODE_HEIGHT = NODE_HEIGHT; +export const NODE_PADDING_VERTICAL = 40; +export const NODE_PADDING_HORIZONTAL = 15; + export const createNode = (details: NodeConstructDetails): PipelineNodeModelExpanded => ({ id: details.id, label: details.label, @@ -33,3 +36,23 @@ export const createArtifactNode = (details: NodeConstructDetails): PipelineNodeM runAfterTasks: details.runAfter, data: { status: details.status ?? undefined, artifactType: details.artifactType }, }); + +export const createGroupNode = (details: NodeConstructDetails): PipelineNodeModelExpanded => ({ + id: details.id, + label: details.id, + type: 'Execution', + group: true, + width: NODE_WIDTH, + height: NODE_HEIGHT, + runAfterTasks: details.runAfter, + style: { + padding: [NODE_PADDING_VERTICAL, NODE_PADDING_HORIZONTAL], + }, + children: details.tasks ?? undefined, + data: details.status + ? { + status: details.status, + artifactType: details.artifactType, + } + : undefined, +}); From bcff15af767571474744100aecc6e034dbd57196 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:03:25 -0400 Subject: [PATCH 02/18] use es6 --- frontend/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index b29efed6c9..da00f31ff7 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,7 +3,7 @@ "rootDir": ".", "outDir": "public", "module": "esnext", - "target": "es5", + "target": "es6", "lib": ["es6", "dom"], "sourceMap": true, "jsx": "react", From 2c00b68d6382e3819427098951ddae31703dd33d Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Tue, 9 Apr 2024 08:01:55 -0400 Subject: [PATCH 03/18] Handle expandable groups --- .../topology/usePipelineTaskTopology.ts | 260 +++++++++++------- .../src/concepts/topology/BasePipelineNode.ts | 28 ++ .../concepts/topology/PipelineTaskGroup.tsx | 83 ++---- .../topology/PipelineVisualizationSurface.tsx | 14 +- .../topology/TaskGroupSourceAnchor.tsx | 16 -- .../topology/TaskGroupTargetAnchor.tsx | 23 -- frontend/src/concepts/topology/factories.ts | 2 - .../topology/pipelineElementFactory.ts | 13 + .../topology/useTopologyController.ts | 6 +- frontend/src/concepts/topology/utils.ts | 10 +- 10 files changed, 261 insertions(+), 194 deletions(-) create mode 100644 frontend/src/concepts/topology/BasePipelineNode.ts delete mode 100644 frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx delete mode 100644 frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx create mode 100644 frontend/src/concepts/topology/pipelineElementFactory.ts diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index cdc38e27c6..d96eadba42 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -1,6 +1,12 @@ -import { PipelineRunKFv2, PipelineSpecVariable, TaskKF } from '~/concepts/pipelines/kfTypes'; +import { + PipelineComponentsKF, + PipelineRunKFv2, + PipelineSpecVariable, + RunDetailsKF, + TaskKF, +} from '~/concepts/pipelines/kfTypes'; import { createNode } from '~/concepts/topology'; -import { NodeConstructDetails, PipelineNodeModelExpanded } from '~/concepts/topology/types'; +import { PipelineNodeModelExpanded } from '~/concepts/topology/types'; import { createArtifactNode, createGroupNode } from '~/concepts/topology/utils'; import { composeArtifactType, @@ -13,12 +19,59 @@ import { } from './parseUtils'; import { KubeFlowTaskTopology } from './pipelineTaskTypes'; +const EMPTY_STATE: KubeFlowTaskTopology = { taskMap: {}, nodes: [] }; + +const getNestedNodes = ( + items: Record, + components: PipelineComponentsKF, + runDetails?: RunDetailsKF, +): [nestedNodes: PipelineNodeModelExpanded[], children: string[]] => { + const nodes: PipelineNodeModelExpanded[] = []; + const children: string[] = []; + + Object.entries(items).forEach(([name, details]) => { + const componentRef = details.componentRef.name; + const status = parseRuntimeInfo(name, runDetails); + const runAfter: string[] = details.dependentTasks ?? []; + const hasSubTask = + Object.keys(components).find((task) => task === componentRef) && + components[componentRef]?.dag; + const subTasks = components[componentRef]?.dag?.tasks; + + if (hasSubTask && subTasks) { + const [nestedNodes, nestedChildren] = getNestedNodes(subTasks, components); + const itemNode = createGroupNode( + { + id: name, + label: name, + runAfter, + status: translateStatusForNode(status?.state), + }, + nestedChildren, + ); + nodes.push(itemNode, ...nestedNodes); + } else { + nodes.push( + createNode({ + id: name, + label: name, + runAfter, + status: translateStatusForNode(status?.state), + }), + ); + } + children.push(name); + }); + + return [nodes, children]; +}; + export const usePipelineTaskTopology = ( spec?: PipelineSpecVariable, run?: PipelineRunKFv2, ): KubeFlowTaskTopology => { if (!spec) { - return { taskMap: {}, nodes: [] }; + return EMPTY_STATE; } const pipelineSpec = spec.pipeline_spec ?? spec; @@ -26,107 +79,130 @@ export const usePipelineTaskTopology = ( components, deploymentSpec: { executors }, root: { - dag: { tasks: rootTasks }, + dag: { tasks }, }, } = pipelineSpec; const { run_details: runDetails } = run || {}; const componentArtifactMap = parseComponentsForArtifactRelationship(components); - const nodes: PipelineNodeModelExpanded[] = []; - const taskMap: KubeFlowTaskTopology['taskMap'] = {}; - - const createNodes = (tasks: Record, parentTask?: string) => { - const taskArtifactMap = parseTasksForArtifactRelationship(tasks); - Object.entries(tasks).forEach(([taskId, taskValue]) => { - const taskName = taskValue.taskInfo.name; - - const componentRef = taskValue.componentRef.name; - const component = components[componentRef]; - const artifactsInComponent = componentArtifactMap[componentRef]; - const isGroupNode = !!component?.dag; - - const executorLabel = component?.executorLabel; - const executor = executorLabel ? executors[executorLabel] : undefined; - - const status = parseRuntimeInfo(taskId, runDetails); - - const runAfter: string[] = taskValue.dependentTasks ?? []; - - if (artifactsInComponent) { - const artifactNodeData = taskArtifactMap[taskId]; - - Object.entries(artifactsInComponent).forEach(([artifactKey, data]) => { - const label = artifactKey; - const { artifactId } = - artifactNodeData?.find((a) => artifactKey === a.outputArtifactKey) ?? {}; - - // if no node needs it as an input, we don't really need a well known id - const id = artifactId ?? artifactKey; - - nodes.push( - createArtifactNode({ - id, - label, - artifactType: data.schemaTitle, - runAfter: [taskId], - status: translateStatusForNode(status?.state), - }), - ); - - taskMap[id] = { - type: 'artifact', - name: label, - inputs: { - artifacts: [{ label: id, type: composeArtifactType(data) }], - }, - }; - }); - } - - // This task - taskMap[taskId] = { - type: isGroupNode ? 'groupTask' : 'task', - name: taskName, - steps: executor ? [executor.container] : undefined, - inputs: parseInputOutput(component?.inputDefinitions), - outputs: parseInputOutput(component?.outputDefinitions), - status, - volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), - }; - if (taskValue.dependentTasks) { - // This task's runAfters may need artifact relationships -- find those artifactIds - runAfter.push( - ...taskValue.dependentTasks - .map((dependantTaskId) => { - const art = taskArtifactMap[dependantTaskId]; - return art ? art.map((v) => v.artifactId) : null; - }) - .filter((v): v is string[] => !!v) - .flat(), + const taskArtifactMap = parseTasksForArtifactRelationship(tasks); + + return Object.entries(tasks).reduce((acc, [taskId, taskValue]) => { + const taskName = taskValue.taskInfo.name; + + const componentRef = taskValue.componentRef.name; + const component = components[componentRef]; + const artifactsInComponent = componentArtifactMap[componentRef]; + const isGroupNode = !!component?.dag; + const groupTasks = component?.dag?.tasks; + + const executorLabel = component?.executorLabel; + const executor = executorLabel ? executors[executorLabel] : undefined; + + const status = parseRuntimeInfo(taskId, runDetails); + + const newTaskMapEntries: KubeFlowTaskTopology['taskMap'] = {}; + const nodes: PipelineNodeModelExpanded[] = []; + const runAfter: string[] = taskValue.dependentTasks ?? []; + + if (artifactsInComponent) { + const artifactNodeData = taskArtifactMap[taskId]; + + Object.entries(artifactsInComponent).forEach(([artifactKey, data]) => { + const label = artifactKey; + const { artifactId } = + artifactNodeData?.find((a) => artifactKey === a.outputArtifactKey) ?? {}; + + // if no node needs it as an input, we don't really need a well known id + const id = artifactId ?? artifactKey; + + nodes.push( + createArtifactNode({ + id, + label, + artifactType: data.schemaTitle, + runAfter: [taskId], + status: translateStatusForNode(status?.state), + }), ); - } else if (parentTask) { - // Create an edge from the grouped task to its parent task - // Prevent the node floating on the topology - // This logic could be removed once we have the stacked node to better deal with groups - runAfter.push(parentTask); - } + newTaskMapEntries[id] = { + type: 'artifact', + name: label, + inputs: { + artifacts: [{ label: id, type: composeArtifactType(data) }], + }, + }; + }); + } + + // This task + newTaskMapEntries[taskId] = { + type: isGroupNode ? 'groupTask' : 'task', + name: taskName, + steps: executor ? [executor.container] : undefined, + inputs: parseInputOutput(component?.inputDefinitions), + outputs: parseInputOutput(component?.outputDefinitions), + status, + volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), + }; + if (taskValue.dependentTasks) { + // This task's runAfters may need artifact relationships -- find those artifactIds + runAfter.push( + ...taskValue.dependentTasks + .map((dependantTaskId) => { + const art = taskArtifactMap[dependantTaskId]; + return art ? art.map((v) => v.artifactId) : null; + }) + .filter((v): v is string[] => !!v) + .flat(), + ); + } + + // This task's rendering information + if (isGroupNode && groupTasks) { + const [nestedNodes, children] = getNestedNodes(groupTasks, components, runDetails); + const itemNode = createGroupNode( + { + id: taskId, + label: taskName, + runAfter, + status: translateStatusForNode(status?.state), + }, + children, + ); + nodes.push(itemNode, ...nestedNodes); + + // Extract IDs and create new entries + nestedNodes.forEach((node) => { + const { id } = node; + newTaskMapEntries[id] = { + type: 'groupTask', + name: id, + steps: executor ? [executor.container] : undefined, + inputs: parseInputOutput(component.inputDefinitions), + outputs: parseInputOutput(component.outputDefinitions), + status, + volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), + }; + }); + } else { nodes.push( - createGroupNode({ + createNode({ id: taskId, label: taskName, runAfter, - tasks: tasks.map((i) => i.id), status: translateStatusForNode(status?.state), }), ); - // This task's rendering information - if (isGroupNode) { - // TODO: better handle group nodes - createNodes(component.dag.tasks, taskId); - } - }); - }; - createNodes(rootTasks); - return { nodes, taskMap }; + } + + return { + taskMap: { + ...acc.taskMap, + ...newTaskMapEntries, + }, + nodes: [...acc.nodes, ...nodes], + }; + }, EMPTY_STATE); }; diff --git a/frontend/src/concepts/topology/BasePipelineNode.ts b/frontend/src/concepts/topology/BasePipelineNode.ts new file mode 100644 index 0000000000..8a8160cbec --- /dev/null +++ b/frontend/src/concepts/topology/BasePipelineNode.ts @@ -0,0 +1,28 @@ +import { BaseNode, PipelineNodeModel } from '@patternfly/react-topology'; + +class BasePipelineNode extends BaseNode { + private runAfterTasks?: string[]; + + constructor() { + super(); + } + + setModel(model: PipelineNodeModel): void { + super.setModel(model); + + if ('runAfterTasks' in model) { + this.runAfterTasks = model.runAfterTasks; + } + }; + + toModel(): PipelineNodeModel { + return { + ...super.toModel(), + runAfterTasks: this.runAfterTasks, + children: super.getAllChildren().map((c) => c.getId()) + }; + } + +}; + +export default BasePipelineNode; diff --git a/frontend/src/concepts/topology/PipelineTaskGroup.tsx b/frontend/src/concepts/topology/PipelineTaskGroup.tsx index 9e4573e90e..94c2b8c5d5 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroup.tsx @@ -1,69 +1,46 @@ import * as React from 'react'; -import { observer } from 'mobx-react'; import { - AnchorEnd, - DefaultTaskGroup, + DEFAULT_EDGE_TYPE, + DefaultTaskGroup, EdgeCreationTypes, GraphElement, - isNode, LabelPosition, - Node, - useAnchor, - WithContextMenuProps, WithSelectionProps, - ShapeProps, - WithDragNodeProps, - EdgeCreationTypes, } from '@patternfly/react-topology'; -import TaskGroupSourceAnchor from './TaskGroupSourceAnchor'; -import TaskGroupTargetAnchor from './TaskGroupTargetAnchor'; +import { NODE_HEIGHT, NODE_WIDTH } from '~/concepts/topology/const'; type PipelineTaskGroupProps = { element: GraphElement; - collapsible?: boolean; - collapsedWidth?: number; - collapsedHeight?: number; - onCollapseChange?: (group: Node, collapsed: boolean) => void; - getCollapsedShape?: (node: Node) => React.FunctionComponent; - collapsedShadowOffset?: number; // defaults to 10 -} & WithContextMenuProps & - WithDragNodeProps & - WithSelectionProps; - -export const DEFAULT_TASK_WIDTH = 180; -export const DEFAULT_TASK_HEIGHT = 32; +} & WithSelectionProps; const getEdgeCreationTypes = (): EdgeCreationTypes => ({ - edgeType: 'edge', - spacerEdgeType: 'edge', + edgeType: DEFAULT_EDGE_TYPE, + spacerEdgeType: DEFAULT_EDGE_TYPE, }); +const onCollapseChange = (element: GraphElement): void => { + const controller = element.getController(); + const model = controller.toModel(); + console.log(model.nodes); + console.log(model.edges); +}; + const PipelineTaskGroup: React.FunctionComponent = ({ element, - ...rest -}) => { - useAnchor( - React.useCallback((node: Node) => new TaskGroupSourceAnchor(node), []), - AnchorEnd.source, - ); - useAnchor( - React.useCallback((node: Node) => new TaskGroupTargetAnchor(node), []), - AnchorEnd.target, - ); - if (!isNode(element)) { - return null; - } - return ( - - ); -}; + selected, + onSelect, +}) => ( + onCollapseChange(element)} + /> +); -export default observer(PipelineTaskGroup); +export default PipelineTaskGroup; diff --git a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx index 29bc56cff3..0d0a8bc305 100644 --- a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx +++ b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx @@ -31,15 +31,25 @@ const PipelineVisualizationSurface: React.FC const controller = useVisualizationController(); const [error, setError] = React.useState(); React.useEffect(() => { - const spacerNodes = getSpacerNodes(nodes); + const currentModel = controller.toModel(); + const updateNodes = nodes.map((node) => { + const currentNode = currentModel.nodes?.find((n) => n.id === node.id); + if (currentNode) { + return { ...node, collapsed: currentNode.collapsed }; + } + return node; + }); + + const spacerNodes = getSpacerNodes(updateNodes); // Dagre likes the root nodes to be first in the order - const renderNodes = [...spacerNodes, ...nodes].sort( + const renderNodes = [...spacerNodes, ...updateNodes].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 { controller.fromModel( { diff --git a/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx b/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx deleted file mode 100644 index 6a7585c368..0000000000 --- a/frontend/src/concepts/topology/TaskGroupSourceAnchor.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; - -export default class TaskGroupSourceAnchor extends AbstractAnchor { - constructor(owner: E) { - super(owner); - } - - getLocation(): Point { - return this.getReferencePoint(); - } - - getReferencePoint(): Point { - const bounds = this.owner.getBounds(); - return new Point(bounds.right(), bounds.y + bounds.height / 2); - } -} diff --git a/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx b/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx deleted file mode 100644 index c349cf72c7..0000000000 --- a/frontend/src/concepts/topology/TaskGroupTargetAnchor.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; - -export default class TaskGroupTargetAnchor extends AbstractAnchor { - private vertical = false; - - constructor(owner: E, vertical = false) { - super(owner); - this.vertical = vertical; - } - - getLocation(): Point { - return this.getReferencePoint(); - } - - getReferencePoint(): Point { - const bounds = this.owner.getBounds(); - - if (this.vertical) { - return new Point(bounds.x + bounds.width / 2, bounds.y); - } - return new Point(bounds.x, bounds.y + bounds.height / 2); - } -} diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index 952b216c48..a9011b6ae7 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -55,5 +55,3 @@ export const pipelineGroupsComponentFactory = (kind: ModelKind, type: string) => return undefined; } }; - -export default pipelineGroupsComponentFactory; diff --git a/frontend/src/concepts/topology/pipelineElementFactory.ts b/frontend/src/concepts/topology/pipelineElementFactory.ts new file mode 100644 index 0000000000..ce020e7e8a --- /dev/null +++ b/frontend/src/concepts/topology/pipelineElementFactory.ts @@ -0,0 +1,13 @@ +import { ElementFactory, GraphElement, ModelKind } from '@patternfly/react-topology'; +import BasePipelineNode from './BasePipelineNode'; + +const pipelineElementFactory: ElementFactory = (kind: ModelKind): GraphElement | undefined => { + switch (kind) { + case ModelKind.node: + return new BasePipelineNode(); + default: + return undefined; + } +}; + +export default pipelineElementFactory; diff --git a/frontend/src/concepts/topology/useTopologyController.ts b/frontend/src/concepts/topology/useTopologyController.ts index 3dde01bdf8..2b531c2f0e 100644 --- a/frontend/src/concepts/topology/useTopologyController.ts +++ b/frontend/src/concepts/topology/useTopologyController.ts @@ -7,9 +7,8 @@ import { PipelineDagreGroupsLayout, Visualization, } from '@patternfly/react-topology'; -import pipelineGroupsComponentFactory, { - pipelineComponentFactory, -} from '~/concepts/topology/factories'; +import { pipelineComponentFactory, pipelineGroupsComponentFactory } from './factories'; +import pipelineElementFactory from './pipelineElementFactory'; import { PIPELINE_LAYOUT, PIPELINE_NODE_SEPARATION_VERTICAL } from './const'; const useTopologyController = (graphId: string): Visualization | null => { @@ -18,6 +17,7 @@ const useTopologyController = (graphId: string): Visualization | null => { React.useEffect(() => { const visualizationController = new Visualization(); visualizationController.setFitToScreenOnLayout(true); + visualizationController.registerElementFactory(pipelineElementFactory); visualizationController.registerComponentFactory(pipelineComponentFactory); visualizationController.registerComponentFactory(pipelineGroupsComponentFactory); visualizationController.registerLayoutFactory( diff --git a/frontend/src/concepts/topology/utils.ts b/frontend/src/concepts/topology/utils.ts index cfacd790e2..236d180d0e 100644 --- a/frontend/src/concepts/topology/utils.ts +++ b/frontend/src/concepts/topology/utils.ts @@ -37,18 +37,22 @@ export const createArtifactNode = (details: NodeConstructDetails): PipelineNodeM data: { status: details.status ?? undefined, artifactType: details.artifactType }, }); -export const createGroupNode = (details: NodeConstructDetails): PipelineNodeModelExpanded => ({ +export const createGroupNode = ( + details: NodeConstructDetails, + children: string[], +): PipelineNodeModelExpanded => ({ id: details.id, label: details.id, type: 'Execution', group: true, + collapsed: true, width: NODE_WIDTH, height: NODE_HEIGHT, runAfterTasks: details.runAfter, style: { - padding: [NODE_PADDING_VERTICAL, NODE_PADDING_HORIZONTAL], + padding: [NODE_PADDING_VERTICAL + 24, NODE_PADDING_HORIZONTAL], }, - children: details.tasks ?? undefined, + children, data: details.status ? { status: details.status, From b0e91a0e48a79e648d6495591dcf77407acc1c3e Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:06:46 -0400 Subject: [PATCH 04/18] wip support status on task group --- frontend/src/concepts/topology/PipelineTaskGroup.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/concepts/topology/PipelineTaskGroup.tsx b/frontend/src/concepts/topology/PipelineTaskGroup.tsx index 94c2b8c5d5..c1b4c491a7 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroup.tsx @@ -40,6 +40,8 @@ const PipelineTaskGroup: React.FunctionComponent = ({ recreateLayoutOnCollapseChange getEdgeCreationTypes={getEdgeCreationTypes} onCollapseChange={() => onCollapseChange(element)} + // TODO: support in PF Topology + // status={status} /> ); From 13a35609cca7d4495d98f9751bc071ba37f9b376 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Thu, 11 Apr 2024 08:58:23 -0400 Subject: [PATCH 05/18] wip custom collapsible group --- .../topology/PipelineDefaultTaskGroup.tsx | 213 ++++++++++++++++++ .../topology/PipelineTaskGroupCollapsed.tsx | 69 ++++++ .../topology/customNodes/ArtifactTaskNode.tsx | 2 + 3 files changed, 284 insertions(+) create mode 100644 frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx create mode 100644 frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx new file mode 100644 index 0000000000..14502ac10d --- /dev/null +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -0,0 +1,213 @@ +import * as React from 'react'; + +import { + BadgeLocation, + ConnectDragSource, + ConnectDropTarget, + Dimensions, + GraphElement, + LabelPosition, + OnSelect, + ShapeProps, + WithDndDragProps, + WithSelectionProps, + getEdgesFromNodes, + getSpacerNodes, + isNode, + DefaultTaskGroup, + observer, + Node, +} from '@patternfly/react-topology'; +import PipelineTaskGroupCollapsed from "./PipelineTaskGroupCollapsed"; + +export interface EdgeCreationTypes { + spacerNodeType?: string; + edgeType?: string; + spacerEdgeType?: string; + finallyNodeTypes?: string[]; + finallyEdgeType?: string; +} + +interface PipelinesDefaultGroupProps { + /** Additional content added to the node */ + children?: React.ReactNode; + /** Additional classes added to the group */ + className?: string; + /** The graph group node element to represent */ + element: GraphElement; + /** Flag if the node accepts drop operations */ + droppable?: boolean; + /** Flag if the current drag operation can be dropped on the node */ + canDrop?: boolean; + /** Flag if the node is the current drop target */ + dropTarget?: boolean; + /** Flag if the user is dragging the node */ + dragging?: boolean; + /** Flag if drag operation is a regroup operation */ + dragRegroupable?: boolean; + /** Flag if the user is hovering on the node */ + hover?: boolean; + /** Label for the node. Defaults to element.getLabel() */ + label?: string; // Defaults to element.getLabel() + /** Secondary label for the node */ + secondaryLabel?: string; + /** Flag to show the label */ + showLabel?: boolean; // Defaults to true + /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ + labelPosition?: LabelPosition; + /** The maximum length of the label before truncation */ + truncateLength?: number; + /** The Icon class to show in the label, ignored when labelIcon is specified */ + labelIconClass?: string; + /** The label icon component to show in the label, takes precedence over labelIconClass */ + labelIcon?: string; + /** Padding for the label's icon */ + labelIconPadding?: number; + /** Text for the label's badge */ + badge?: string; + /** Color to use for the label's badge background */ + badgeColor?: string; + /** Color to use for the label's badge text */ + badgeTextColor?: string; + /** Color to use for the label's badge border */ + badgeBorderColor?: string; + /** Additional classes to use for the label's badge */ + badgeClassName?: string; + /** Location of the badge relative to the label's text, inner or below */ + badgeLocation?: BadgeLocation; + /** Flag if the group is collapsible */ + collapsible?: boolean; + /** Width of the collapsed group */ + collapsedWidth?: number; + /** Height of the collapsed group */ + collapsedHeight?: number; + /** Callback when the group is collapsed */ + onCollapseChange?: (group: Node, collapsed: boolean) => void; + /** Shape of the collapsed group */ + getCollapsedShape?: (node: Node) => React.FunctionComponent; + /** Shadow offset for the collapsed group */ + collapsedShadowOffset?: number; + /** Flag if the element selected. Part of WithSelectionProps */ + selected?: boolean; + /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ + onSelect?: OnSelect; + /** A ref to add to the node for dragging. Part of WithDragNodeProps */ + dragNodeRef?: WithDndDragProps['dndDragRef']; + /** A ref to add to the node for drag and drop. Part of WithDndDragProps */ + dndDragRef?: ConnectDragSource; + /** A ref to add to the node for dropping. Part of WithDndDropProps */ + dndDropRef?: ConnectDropTarget; + /** Function to call to show a context menu for the node */ + onContextMenu?: (e: React.MouseEvent) => void; + /** Flag indicating that the context menu for the node is currently open */ + contextMenuOpen?: boolean; + /** Flag to recreate the layout when the group is expanded/collapsed. Be sure you are registering "pipelineElementFactory" when set to true. */ + recreateLayoutOnCollapseChange?: boolean; + /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ + getEdgeCreationTypes?: () => { + spacerNodeType?: string; + edgeType?: string; + spacerEdgeType?: string; + finallyNodeTypes?: string[]; + finallyEdgeType?: string; + }; +} + +type PipelinesDefaultGroupInnerProps = Omit & { + element: Node; +} & WithSelectionProps; + +const DefaultTaskGroupInner: React.FunctionComponent = observer( + ({ + className, + element, + onCollapseChange, + recreateLayoutOnCollapseChange, + getEdgeCreationTypes, + ...rest + }) => { + const childCount = element.getAllNodeChildren().length; + + const handleCollapse = (group: Node, collapsed: boolean): void => { + if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { + group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); + } + group.setCollapsed(collapsed); + + if (recreateLayoutOnCollapseChange) { + const controller = group.hasController() && group.getController(); + if (controller) { + const model = controller.toModel(); + const creationTypes: EdgeCreationTypes = getEdgeCreationTypes + ? getEdgeCreationTypes() + : {}; + + const pipelineNodes = model.nodes! + .filter((n) => n.type !== creationTypes.spacerNodeType) + .map((n) => ({ + ...n, + visible: true, + })); + const spacerNodes = getSpacerNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.finallyNodeTypes, + ); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.edgeType, + creationTypes.edgeType, + creationTypes.finallyNodeTypes, + creationTypes.finallyEdgeType, + ); + controller.fromModel({ nodes, edges }, true); + controller.getGraph().layout(); + } + } + + onCollapseChange && onCollapseChange(group, collapsed); + }; + + if (element.isCollapsed()) { + return ( + + ); + } + return ( + + ); + }, +); + +const PipelineDefaultTaskGroup: React.FunctionComponent = ({ + element, + ...rest +}: PipelinesDefaultGroupProps) => { + if (!isNode(element)) { + throw new Error('DefaultTaskGroup must be used only on Node elements'); + } + + return ; +}; + +export default PipelineDefaultTaskGroup; diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx new file mode 100644 index 0000000000..b6bfb2ef75 --- /dev/null +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; +import { + RunStatus, + LabelPosition, + BadgeLocation, + CollapsibleGroupProps, + TaskNode, + WithContextMenuProps, + WithDndDropProps, + WithDragNodeProps, + WithSelectionProps, + Node, +} from '@patternfly/react-topology'; + +type PipelineTaskGroupCollapsedProps = { + children?: React.ReactNode; + className?: string; + element: Node; + droppable?: boolean; + canDrop?: boolean; + dropTarget?: boolean; + dragging?: boolean; + hover?: boolean; + label?: string; // Defaults to element.getLabel() + secondaryLabel?: string; + showLabel?: boolean; // Defaults to true + status?: RunStatus; + statusIconSize?: number; + showStatusState?: boolean; + scaleNode?: boolean; + hideDetailsAtMedium?: boolean; + hiddenDetailsShownStatuses?: RunStatus[]; + labelPosition?: LabelPosition; // Defaults to bottom + truncateLength?: number; // Defaults to 13 + labelIconClass?: string; // Icon to show in label + labelIcon?: string; + labelIconPadding?: number; + badge?: string; + badgeColor?: string; + badgeTextColor?: string; + badgeBorderColor?: string; + badgeClassName?: string; + badgeLocation?: BadgeLocation; +} & CollapsibleGroupProps & + WithDragNodeProps & + WithSelectionProps & + WithDndDropProps & + WithContextMenuProps; + +const PipelineTaskGroupCollapsed: React.FunctionComponent = ({ + element, + collapsible, + onCollapseChange, + ...rest +}) => { + return ( + : undefined} + // onActionIconClick={() => onCollapseChange(element, false)} + shadowCount={2} + {...rest} + /> + ); +}; + +export default observer(PipelineTaskGroupCollapsed); diff --git a/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx index 68ee73c6a6..1a2f4113e1 100644 --- a/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx +++ b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx @@ -15,6 +15,7 @@ import { TaskNodeSourceAnchor, TaskNodeTargetAnchor, GraphElement, + RunStatus, } from '@patternfly/react-topology'; import { ListIcon, MonitoringIcon } from '@patternfly/react-icons'; import { TaskNodeProps } from '@patternfly/react-topology/dist/esm/pipelines/components/nodes/TaskNode'; @@ -118,6 +119,7 @@ const ArtifactTaskNodeInner: React.FC = observer( hover selected={selected} onSelect={onSelect} + hiddenDetailsShownStatuses={[]} status={data?.status} scaleNode={isHover} {...rest} From 1558f6077d16f5830b15a1f2d4e97d7698ea3ab8 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:46:17 -0400 Subject: [PATCH 06/18] add custom pipeline group collapsed --- .../topology/PipelineDefaultTaskGroup.tsx | 73 +++++-------------- .../topology/PipelineTaskGroupCollapsed.tsx | 30 ++++++-- frontend/src/concepts/topology/const.ts | 2 +- frontend/src/concepts/topology/factories.ts | 4 +- 4 files changed, 44 insertions(+), 65 deletions(-) diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index 14502ac10d..b257308a09 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -18,7 +18,8 @@ import { observer, Node, } from '@patternfly/react-topology'; -import PipelineTaskGroupCollapsed from "./PipelineTaskGroupCollapsed"; +import PipelineTaskGroupCollapsed from './PipelineTaskGroupCollapsed'; +import { NODE_HEIGHT, NODE_WIDTH } from './const'; export interface EdgeCreationTypes { spacerNodeType?: string; @@ -35,46 +36,8 @@ interface PipelinesDefaultGroupProps { className?: string; /** The graph group node element to represent */ element: GraphElement; - /** Flag if the node accepts drop operations */ - droppable?: boolean; - /** Flag if the current drag operation can be dropped on the node */ - canDrop?: boolean; - /** Flag if the node is the current drop target */ - dropTarget?: boolean; - /** Flag if the user is dragging the node */ - dragging?: boolean; - /** Flag if drag operation is a regroup operation */ - dragRegroupable?: boolean; /** Flag if the user is hovering on the node */ hover?: boolean; - /** Label for the node. Defaults to element.getLabel() */ - label?: string; // Defaults to element.getLabel() - /** Secondary label for the node */ - secondaryLabel?: string; - /** Flag to show the label */ - showLabel?: boolean; // Defaults to true - /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ - labelPosition?: LabelPosition; - /** The maximum length of the label before truncation */ - truncateLength?: number; - /** The Icon class to show in the label, ignored when labelIcon is specified */ - labelIconClass?: string; - /** The label icon component to show in the label, takes precedence over labelIconClass */ - labelIcon?: string; - /** Padding for the label's icon */ - labelIconPadding?: number; - /** Text for the label's badge */ - badge?: string; - /** Color to use for the label's badge background */ - badgeColor?: string; - /** Color to use for the label's badge text */ - badgeTextColor?: string; - /** Color to use for the label's badge border */ - badgeBorderColor?: string; - /** Additional classes to use for the label's badge */ - badgeClassName?: string; - /** Location of the badge relative to the label's text, inner or below */ - badgeLocation?: BadgeLocation; /** Flag if the group is collapsible */ collapsible?: boolean; /** Width of the collapsed group */ @@ -85,22 +48,10 @@ interface PipelinesDefaultGroupProps { onCollapseChange?: (group: Node, collapsed: boolean) => void; /** Shape of the collapsed group */ getCollapsedShape?: (node: Node) => React.FunctionComponent; - /** Shadow offset for the collapsed group */ - collapsedShadowOffset?: number; /** Flag if the element selected. Part of WithSelectionProps */ selected?: boolean; /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ onSelect?: OnSelect; - /** A ref to add to the node for dragging. Part of WithDragNodeProps */ - dragNodeRef?: WithDndDragProps['dndDragRef']; - /** A ref to add to the node for drag and drop. Part of WithDndDragProps */ - dndDragRef?: ConnectDragSource; - /** A ref to add to the node for dropping. Part of WithDndDropProps */ - dndDropRef?: ConnectDropTarget; - /** Function to call to show a context menu for the node */ - onContextMenu?: (e: React.MouseEvent) => void; - /** Flag indicating that the context menu for the node is currently open */ - contextMenuOpen?: boolean; /** Flag to recreate the layout when the group is expanded/collapsed. Be sure you are registering "pipelineElementFactory" when set to true. */ recreateLayoutOnCollapseChange?: boolean; /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ @@ -124,9 +75,12 @@ const DefaultTaskGroupInner: React.FunctionComponent { const childCount = element.getAllNodeChildren().length; + const data = element.getData(); const handleCollapse = (group: Node, collapsed: boolean): void => { if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { @@ -134,7 +88,6 @@ const DefaultTaskGroupInner: React.FunctionComponent n.type !== creationTypes.spacerNodeType) + const pipelineNodes = model + .nodes!.filter((n) => n.type !== creationTypes.spacerNodeType) .map((n) => ({ ...n, visible: true, @@ -165,7 +118,6 @@ const DefaultTaskGroupInner: React.FunctionComponent ); @@ -189,7 +149,10 @@ const DefaultTaskGroupInner: React.FunctionComponent { + const [hover, hoverRef] = useHover(); + const detailsLevel = element.getGraph().getDetailsLevel(); + return ( - : undefined} - // onActionIconClick={() => onCollapseChange(element, false)} - shadowCount={2} - {...rest} - /> + + }> + : undefined} + onActionIconClick={() => onCollapseChange!(element, false)} + shadowCount={2} + hiddenDetailsShownStatuses={[RunStatus.Succeeded]} + scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high} + hideDetailsAtMedium + showStatusState + {...rest} + /> + + ); }; diff --git a/frontend/src/concepts/topology/const.ts b/frontend/src/concepts/topology/const.ts index 72073d2cdf..a4c3324928 100644 --- a/frontend/src/concepts/topology/const.ts +++ b/frontend/src/concepts/topology/const.ts @@ -1,5 +1,5 @@ export const PIPELINE_LAYOUT = 'PipelineLayout'; export const PIPELINE_NODE_SEPARATION_VERTICAL = 100; -export const NODE_WIDTH = 100; +export const NODE_WIDTH = 130; export const NODE_HEIGHT = 35; diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index a9011b6ae7..b7896dc529 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -13,8 +13,8 @@ import { import StandardTaskNode from '~/concepts/topology/customNodes/StandardTaskNode'; import { ICON_TASK_NODE_TYPE } from './utils'; import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; -import PipelineTaskGroup from './PipelineTaskGroup'; import PipelineTaskEdge from './PipelineTaskEdge'; +import PipelineDefaultTaskGroup from "./PipelineDefaultTaskGroup"; export const pipelineComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { @@ -40,7 +40,7 @@ export const pipelineGroupsComponentFactory = (kind: ModelKind, type: string) => } switch (type) { case 'Execution': - return withSelection()(PipelineTaskGroup); + return withSelection()(PipelineDefaultTaskGroup); case 'Task': return withSelection()(StandardTaskNode); case ICON_TASK_NODE_TYPE: From 4a9171cef1e8011fb70770a659170c3f1a321554 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:49:08 -0400 Subject: [PATCH 07/18] add popover, wip drawer --- frontend/package.json | 2 +- .../topology/usePipelineTaskTopology.ts | 3 ++ .../topology/PipelineTaskGroupCollapsed.tsx | 52 +++++++++++++++---- .../concepts/topology/{utils.ts => utils.tsx} | 48 +++++++++++++++++ 4 files changed, 93 insertions(+), 12 deletions(-) rename frontend/src/concepts/topology/{utils.ts => utils.tsx} (61%) diff --git a/frontend/package.json b/frontend/package.json index ac15bdb206..9480cf575f 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.3.0-prerelease.5", + "@patternfly/react-topology": "^5.3.0-prerelease.10", "@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 d96eadba42..ffb62740fb 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -159,6 +159,9 @@ export const usePipelineTaskTopology = ( ); } + console.log('newTaskMapEntries', newTaskMapEntries); + console.log('nodes usepipelinetasktpoplogy', nodes) + // This task's rendering information if (isGroupNode && groupTasks) { const [nestedNodes, children] = getNestedNodes(groupTasks, components, runDetails); diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx index 595486be19..207c69622b 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -18,6 +18,8 @@ import { DEFAULT_LAYER, TOP_LAYER, } from '@patternfly/react-topology'; +import { Icon, Popover } from '@patternfly/react-core';; +import { getNodeStatusIcon } from './utils'; type PipelineTaskGroupCollapsedProps = { children?: React.ReactNode; @@ -61,22 +63,50 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent { const [hover, hoverRef] = useHover(); + const myRef = React.useRef(); const detailsLevel = element.getGraph().getDetailsLevel(); + const PopoverTasksList = ({ items }) => { + const getPopoverTasksList = () => { + return items.map((item: Node) => ( +
+ + {getNodeStatusIcon(item.getData()?.status).icon} + + {item.getId()} +
+ )); + }; + + return getPopoverTasksList(); + }; + + const popoverBodyContent = () => ; + return ( }> - : undefined} - onActionIconClick={() => onCollapseChange!(element, false)} - shadowCount={2} - hiddenDetailsShownStatuses={[RunStatus.Succeeded]} - scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high} - hideDetailsAtMedium - showStatusState - {...rest} - /> + + }> + : undefined} + onActionIconClick={() => onCollapseChange!(element, false)} + shadowCount={2} + hiddenDetailsShownStatuses={[RunStatus.Succeeded]} + scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high} + hideDetailsAtMedium + showStatusState + {...rest} + /> + + ); diff --git a/frontend/src/concepts/topology/utils.ts b/frontend/src/concepts/topology/utils.tsx similarity index 61% rename from frontend/src/concepts/topology/utils.ts rename to frontend/src/concepts/topology/utils.tsx index 236d180d0e..1a6a8cc45d 100644 --- a/frontend/src/concepts/topology/utils.ts +++ b/frontend/src/concepts/topology/utils.tsx @@ -2,6 +2,15 @@ import { DEFAULT_TASK_NODE_TYPE } from '@patternfly/react-topology'; import { genRandomChars } from '~/utilities/string'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; +import { + NotStartedIcon, + SyncAltIcon, + CheckCircleIcon, + ExclamationCircleIcon, + BanIcon, +} from '@patternfly/react-icons'; +import { RuntimeStateKF, runtimeStateLabels } from '../pipelines/kfTypes'; +import React from 'react'; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; @@ -60,3 +69,42 @@ export const createGroupNode = ( } : undefined, }); + +export const getNodeStatusIcon = (status) => { + let icon: React.ReactNode; + let details: string | undefined; + + switch (status) { + case RuntimeStateKF.PENDING: + case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: + case undefined: + icon = ; + break; + case RuntimeStateKF.RUNNING: + icon = ; + break; + case RuntimeStateKF.SKIPPED: + icon = ; + break; + case 'Succeeded': + icon = ; + status = 'success'; + break; + case RuntimeStateKF.FAILED: + icon = ; + status = 'danger'; + break; + case RuntimeStateKF.CANCELING: + icon = ; + break; + case RuntimeStateKF.CANCELED: + icon = ; + break; + case RuntimeStateKF.PAUSED: + icon = ; + default: + icon = null; + } + + return { icon, status, details }; +}; From 179ad7dcdfce667880cc734603f22271186f2cdf Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:16:58 -0400 Subject: [PATCH 08/18] update taskmap with groups --- frontend/package-lock.json | 8 ++++---- .../pipelines/topology/usePipelineTaskTopology.ts | 6 ++---- .../topology/PipelineTaskGroupCollapsed.tsx | 13 ++++--------- frontend/src/concepts/topology/utils.tsx | 7 +++---- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 700d7be232..369786593a 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.3.0-prerelease.5", + "@patternfly/react-topology": "^5.3.0-prerelease.10", "@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.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==", + "version": "5.3.0-prerelease.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.3.0-prerelease.10.tgz", + "integrity": "sha512-Ekse0rn5ewx+zxziLHlFq3bOWOK6wDAcfwxdtgWmy09vQdww5+POeesmspnWzBHp3V9X2+EiAkQ30gxAKmL6mQ==", "dependencies": { "@patternfly/react-core": "^5.1.1", "@patternfly/react-icons": "^5.1.1", diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index ffb62740fb..3c23302042 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -159,8 +159,6 @@ export const usePipelineTaskTopology = ( ); } - console.log('newTaskMapEntries', newTaskMapEntries); - console.log('nodes usepipelinetasktpoplogy', nodes) // This task's rendering information if (isGroupNode && groupTasks) { @@ -183,8 +181,8 @@ export const usePipelineTaskTopology = ( type: 'groupTask', name: id, steps: executor ? [executor.container] : undefined, - inputs: parseInputOutput(component.inputDefinitions), - outputs: parseInputOutput(component.outputDefinitions), + inputs: parseInputOutput(component?.inputDefinitions), + outputs: parseInputOutput(component?.outputDefinitions), status, volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), }; diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx index 207c69622b..2529b57aa4 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -17,8 +17,9 @@ import { Layer, DEFAULT_LAYER, TOP_LAYER, + NodeModel, } from '@patternfly/react-topology'; -import { Icon, Popover } from '@patternfly/react-core';; +import { Icon, Popover } from '@patternfly/react-core'; import { getNodeStatusIcon } from './utils'; type PipelineTaskGroupCollapsedProps = { @@ -66,8 +67,7 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent { - const getPopoverTasksList = () => { + const getPopoverTasksList = (items: Node[]) => { return items.map((item: Node) => (
@@ -78,11 +78,6 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent ; - return ( }> @@ -91,7 +86,7 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent }> `${prefix}-${genRandomChars()}`; @@ -70,9 +70,8 @@ export const createGroupNode = ( : undefined, }); -export const getNodeStatusIcon = (status) => { +export const getNodeStatusIcon = (status: RuntimeStateKF | string): any => { let icon: React.ReactNode; - let details: string | undefined; switch (status) { case RuntimeStateKF.PENDING: @@ -106,5 +105,5 @@ export const getNodeStatusIcon = (status) => { icon = null; } - return { icon, status, details }; + return { icon, status }; }; From 8348e8e412c8bc8334cec37aed76eceabe8377ea Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:02:14 -0400 Subject: [PATCH 09/18] clean up unused props, attributes --- .../topology/PipelineDefaultTaskGroup.tsx | 86 +++++++------------ .../concepts/topology/PipelineTaskGroup.tsx | 48 ----------- .../topology/PipelineTaskGroupCollapsed.tsx | 45 +++------- 3 files changed, 41 insertions(+), 138 deletions(-) delete mode 100644 frontend/src/concepts/topology/PipelineTaskGroup.tsx diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index b257308a09..f0c3cde183 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -1,15 +1,9 @@ import * as React from 'react'; import { - BadgeLocation, - ConnectDragSource, - ConnectDropTarget, Dimensions, - GraphElement, LabelPosition, OnSelect, - ShapeProps, - WithDndDragProps, WithSelectionProps, getEdgesFromNodes, getSpacerNodes, @@ -17,6 +11,7 @@ import { DefaultTaskGroup, observer, Node, + GraphElement, } from '@patternfly/react-topology'; import PipelineTaskGroupCollapsed from './PipelineTaskGroupCollapsed'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; @@ -30,31 +25,17 @@ export interface EdgeCreationTypes { } interface PipelinesDefaultGroupProps { - /** Additional content added to the node */ children?: React.ReactNode; - /** Additional classes added to the group */ className?: string; - /** The graph group node element to represent */ element: GraphElement; - /** Flag if the user is hovering on the node */ hover?: boolean; - /** Flag if the group is collapsible */ collapsible?: boolean; - /** Width of the collapsed group */ collapsedWidth?: number; - /** Height of the collapsed group */ collapsedHeight?: number; - /** Callback when the group is collapsed */ onCollapseChange?: (group: Node, collapsed: boolean) => void; - /** Shape of the collapsed group */ - getCollapsedShape?: (node: Node) => React.FunctionComponent; - /** Flag if the element selected. Part of WithSelectionProps */ selected?: boolean; - /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ onSelect?: OnSelect; - /** Flag to recreate the layout when the group is expanded/collapsed. Be sure you are registering "pipelineElementFactory" when set to true. */ recreateLayoutOnCollapseChange?: boolean; - /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ getEdgeCreationTypes?: () => { spacerNodeType?: string; edgeType?: string; @@ -88,36 +69,34 @@ const DefaultTaskGroupInner: React.FunctionComponent n.type !== creationTypes.spacerNodeType) - .map((n) => ({ - ...n, - visible: true, - })); - const spacerNodes = getSpacerNodes( - pipelineNodes, - creationTypes.spacerNodeType, - creationTypes.finallyNodeTypes, - ); - const nodes = [...pipelineNodes, ...spacerNodes]; - const edges = getEdgesFromNodes( - pipelineNodes, - creationTypes.spacerNodeType, - creationTypes.edgeType, - creationTypes.edgeType, - creationTypes.finallyNodeTypes, - creationTypes.finallyEdgeType, - ); - controller.fromModel({ nodes, edges }, true); - controller.getGraph().layout(); - } + const pipelineNodes = model + .nodes!.filter((n) => n.type !== creationTypes.spacerNodeType) + .map((n) => ({ + ...n, + visible: true, + })); + const spacerNodes = getSpacerNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.finallyNodeTypes, + ); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.edgeType, + creationTypes.edgeType, + creationTypes.finallyNodeTypes, + creationTypes.finallyEdgeType, + ); + controller.fromModel({ nodes, edges }, true); + controller.getGraph().layout(); + } onCollapseChange && onCollapseChange(group, collapsed); }; @@ -129,14 +108,11 @@ const DefaultTaskGroupInner: React.FunctionComponent ); diff --git a/frontend/src/concepts/topology/PipelineTaskGroup.tsx b/frontend/src/concepts/topology/PipelineTaskGroup.tsx deleted file mode 100644 index c1b4c491a7..0000000000 --- a/frontend/src/concepts/topology/PipelineTaskGroup.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import { - DEFAULT_EDGE_TYPE, - DefaultTaskGroup, EdgeCreationTypes, - GraphElement, - LabelPosition, - WithSelectionProps, -} from '@patternfly/react-topology'; -import { NODE_HEIGHT, NODE_WIDTH } from '~/concepts/topology/const'; - -type PipelineTaskGroupProps = { - element: GraphElement; -} & WithSelectionProps; - -const getEdgeCreationTypes = (): EdgeCreationTypes => ({ - edgeType: DEFAULT_EDGE_TYPE, - spacerEdgeType: DEFAULT_EDGE_TYPE, -}); - -const onCollapseChange = (element: GraphElement): void => { - const controller = element.getController(); - const model = controller.toModel(); - console.log(model.nodes); - console.log(model.edges); -}; - -const PipelineTaskGroup: React.FunctionComponent = ({ - element, - selected, - onSelect, -}) => ( - onCollapseChange(element)} - // TODO: support in PF Topology - // status={status} - /> -); - -export default PipelineTaskGroup; diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx index 2529b57aa4..e19c266a73 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -4,12 +4,8 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; import { RunStatus, LabelPosition, - BadgeLocation, CollapsibleGroupProps, TaskNode, - WithContextMenuProps, - WithDndDropProps, - WithDragNodeProps, WithSelectionProps, Node, ScaleDetailsLevel, @@ -26,36 +22,17 @@ type PipelineTaskGroupCollapsedProps = { children?: React.ReactNode; className?: string; element: Node; - droppable?: boolean; - canDrop?: boolean; - dropTarget?: boolean; - dragging?: boolean; hover?: boolean; label?: string; // Defaults to element.getLabel() - secondaryLabel?: string; - showLabel?: boolean; // Defaults to true status?: RunStatus; - statusIconSize?: number; showStatusState?: boolean; scaleNode?: boolean; hideDetailsAtMedium?: boolean; hiddenDetailsShownStatuses?: RunStatus[]; labelPosition?: LabelPosition; // Defaults to bottom - truncateLength?: number; // Defaults to 13 - labelIconClass?: string; // Icon to show in label - labelIcon?: string; - labelIconPadding?: number; badge?: string; - badgeColor?: string; - badgeTextColor?: string; - badgeBorderColor?: string; - badgeClassName?: string; - badgeLocation?: BadgeLocation; } & CollapsibleGroupProps & - WithDragNodeProps & - WithSelectionProps & - WithDndDropProps & - WithContextMenuProps; + WithSelectionProps; const PipelineTaskGroupCollapsed: React.FunctionComponent = ({ element, @@ -67,16 +44,16 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent[]) => { - return items.map((item: Node) => ( -
- - {getNodeStatusIcon(item.getData()?.status).icon} - - {item.getId()} -
- )); - }; + const getPopoverTasksList = (items: Node[]) => { + return items.map((item: Node) => ( +
+ + {getNodeStatusIcon(item.getData()?.status).icon} + + {item.getId()} +
+ )); + }; return ( From df71f2b86a708a7573e87d408bc1a79a24896eea Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:05:20 -0400 Subject: [PATCH 10/18] revert es6 --- frontend/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index da00f31ff7..b29efed6c9 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,7 +3,7 @@ "rootDir": ".", "outDir": "public", "module": "esnext", - "target": "es6", + "target": "es5", "lib": ["es6", "dom"], "sourceMap": true, "jsx": "react", From 2a99aece1c960d4c17c750cdf3745864f6313ef8 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:54:09 -0400 Subject: [PATCH 11/18] fix type and linting errors --- .../src/concepts/pipelines/content/utils.tsx | 2 +- .../topology/usePipelineTaskTopology.ts | 5 ++-- .../src/concepts/topology/BasePipelineNode.ts | 28 ------------------- .../topology/PipelineDefaultTaskGroup.tsx | 11 +------- .../topology/PipelineTaskGroupCollapsed.tsx | 7 ++--- .../topology/customNodes/ArtifactTaskNode.tsx | 1 - frontend/src/concepts/topology/factories.ts | 5 ++-- .../topology/pipelineElementFactory.ts | 13 --------- frontend/src/concepts/topology/types.ts | 1 - .../topology/useTopologyController.ts | 2 +- frontend/src/concepts/topology/utils.tsx | 16 +++++++---- 11 files changed, 20 insertions(+), 71 deletions(-) delete mode 100644 frontend/src/concepts/topology/BasePipelineNode.ts delete mode 100644 frontend/src/concepts/topology/pipelineElementFactory.ts diff --git a/frontend/src/concepts/pipelines/content/utils.tsx b/frontend/src/concepts/pipelines/content/utils.tsx index ddcf80c8b4..37d3f55bda 100644 --- a/frontend/src/concepts/pipelines/content/utils.tsx +++ b/frontend/src/concepts/pipelines/content/utils.tsx @@ -21,7 +21,7 @@ import { relativeTime } from '~/utilities/time'; export type RunStatusDetails = { icon: React.ReactNode; - label: PipelineRunKFv2['state'] | string; + label?: PipelineRunKFv2['state'] | string; status?: React.ComponentProps['status']; details?: string; createdAt?: string; diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index 3c23302042..d96eadba42 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -159,7 +159,6 @@ export const usePipelineTaskTopology = ( ); } - // This task's rendering information if (isGroupNode && groupTasks) { const [nestedNodes, children] = getNestedNodes(groupTasks, components, runDetails); @@ -181,8 +180,8 @@ export const usePipelineTaskTopology = ( type: 'groupTask', name: id, steps: executor ? [executor.container] : undefined, - inputs: parseInputOutput(component?.inputDefinitions), - outputs: parseInputOutput(component?.outputDefinitions), + inputs: parseInputOutput(component.inputDefinitions), + outputs: parseInputOutput(component.outputDefinitions), status, volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), }; diff --git a/frontend/src/concepts/topology/BasePipelineNode.ts b/frontend/src/concepts/topology/BasePipelineNode.ts deleted file mode 100644 index 8a8160cbec..0000000000 --- a/frontend/src/concepts/topology/BasePipelineNode.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseNode, PipelineNodeModel } from '@patternfly/react-topology'; - -class BasePipelineNode extends BaseNode { - private runAfterTasks?: string[]; - - constructor() { - super(); - } - - setModel(model: PipelineNodeModel): void { - super.setModel(model); - - if ('runAfterTasks' in model) { - this.runAfterTasks = model.runAfterTasks; - } - }; - - toModel(): PipelineNodeModel { - return { - ...super.toModel(), - runAfterTasks: this.runAfterTasks, - children: super.getAllChildren().map((c) => c.getId()) - }; - } - -}; - -export default BasePipelineNode; diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index f0c3cde183..d1384cdbb3 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -50,16 +50,7 @@ type PipelinesDefaultGroupInnerProps = Omit = observer( - ({ - className, - element, - onCollapseChange, - recreateLayoutOnCollapseChange, - getEdgeCreationTypes, - selected, - onSelect, - ...rest - }) => { + ({ className, element, onCollapseChange, getEdgeCreationTypes, selected, onSelect, ...rest }) => { const childCount = element.getAllNodeChildren().length; const data = element.getData(); diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx index e19c266a73..fa93e5da2b 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { observer } from 'mobx-react'; import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; import { RunStatus, @@ -14,6 +13,7 @@ import { DEFAULT_LAYER, TOP_LAYER, NodeModel, + observer, } from '@patternfly/react-topology'; import { Icon, Popover } from '@patternfly/react-core'; import { getNodeStatusIcon } from './utils'; @@ -44,8 +44,8 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent[]) => { - return items.map((item: Node) => ( + const getPopoverTasksList = (items: Node[]) => + items.map((item: Node) => (
{getNodeStatusIcon(item.getData()?.status).icon} @@ -53,7 +53,6 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent )); - }; return ( diff --git a/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx index 1a2f4113e1..a3676fa138 100644 --- a/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx +++ b/frontend/src/concepts/topology/customNodes/ArtifactTaskNode.tsx @@ -15,7 +15,6 @@ import { TaskNodeSourceAnchor, TaskNodeTargetAnchor, GraphElement, - RunStatus, } from '@patternfly/react-topology'; import { ListIcon, MonitoringIcon } from '@patternfly/react-icons'; import { TaskNodeProps } from '@patternfly/react-topology/dist/esm/pipelines/components/nodes/TaskNode'; diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index b7896dc529..c766551400 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -8,13 +8,12 @@ import { SpacerNode, withPanZoom, withSelection, - TaskEdge as PFTaskEdge, } from '@patternfly/react-topology'; import StandardTaskNode from '~/concepts/topology/customNodes/StandardTaskNode'; import { ICON_TASK_NODE_TYPE } from './utils'; import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; import PipelineTaskEdge from './PipelineTaskEdge'; -import PipelineDefaultTaskGroup from "./PipelineDefaultTaskGroup"; +import PipelineDefaultTaskGroup from './PipelineDefaultTaskGroup'; export const pipelineComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { @@ -34,7 +33,7 @@ export const pipelineComponentFactory: ComponentFactory = (kind, type) => { } }; -export const pipelineGroupsComponentFactory = (kind: ModelKind, type: string) => { +export const pipelineGroupsComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { return withPanZoom()(GraphComponent); } diff --git a/frontend/src/concepts/topology/pipelineElementFactory.ts b/frontend/src/concepts/topology/pipelineElementFactory.ts deleted file mode 100644 index ce020e7e8a..0000000000 --- a/frontend/src/concepts/topology/pipelineElementFactory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ElementFactory, GraphElement, ModelKind } from '@patternfly/react-topology'; -import BasePipelineNode from './BasePipelineNode'; - -const pipelineElementFactory: ElementFactory = (kind: ModelKind): GraphElement | undefined => { - switch (kind) { - case ModelKind.node: - return new BasePipelineNode(); - default: - return undefined; - } -}; - -export default pipelineElementFactory; diff --git a/frontend/src/concepts/topology/types.ts b/frontend/src/concepts/topology/types.ts index 88db4c1ba4..1c35f045b2 100644 --- a/frontend/src/concepts/topology/types.ts +++ b/frontend/src/concepts/topology/types.ts @@ -1,5 +1,4 @@ import { PipelineNodeModel, RunStatus, WhenStatus } from '@patternfly/react-topology'; -import React from 'react'; export type NodeConstructDetails = { id: string; diff --git a/frontend/src/concepts/topology/useTopologyController.ts b/frontend/src/concepts/topology/useTopologyController.ts index 2b531c2f0e..0dbc53a902 100644 --- a/frontend/src/concepts/topology/useTopologyController.ts +++ b/frontend/src/concepts/topology/useTopologyController.ts @@ -7,8 +7,8 @@ import { PipelineDagreGroupsLayout, Visualization, } from '@patternfly/react-topology'; +import pipelineElementFactory from '@patternfly/react-topology/dist/esm/pipelines/elements/pipelineElementFactory'; import { pipelineComponentFactory, pipelineGroupsComponentFactory } from './factories'; -import pipelineElementFactory from './pipelineElementFactory'; import { PIPELINE_LAYOUT, PIPELINE_NODE_SEPARATION_VERTICAL } from './const'; const useTopologyController = (graphId: string): Visualization | null => { diff --git a/frontend/src/concepts/topology/utils.tsx b/frontend/src/concepts/topology/utils.tsx index f3ee8f9783..cf0eb8933f 100644 --- a/frontend/src/concepts/topology/utils.tsx +++ b/frontend/src/concepts/topology/utils.tsx @@ -1,7 +1,4 @@ import { DEFAULT_TASK_NODE_TYPE } from '@patternfly/react-topology'; -import { genRandomChars } from '~/utilities/string'; -import { NODE_HEIGHT, NODE_WIDTH } from './const'; -import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; import { NotStartedIcon, SyncAltIcon, @@ -9,8 +6,13 @@ import { ExclamationCircleIcon, BanIcon, } from '@patternfly/react-icons'; -import { RuntimeStateKF } from '../pipelines/kfTypes'; import React from 'react'; +import { Icon } from '@patternfly/react-core'; +import { genRandomChars } from '~/utilities/string'; +import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; +import { RunStatusDetails } from '~/concepts/pipelines/content/utils'; +import { NODE_HEIGHT, NODE_WIDTH } from './const'; +import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; @@ -70,10 +72,11 @@ export const createGroupNode = ( : undefined, }); -export const getNodeStatusIcon = (status: RuntimeStateKF | string): any => { +export const getNodeStatusIcon = (runStatus: RuntimeStateKF | string): RunStatusDetails => { let icon: React.ReactNode; + let status: React.ComponentProps['status']; - switch (status) { + switch (runStatus) { case RuntimeStateKF.PENDING: case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: case undefined: @@ -101,6 +104,7 @@ export const getNodeStatusIcon = (status: RuntimeStateKF | string): any => { break; case RuntimeStateKF.PAUSED: icon = ; + break; default: icon = null; } From d5c72485b24d48c9087f03c68db213518fea8b06 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 15 Apr 2024 08:44:33 -0400 Subject: [PATCH 12/18] update getNodeStatusIcon func --- frontend/src/concepts/pipelines/content/utils.tsx | 2 +- frontend/src/concepts/topology/utils.tsx | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/concepts/pipelines/content/utils.tsx b/frontend/src/concepts/pipelines/content/utils.tsx index 37d3f55bda..ddcf80c8b4 100644 --- a/frontend/src/concepts/pipelines/content/utils.tsx +++ b/frontend/src/concepts/pipelines/content/utils.tsx @@ -21,7 +21,7 @@ import { relativeTime } from '~/utilities/time'; export type RunStatusDetails = { icon: React.ReactNode; - label?: PipelineRunKFv2['state'] | string; + label: PipelineRunKFv2['state'] | string; status?: React.ComponentProps['status']; details?: string; createdAt?: string; diff --git a/frontend/src/concepts/topology/utils.tsx b/frontend/src/concepts/topology/utils.tsx index cf0eb8933f..72d98cd0c3 100644 --- a/frontend/src/concepts/topology/utils.tsx +++ b/frontend/src/concepts/topology/utils.tsx @@ -13,6 +13,7 @@ import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; import { RunStatusDetails } from '~/concepts/pipelines/content/utils'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; +import { runtimeStateLabels } from "../pipelines/kfTypes"; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; @@ -75,39 +76,49 @@ export const createGroupNode = ( export const getNodeStatusIcon = (runStatus: RuntimeStateKF | string): RunStatusDetails => { let icon: React.ReactNode; let status: React.ComponentProps['status']; + let label: string; switch (runStatus) { case RuntimeStateKF.PENDING: case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: case undefined: icon = ; + label = runtimeStateLabels[RuntimeStateKF.PENDING]; break; case RuntimeStateKF.RUNNING: icon = ; + label = runtimeStateLabels[RuntimeStateKF.RUNNING]; break; case RuntimeStateKF.SKIPPED: icon = ; + label = runtimeStateLabels[RuntimeStateKF.SKIPPED]; break; - case 'Succeeded': + case RuntimeStateKF.SUCCEEDED: icon = ; status = 'success'; + label = runtimeStateLabels[RuntimeStateKF.SUCCEEDED]; break; case RuntimeStateKF.FAILED: icon = ; status = 'danger'; + label = runtimeStateLabels[RuntimeStateKF.FAILED]; break; case RuntimeStateKF.CANCELING: icon = ; + label = runtimeStateLabels[RuntimeStateKF.CANCELING]; break; case RuntimeStateKF.CANCELED: icon = ; + label = runtimeStateLabels[RuntimeStateKF.CANCELED]; break; case RuntimeStateKF.PAUSED: icon = ; + label = runtimeStateLabels[RuntimeStateKF.PAUSED]; break; default: icon = null; + label = ''; } - return { icon, status }; + return { label, icon, status }; }; From b61bc352b7dc4b1e041500da95b0a589dc7bde5e Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:55:43 -0400 Subject: [PATCH 13/18] try fixing lint issues --- .../src/concepts/topology/PipelineDefaultTaskGroup.tsx | 8 +++----- frontend/src/concepts/topology/utils.tsx | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index d1384cdbb3..942b910234 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -12,6 +12,7 @@ import { observer, Node, GraphElement, + action, } from '@patternfly/react-topology'; import PipelineTaskGroupCollapsed from './PipelineTaskGroupCollapsed'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; @@ -54,7 +55,7 @@ const DefaultTaskGroupInner: React.FunctionComponent { + const handleCollapse = action((group: Node, collapsed: boolean): void => { if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); } @@ -88,9 +89,7 @@ const DefaultTaskGroupInner: React.FunctionComponent ); diff --git a/frontend/src/concepts/topology/utils.tsx b/frontend/src/concepts/topology/utils.tsx index 72d98cd0c3..7a5fc89432 100644 --- a/frontend/src/concepts/topology/utils.tsx +++ b/frontend/src/concepts/topology/utils.tsx @@ -9,11 +9,10 @@ import { import React from 'react'; import { Icon } from '@patternfly/react-core'; import { genRandomChars } from '~/utilities/string'; -import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; +import { RuntimeStateKF, runtimeStateLabels } from '~/concepts/pipelines/kfTypes'; import { RunStatusDetails } from '~/concepts/pipelines/content/utils'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; -import { runtimeStateLabels } from "../pipelines/kfTypes"; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; From 11ddb94a6c4c3c45120430dcab3781c61c73d55a Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 15 Apr 2024 11:04:13 -0400 Subject: [PATCH 14/18] remove unused onCollapseChange --- frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index 942b910234..c0490b8902 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -51,7 +51,7 @@ type PipelinesDefaultGroupInnerProps = Omit = observer( - ({ className, element, onCollapseChange, getEdgeCreationTypes, selected, onSelect, ...rest }) => { + ({ className, element, getEdgeCreationTypes, selected, onSelect, ...rest }) => { const childCount = element.getAllNodeChildren().length; const data = element.getData(); From 2c7e726fd4d2cdd8b0c88d0f8ad56db1a076ec18 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:59:05 -0400 Subject: [PATCH 15/18] PR feedback from Jeff --- .../concepts/pipelines/topology/parseUtils.ts | 35 +++++++ .../topology/usePipelineTaskTopology.ts | 11 ++- .../topology/PipelineDefaultTaskGroup.tsx | 92 +++---------------- .../topology/PipelineTaskGroupCollapsed.tsx | 51 +++++++--- frontend/src/concepts/topology/const.ts | 2 + .../topology/customNodes/IconSourceAnchor.tsx | 19 ---- frontend/src/concepts/topology/factories.ts | 7 +- frontend/src/concepts/topology/utils.tsx | 4 +- 8 files changed, 105 insertions(+), 116 deletions(-) delete mode 100644 frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx diff --git a/frontend/src/concepts/pipelines/topology/parseUtils.ts b/frontend/src/concepts/pipelines/topology/parseUtils.ts index baa3320e5d..9d630bd435 100644 --- a/frontend/src/concepts/pipelines/topology/parseUtils.ts +++ b/frontend/src/concepts/pipelines/topology/parseUtils.ts @@ -8,6 +8,7 @@ import { RunDetailsKF, RuntimeStateKF, TaskDetailKF, + TaskKF, } from '~/concepts/pipelines/kfTypes'; import { VolumeMount } from '~/types'; import { PipelineTaskInputOutput, PipelineTaskRunStatus } from './pipelineTaskTypes'; @@ -105,6 +106,40 @@ export const parseInputOutput = ( return data; }; +export const parseSubTaskInputOutput = (definition: TaskKF['inputs']) => { + let data; + if (definition) { + const { artifacts, parameters } = definition; + data = {}; + + if (parameters) { + data = { + ...data, + params: Object.entries(parameters).map( + ([paramLabel, { componentInputParameter }]) => ({ + label: paramLabel, + type: typeof(componentInputParameter), + // TODO: support value + }), + ), + }; + } + + if (artifacts) { + data = { + ...data, + artifacts: Object.entries(artifacts).map(([paramLabel, { taskOutputArtifact }]) => ({ + label: paramLabel, + type: typeof(taskOutputArtifact?.producerTask), + // TODO: support value + })), + }; + } + } + + return data; +}; + export const lowestProgress = (details: TaskDetailKF[]): PipelineTaskRunStatus['state'] => { const statusWeight = (status?: RuntimeStateKF) => { switch (status) { diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index d96eadba42..bb01f0751e 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -136,6 +136,11 @@ export const usePipelineTaskTopology = ( }); } + console.log( + 'hello component', component + ); + + console.log('thisTaskid', taskId) // This task newTaskMapEntries[taskId] = { type: isGroupNode ? 'groupTask' : 'task', @@ -176,12 +181,14 @@ export const usePipelineTaskTopology = ( // Extract IDs and create new entries nestedNodes.forEach((node) => { const { id } = node; + newTaskMapEntries[id] = { type: 'groupTask', name: id, steps: executor ? [executor.container] : undefined, - inputs: parseInputOutput(component.inputDefinitions), - outputs: parseInputOutput(component.outputDefinitions), + // TODO: render data + // inputs: parseInputOutput(component.dag.tasks[id]?.inputs), + // outputs: parseInputOutput(component.dag.tasks[id]?.inputs), status, volumeMounts: parseVolumeMounts(spec.platform_spec, executorLabel), }; diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index c0490b8902..c899f60104 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -1,109 +1,41 @@ import * as React from 'react'; import { - Dimensions, LabelPosition, - OnSelect, WithSelectionProps, - getEdgesFromNodes, - getSpacerNodes, isNode, DefaultTaskGroup, observer, Node, GraphElement, - action, } from '@patternfly/react-topology'; import PipelineTaskGroupCollapsed from './PipelineTaskGroupCollapsed'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; -export interface EdgeCreationTypes { - spacerNodeType?: string; - edgeType?: string; - spacerEdgeType?: string; - finallyNodeTypes?: string[]; - finallyEdgeType?: string; -} -interface PipelinesDefaultGroupProps { +type PipelinesDefaultGroupProps = { children?: React.ReactNode; - className?: string; element: GraphElement; - hover?: boolean; - collapsible?: boolean; - collapsedWidth?: number; - collapsedHeight?: number; onCollapseChange?: (group: Node, collapsed: boolean) => void; - selected?: boolean; - onSelect?: OnSelect; - recreateLayoutOnCollapseChange?: boolean; - getEdgeCreationTypes?: () => { - spacerNodeType?: string; - edgeType?: string; - spacerEdgeType?: string; - finallyNodeTypes?: string[]; - finallyEdgeType?: string; - }; -} +} & WithSelectionProps; type PipelinesDefaultGroupInnerProps = Omit & { element: Node; -} & WithSelectionProps; + onCollapseChange?: () => void; +}; const DefaultTaskGroupInner: React.FunctionComponent = observer( - ({ className, element, getEdgeCreationTypes, selected, onSelect, ...rest }) => { - const childCount = element.getAllNodeChildren().length; - const data = element.getData(); - - const handleCollapse = action((group: Node, collapsed: boolean): void => { - if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { - group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); - } - group.setCollapsed(collapsed); - - const controller = group.hasController() && group.getController(); - if (controller) { - const model = controller.toModel(); - const creationTypes: EdgeCreationTypes = getEdgeCreationTypes ? getEdgeCreationTypes() : {}; - - const pipelineNodes = model - .nodes!.filter((n) => n.type !== creationTypes.spacerNodeType) - .map((n) => ({ - ...n, - visible: true, - })); - const spacerNodes = getSpacerNodes( - pipelineNodes, - creationTypes.spacerNodeType, - creationTypes.finallyNodeTypes, - ); - const nodes = [...pipelineNodes, ...spacerNodes]; - const edges = getEdgesFromNodes( - pipelineNodes, - creationTypes.spacerNodeType, - creationTypes.edgeType, - creationTypes.edgeType, - creationTypes.finallyNodeTypes, - creationTypes.finallyEdgeType, - ); - controller.fromModel({ nodes, edges }, true); - controller.getGraph().layout(); - } - }); + ({ element, onCollapseChange, selected, onSelect, ...rest }) => { + // TODO: remove when https://github.com/patternfly/react-topology/issues/171 merged if (element.isCollapsed()) { return ( ); @@ -127,8 +60,9 @@ const DefaultTaskGroupInner: React.FunctionComponent = ({ element, + onCollapseChange, ...rest -}: PipelinesDefaultGroupProps) => { +}: PipelinesDefaultGroupProps & WithSelectionProps) => { if (!isNode(element)) { throw new Error('DefaultTaskGroup must be used only on Node elements'); } @@ -136,4 +70,4 @@ const PipelineDefaultTaskGroup: React.FunctionComponent; }; -export default PipelineDefaultTaskGroup; +export default observer(PipelineDefaultTaskGroup); diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx index fa93e5da2b..4e7ab7dfed 100644 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx @@ -14,23 +14,18 @@ import { TOP_LAYER, NodeModel, observer, + Dimensions, + action, + getEdgesFromNodes, + getSpacerNodes, } from '@patternfly/react-topology'; import { Icon, Popover } from '@patternfly/react-core'; import { getNodeStatusIcon } from './utils'; type PipelineTaskGroupCollapsedProps = { children?: React.ReactNode; - className?: string; element: Node; - hover?: boolean; - label?: string; // Defaults to element.getLabel() - status?: RunStatus; - showStatusState?: boolean; - scaleNode?: boolean; hideDetailsAtMedium?: boolean; - hiddenDetailsShownStatuses?: RunStatus[]; - labelPosition?: LabelPosition; // Defaults to bottom - badge?: string; } & CollapsibleGroupProps & WithSelectionProps; @@ -38,12 +33,16 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent { const [hover, hoverRef] = useHover(); const myRef = React.useRef(); const detailsLevel = element.getGraph().getDetailsLevel(); + const childCount = element.getAllNodeChildren().length; + + const getPopoverTasksList = (items: Node[]) => items.map((item: Node) => (
@@ -54,6 +53,34 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent )); + const handleCollapse = action((group: Node, collapsed: boolean): void => { + if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { + group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); + } + group.setCollapsed(collapsed); + + const controller = group.hasController() && group.getController(); + if (controller) { + const model = controller.toModel(); + + const pipelineNodes = model + .nodes!.filter((n) => n.type) + .map((n) => ({ + ...n, + visible: true, + })); + const spacerNodes = getSpacerNodes( + pipelineNodes, + ); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + pipelineNodes, + ); + controller.fromModel({ nodes, edges }, true); + controller.getGraph().layout(); + } + }); + return ( }> @@ -67,13 +94,15 @@ const PipelineTaskGroupCollapsed: React.FunctionComponent}> : undefined} - onActionIconClick={() => onCollapseChange!(element, false)} + onActionIconClick={() => handleCollapse(element, false)} shadowCount={2} hiddenDetailsShownStatuses={[RunStatus.Succeeded]} scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high} - hideDetailsAtMedium showStatusState + badge={`${childCount}`} + status={element.getData().status} {...rest} /> diff --git a/frontend/src/concepts/topology/const.ts b/frontend/src/concepts/topology/const.ts index a4c3324928..5db9ed4171 100644 --- a/frontend/src/concepts/topology/const.ts +++ b/frontend/src/concepts/topology/const.ts @@ -3,3 +3,5 @@ export const PIPELINE_NODE_SEPARATION_VERTICAL = 100; export const NODE_WIDTH = 130; export const NODE_HEIGHT = 35; + +export const EXECUTION_TASK_NODE_TYPE = 'EXECUTION_TASK_NODE'; diff --git a/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx b/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx deleted file mode 100644 index bc1780759b..0000000000 --- a/frontend/src/concepts/topology/customNodes/IconSourceAnchor.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; - -export default class IconSourceAnchor extends AbstractAnchor { - private size: number; - - constructor(owner: E, size: number) { - super(owner); - this.size = size; - } - - getLocation(): Point { - return this.getReferencePoint(); - } - - getReferencePoint(): Point { - const bounds = this.owner.getBounds(); - return new Point(bounds.x + this.size, bounds.y + bounds.height / 2); - } -} diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index c766551400..72cd1ae737 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -14,6 +14,7 @@ import { ICON_TASK_NODE_TYPE } from './utils'; import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; import PipelineTaskEdge from './PipelineTaskEdge'; import PipelineDefaultTaskGroup from './PipelineDefaultTaskGroup'; +import { EXECUTION_TASK_NODE_TYPE } from "./const"; export const pipelineComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { @@ -38,9 +39,9 @@ export const pipelineGroupsComponentFactory: ComponentFactory = (kind, type) => return withPanZoom()(GraphComponent); } switch (type) { - case 'Execution': + case EXECUTION_TASK_NODE_TYPE: return withSelection()(PipelineDefaultTaskGroup); - case 'Task': + case DEFAULT_TASK_NODE_TYPE: return withSelection()(StandardTaskNode); case ICON_TASK_NODE_TYPE: // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -48,7 +49,7 @@ export const pipelineGroupsComponentFactory: ComponentFactory = (kind, type) => return withSelection()(ArtifactTaskNode); case DEFAULT_SPACER_NODE_TYPE: return SpacerNode; - case 'edge': + case DEFAULT_EDGE_TYPE: return PipelineTaskEdge; default: return undefined; diff --git a/frontend/src/concepts/topology/utils.tsx b/frontend/src/concepts/topology/utils.tsx index 7a5fc89432..0a29545886 100644 --- a/frontend/src/concepts/topology/utils.tsx +++ b/frontend/src/concepts/topology/utils.tsx @@ -11,7 +11,7 @@ import { Icon } from '@patternfly/react-core'; import { genRandomChars } from '~/utilities/string'; import { RuntimeStateKF, runtimeStateLabels } from '~/concepts/pipelines/kfTypes'; import { RunStatusDetails } from '~/concepts/pipelines/content/utils'; -import { NODE_HEIGHT, NODE_WIDTH } from './const'; +import { EXECUTION_TASK_NODE_TYPE, NODE_HEIGHT, NODE_WIDTH } from './const'; import { NodeConstructDetails, PipelineNodeModelExpanded } from './types'; export const createNodeId = (prefix = 'node'): string => `${prefix}-${genRandomChars()}`; @@ -54,7 +54,7 @@ export const createGroupNode = ( ): PipelineNodeModelExpanded => ({ id: details.id, label: details.id, - type: 'Execution', + type: EXECUTION_TASK_NODE_TYPE, group: true, collapsed: true, width: NODE_WIDTH, From 4821adea9537adf4c8c680e61578a3b4ce686dd7 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:23:39 -0400 Subject: [PATCH 16/18] fix linting errors --- .../concepts/pipelines/topology/parseUtils.ts | 34 ------------------- .../topology/usePipelineTaskTopology.ts | 6 ++-- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/frontend/src/concepts/pipelines/topology/parseUtils.ts b/frontend/src/concepts/pipelines/topology/parseUtils.ts index 9d630bd435..f9aa067db9 100644 --- a/frontend/src/concepts/pipelines/topology/parseUtils.ts +++ b/frontend/src/concepts/pipelines/topology/parseUtils.ts @@ -106,40 +106,6 @@ export const parseInputOutput = ( return data; }; -export const parseSubTaskInputOutput = (definition: TaskKF['inputs']) => { - let data; - if (definition) { - const { artifacts, parameters } = definition; - data = {}; - - if (parameters) { - data = { - ...data, - params: Object.entries(parameters).map( - ([paramLabel, { componentInputParameter }]) => ({ - label: paramLabel, - type: typeof(componentInputParameter), - // TODO: support value - }), - ), - }; - } - - if (artifacts) { - data = { - ...data, - artifacts: Object.entries(artifacts).map(([paramLabel, { taskOutputArtifact }]) => ({ - label: paramLabel, - type: typeof(taskOutputArtifact?.producerTask), - // TODO: support value - })), - }; - } - } - - return data; -}; - export const lowestProgress = (details: TaskDetailKF[]): PipelineTaskRunStatus['state'] => { const statusWeight = (status?: RuntimeStateKF) => { switch (status) { diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index bb01f0751e..6a734728d6 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -136,11 +136,9 @@ export const usePipelineTaskTopology = ( }); } - console.log( - 'hello component', component - ); + console.log('hello component', component); - console.log('thisTaskid', taskId) + console.log('thisTaskid', taskId); // This task newTaskMapEntries[taskId] = { type: isGroupNode ? 'groupTask' : 'task', From 7342688185ce703c89b1cfb50f77ae70fdb61a1a Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:33:01 -0400 Subject: [PATCH 17/18] fix popover status icons rendering --- frontend/src/concepts/topology/factories.ts | 2 +- frontend/src/concepts/topology/utils.tsx | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/concepts/topology/factories.ts b/frontend/src/concepts/topology/factories.ts index 72cd1ae737..bbd4ac1919 100644 --- a/frontend/src/concepts/topology/factories.ts +++ b/frontend/src/concepts/topology/factories.ts @@ -14,7 +14,7 @@ import { ICON_TASK_NODE_TYPE } from './utils'; import ArtifactTaskNode from './customNodes/ArtifactTaskNode'; import PipelineTaskEdge from './PipelineTaskEdge'; import PipelineDefaultTaskGroup from './PipelineDefaultTaskGroup'; -import { EXECUTION_TASK_NODE_TYPE } from "./const"; +import { EXECUTION_TASK_NODE_TYPE } from './const'; export const pipelineComponentFactory: ComponentFactory = (kind, type) => { if (kind === ModelKind.graph) { diff --git a/frontend/src/concepts/topology/utils.tsx b/frontend/src/concepts/topology/utils.tsx index 0a29545886..58640e2b1d 100644 --- a/frontend/src/concepts/topology/utils.tsx +++ b/frontend/src/concepts/topology/utils.tsx @@ -78,39 +78,39 @@ export const getNodeStatusIcon = (runStatus: RuntimeStateKF | string): RunStatus let label: string; switch (runStatus) { - case RuntimeStateKF.PENDING: - case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: + case runtimeStateLabels[RuntimeStateKF.PENDING]: + case runtimeStateLabels[RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED]: case undefined: icon = ; label = runtimeStateLabels[RuntimeStateKF.PENDING]; break; - case RuntimeStateKF.RUNNING: + case runtimeStateLabels[RuntimeStateKF.RUNNING]: icon = ; label = runtimeStateLabels[RuntimeStateKF.RUNNING]; break; - case RuntimeStateKF.SKIPPED: + case runtimeStateLabels[RuntimeStateKF.SKIPPED]: icon = ; label = runtimeStateLabels[RuntimeStateKF.SKIPPED]; break; - case RuntimeStateKF.SUCCEEDED: + case runtimeStateLabels[RuntimeStateKF.SUCCEEDED]: icon = ; status = 'success'; label = runtimeStateLabels[RuntimeStateKF.SUCCEEDED]; break; - case RuntimeStateKF.FAILED: + case runtimeStateLabels[RuntimeStateKF.FAILED]: icon = ; status = 'danger'; label = runtimeStateLabels[RuntimeStateKF.FAILED]; break; - case RuntimeStateKF.CANCELING: + case runtimeStateLabels[RuntimeStateKF.CANCELING]: icon = ; label = runtimeStateLabels[RuntimeStateKF.CANCELING]; break; - case RuntimeStateKF.CANCELED: + case runtimeStateLabels[RuntimeStateKF.CANCELED]: icon = ; label = runtimeStateLabels[RuntimeStateKF.CANCELED]; break; - case RuntimeStateKF.PAUSED: + case runtimeStateLabels[RuntimeStateKF.PAUSED]: icon = ; label = runtimeStateLabels[RuntimeStateKF.PAUSED]; break; From 66c8cfe7540d73735ccf89afbd6ed7553d68de51 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:49:43 -0400 Subject: [PATCH 18/18] delete PipelineDefaultGroupCollapsed logic --- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- .../topology/PipelineDefaultTaskGroup.tsx | 93 +++++++++----- .../topology/PipelineTaskGroupCollapsed.tsx | 115 ------------------ 4 files changed, 70 insertions(+), 148 deletions(-) delete mode 100644 frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 369786593a..e57cb33b7e 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.3.0-prerelease.10", + "@patternfly/react-topology": "^5.3.0-prerelease.12", "@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.3.0-prerelease.10", - "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.3.0-prerelease.10.tgz", - "integrity": "sha512-Ekse0rn5ewx+zxziLHlFq3bOWOK6wDAcfwxdtgWmy09vQdww5+POeesmspnWzBHp3V9X2+EiAkQ30gxAKmL6mQ==", + "version": "5.3.0-prerelease.12", + "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.3.0-prerelease.12.tgz", + "integrity": "sha512-bwJ6/5ZeiMV8uj+fYQHYnTp+5xQ99r1ejCXmBx2z1hSxIwLMt0DrtMK7Lorj5qfdcYDfM2G85+tNonAFZd0GQg==", "dependencies": { "@patternfly/react-core": "^5.1.1", "@patternfly/react-icons": "^5.1.1", diff --git a/frontend/package.json b/frontend/package.json index 9480cf575f..be11b857ea 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.3.0-prerelease.10", + "@patternfly/react-topology": "^5.3.0-prerelease.12", "@patternfly/react-virtualized-extension": "^5.0.0", "@types/classnames": "^2.3.1", "axios": "^1.6.4", diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index c899f60104..49b6203d82 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -8,14 +8,29 @@ import { observer, Node, GraphElement, + RunStatus, + useAnchor, + TaskGroupSourceAnchor, + AnchorEnd, + TaskGroupTargetAnchor, + DEFAULT_LAYER, + Layer, + ScaleDetailsLevel, + TOP_LAYER, + NodeModel, + useHover, } from '@patternfly/react-topology'; -import PipelineTaskGroupCollapsed from './PipelineTaskGroupCollapsed'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; - +import { Icon, Popover } from '@patternfly/react-core'; +import { getNodeStatusIcon } from './utils'; type PipelinesDefaultGroupProps = { children?: React.ReactNode; element: GraphElement; + showStatusState?: boolean; + status?: RunStatus; + hiddenDetailsShownStatuses?: RunStatus[]; + hideDetailsAtMedium?: boolean; onCollapseChange?: (group: Node, collapsed: boolean) => void; } & WithSelectionProps; @@ -26,34 +41,56 @@ type PipelinesDefaultGroupInnerProps = Omit = observer( ({ element, onCollapseChange, selected, onSelect, ...rest }) => { + const [hover, hoverRef] = useHover(); + const popoverRef = React.useRef(); + const detailsLevel = element.getGraph().getDetailsLevel(); + + const getPopoverTasksList = (items: Node[]) => + items.map((item: Node) => ( +
+ + {getNodeStatusIcon(item.getData()?.status).icon} + + {item.getId()} +
+ )); - // TODO: remove when https://github.com/patternfly/react-topology/issues/171 merged - if (element.isCollapsed()) { - return ( - - ); - } return ( - + + }> + + }> + + + + + ); }, ); diff --git a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx b/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx deleted file mode 100644 index 4e7ab7dfed..0000000000 --- a/frontend/src/concepts/topology/PipelineTaskGroupCollapsed.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; -import { - RunStatus, - LabelPosition, - CollapsibleGroupProps, - TaskNode, - WithSelectionProps, - Node, - ScaleDetailsLevel, - useHover, - Layer, - DEFAULT_LAYER, - TOP_LAYER, - NodeModel, - observer, - Dimensions, - action, - getEdgesFromNodes, - getSpacerNodes, -} from '@patternfly/react-topology'; -import { Icon, Popover } from '@patternfly/react-core'; -import { getNodeStatusIcon } from './utils'; - -type PipelineTaskGroupCollapsedProps = { - children?: React.ReactNode; - element: Node; - hideDetailsAtMedium?: boolean; -} & CollapsibleGroupProps & - WithSelectionProps; - -const PipelineTaskGroupCollapsed: React.FunctionComponent = ({ - element, - collapsible, - onCollapseChange, - hideDetailsAtMedium, - ...rest -}) => { - const [hover, hoverRef] = useHover(); - const myRef = React.useRef(); - const detailsLevel = element.getGraph().getDetailsLevel(); - - const childCount = element.getAllNodeChildren().length; - - - const getPopoverTasksList = (items: Node[]) => - items.map((item: Node) => ( -
- - {getNodeStatusIcon(item.getData()?.status).icon} - - {item.getId()} -
- )); - - const handleCollapse = action((group: Node, collapsed: boolean): void => { - if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { - group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); - } - group.setCollapsed(collapsed); - - const controller = group.hasController() && group.getController(); - if (controller) { - const model = controller.toModel(); - - const pipelineNodes = model - .nodes!.filter((n) => n.type) - .map((n) => ({ - ...n, - visible: true, - })); - const spacerNodes = getSpacerNodes( - pipelineNodes, - ); - const nodes = [...pipelineNodes, ...spacerNodes]; - const edges = getEdgesFromNodes( - pipelineNodes, - ); - controller.fromModel({ nodes, edges }, true); - controller.getGraph().layout(); - } - }); - - return ( - - }> - - }> - : undefined} - onActionIconClick={() => handleCollapse(element, false)} - shadowCount={2} - hiddenDetailsShownStatuses={[RunStatus.Succeeded]} - scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high} - showStatusState - badge={`${childCount}`} - status={element.getData().status} - {...rest} - /> - - - - - ); -}; - -export default observer(PipelineTaskGroupCollapsed);