darabos commited on
Commit
8ef8567
·
unverified ·
2 Parent(s): 4ee559d aa9e014

Merge pull request #12 from lynxkite/darabos-shadows

Browse files
lynxkite-app/tests/test_main.py CHANGED
@@ -44,6 +44,8 @@ def test_save_and_load():
44
  "params": {"param1": "value"},
45
  },
46
  "position": {"x": -493.5496596237119, "y": 20.90123252513356},
 
 
47
  }
48
  ],
49
  "edges": [],
 
44
  "params": {"param1": "value"},
45
  },
46
  "position": {"x": -493.5496596237119, "y": 20.90123252513356},
47
+ "width": 100,
48
+ "height": 100,
49
  }
50
  ],
51
  "edges": [],
lynxkite-app/uv.lock CHANGED
The diff for this file is too large to render. See raw diff
 
lynxkite-app/web/src/apiTypes.ts CHANGED
@@ -34,6 +34,8 @@ export interface WorkspaceNode {
34
  type: string;
35
  data: WorkspaceNodeData;
36
  position: Position;
 
 
37
  [k: string]: unknown;
38
  }
39
  export interface WorkspaceNodeData {
 
34
  type: string;
35
  data: WorkspaceNodeData;
36
  position: Position;
37
+ width: number;
38
+ height: number;
39
  [k: string]: unknown;
40
  }
41
  export interface WorkspaceNodeData {
lynxkite-app/web/src/index.css CHANGED
@@ -76,6 +76,7 @@ body {
76
  .lynxkite-node {
77
  box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
78
  border-radius: 4px;
 
79
  background: white;
80
  display: flex;
81
  flex-direction: column;
 
76
  .lynxkite-node {
77
  box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
78
  border-radius: 4px;
79
+ overflow: hidden;
80
  background: white;
81
  display: flex;
82
  flex-direction: column;
lynxkite-app/web/src/workspace/Workspace.tsx CHANGED
@@ -16,7 +16,7 @@ import {
16
  useUpdateNodeInternals,
17
  } from "@xyflow/react";
18
  import axios from "axios";
19
- import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
20
  import { Link } from "react-router";
21
  import useSWR, { type Fetcher } from "swr";
22
  import { WebsocketProvider } from "y-websocket";
@@ -61,6 +61,7 @@ export default function Workspace(props: any) {
61
  function LynxKiteFlow() {
62
  const updateNodeInternals = useUpdateNodeInternals();
63
  const reactFlow = useReactFlow();
 
64
  const [nodes, setNodes] = useState([] as Node[]);
65
  const [edges, setEdges] = useState([] as Edge[]);
66
  const path = usePath().replace(/^[/]edit[/]/, "");
@@ -210,7 +211,7 @@ function LynxKiteFlow() {
210
  if (event.key === "/") {
211
  event.preventDefault();
212
  setNodeSearchSettings({
213
- pos: { x: 100, y: 100 },
214
  boxes: catalog.data![state.workspace.env!],
215
  });
216
  } else if (event.key === "r") {
@@ -225,6 +226,39 @@ function LynxKiteFlow() {
225
  };
226
  }, [catalog.data, nodeSearchSettings, state.workspace.env]);
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  function isTypingInFormElement() {
229
  const activeElement = document.activeElement;
230
  return (
@@ -504,7 +538,12 @@ function LynxKiteFlow() {
504
  </Tooltip>
505
  </div>
506
  </div>
507
- <div style={{ height: "100%", width: "100vw" }} onDragOver={onDragOver} onDrop={onDrop}>
 
 
 
 
 
508
  <LynxKiteState.Provider value={state}>
509
  <ReactFlow
510
  nodes={nodes}
@@ -517,9 +556,13 @@ function LynxKiteFlow() {
517
  onPaneClick={toggleNodeSearch}
518
  onConnect={onConnect}
519
  proOptions={{ hideAttribution: true }}
520
- maxZoom={1}
521
  minZoom={0.2}
522
  zoomOnScroll={false}
 
 
 
 
523
  preventScrolling={false}
524
  defaultEdgeOptions={{
525
  markerEnd: {
@@ -533,6 +576,7 @@ function LynxKiteFlow() {
533
  stroke: "black",
534
  },
535
  }}
 
536
  >
537
  <Controls />
538
  {nodeSearchSettings && (
 
16
  useUpdateNodeInternals,
17
  } from "@xyflow/react";
18
  import axios from "axios";
19
+ import { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
20
  import { Link } from "react-router";
21
  import useSWR, { type Fetcher } from "swr";
22
  import { WebsocketProvider } from "y-websocket";
 
61
  function LynxKiteFlow() {
62
  const updateNodeInternals = useUpdateNodeInternals();
63
  const reactFlow = useReactFlow();
64
+ const reactFlowContainer = useRef<HTMLDivElement>(null);
65
  const [nodes, setNodes] = useState([] as Node[]);
66
  const [edges, setEdges] = useState([] as Edge[]);
67
  const path = usePath().replace(/^[/]edit[/]/, "");
 
211
  if (event.key === "/") {
212
  event.preventDefault();
213
  setNodeSearchSettings({
214
+ pos: getBestPosition(),
215
  boxes: catalog.data![state.workspace.env!],
216
  });
217
  } else if (event.key === "r") {
 
226
  };
227
  }, [catalog.data, nodeSearchSettings, state.workspace.env]);
228
 
229
+ function getBestPosition() {
230
+ const W = reactFlowContainer.current!.clientWidth;
231
+ const H = reactFlowContainer.current!.clientHeight;
232
+ const w = 200;
233
+ const h = 200;
234
+ const SPEED = 20;
235
+ const GAP = 50;
236
+ const pos = { x: 100, y: 100 };
237
+ while (pos.y < H) {
238
+ // Find a position that is not occupied by a node.
239
+ const fpos = reactFlow.screenToFlowPosition(pos);
240
+ const occupied = state.workspace.nodes!.some((n) => {
241
+ const np = n.position;
242
+ return (
243
+ np.x < fpos.x + w + GAP &&
244
+ np.x + n.width + GAP > fpos.x &&
245
+ np.y < fpos.y + h + GAP &&
246
+ np.y + n.height + GAP > fpos.y
247
+ );
248
+ });
249
+ if (!occupied) {
250
+ return pos;
251
+ }
252
+ // Move the position to the right and down until we find a free spot.
253
+ pos.x += SPEED;
254
+ if (pos.x + w > W) {
255
+ pos.x = 100;
256
+ pos.y += SPEED;
257
+ }
258
+ }
259
+ return { x: 100, y: 100 };
260
+ }
261
+
262
  function isTypingInFormElement() {
263
  const activeElement = document.activeElement;
264
  return (
 
538
  </Tooltip>
539
  </div>
540
  </div>
541
+ <div
542
+ style={{ height: "100%", width: "100vw" }}
543
+ onDragOver={onDragOver}
544
+ onDrop={onDrop}
545
+ ref={reactFlowContainer}
546
+ >
547
  <LynxKiteState.Provider value={state}>
548
  <ReactFlow
549
  nodes={nodes}
 
556
  onPaneClick={toggleNodeSearch}
557
  onConnect={onConnect}
558
  proOptions={{ hideAttribution: true }}
559
+ maxZoom={10}
560
  minZoom={0.2}
561
  zoomOnScroll={false}
562
+ panOnScroll={true}
563
+ panOnDrag={false}
564
+ selectionOnDrag={true}
565
+ panOnScrollSpeed={1}
566
  preventScrolling={false}
567
  defaultEdgeOptions={{
568
  markerEnd: {
 
576
  stroke: "black",
577
  },
578
  }}
579
+ fitViewOptions={{ maxZoom: 1 }}
580
  >
581
  <Controls />
582
  {nodeSearchSettings && (
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -237,7 +237,7 @@ export default function NodeParameter({ name, value, meta, data, setParam }: Nod
237
  <textarea
238
  className="textarea textarea-bordered w-full"
239
  rows={6}
240
- value={value}
241
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
242
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
243
  />
 
237
  <textarea
238
  className="textarea textarea-bordered w-full"
239
  rows={6}
240
+ value={value || ""}
241
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
242
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
243
  />
lynxkite-app/web/tests/lynxkite.ts CHANGED
@@ -53,14 +53,8 @@ export class Workspace {
53
  }
54
 
55
  async addBox(boxName) {
56
- //TODO: Support passing box parameters (id, position, etc.)
57
  const allBoxes = await this.getBoxes().all();
58
- if (allBoxes) {
59
- // Avoid overlapping with existing nodes
60
- const numNodes = allBoxes.length || 1;
61
- await this.page.mouse.wheel(0, numNodes * 400);
62
- }
63
-
64
  await this.page.locator(".ws-name").click();
65
  await this.page.keyboard.press("/");
66
  await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
 
53
  }
54
 
55
  async addBox(boxName) {
56
+ // TODO: Support passing box parameters.
57
  const allBoxes = await this.getBoxes().all();
 
 
 
 
 
 
58
  await this.page.locator(".ws-name").click();
59
  await this.page.keyboard.press("/");
60
  await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -40,12 +40,13 @@ class WorkspaceNodeData(BaseConfig):
40
 
41
 
42
  class WorkspaceNode(BaseConfig):
43
- # The naming of these attributes matches the ones for the NodeBase type in React flow
44
- # modyfing them will break the frontend.
45
  id: str
46
  type: str
47
  data: WorkspaceNodeData
48
  position: Position
 
 
49
  _crdt: pycrdt.Map
50
 
51
  def publish_started(self):
@@ -202,27 +203,27 @@ class Workspace(BaseConfig):
202
  nc["data"] = pycrdt.Map()
203
  np._crdt = nc
204
 
205
- def add_node(self, func):
206
  """For convenience in e.g. tests."""
207
  random_string = os.urandom(4).hex()
208
- node = WorkspaceNode(
209
- id=f"{func.__op__.name} {random_string}",
210
- type=func.__op__.type,
211
- data=WorkspaceNodeData(
212
- title=func.__op__.name,
213
- params={},
214
- display=None,
215
- input_metadata=None,
216
- error=None,
217
- status=NodeStatus.planned,
218
- ),
219
- position=Position(x=0, y=0),
220
- )
221
  self.nodes.append(node)
222
  return node
223
 
224
  def add_edge(
225
- self, source: WorkspaceNode, sourceHandle: str, target: WorkspaceNode, targetHandle: str
 
 
 
 
226
  ):
227
  """For convenience in e.g. tests."""
228
  edge = WorkspaceEdge(
 
40
 
41
 
42
  class WorkspaceNode(BaseConfig):
43
+ # Most of these fields are shared with ReactFlow.
 
44
  id: str
45
  type: str
46
  data: WorkspaceNodeData
47
  position: Position
48
+ width: float
49
+ height: float
50
  _crdt: pycrdt.Map
51
 
52
  def publish_started(self):
 
203
  nc["data"] = pycrdt.Map()
204
  np._crdt = nc
205
 
206
+ def add_node(self, func=None, **kwargs):
207
  """For convenience in e.g. tests."""
208
  random_string = os.urandom(4).hex()
209
+ if func:
210
+ kwargs["type"] = func.__op__.type
211
+ kwargs["data"] = WorkspaceNodeData(title=func.__op__.name, params={})
212
+ kwargs.setdefault("type", "basic")
213
+ kwargs.setdefault("id", f"{kwargs['data'].title} {random_string}")
214
+ kwargs.setdefault("position", Position(x=0, y=0))
215
+ kwargs.setdefault("width", 100)
216
+ kwargs.setdefault("height", 100)
217
+ node = WorkspaceNode(**kwargs)
 
 
 
 
218
  self.nodes.append(node)
219
  return node
220
 
221
  def add_edge(
222
+ self,
223
+ source: WorkspaceNode,
224
+ sourceHandle: str,
225
+ target: WorkspaceNode,
226
+ targetHandle: str,
227
  ):
228
  """For convenience in e.g. tests."""
229
  edge = WorkspaceEdge(
lynxkite-core/tests/test_workspace.py CHANGED
@@ -6,21 +6,15 @@ from lynxkite.core import workspace
6
 
7
  def test_save_load():
8
  ws = workspace.Workspace(env="test")
9
- ws.nodes.append(
10
- workspace.WorkspaceNode(
11
- id="1",
12
- type="node_type",
13
- data=workspace.WorkspaceNodeData(title="Node 1", params={}),
14
- position=workspace.Position(x=0, y=0),
15
- )
16
  )
17
- ws.nodes.append(
18
- workspace.WorkspaceNode(
19
- id="2",
20
- type="node_type",
21
- data=workspace.WorkspaceNodeData(title="Node 2", params={}),
22
- position=workspace.Position(x=0, y=0),
23
- )
24
  )
25
  ws.edges.append(
26
  workspace.WorkspaceEdge(
@@ -72,21 +66,15 @@ def populate_ops_catalog():
72
 
73
  def test_update_metadata():
74
  ws = workspace.Workspace(env="test")
75
- ws.nodes.append(
76
- workspace.WorkspaceNode(
77
- id="1",
78
- type="basic",
79
- data=workspace.WorkspaceNodeData(title="Test Operation", params={}),
80
- position=workspace.Position(x=0, y=0),
81
- )
82
  )
83
- ws.nodes.append(
84
- workspace.WorkspaceNode(
85
- id="2",
86
- type="basic",
87
- data=workspace.WorkspaceNodeData(title="Unknown Operation", params={}),
88
- position=workspace.Position(x=0, y=0),
89
- )
90
  )
91
  ws.update_metadata()
92
  assert ws.nodes[0].data.meta.name == "Test Operation"
 
6
 
7
  def test_save_load():
8
  ws = workspace.Workspace(env="test")
9
+ ws.add_node(
10
+ id="1",
11
+ type="node_type",
12
+ data=workspace.WorkspaceNodeData(title="Node 1", params={}),
 
 
 
13
  )
14
+ ws.add_node(
15
+ id="2",
16
+ type="node_type",
17
+ data=workspace.WorkspaceNodeData(title="Node 2", params={}),
 
 
 
18
  )
19
  ws.edges.append(
20
  workspace.WorkspaceEdge(
 
66
 
67
  def test_update_metadata():
68
  ws = workspace.Workspace(env="test")
69
+ ws.add_node(
70
+ id="1",
71
+ type="basic",
72
+ data=workspace.WorkspaceNodeData(title="Test Operation", params={}),
 
 
 
73
  )
74
+ ws.add_node(
75
+ id="2",
76
+ type="basic",
77
+ data=workspace.WorkspaceNodeData(title="Unknown Operation", params={}),
 
 
 
78
  )
79
  ws.update_metadata()
80
  assert ws.nodes[0].data.meta.name == "Test Operation"
lynxkite-graph-analytics/tests/test_lynxkite_ops.py CHANGED
@@ -8,13 +8,11 @@ from lynxkite_graph_analytics.core import Bundle, execute, ENV
8
 
9
  async def test_execute_operation_not_in_catalog():
10
  ws = workspace.Workspace(env=ENV)
11
- ws.nodes.append(
12
- workspace.WorkspaceNode(
13
- id="1",
14
- type="node_type",
15
- data=workspace.WorkspaceNodeData(title="Non existing op", params={}),
16
- position=workspace.Position(x=0, y=0),
17
- )
18
  )
19
  await execute(ws)
20
  assert ws.nodes[0].data.error == "Operation not found in catalog"
@@ -43,37 +41,29 @@ async def test_execute_operation_inputs_correct_cast():
43
  return bundle
44
 
45
  ws = workspace.Workspace(env="test")
46
- ws.nodes.append(
47
- workspace.WorkspaceNode(
48
- id="1",
49
- type="node_type",
50
- data=workspace.WorkspaceNodeData(title="Create Bundle", params={}),
51
- position=workspace.Position(x=0, y=0),
52
- )
53
  )
54
- ws.nodes.append(
55
- workspace.WorkspaceNode(
56
- id="2",
57
- type="node_type",
58
- data=workspace.WorkspaceNodeData(title="Bundle to Graph", params={}),
59
- position=workspace.Position(x=100, y=0),
60
- )
61
  )
62
- ws.nodes.append(
63
- workspace.WorkspaceNode(
64
- id="3",
65
- type="node_type",
66
- data=workspace.WorkspaceNodeData(title="Graph to Bundle", params={}),
67
- position=workspace.Position(x=200, y=0),
68
- )
69
  )
70
- ws.nodes.append(
71
- workspace.WorkspaceNode(
72
- id="4",
73
- type="node_type",
74
- data=workspace.WorkspaceNodeData(title="Dataframe to Bundle", params={}),
75
- position=workspace.Position(x=300, y=0),
76
- )
77
  )
78
  ws.edges = [
79
  workspace.WorkspaceEdge(
@@ -109,29 +99,23 @@ async def test_multiple_inputs():
109
  return a < b
110
 
111
  ws = workspace.Workspace(env="test")
112
- ws.nodes.append(
113
- workspace.WorkspaceNode(
114
- id="one",
115
- type="cool",
116
- data=workspace.WorkspaceNodeData(title="One", params={}),
117
- position=workspace.Position(x=0, y=0),
118
- )
119
  )
120
- ws.nodes.append(
121
- workspace.WorkspaceNode(
122
- id="two",
123
- type="cool",
124
- data=workspace.WorkspaceNodeData(title="Two", params={}),
125
- position=workspace.Position(x=100, y=0),
126
- )
127
  )
128
- ws.nodes.append(
129
- workspace.WorkspaceNode(
130
- id="smaller",
131
- type="cool",
132
- data=workspace.WorkspaceNodeData(title="Smaller?", params={}),
133
- position=workspace.Position(x=200, y=0),
134
- )
135
  )
136
  ws.edges = [
137
  workspace.WorkspaceEdge(
 
8
 
9
  async def test_execute_operation_not_in_catalog():
10
  ws = workspace.Workspace(env=ENV)
11
+ ws.add_node(
12
+ id="1",
13
+ type="node_type",
14
+ data=workspace.WorkspaceNodeData(title="Non existing op", params={}),
15
+ position=workspace.Position(x=0, y=0),
 
 
16
  )
17
  await execute(ws)
18
  assert ws.nodes[0].data.error == "Operation not found in catalog"
 
41
  return bundle
42
 
43
  ws = workspace.Workspace(env="test")
44
+ ws.add_node(
45
+ id="1",
46
+ type="node_type",
47
+ data=workspace.WorkspaceNodeData(title="Create Bundle", params={}),
48
+ position=workspace.Position(x=0, y=0),
 
 
49
  )
50
+ ws.add_node(
51
+ id="2",
52
+ type="node_type",
53
+ data=workspace.WorkspaceNodeData(title="Bundle to Graph", params={}),
54
+ position=workspace.Position(x=100, y=0),
 
 
55
  )
56
+ ws.add_node(
57
+ id="3",
58
+ type="node_type",
59
+ data=workspace.WorkspaceNodeData(title="Graph to Bundle", params={}),
60
+ position=workspace.Position(x=200, y=0),
 
 
61
  )
62
+ ws.add_node(
63
+ id="4",
64
+ type="node_type",
65
+ data=workspace.WorkspaceNodeData(title="Dataframe to Bundle", params={}),
66
+ position=workspace.Position(x=300, y=0),
 
 
67
  )
68
  ws.edges = [
69
  workspace.WorkspaceEdge(
 
99
  return a < b
100
 
101
  ws = workspace.Workspace(env="test")
102
+ ws.add_node(
103
+ id="one",
104
+ type="cool",
105
+ data=workspace.WorkspaceNodeData(title="One", params={}),
106
+ position=workspace.Position(x=0, y=0),
 
 
107
  )
108
+ ws.add_node(
109
+ id="two",
110
+ type="cool",
111
+ data=workspace.WorkspaceNodeData(title="Two", params={}),
112
+ position=workspace.Position(x=100, y=0),
 
 
113
  )
114
+ ws.add_node(
115
+ id="smaller",
116
+ type="cool",
117
+ data=workspace.WorkspaceNodeData(title="Smaller?", params={}),
118
+ position=workspace.Position(x=200, y=0),
 
 
119
  )
120
  ws.edges = [
121
  workspace.WorkspaceEdge(
lynxkite-graph-analytics/tests/test_pytorch_model_ops.py CHANGED
@@ -9,16 +9,9 @@ def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str]]):
9
  for id, data in nodes.items():
10
  title = data["title"]
11
  del data["title"]
12
- ws.nodes.append(
13
- workspace.WorkspaceNode(
14
- id=id,
15
- type="basic",
16
- data=workspace.WorkspaceNodeData(title=title, params=data),
17
- position=workspace.Position(
18
- x=data.get("x", 0),
19
- y=data.get("y", 0),
20
- ),
21
- )
22
  )
23
  ws.edges = [
24
  workspace.WorkspaceEdge(
 
9
  for id, data in nodes.items():
10
  title = data["title"]
11
  del data["title"]
12
+ ws.add_node(
13
+ id=id,
14
+ data=workspace.WorkspaceNodeData(title=title, params=data),
 
 
 
 
 
 
 
15
  )
16
  ws.edges = [
17
  workspace.WorkspaceEdge(