Skip to content

Commit

Permalink
feat(pipelines): add taskpill component
Browse files Browse the repository at this point in the history
fix(pipelines): add correct location, scaling, and anchors for expanded task group's pill labels. (#10)

fix(pipelines): add status prop to group labels

update import statement
  • Loading branch information
jenny-s51 committed Jun 24, 2024
1 parent d6479b5 commit 14c3186
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 438 deletions.
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

0 comments on commit 14c3186

Please sign in to comment.