import { useRef, useCallback, useEffect } from 'react'; import { action } from 'mobx'; import { TooltipProps } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import styles from '../../../css/topology-pipelines'; import { PopoverProps, Tooltip } from '@patternfly/react-core'; import { observer } from '../../../mobx-exports'; import { AnchorEnd, GraphElement, isNode, Node, ScaleDetailsLevel } from '../../../types'; import { RunStatus } from '../../types'; import { OnSelect, useAnchor } from '../../../behavior'; import { getNodeScaleTranslation, useHover, useSize } from '../../../utils'; import { TaskNodeSourceAnchor, TaskNodeTargetAnchor } from '../anchors'; import { useScaleNode } from '../../../hooks'; import { DagreLayoutOptions, TOP_TO_BOTTOM } from '../../../layouts'; import TaskPill from './TaskPill'; const STATUS_ICON_SIZE = 16; const SCALE_UP_TIME = 200; export interface TaskNodeProps { /** Additional content added to the node */ children?: React.ReactNode; /** Additional classes added to the node */ className?: string; /** The graph node element to represent */ element: GraphElement; /** Padding to use before and after contents */ paddingX?: number; /** Padding to use above and below contents */ paddingY?: number; /** Additional classes added to the label */ nameLabelClass?: string; /** RunStatus to depict */ status?: RunStatus; /** Size of the status icon */ statusIconSize?: number; /** Flag indicating the status indicator */ showStatusState?: boolean; /** Custom icon to use as the status icon */ customStatusIcon?: React.ReactNode; /** Flag indicating the node should be scaled, best on hover of the node at lowest scale level */ scaleNode?: boolean; /** Flag to hide details at medium scale */ hideDetailsAtMedium?: boolean; /** Statuses to show at when details are hidden */ hiddenDetailsShownStatuses?: RunStatus[]; /** Additional icon to be shown before the task label*/ leadIcon?: React.ReactNode; /** 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; /** Set to use a tooltip on the badge, takes precedence over the badgePopoverParams */ badgeTooltip?: React.ReactNode; /** Set to use a popover on the badge, ignored if the badgeTooltip parameter is set */ badgePopoverParams?: PopoverProps; /** Icon to show for the task */ taskIconClass?: string; /** Element to show for the task icon */ taskIcon?: React.ReactNode; /** Set to use a tooltip on the task icon */ taskIconTooltip?: React.ReactNode; /** Padding to use around the task icon */ taskIconPadding?: number; /** Flag if the user is hovering on the node */ hover?: boolean; /** The maximum length of the label before truncation */ truncateLength?: number; /** Flag if the tooltip is disabled */ disableTooltip?: boolean; /** Tooltip to show on node hover */ toolTip?: React.ReactNode; /** Tooltip properties to pass along to the node's tooltip */ toolTipProps?: Omit; /** Size of the when expression indicator */ whenSize?: number; /** Distance from the when expression indicator to the node */ whenOffset?: number; /** Icon to use for the action menu */ actionIcon?: React.ReactElement; /** Additional classes to use for the action icon */ actionIconClassName?: string; /** Callback when the action icon is clicked */ onActionIconClick?: (e: React.MouseEvent) => void; /** Flag if the element is selected. Part of WithSelectionProps */ selected?: boolean; /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ onSelect?: OnSelect; /** 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; /** Hide context menu kebab for the node */ hideContextMenuKebab?: boolean; /** Number of shadowed pills to show */ shadowCount?: number; /** Offset for each shadow */ shadowOffset?: number; } type TaskNodeInnerProps = Omit & { element: Node }; const TaskNodeInner: React.FC = observer( ({ element, className, statusIconSize = STATUS_ICON_SIZE, scaleNode, toolTip, toolTipProps, disableTooltip = false, whenSize = 0, whenOffset = 0, ...rest }) => { const [hovered, hoverRef] = useHover(); // const isHover = hover !== undefined ? hover : hovered; const taskRef = useRef(null); const [pillSize, pillRef] = useSize(); const pillWidth = pillSize?.width || 0; const { width } = element.getBounds(); const detailsLevel = element.getGraph().getDetailsLevel(); const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; useAnchor( useCallback( (node: Node) => new TaskNodeSourceAnchor(node, detailsLevel, statusIconSize + 4, verticalLayout), // Include scaleNode to cause an update when it changes // eslint-disable-next-line react-hooks/exhaustive-deps [detailsLevel, statusIconSize, scaleNode, verticalLayout] ), AnchorEnd.source ); useAnchor( useCallback( (node: Node) => new TaskNodeTargetAnchor(node, whenSize + whenOffset, detailsLevel, statusIconSize + 4, verticalLayout), // Include scaleNode to cause an update when it changes // eslint-disable-next-line react-hooks/exhaustive-deps [whenSize, whenOffset, detailsLevel, statusIconSize, scaleNode, verticalLayout] ), AnchorEnd.target ); useEffect(() => { const sourceEdges = element.getSourceEdges(); action(() => { const indent = detailsLevel === ScaleDetailsLevel.high && !verticalLayout ? width - pillWidth : 0; sourceEdges.forEach((edge) => { const data = edge.getData(); if ((data?.indent ?? 0) !== indent) { edge.setData({ ...(data || {}), indent }); } }); })(); return action(() => { sourceEdges.forEach((edge) => { const data = edge.getData(); if (data?.indent) { edge.setData({ ...(data || {}), indent: 0 }); } }); }); }, [detailsLevel, element, pillWidth, verticalLayout, width]); const scale = element.getGraph().getScale(); const nodeScale = useScaleNode(scaleNode, scale, SCALE_UP_TIME); const { translateX, translateY } = getNodeScaleTranslation(element, nodeScale, scaleNode); const taskPill = ( ); return ( {!toolTip || disableTooltip ? ( taskPill ) : ( {taskPill} )} ); } ); const TaskNode: React.FC = ({ element, paddingX = 8, paddingY = 8, statusIconSize = STATUS_ICON_SIZE, showStatusState = true, hiddenDetailsShownStatuses = [RunStatus.Failed, RunStatus.FailedToStart, RunStatus.Cancelled], badgeClassName = styles.topologyPipelinesPillBadge, taskIconPadding = 4, truncateLength = 14, disableTooltip = false, whenSize = 0, whenOffset = 0, ...rest }) => { if (!isNode(element)) { throw new Error('TaskNode must be used only on Node elements'); } return ( ); }; export default TaskNode;