import { useRef, useMemo, useCallback } from 'react'; import { css } from '@patternfly/react-styles'; import styles from '../../../css/topology-pipelines'; import topologyStyles from '../../../css/topology-components'; import { Popover, Tooltip } from '@patternfly/react-core'; import { observer } from '../../../mobx-exports'; import { Node, ScaleDetailsLevel } from '../../../types'; import { RunStatus } from '../../types'; import { truncateMiddle } from '../../../utils/truncate-middle'; import { createSvgIdUrl, useCombineRefs, useHover, useSize } from '../../../utils'; import { getRunStatusModifier, nonShadowModifiers } from '../../utils'; import StatusIcon from '../../utils/StatusIcon'; import LabelActionIcon from '../../../components/nodes/labels/LabelActionIcon'; import LabelContextMenu from '../../../components/nodes/labels/LabelContextMenu'; import NodeShadows, { NODE_SHADOW_FILTER_ID_DANGER, NODE_SHADOW_FILTER_ID_HOVER } from '../../../components/nodes/NodeShadows'; import LabelBadge from '../../../components/nodes/labels/LabelBadge'; import LabelIcon from '../../../components/nodes/labels/LabelIcon'; import { DagreLayoutOptions, TOP_TO_BOTTOM } from '../../../layouts'; import { TaskNodeProps } from './TaskNode'; const STATUS_ICON_SIZE = 16; export interface TaskPillProps extends Omit { verticalLayout?: boolean; width?: number; x: number; y: number; taskRef?: React.Ref; pillRef: (node: SVGGraphicsElement) => void; element: Node; } const TaskPill: React.FC = observer( ({ element, taskRef, pillRef, className, width = 0, paddingX = 8, paddingY = 8, status, statusIconSize = STATUS_ICON_SIZE, showStatusState = true, scaleNode, hideDetailsAtMedium, hiddenDetailsShownStatuses = [RunStatus.Failed, RunStatus.FailedToStart, RunStatus.Cancelled], leadIcon, badge, badgeColor, badgeTextColor, badgeBorderColor, badgeClassName = styles.topologyPipelinesPillBadge, badgeTooltip, badgePopoverParams, customStatusIcon, nameLabelClass, taskIconClass, taskIcon, taskIconTooltip, taskIconPadding = 4, hover, truncateLength = 14, disableTooltip = false, selected, onSelect, onContextMenu, contextMenuOpen, hideContextMenuKebab, actionIcon, actionIconClassName, onActionIconClick, shadowCount = 0, shadowOffset = 8, children, x, y }) => { const [hovered] = useHover(); const taskIconComponentRef = useRef(null); const isHover = hover !== undefined ? hover : hovered; const label = truncateMiddle(element.getLabel(), { length: truncateLength, omission: '...' }); const [textSize, textRef] = useSize([label, className]); const nameLabelTriggerRef = useRef(null); const nameLabelRef = useCombineRefs(textRef, nameLabelTriggerRef); const [statusSize, statusRef] = useSize([status, showStatusState, statusIconSize]); const [leadSize, leadIconRef] = useSize([leadIcon]); const [badgeSize, badgeRef] = useSize([badge]); const badgeLabelTriggerRef = useRef(null); const [actionSize, actionRef] = useSize([actionIcon, paddingX]); const [contextSize, contextRef] = useSize([onContextMenu, paddingX]); const detailsLevel = element.getGraph().getDetailsLevel(); const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; const textWidth = textSize?.width ?? 0; const textHeight = textSize?.height ?? 0; const { height, statusStartX, textStartX, actionStartX, contextStartX, pillWidth, badgeStartX, iconWidth, iconStartX, leadIconStartX, offsetX } = useMemo(() => { if (!textSize) { return { height: 0, statusStartX: 0, textStartX: 0, actionStartX: 0, contextStartX: 0, pillWidth: 0, badgeStartX: 0, iconWidth: 0, iconStartX: 0, leadIconStartX: 0, offsetX: 0 }; } const height: number = textHeight + 2 * paddingY; const startX = paddingX + paddingX / 2; const iconWidth = taskIconClass || taskIcon ? height - taskIconPadding : 0; const iconStartX = -(iconWidth * 0.75); const statusStartX = startX - statusIconSize / 4; // Adjust for icon padding const statusSpace = status && showStatusState && statusSize ? statusSize.width + paddingX : 0; const leadIconStartX = startX + statusSpace; const leadIconSpace = leadIcon ? leadSize.width + paddingX : 0; const textStartX = leadIconStartX + leadIconSpace; const textSpace = textWidth + paddingX; const badgeStartX = textStartX + textSpace; const badgeSpace = badge && badgeSize ? badgeSize.width + paddingX : 0; const actionStartX = badgeStartX + badgeSpace; const actionSpace = actionIcon && actionSize ? actionSize.width + paddingX : 0; const contextStartX = actionStartX + actionSpace; const contextSpace = !hideContextMenuKebab && onContextMenu && contextSize ? contextSize.width + paddingX / 2 : 0; const pillWidth = contextStartX + contextSpace + paddingX / 2; const offsetX = verticalLayout ? (width - pillWidth) / 2 : 0; return { height, statusStartX, textStartX, actionStartX, contextStartX, badgeStartX, iconWidth, iconStartX, leadIconStartX, pillWidth, offsetX }; }, [ textSize, textHeight, textWidth, paddingY, paddingX, taskIconClass, taskIcon, taskIconPadding, statusIconSize, status, showStatusState, leadSize, leadIcon, statusSize, badgeSize, badge, actionIcon, actionSize, hideContextMenuKebab, onContextMenu, contextSize, verticalLayout, width ]); const scale = element.getGraph().getScale(); const nameLabel = ( {label} ); const runStatusModifier = getRunStatusModifier(status); const pillClasses = css( styles.topologyPipelinesPill, className, isHover && styles.modifiers.hover, runStatusModifier, selected && styles.modifiers.selected, onSelect && styles.modifiers.selectable ); // Force an update of the given pillRef when dependencies change const pillUpdatedRef = useCallback( (node: SVGGraphicsElement): void => { pillRef(node); }, // dependencies causing the pill rect to resize // eslint-disable-next-line react-hooks/exhaustive-deps [pillClasses, width, height] ); let filter: string; if (runStatusModifier === styles.modifiers.danger) { filter = createSvgIdUrl(NODE_SHADOW_FILTER_ID_DANGER); } else if (isHover && !nonShadowModifiers.includes(runStatusModifier)) { filter = createSvgIdUrl(NODE_SHADOW_FILTER_ID_HOVER); } const taskIconComponent = (taskIconClass || taskIcon) && ( ); const badgeLabel = badge ? ( ) : null; let badgeComponent: React.ReactNode; if (badgeLabel && badgeTooltip) { badgeComponent = ( {badgeLabel} ); } else if (badgeLabel && badgePopoverParams) { badgeComponent = ( e.stopPropagation()}> {badgeLabel} ); } else { badgeComponent = badgeLabel; } if (showStatusState && !scaleNode && hideDetailsAtMedium && detailsLevel !== ScaleDetailsLevel.high) { const statusBackgroundRadius = statusIconSize / 2 + 4; const upScale = 1 / scale; const { height: boundsHeight } = element.getBounds(); const translateX = verticalLayout ? width / 2 - statusBackgroundRadius * upScale : 0; const translateY = verticalLayout ? 0 : (boundsHeight - statusBackgroundRadius * 2 * upScale) / 2; return ( {status && (!hiddenDetailsShownStatuses || hiddenDetailsShownStatuses.includes(status)) ? ( {customStatusIcon ?? } ) : null} ); } const shadows = []; for (let i = shadowCount; i > 0; i--) { shadows.push( ); } return ( {shadows} {element.getLabel() !== label && !disableTooltip ? ( {nameLabel} ) : ( nameLabel )} {status && showStatusState && ( {customStatusIcon ?? } )} {leadIcon && ( {leadIcon} )} {taskIconComponent && (taskIconTooltip ? ( {taskIconComponent} ) : ( taskIconComponent ))} {badgeComponent} {actionIcon && ( <> )} {textSize && onContextMenu && !hideContextMenuKebab && ( <> )} {children} ); } ); export default TaskPill;