darabos commited on
Commit
9a98e24
·
1 Parent(s): 9fbd9fa

Run SQL on GPU with Polars.

Browse files
requirements.txt CHANGED
@@ -1,15 +1,24 @@
 
1
  fastapi
2
- matplotlib
3
- networkx
4
- numpy
5
  orjson
6
- pandas
7
  pydantic-to-typescript
8
- scipy
9
  uvicorn[standard]
10
  pycrdt
11
  pycrdt-websocket
12
- # For llm_ops
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  chromadb
14
  Jinja2
15
  openai
 
1
+ # Dependencies for basic operation.
2
  fastapi
 
 
 
3
  orjson
 
4
  pydantic-to-typescript
 
5
  uvicorn[standard]
6
  pycrdt
7
  pycrdt-websocket
8
+
9
+ # For lynxkite_ops.
10
+ matplotlib
11
+ networkx
12
+ numpy
13
+ pandas
14
+ scipy
15
+
16
+ # GPU-accelerated graph analytics.
17
+ cugraph-cu12
18
+ cudf-cu12
19
+ nx-cugraph-cu12
20
+
21
+ # For llm_ops.
22
  chromadb
23
  Jinja2
24
  openai
run.sh CHANGED
@@ -1,2 +1,3 @@
1
  #!/bin/bash -xue
 
2
  uvicorn server.main:app --reload
 
1
  #!/bin/bash -xue
2
+ export NX_CUGRAPH_AUTOCONFIG=True
3
  uvicorn server.main:app --reload
server/lynxkite_ops.py CHANGED
@@ -1,5 +1,6 @@
1
- """Some operations. To be split into separate files when we have more."""
2
 
 
3
  from . import ops
4
  from collections import deque
5
  import dataclasses
@@ -7,6 +8,7 @@ import functools
7
  import matplotlib
8
  import networkx as nx
9
  import pandas as pd
 
10
  import traceback
11
  import typing
12
 
@@ -63,13 +65,27 @@ class Bundle:
63
  ],
64
  )
65
 
 
 
 
 
66
  def to_nx(self):
 
67
  graph = nx.from_pandas_edgelist(self.dfs["edges"])
68
- nx.set_node_attributes(
69
- graph, self.dfs["nodes"].set_index("id").to_dict("index")
70
- )
 
71
  return graph
72
 
 
 
 
 
 
 
 
 
73
 
74
  def nx_node_attribute_func(name):
75
  """Decorator for wrapping a function that adds a NetworkX node attribute."""
@@ -99,7 +115,6 @@ def disambiguate_edges(ws):
99
  @ops.register_executor("LynxKite")
100
  async def execute(ws):
101
  catalog = ops.CATALOGS["LynxKite"]
102
- # Nodes are responsible for interpreting/executing their child nodes.
103
  disambiguate_edges(ws)
104
  outputs = {}
105
  failed = 0
@@ -115,12 +130,14 @@ async def execute(ws):
115
  op = catalog[data.title]
116
  params = {**data.params}
117
  # Convert inputs.
118
- for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
119
- if p.type == nx.Graph and isinstance(x, Bundle):
120
- inputs[i] = x.to_nx()
121
- elif p.type == Bundle and isinstance(x, nx.Graph):
122
- inputs[i] = Bundle.from_nx(x)
123
  try:
 
 
 
 
 
 
 
124
  output = op(*inputs, **params)
125
  except Exception as e:
126
  traceback.print_exc()
@@ -142,10 +159,24 @@ async def execute(ws):
142
 
143
  @op("Import Parquet")
144
  def import_parquet(*, filename: str):
145
- """Imports a parquet file."""
146
  return pd.read_parquet(filename)
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  @op("Create scale-free graph")
150
  def create_scale_free_graph(*, nodes: int = 10):
151
  """Creates a scale-free graph with the given number of nodes."""
@@ -158,6 +189,12 @@ def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
158
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
159
 
160
 
 
 
 
 
 
 
161
  @op("Discard loop edges")
162
  def discard_loop_edges(graph: nx.Graph):
163
  graph = graph.copy()
@@ -165,6 +202,35 @@ def discard_loop_edges(graph: nx.Graph):
165
  return graph
166
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  @op("Sample graph")
169
  def sample_graph(graph: nx.Graph, *, nodes: int = 100):
170
  """Takes a (preferably connected) subgraph."""
@@ -237,13 +303,21 @@ def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
237
  return v
238
 
239
 
 
 
 
 
 
 
 
 
240
  @op("View tables", view="table_view")
241
  def view_tables(bundle: Bundle):
242
  v = {
243
  "dataframes": {
244
  name: {
245
  "columns": [str(c) for c in df.columns],
246
- "data": df.values.tolist(),
247
  }
248
  for name, df in bundle.dfs.items()
249
  },
 
1
+ """Graph analytics operations. To be split into separate files when we have more."""
2
 
3
+ import os
4
  from . import ops
5
  from collections import deque
6
  import dataclasses
 
8
  import matplotlib
9
  import networkx as nx
10
  import pandas as pd
11
+ import polars as pl
12
  import traceback
13
  import typing
14
 
 
65
  ],
66
  )
67
 
68
+ @classmethod
69
+ def from_df(cls, df: pd.DataFrame):
70
+ return cls(dfs={"df": df})
71
+
72
  def to_nx(self):
73
+ # TODO: Use relations.
74
  graph = nx.from_pandas_edgelist(self.dfs["edges"])
75
+ if "nodes" in self.dfs:
76
+ nx.set_node_attributes(
77
+ graph, self.dfs["nodes"].set_index("id").to_dict("index")
78
+ )
79
  return graph
80
 
81
+ def copy(self):
82
+ """Returns a medium depth copy of the bundle. The Bundle is completely new, but the DataFrames and RelationDefinitions are shared."""
83
+ return Bundle(
84
+ dfs=dict(self.dfs),
85
+ relations=list(self.relations),
86
+ other=dict(self.other) if self.other else None,
87
+ )
88
+
89
 
90
  def nx_node_attribute_func(name):
91
  """Decorator for wrapping a function that adds a NetworkX node attribute."""
 
115
  @ops.register_executor("LynxKite")
116
  async def execute(ws):
117
  catalog = ops.CATALOGS["LynxKite"]
 
118
  disambiguate_edges(ws)
119
  outputs = {}
120
  failed = 0
 
130
  op = catalog[data.title]
131
  params = {**data.params}
132
  # Convert inputs.
 
 
 
 
 
133
  try:
134
+ for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
135
+ if p.type == nx.Graph and isinstance(x, Bundle):
136
+ inputs[i] = x.to_nx()
137
+ elif p.type == Bundle and isinstance(x, nx.Graph):
138
+ inputs[i] = Bundle.from_nx(x)
139
+ elif p.type == Bundle and isinstance(x, pd.DataFrame):
140
+ inputs[i] = Bundle.from_df(x)
141
  output = op(*inputs, **params)
142
  except Exception as e:
143
  traceback.print_exc()
 
159
 
160
  @op("Import Parquet")
161
  def import_parquet(*, filename: str):
162
+ """Imports a Parquet file."""
163
  return pd.read_parquet(filename)
164
 
165
 
166
+ @op("Import CSV")
167
+ def import_csv(
168
+ *, filename: str, columns: str = "<from file>", separator: str = "<auto>"
169
+ ):
170
+ """Imports a CSV file."""
171
+ return pd.read_csv(
172
+ filename,
173
+ names=pd.api.extensions.no_default
174
+ if columns == "<from file>"
175
+ else columns.split(","),
176
+ sep=pd.api.extensions.no_default if separator == "<auto>" else separator,
177
+ )
178
+
179
+
180
  @op("Create scale-free graph")
181
  def create_scale_free_graph(*, nodes: int = 10):
182
  """Creates a scale-free graph with the given number of nodes."""
 
189
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
190
 
191
 
192
+ @op("Compute betweenness centrality")
193
+ @nx_node_attribute_func("betweenness_centrality")
194
+ def compute_betweenness_centrality(graph: nx.Graph, *, k=10):
195
+ return nx.betweenness_centrality(graph, k=k, backend="cugraph")
196
+
197
+
198
  @op("Discard loop edges")
199
  def discard_loop_edges(graph: nx.Graph):
200
  graph = graph.copy()
 
202
  return graph
203
 
204
 
205
+ @op("SQL")
206
+ def sql(bundle: Bundle, *, query: ops.LongStr, save_as: str = "result"):
207
+ """Run a SQL query on the DataFrames in the bundle. Save the results as a new DataFrame."""
208
+ bundle = bundle.copy()
209
+ if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
210
+ with pl.Config() as cfg:
211
+ cfg.set_verbose(True)
212
+ res = (
213
+ pl.SQLContext(bundle.dfs)
214
+ .execute(query)
215
+ .collect(engine="gpu")
216
+ .to_pandas()
217
+ )
218
+ # TODO: Currently `collect()` moves the data from cuDF to Polars. Then we convert it to Pandas,
219
+ # which (hopefully) puts it back into cuDF. Hopefully we will be able to keep it in cuDF.
220
+ else:
221
+ res = pl.SQLContext(bundle.dfs).execute(query)
222
+ bundle.dfs[save_as] = res
223
+ return bundle
224
+
225
+
226
+ @op("Organize bundle")
227
+ def organize_bundle(bundle: Bundle, *, code: ops.LongStr):
228
+ """Lets you rename/copy/delete DataFrames, and modify relations. TODO: Use a declarative solution instead of Python code. Add UI."""
229
+ bundle = bundle.copy()
230
+ exec(code, globals(), {"bundle": bundle})
231
+ return bundle
232
+
233
+
234
  @op("Sample graph")
235
  def sample_graph(graph: nx.Graph, *, nodes: int = 100):
236
  """Takes a (preferably connected) subgraph."""
 
303
  return v
304
 
305
 
306
+ def collect(df: pd.DataFrame):
307
+ if isinstance(df, pl.LazyFrame):
308
+ df = df.collect()
309
+ if isinstance(df, pl.DataFrame):
310
+ return [[d[c] for c in df.columns] for d in df.to_dicts()]
311
+ return df.values.tolist()
312
+
313
+
314
  @op("View tables", view="table_view")
315
  def view_tables(bundle: Bundle):
316
  v = {
317
  "dataframes": {
318
  name: {
319
  "columns": [str(c) for c in df.columns],
320
+ "data": collect(df),
321
  }
322
  for name, df in bundle.dfs.items()
323
  },
web/package-lock.json CHANGED
@@ -8,9 +8,11 @@
8
  "name": "lynxkite",
9
  "version": "0.0.0",
10
  "dependencies": {
 
11
  "@iconify-json/tabler": "^1.2.10",
12
  "@svgr/core": "^8.1.0",
13
  "@svgr/plugin-jsx": "^8.1.0",
 
14
  "@syncedstore/core": "^0.6.0",
15
  "@syncedstore/react": "^0.6.0",
16
  "@types/node": "^22.10.1",
@@ -42,6 +44,9 @@
42
  "typescript": "~5.6.2",
43
  "typescript-eslint": "^8.15.0",
44
  "vite": "^6.0.1"
 
 
 
45
  }
46
  },
47
  "node_modules/@alloc/quick-lru": {
@@ -351,6 +356,21 @@
351
  "node": ">=18"
352
  }
353
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  "node_modules/@eslint-community/eslint-utils": {
355
  "version": "4.4.1",
356
  "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@@ -748,6 +768,19 @@
748
  "darwin"
749
  ]
750
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
752
  "version": "8.0.0",
753
  "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@@ -965,7 +998,6 @@
965
  "version": "1.10.1",
966
  "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz",
967
  "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==",
968
- "dev": true,
969
  "hasInstallScript": true,
970
  "license": "Apache-2.0",
971
  "dependencies": {
@@ -1007,7 +1039,6 @@
1007
  "cpu": [
1008
  "arm64"
1009
  ],
1010
- "dev": true,
1011
  "license": "Apache-2.0 AND MIT",
1012
  "optional": true,
1013
  "os": [
@@ -1017,18 +1048,160 @@
1017
  "node": ">=10"
1018
  }
1019
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
  "node_modules/@swc/counter": {
1021
  "version": "0.1.3",
1022
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
1023
  "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
1024
- "dev": true,
1025
  "license": "Apache-2.0"
1026
  },
1027
  "node_modules/@swc/types": {
1028
  "version": "0.1.17",
1029
  "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
1030
  "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
1031
- "dev": true,
1032
  "license": "Apache-2.0",
1033
  "dependencies": {
1034
  "@swc/counter": "^0.1.3"
 
8
  "name": "lynxkite",
9
  "version": "0.0.0",
10
  "dependencies": {
11
+ "@esbuild/linux-x64": "^0.24.0",
12
  "@iconify-json/tabler": "^1.2.10",
13
  "@svgr/core": "^8.1.0",
14
  "@svgr/plugin-jsx": "^8.1.0",
15
+ "@swc/core": "^1.10.1",
16
  "@syncedstore/core": "^0.6.0",
17
  "@syncedstore/react": "^0.6.0",
18
  "@types/node": "^22.10.1",
 
44
  "typescript": "~5.6.2",
45
  "typescript-eslint": "^8.15.0",
46
  "vite": "^6.0.1"
47
+ },
48
+ "optionalDependencies": {
49
+ "@rollup/rollup-linux-x64-gnu": "^4.28.1"
50
  }
51
  },
52
  "node_modules/@alloc/quick-lru": {
 
356
  "node": ">=18"
357
  }
358
  },
359
+ "node_modules/@esbuild/linux-x64": {
360
+ "version": "0.24.0",
361
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
362
+ "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
363
+ "cpu": [
364
+ "x64"
365
+ ],
366
+ "license": "MIT",
367
+ "os": [
368
+ "linux"
369
+ ],
370
+ "engines": {
371
+ "node": ">=18"
372
+ }
373
+ },
374
  "node_modules/@eslint-community/eslint-utils": {
375
  "version": "4.4.1",
376
  "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
 
768
  "darwin"
769
  ]
770
  },
771
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
772
+ "version": "4.28.1",
773
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
774
+ "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
775
+ "cpu": [
776
+ "x64"
777
+ ],
778
+ "license": "MIT",
779
+ "optional": true,
780
+ "os": [
781
+ "linux"
782
+ ]
783
+ },
784
  "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
785
  "version": "8.0.0",
786
  "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
 
998
  "version": "1.10.1",
999
  "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz",
1000
  "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==",
 
1001
  "hasInstallScript": true,
1002
  "license": "Apache-2.0",
1003
  "dependencies": {
 
1039
  "cpu": [
1040
  "arm64"
1041
  ],
 
1042
  "license": "Apache-2.0 AND MIT",
1043
  "optional": true,
1044
  "os": [
 
1048
  "node": ">=10"
1049
  }
1050
  },
1051
+ "node_modules/@swc/core-darwin-x64": {
1052
+ "version": "1.10.1",
1053
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz",
1054
+ "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==",
1055
+ "cpu": [
1056
+ "x64"
1057
+ ],
1058
+ "license": "Apache-2.0 AND MIT",
1059
+ "optional": true,
1060
+ "os": [
1061
+ "darwin"
1062
+ ],
1063
+ "engines": {
1064
+ "node": ">=10"
1065
+ }
1066
+ },
1067
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
1068
+ "version": "1.10.1",
1069
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz",
1070
+ "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==",
1071
+ "cpu": [
1072
+ "arm"
1073
+ ],
1074
+ "license": "Apache-2.0",
1075
+ "optional": true,
1076
+ "os": [
1077
+ "linux"
1078
+ ],
1079
+ "engines": {
1080
+ "node": ">=10"
1081
+ }
1082
+ },
1083
+ "node_modules/@swc/core-linux-arm64-gnu": {
1084
+ "version": "1.10.1",
1085
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz",
1086
+ "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==",
1087
+ "cpu": [
1088
+ "arm64"
1089
+ ],
1090
+ "license": "Apache-2.0 AND MIT",
1091
+ "optional": true,
1092
+ "os": [
1093
+ "linux"
1094
+ ],
1095
+ "engines": {
1096
+ "node": ">=10"
1097
+ }
1098
+ },
1099
+ "node_modules/@swc/core-linux-arm64-musl": {
1100
+ "version": "1.10.1",
1101
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz",
1102
+ "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==",
1103
+ "cpu": [
1104
+ "arm64"
1105
+ ],
1106
+ "license": "Apache-2.0 AND MIT",
1107
+ "optional": true,
1108
+ "os": [
1109
+ "linux"
1110
+ ],
1111
+ "engines": {
1112
+ "node": ">=10"
1113
+ }
1114
+ },
1115
+ "node_modules/@swc/core-linux-x64-gnu": {
1116
+ "version": "1.10.1",
1117
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz",
1118
+ "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==",
1119
+ "cpu": [
1120
+ "x64"
1121
+ ],
1122
+ "license": "Apache-2.0 AND MIT",
1123
+ "optional": true,
1124
+ "os": [
1125
+ "linux"
1126
+ ],
1127
+ "engines": {
1128
+ "node": ">=10"
1129
+ }
1130
+ },
1131
+ "node_modules/@swc/core-linux-x64-musl": {
1132
+ "version": "1.10.1",
1133
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz",
1134
+ "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==",
1135
+ "cpu": [
1136
+ "x64"
1137
+ ],
1138
+ "license": "Apache-2.0 AND MIT",
1139
+ "optional": true,
1140
+ "os": [
1141
+ "linux"
1142
+ ],
1143
+ "engines": {
1144
+ "node": ">=10"
1145
+ }
1146
+ },
1147
+ "node_modules/@swc/core-win32-arm64-msvc": {
1148
+ "version": "1.10.1",
1149
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz",
1150
+ "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==",
1151
+ "cpu": [
1152
+ "arm64"
1153
+ ],
1154
+ "license": "Apache-2.0 AND MIT",
1155
+ "optional": true,
1156
+ "os": [
1157
+ "win32"
1158
+ ],
1159
+ "engines": {
1160
+ "node": ">=10"
1161
+ }
1162
+ },
1163
+ "node_modules/@swc/core-win32-ia32-msvc": {
1164
+ "version": "1.10.1",
1165
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz",
1166
+ "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==",
1167
+ "cpu": [
1168
+ "ia32"
1169
+ ],
1170
+ "license": "Apache-2.0 AND MIT",
1171
+ "optional": true,
1172
+ "os": [
1173
+ "win32"
1174
+ ],
1175
+ "engines": {
1176
+ "node": ">=10"
1177
+ }
1178
+ },
1179
+ "node_modules/@swc/core-win32-x64-msvc": {
1180
+ "version": "1.10.1",
1181
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz",
1182
+ "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==",
1183
+ "cpu": [
1184
+ "x64"
1185
+ ],
1186
+ "license": "Apache-2.0 AND MIT",
1187
+ "optional": true,
1188
+ "os": [
1189
+ "win32"
1190
+ ],
1191
+ "engines": {
1192
+ "node": ">=10"
1193
+ }
1194
+ },
1195
  "node_modules/@swc/counter": {
1196
  "version": "0.1.3",
1197
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
1198
  "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
 
1199
  "license": "Apache-2.0"
1200
  },
1201
  "node_modules/@swc/types": {
1202
  "version": "0.1.17",
1203
  "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
1204
  "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
 
1205
  "license": "Apache-2.0",
1206
  "dependencies": {
1207
  "@swc/counter": "^0.1.3"
web/package.json CHANGED
@@ -10,9 +10,11 @@
10
  "preview": "vite preview"
11
  },
12
  "dependencies": {
 
13
  "@iconify-json/tabler": "^1.2.10",
14
  "@svgr/core": "^8.1.0",
15
  "@svgr/plugin-jsx": "^8.1.0",
 
16
  "@syncedstore/core": "^0.6.0",
17
  "@syncedstore/react": "^0.6.0",
18
  "@types/node": "^22.10.1",
@@ -44,5 +46,8 @@
44
  "typescript": "~5.6.2",
45
  "typescript-eslint": "^8.15.0",
46
  "vite": "^6.0.1"
 
 
 
47
  }
48
  }
 
10
  "preview": "vite preview"
11
  },
12
  "dependencies": {
13
+ "@esbuild/linux-x64": "^0.24.0",
14
  "@iconify-json/tabler": "^1.2.10",
15
  "@svgr/core": "^8.1.0",
16
  "@svgr/plugin-jsx": "^8.1.0",
17
+ "@swc/core": "^1.10.1",
18
  "@syncedstore/core": "^0.6.0",
19
  "@syncedstore/react": "^0.6.0",
20
  "@types/node": "^22.10.1",
 
46
  "typescript": "~5.6.2",
47
  "typescript-eslint": "^8.15.0",
48
  "vite": "^6.0.1"
49
+ },
50
+ "optionalDependencies": {
51
+ "@rollup/rollup-linux-x64-gnu": "^4.28.1"
52
  }
53
  }
web/src/workspace/Workspace.tsx CHANGED
@@ -57,7 +57,7 @@ function LynxKiteFlow() {
57
  const state = syncedStore({ workspace: {} as Workspace });
58
  setState(state);
59
  const doc = getYjsDoc(state);
60
- const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc);
61
  const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
62
  if (origin === wsProvider) {
63
  // An update from the CRDT. Apply it to the local state.
@@ -125,6 +125,7 @@ function LynxKiteFlow() {
125
  const edgeIndex = wedges.findIndex((e) => e.id === ch.id);
126
  if (ch.type === 'remove') {
127
  wedges.splice(edgeIndex, 1);
 
128
  } else {
129
  console.log('Unknown edge change', ch);
130
  }
 
57
  const state = syncedStore({ workspace: {} as Workspace });
58
  setState(state);
59
  const doc = getYjsDoc(state);
60
+ const wsProvider = new WebsocketProvider("ws://localhost:5173/ws/crdt", path!, doc);
61
  const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
62
  if (origin === wsProvider) {
63
  // An update from the CRDT. Apply it to the local state.
 
125
  const edgeIndex = wedges.findIndex((e) => e.id === ch.id);
126
  if (ch.type === 'remove') {
127
  wedges.splice(edgeIndex, 1);
128
+ } else if (ch.type === 'select') {
129
  } else {
130
  console.log('Unknown edge change', ch);
131
  }
web/src/workspace/nodes/LynxKiteNode.tsx CHANGED
@@ -45,9 +45,6 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
45
  const data = props.data;
46
  const expanded = !data.collapsed;
47
  const handles = getHandles(data.meta?.inputs || {}, data.meta?.outputs || {});
48
- function asPx(n: number | undefined) {
49
- return (n ? n + 'px' : undefined) || '200px';
50
- }
51
  function titleClicked() {
52
  reactFlow.updateNodeData(props.id, { collapsed: expanded });
53
  }
@@ -55,7 +52,7 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
55
 
56
  return (
57
  <div className={'node-container ' + (expanded ? 'expanded' : 'collapsed')}
58
- style={{ width: asPx(props.width), height: asPx(expanded ? props.height : undefined) }}>
59
  <div className="lynxkite-node" style={props.nodeStyle}>
60
  <div className="title bg-primary" onClick={titleClicked}>
61
  {data.title}
@@ -67,14 +64,6 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
67
  <div className="error">{data.error}</div>
68
  }
69
  {props.children}
70
- {handles.map(handle => (
71
- <Handle
72
- key={handle.name}
73
- id={handle.name} type={handle.type} position={handle.position as Position}
74
- style={{ [handleOffsetDirection[handle.position]]: handle.offsetPercentage + '%' }}>
75
- {handle.showLabel && <span className="handle-name">{handle.name.replace(/_/g, " ")}</span>}
76
- </Handle >
77
- ))}
78
  <NodeResizeControl
79
  minWidth={100}
80
  minHeight={50}
@@ -83,6 +72,14 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
83
  <ChevronDownRight className="node-resizer" />
84
  </NodeResizeControl>
85
  </>}
 
 
 
 
 
 
 
 
86
  </div>
87
  </div>
88
  );
 
45
  const data = props.data;
46
  const expanded = !data.collapsed;
47
  const handles = getHandles(data.meta?.inputs || {}, data.meta?.outputs || {});
 
 
 
48
  function titleClicked() {
49
  reactFlow.updateNodeData(props.id, { collapsed: expanded });
50
  }
 
52
 
53
  return (
54
  <div className={'node-container ' + (expanded ? 'expanded' : 'collapsed')}
55
+ style={{ width: props.width || 200, height: expanded ? props.height || 200 : undefined }}>
56
  <div className="lynxkite-node" style={props.nodeStyle}>
57
  <div className="title bg-primary" onClick={titleClicked}>
58
  {data.title}
 
64
  <div className="error">{data.error}</div>
65
  }
66
  {props.children}
 
 
 
 
 
 
 
 
67
  <NodeResizeControl
68
  minWidth={100}
69
  minHeight={50}
 
72
  <ChevronDownRight className="node-resizer" />
73
  </NodeResizeControl>
74
  </>}
75
+ {handles.map(handle => (
76
+ <Handle
77
+ key={handle.name}
78
+ id={handle.name} type={handle.type} position={handle.position as Position}
79
+ style={{ [handleOffsetDirection[handle.position]]: handle.offsetPercentage + '%' }}>
80
+ {handle.showLabel && <span className="handle-name">{handle.name.replace(/_/g, " ")}</span>}
81
+ </Handle >
82
+ ))}
83
  </div>
84
  </div>
85
  );
web/src/workspace/nodes/NodeWithTableView.tsx CHANGED
@@ -22,12 +22,12 @@ export default function NodeWithTableView(props: any) {
22
  <LynxKiteNode {...props}>
23
  {display && [
24
  Object.entries(display.dataframes || {}).map(([name, df]: [string, any]) => <React.Fragment key={name}>
25
- {!single && <div key={name} className="df-head" onClick={() => setOpen({ ...open, [name]: !open[name] })}>{name}</div>}
26
  {(single || open[name]) &&
27
  (df.data.length > 1 ?
28
- <Table key={name} columns={df.columns} data={df.data} />
29
  :
30
- <dl key={name}>
31
  {df.columns.map((c: string, i: number) =>
32
  <React.Fragment key={name + '-' + c}>
33
  <dt>{c}</dt>
 
22
  <LynxKiteNode {...props}>
23
  {display && [
24
  Object.entries(display.dataframes || {}).map(([name, df]: [string, any]) => <React.Fragment key={name}>
25
+ {!single && <div key={name + '-header'} className="df-head" onClick={() => setOpen({ ...open, [name]: !open[name] })}>{name}</div>}
26
  {(single || open[name]) &&
27
  (df.data.length > 1 ?
28
+ <Table key={name + '-table'} columns={df.columns} data={df.data} />
29
  :
30
+ <dl key={name + '-dl'}>
31
  {df.columns.map((c: string, i: number) =>
32
  <React.Fragment key={name + '-' + c}>
33
  <dt>{c}</dt>