Skip to content

Commit f3343f8

Browse files
committed
Add new DAG Editor
1 parent a0a5daa commit f3343f8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5201
-419
lines changed

app/cdap/components/AbstractWidget/Comment/CommentBox.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export default function CommentBox({
129129
)}
130130
</div>
131131
);
132+
132133
if (!editMode) {
133134
return (
134135
<Card className={classes.root}>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright © 2025 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
import { createContext, useContext, useEffect } from 'react';
18+
19+
interface IFooterContext {
20+
show: boolean;
21+
setShow(val?: boolean): void;
22+
}
23+
24+
export const FooterContext = createContext<IFooterContext>({
25+
show: true,
26+
setShow() {
27+
return;
28+
},
29+
});
30+
31+
export function useHideFooterInPage() {
32+
const { setShow } = useContext(FooterContext);
33+
34+
useEffect(() => {
35+
setShow(false);
36+
37+
return () => setShow(true);
38+
}, []);
39+
40+
return setShow;
41+
}

app/cdap/components/PipelineCanvasActions/PipelineCommentsActionBtn.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import uuidv4 from 'uuid/v4';
2626
import { PipelineComments } from 'components/PipelineCanvasActions/PipelineComments';
2727
import { IPipelineComment } from 'components/PipelineCanvasActions/PipelineCommentsConstants';
2828
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
29+
import { ControlButton } from 'reactflow';
2930

3031
const useStyle = makeStyles<Theme, { toggle: boolean }>((theme) => {
3132
return {
@@ -79,13 +80,15 @@ interface IPipelineCommentsActionBtnProps {
7980
onChange: (comments: IPipelineComment[]) => void;
8081
comments: IPipelineComment[];
8182
disabled?: boolean;
83+
isV2?: boolean;
8284
}
8385

8486
function PipelineCommentsActionBtn({
8587
tooltip,
8688
onChange,
8789
comments = [],
8890
disabled,
91+
isV2 = false,
8992
}: IPipelineCommentsActionBtnProps) {
9093
const [localToggle, setLocalToggle] = React.useState(false);
9194
const [showMarker, setShowMarker] = React.useState(comments.length > 0);
@@ -122,6 +125,25 @@ function PipelineCommentsActionBtn({
122125
React.useEffect(() => {
123126
setShowMarker(Array.isArray(comments) && comments.length > 0);
124127
}, [comments]);
128+
129+
if (isV2) {
130+
return (
131+
<ClickAwayListener onClickAway={onClose}>
132+
<ControlButton title={tooltip} disabled={!showMarker && disabled} onClick={onClick}>
133+
{showMarker && <span className={classes.marker}></span>}
134+
<CommentRounded fontSize="small" />
135+
<PipelineComments
136+
comments={comments}
137+
onChange={onChange}
138+
anchorEl={anchorEl}
139+
disabled={disabled}
140+
onClose={onClose}
141+
/>
142+
</ControlButton>
143+
</ClickAwayListener>
144+
);
145+
}
146+
125147
return (
126148
<ClickAwayListener onClickAway={onClose}>
127149
<Tooltip

app/cdap/components/PluginContextMenu/index.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ import { copyToClipBoard } from 'services/Clipboard';
2121
import IconSVG from 'components/shared/IconSVG';
2222
import CommentIcon from 'components/AbstractWidget/Comment/CommentIcon';
2323

24-
export default function PluginContextMenu({
24+
export function getPluginMenuOptions({
2525
nodeId,
2626
getPluginConfiguration,
2727
getSelectedConnections,
2828
getSelectedNodes,
2929
onDelete,
30-
onOpen,
3130
onAddComment,
32-
}) {
33-
const PluginContextMenuOptions: IContextMenuOption[] = [
31+
}): IContextMenuOption[] {
32+
return [
3433
{
3534
name: 'plugin comment',
3635
label: 'Add a comment',
@@ -71,6 +70,26 @@ export default function PluginContextMenu({
7170
},
7271
},
7372
];
73+
}
74+
75+
export default function PluginContextMenu({
76+
nodeId,
77+
getPluginConfiguration,
78+
getSelectedConnections,
79+
getSelectedNodes,
80+
onDelete,
81+
onOpen,
82+
onAddComment,
83+
}) {
84+
const PluginContextMenuOptions: IContextMenuOption[] = getPluginMenuOptions({
85+
nodeId,
86+
getPluginConfiguration,
87+
getSelectedConnections,
88+
getSelectedNodes,
89+
onDelete,
90+
onAddComment,
91+
});
92+
7493
const onPluginContextMenuOpen = () => {
7594
onOpen(nodeId);
7695
};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright © 2025 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
import React, { useEffect, useMemo, useRef } from 'react';
18+
import {
19+
BaseEdge,
20+
ConnectionLineComponentProps,
21+
EdgeProps,
22+
Position,
23+
getSimpleBezierPath,
24+
getSmoothStepPath,
25+
} from 'reactflow';
26+
import { cartesianDistance } from '../../utils/geometry';
27+
28+
const END_MARKER_PREFIX = 'cdap-dag-edge-end-marker';
29+
const EDGE_BORDER_RADIUS = 20;
30+
31+
const EndMarkers = {
32+
FILLED_TRIANGLE: `${END_MARKER_PREFIX}-traiangular-filled`,
33+
FILLED_TRIANGLE_SELECTED: `${END_MARKER_PREFIX}-traiangular-filled-selected`,
34+
};
35+
36+
const endMarkersSvg = `
37+
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
38+
<defs>
39+
<marker
40+
id="${EndMarkers.FILLED_TRIANGLE}"
41+
viewBox="0 0 5 5"
42+
refX="4"
43+
refY="2.5"
44+
markerUnits="strokeWidth"
45+
markerWidth="5"
46+
markerHeight="5"
47+
orient="auto">
48+
<path d="M 0 0 L 5 2.5 L 0 5 z" fill="#b1b1b7"/>
49+
</marker>
50+
51+
<marker
52+
id="${EndMarkers.FILLED_TRIANGLE_SELECTED}"
53+
viewBox="0 0 6 6"
54+
refX="5"
55+
refY="3"
56+
markerUnits="strokeWidth"
57+
markerWidth="6"
58+
markerHeight="6"
59+
orient="auto">
60+
<path d="M 0 0 L 6 3 L 0 6 z" fill="#000"/>
61+
</marker>
62+
</defs>
63+
</svg>
64+
`;
65+
66+
function appendMarkersSvg() {
67+
const markers = Array.from(document.querySelectorAll(`marker[id*="${END_MARKER_PREFIX}"]`));
68+
if (!markers.length) {
69+
const svgWrapperEl = document.createElement('div');
70+
svgWrapperEl.innerHTML = endMarkersSvg;
71+
document.body.appendChild(svgWrapperEl);
72+
}
73+
}
74+
75+
function getEdgePath(
76+
sourceX: number,
77+
sourceY: number,
78+
sourcePosition: Position,
79+
targetX: number,
80+
targetY: number,
81+
targetPosition: Position,
82+
inProgress: boolean = false
83+
) {
84+
const distX = Math.abs(targetX - sourceX);
85+
const distY = Math.abs(targetY - sourceY);
86+
if (inProgress && distX < EDGE_BORDER_RADIUS && distY < 2 * EDGE_BORDER_RADIUS) {
87+
return getSimpleBezierPath({
88+
sourceX,
89+
sourceY,
90+
sourcePosition,
91+
targetX,
92+
targetY,
93+
targetPosition,
94+
});
95+
}
96+
97+
return getSmoothStepPath({
98+
sourceX,
99+
sourceY,
100+
sourcePosition,
101+
targetX,
102+
targetY,
103+
targetPosition,
104+
borderRadius: EDGE_BORDER_RADIUS,
105+
});
106+
}
107+
108+
export function StandardEdge({ id, sourceX, sourceY, targetX, targetY, selected, data }: EdgeProps) {
109+
const [path] = getEdgePath(
110+
sourceX,
111+
sourceY,
112+
data?.isSourceAtBottom ? Position.Bottom : Position.Right,
113+
targetX,
114+
targetY,
115+
Position.Left
116+
);
117+
118+
useEffect(() => {
119+
appendMarkersSvg();
120+
}, []);
121+
122+
let markerEnd = selected
123+
? `url(#${EndMarkers.FILLED_TRIANGLE_SELECTED})`
124+
: `url(#${EndMarkers.FILLED_TRIANGLE})`;
125+
if (Math.abs(targetX - sourceX) < EDGE_BORDER_RADIUS) {
126+
markerEnd = '';
127+
}
128+
129+
return <BaseEdge id={id} path={path} markerEnd={markerEnd} interactionWidth={20} />;
130+
}
131+
132+
export function EdgeInProgress({
133+
fromX,
134+
fromY,
135+
toX,
136+
toY,
137+
fromPosition,
138+
toPosition,
139+
}: ConnectionLineComponentProps) {
140+
const pathRef = useRef<SVGPathElement>(null);
141+
const [path] = useMemo(
142+
() => getEdgePath(fromX, fromY, fromPosition, toX, toY, toPosition, true),
143+
[fromX, fromY, toX, toY, toPosition]
144+
);
145+
146+
useEffect(() => {
147+
if (pathRef.current) {
148+
pathRef.current.setAttribute('d', path);
149+
}
150+
}, [path, pathRef.current]);
151+
152+
useEffect(() => {
153+
appendMarkersSvg();
154+
}, []);
155+
156+
return (
157+
<path
158+
d={path}
159+
fill="none"
160+
markerEnd={`url(#${EndMarkers.FILLED_TRIANGLE})`}
161+
className="react-flow__edge-path"
162+
ref={pathRef}
163+
/>
164+
);
165+
}
166+
167+
export const EDGE_TYPES = {
168+
default: StandardEdge,
169+
standard: StandardEdge,
170+
};

0 commit comments

Comments
 (0)