Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pipelines): add support for custom GroupLabelComponent #221

Merged
merged 1 commit into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
EdgeCreationTypes,
useHover,
ScaleDetailsLevel,
RunStatus
RunStatus,
TaskGroupPillLabel
} from '@patternfly/react-topology';
import { DEFAULT_TASK_HEIGHT, GROUP_TASK_WIDTH } from './createDemoPipelineGroupsNodes';

Expand Down Expand Up @@ -42,6 +43,7 @@ const DemoTaskGroup: React.FunctionComponent<DemoTaskGroupProps> = ({ element, .
collapsible
collapsedWidth={GROUP_TASK_WIDTH}
collapsedHeight={DEFAULT_TASK_HEIGHT}
GroupLabelComponent={TaskGroupPillLabel}
element={element as Node}
centerLabelOnEdge
recreateLayoutOnCollapseChange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
GraphElement,
LabelPosition,
observer,
ScaleDetailsLevel,
WithContextMenuProps,
WithDragNodeProps,
WithSelectionProps
Expand All @@ -18,14 +17,14 @@ type DemoPipelinesGroupProps = {

const DemoPipelinesGroup: React.FunctionComponent<DemoPipelinesGroupProps> = ({ element }) => {
const data = element.getData();
const detailsLevel = element.getGraph().getDetailsLevel();

return (
<DefaultTaskGroup
element={element}
collapsible={false}
showLabel={detailsLevel === ScaleDetailsLevel.high}
labelPosition={LabelPosition.top}
showLabelOnHover
hideDetailsAtMedium
badge={data?.badge}
/>
);
Expand Down
5 changes: 3 additions & 2 deletions packages/module/src/components/nodes/labels/NodeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import LabelBadge from './LabelBadge';
import LabelContextMenu from './LabelContextMenu';
import LabelIcon from './LabelIcon';
import LabelActionIcon from './LabelActionIcon';
import { BadgeLocation, LabelPosition, NodeStatus } from '../../../types';
import { BadgeLocation, LabelPosition, Node, NodeStatus } from '../../../types';

type NodeLabelProps = {
export type NodeLabelProps = {
element?: Node;
children?: string;
className?: string;
paddingX?: number;
Expand Down
1 change: 1 addition & 0 deletions packages/module/src/components/nodes/labels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as LabelBadge } from './LabelBadge';
export { default as LabelContextMenu } from './LabelContextMenu';
export { default as LabelIcon } from './LabelIcon';
export { default as NodeLabel } from './NodeLabel';
export type { NodeLabelProps } from './NodeLabel';
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import DefaultTaskGroupCollapsed from './DefaultTaskGroupCollapsed';
import DefaultTaskGroupExpanded from './DefaultTaskGroupExpanded';
import { RunStatus } from '../../types';
import { DEFAULT_SPACER_NODE_TYPE } from '../../const';
import { TaskGroupPillLabelProps } from './TaskGroupPillLabel';

export interface EdgeCreationTypes {
spacerNodeType?: string;
Expand Down Expand Up @@ -62,6 +63,8 @@ export interface DefaultTaskGroupProps {
truncateLength?: number;
/** Space between the label and the group. Defaults to 17 */
labelOffset?: number;
/** Label to show for the group, Defaults to NodeLabel, only applicable to expanded groups */
GroupLabelComponent?: React.FC<TaskGroupPillLabelProps>;
/** Center the label on the edge, overrides the label offset, only applicable to expanded groups */
centerLabelOnEdge?: boolean;
/** The Icon class to show in the label, ignored when labelIcon is specified */
Expand Down Expand Up @@ -126,7 +129,6 @@ type PipelinesDefaultGroupInnerProps = Omit<DefaultTaskGroupProps, 'element'> &

const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerProps> = observer(
({
className,
element,
badge,
onCollapseChange,
Expand Down Expand Up @@ -207,7 +209,6 @@ const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerP
if (element.isCollapsed()) {
return (
<DefaultTaskGroupCollapsed
className={className}
element={element}
shadowCount={collapsedShadowCount}
onCollapseChange={handleCollapse}
Expand All @@ -216,10 +217,7 @@ const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerP
/>
);
}
return (
// TODO: Support status indicators on expanded state.
<DefaultTaskGroupExpanded className={className} element={element} onCollapseChange={handleCollapse} {...rest} />
);
return <DefaultTaskGroupExpanded element={element} badge={badge} onCollapseChange={handleCollapse} {...rest} />;
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-ic
import NodeLabel from '../../../components/nodes/labels/NodeLabel';
import { Layer } from '../../../components/layers';
import { GROUPS_LAYER, TOP_LAYER } from '../../../const';
import { maxPadding, useCombineRefs, useHover, useSize } from '../../../utils';
import { AnchorEnd, isGraph, LabelPosition, Node, NodeStyle, ScaleDetailsLevel } from '../../../types';
import { useCombineRefs, useHover, useSize } from '../../../utils';
import { AnchorEnd, isGraph, LabelPosition, Node, ScaleDetailsLevel } from '../../../types';
import { useAnchor, useDragNode } from '../../../behavior';
import { DagreLayoutOptions, TOP_TO_BOTTOM } from '../../../layouts';
import TaskGroupSourceAnchor from '../anchors/TaskGroupSourceAnchor';
Expand All @@ -28,6 +28,8 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro
showLabel = true,
showLabelOnHover,
hideDetailsAtMedium,
status,
GroupLabelComponent = NodeLabel,
truncateLength,
canDrop,
dropTarget,
Expand All @@ -52,13 +54,14 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro
const [hovered, hoverRef] = useHover(200, 500);
const [labelHover, labelHoverRef] = useHover(0);
const dragLabelRef = useDragNode()[1];
const [labelSize, labelRef] = useSize([centerLabelOnEdge]);
const refs = useCombineRefs<SVGPathElement>(hoverRef, dragNodeRef);
const isHover = hover !== undefined ? hover : hovered || labelHover;
const [labelSize, labelRef] = useSize([centerLabelOnEdge]);
const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM;
const groupLabelPosition = labelPosition ?? element.getLabelPosition() ?? LabelPosition.bottom;
let parent = element.getParent();
const detailsLevel = element.getGraph().getDetailsLevel();

let altGroup = false;
while (!isGraph(parent)) {
altGroup = !altGroup;
Expand Down Expand Up @@ -105,41 +108,37 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro
AnchorEnd.target
);

const children = element.getNodes().filter((c) => c.isVisible());

// cast to number and coerce
const padding = maxPadding(element.getStyle<NodeStyle>().padding ?? 17);

const { minX, minY, maxX, maxY } = children.reduce(
(acc, child) => {
const bounds = child.getBounds();
return {
minX: Math.min(acc.minX, bounds.x - padding),
minY: Math.min(acc.minY, bounds.y - padding),
maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding),
maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding)
};
},
{ minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 }
);
const bounds = element.getBounds();

const [labelX, labelY] = React.useMemo(() => {
if (!showLabel || !(label || element.getLabel())) {
return [0, 0];
}
switch (groupLabelPosition) {
case LabelPosition.top:
return [minX + (maxX - minX) / 2, -minY + (centerLabelOnEdge ? 0 : labelOffset)];
return [bounds.x + bounds.width / 2, -bounds.y + (centerLabelOnEdge ? 0 : labelOffset)];
case LabelPosition.right:
return [maxX + (centerLabelOnEdge ? 0 : labelOffset), minY + (maxY - minY) / 2];
return [bounds.x + bounds.width + (centerLabelOnEdge ? 0 : labelOffset), bounds.y + bounds.height / 2];
case LabelPosition.left:
return [centerLabelOnEdge ? minX : labelOffset, minY + (maxY - minY) / 2];
return [centerLabelOnEdge ? bounds.x : labelOffset, bounds.y + bounds.height / 2];
case LabelPosition.bottom:
default:
return [minX + (maxX - minX) / 2, maxY + (centerLabelOnEdge ? 0 : labelOffset)];
return [bounds.x + bounds.width / 2, bounds.y + bounds.height + (centerLabelOnEdge ? 0 : labelOffset)];
}
}, [showLabel, label, element, groupLabelPosition, minX, maxX, minY, centerLabelOnEdge, labelOffset, maxY]);
}, [
showLabel,
label,
element,
groupLabelPosition,
bounds.x,
bounds.width,
bounds.y,
bounds.height,
centerLabelOnEdge,
labelOffset
]);

const children = element.getNodes().filter((c) => c.isVisible());
if (children.length === 0) {
return null;
}
Expand Down Expand Up @@ -170,17 +169,20 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro

const groupLabel = labelShown ? (
<g ref={labelHoverRef} transform={isHover ? `scale(${labelScale})` : undefined}>
<NodeLabel
<GroupLabelComponent
element={element}
boxRef={labelRef}
className={styles.topologyGroupLabel}
x={labelX * labelPositionScale}
y={labelY * labelPositionScale}
position={labelPosition}
centerLabelOnEdge={centerLabelOnEdge}
runStatus={status}
paddingX={8}
paddingY={5}
dragRef={dragNodeRef ? dragLabelRef : undefined}
status={element.getNodeStatus()}
selected={selected}
secondaryLabel={secondaryLabel}
truncateLength={truncateLength}
badge={badge}
Expand All @@ -194,12 +196,12 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro
labelIconPadding={labelIconPadding}
onContextMenu={onContextMenu}
contextMenuOpen={contextMenuOpen}
hover={isHover}
hover={isHover || labelHover}
actionIcon={collapsible ? <CollapseIcon /> : undefined}
onActionIconClick={() => onCollapseChange(element, true)}
>
{label || element.getLabel()}
</NodeLabel>
</GroupLabelComponent>
</g>
) : null;

Expand All @@ -208,10 +210,10 @@ const DefaultTaskGroupExpanded: React.FunctionComponent<Omit<DefaultTaskGroupPro
<Layer id={GROUPS_LAYER}>
<g ref={refs} onContextMenu={onContextMenu} onClick={onSelect} className={innerGroupClassName}>
<rect
x={minX}
y={minY}
width={maxX - minX}
height={maxY - minY}
x={bounds.x}
y={bounds.y}
width={bounds.width}
height={bounds.height}
className={styles.topologyGroupBackground}
/>
</g>
Expand Down
110 changes: 110 additions & 0 deletions packages/module/src/pipelines/components/groups/TaskGroupPillLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import styles from '../../../css/topology-components';
import TaskPill, { TaskPillProps } from '../nodes/TaskPill';
import { NodeLabelProps } from '../../../components';
import { RunStatus } from '../../types';
import useCombineRefs from '../../../utils/useCombineRefs';
import { useSize } from '../../../utils';
import { LabelPosition, ScaleDetailsLevel } from '../../../types';

export type TaskGroupPillLabelProps = {
shadowCount?: number;
runStatus?: RunStatus;
labelOffset?: number;
} & NodeLabelProps &
Omit<TaskPillProps, 'status' | 'pillRef'>;

const TaskGroupPillLabel: React.FC<TaskGroupPillLabelProps> = ({
element,
labelOffset = 17,
badge,
badgeColor,
badgeTextColor,
badgeBorderColor,
badgeClassName,
runStatus,
truncateLength,
boxRef,
position,
centerLabelOnEdge,
onContextMenu,
contextMenuOpen,
actionIcon,
onActionIconClick,
...rest
}) => {
const [labelSize, labelRef] = useSize([]);
const pillRef = useCombineRefs(boxRef, labelRef);
const labelWidth = labelSize?.width || 0;
const labelHeight = labelSize?.height || 0;

const bounds = element.getBounds();

const detailsLevel = element.getGraph().getDetailsLevel();
const scale = element.getGraph().getScale();
const medScale = element.getGraph().getDetailsLevelThresholds().medium;
const labelScale = detailsLevel !== ScaleDetailsLevel.high ? Math.min(1 / scale, 1 / medScale) : 1;
const labelPositionScale = detailsLevel !== ScaleDetailsLevel.high ? 1 / labelScale : 1;

const { startX, startY } = React.useMemo(() => {
let startX: number;
let startY: number;
const scaledWidth = labelWidth / labelPositionScale;
const scaledHeight = labelHeight / labelPositionScale;

if (position === LabelPosition.top) {
startX = bounds.x + bounds.width / 2 - scaledWidth / 2;
startY = bounds.y - (centerLabelOnEdge ? scaledHeight / 2 : labelOffset);
} else if (position === LabelPosition.right) {
startX = bounds.x + bounds.width + (centerLabelOnEdge ? -scaledWidth / 2 : labelOffset);
startY = bounds.y + bounds.height / 2;
} else if (position === LabelPosition.left) {
startX = bounds.x - (centerLabelOnEdge ? scaledWidth / 2 : scaledWidth + labelOffset);
startY = bounds.y + bounds.height / 2;
} else {
startX = bounds.x + bounds.width / 2 - scaledWidth / 2;
startY = bounds.y + bounds.height + (centerLabelOnEdge ? -scaledHeight / 2 : labelOffset);
}
return { startX, startY };
}, [
labelPositionScale,
position,
bounds.width,
bounds.x,
bounds.y,
bounds.height,
centerLabelOnEdge,
labelHeight,
labelOffset,
labelWidth
]);

return (
<TaskPill
{...rest}
element={element}
width={labelWidth}
pillRef={pillRef}
actionIcon={actionIcon}
onActionIconClick={onActionIconClick}
className={styles.topologyNodeLabel}
status={runStatus}
x={startX * labelPositionScale}
y={startY * labelPositionScale}
paddingX={8}
paddingY={5}
scaleNode={false}
truncateLength={truncateLength}
badge={badge}
badgeColor={badgeColor}
badgeTextColor={badgeTextColor}
badgeBorderColor={badgeBorderColor}
badgeClassName={badgeClassName}
onContextMenu={onContextMenu}
contextMenuOpen={contextMenuOpen}
/>
);
};

export default observer(TaskGroupPillLabel);
1 change: 1 addition & 0 deletions packages/module/src/pipelines/components/groups/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export type { EdgeCreationTypes } from './DefaultTaskGroup';
export { default as DefaultTaskGroup } from './DefaultTaskGroup';
export { default as DefaultTaskGroupExpanded } from './DefaultTaskGroupExpanded';
export { default as DefaultTaskGroupCollapsed } from './DefaultTaskGroupCollapsed';
export { default as TaskGroupPillLabel } from './TaskGroupPillLabel';
Loading
Loading