darabos commited on
Commit
d161f2f
·
1 Parent(s): fc43558

Set up formatting for VS Code and as a pre-commit.

Browse files
.gitignore CHANGED
@@ -1,5 +1,6 @@
1
  .vscode/*
2
  !.vscode/extensions.json
 
3
  .idea
4
  .DS_Store
5
  *.suo
@@ -14,4 +15,4 @@ build
14
  joblib-cache
15
  *.egg-info
16
 
17
- lynxkite-app/crdt_data
 
1
  .vscode/*
2
  !.vscode/extensions.json
3
+ !.vscode/settings.json
4
  .idea
5
  .DS_Store
6
  *.suo
 
15
  joblib-cache
16
  *.egg-info
17
 
18
+ lynxkite-app/crdt_data
.pre-commit-config.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.5.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - repo: https://github.com/astral-sh/ruff-pre-commit
9
+ rev: v0.9.6
10
+ hooks:
11
+ - id: ruff
12
+ args: [ --fix ]
13
+ - id: ruff-format
14
+ - repo: https://github.com/biomejs/pre-commit
15
+ rev: v1.9.4
16
+ hooks:
17
+ - id: biome-check
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
.vscode/settings.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "ruff.enable": true,
4
+ "html.format.enable": true,
5
+ "css.format.enable": true,
6
+ "typescript.format.enable": true,
7
+ "files.insertFinalNewline": true,
8
+ "files.trimTrailingWhitespace": true,
9
+ "[python]": {
10
+ "editor.defaultFormatter": "charliermarsh.ruff"
11
+ }
12
+ }
README.md CHANGED
@@ -23,6 +23,7 @@ Install everything like this:
23
  ```bash
24
  uv venv
25
  source .venv/bin/activate
 
26
  # The [dev] tag is only needed if you intend on running tests
27
  uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
28
  ```
 
23
  ```bash
24
  uv venv
25
  source .venv/bin/activate
26
+ uvx pre-commit install
27
  # The [dev] tag is only needed if you intend on running tests
28
  uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
29
  ```
lynxkite-app/README.md CHANGED
@@ -23,3 +23,9 @@ cd web
23
  npm i
24
  npm run dev
25
  ```
 
 
 
 
 
 
 
23
  npm i
24
  npm run dev
25
  ```
26
+
27
+ To update the frontend types with the backend types:
28
+
29
+ ```bash
30
+ $ uv run pydantic2ts --module lynxkite_app.main --output ./web/src/apiTypes.ts --json2ts-cmd "npx json-schema-to-typescript"
31
+ ```
lynxkite-app/pyproject.toml CHANGED
@@ -9,12 +9,12 @@ dependencies = [
9
  "lynxkite-core",
10
  "orjson>=3.10.13",
11
  "pycrdt-websocket>=0.15.3",
12
- "pydantic-to-typescript>=2.0.0",
13
  "sse-starlette>=2.2.1",
14
  ]
15
 
16
  [project.optional-dependencies]
17
  dev = [
 
18
  "pytest>=8.3.4",
19
  ]
20
 
 
9
  "lynxkite-core",
10
  "orjson>=3.10.13",
11
  "pycrdt-websocket>=0.15.3",
 
12
  "sse-starlette>=2.2.1",
13
  ]
14
 
15
  [project.optional-dependencies]
16
  dev = [
17
+ "pydantic-to-typescript>=2.0.0",
18
  "pytest>=8.3.4",
19
  ]
20
 
lynxkite-app/src/lynxkite_app/main.py CHANGED
@@ -2,12 +2,7 @@
2
 
3
  import os
4
  import shutil
5
-
6
- if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
7
- import cudf.pandas
8
-
9
- cudf.pandas.install()
10
- import dataclasses
11
  import fastapi
12
  import importlib
13
  import pathlib
@@ -18,6 +13,11 @@ from lynxkite.core import ops
18
  from lynxkite.core import workspace
19
  from . import crdt
20
 
 
 
 
 
 
21
 
22
  def detect_plugins():
23
  plugins = {}
@@ -86,8 +86,7 @@ DATA_PATH = pathlib.Path(os.environ.get("LYNXKITE_DATA", "lynxkite_data"))
86
  CRDT_PATH = pathlib.Path(os.environ.get("LYNXKITE_CRDT_DATA", "lynxkite_crdt_data"))
87
 
88
 
89
- @dataclasses.dataclass(order=True)
90
- class DirectoryEntry:
91
  name: str
92
  type: str
93
 
 
2
 
3
  import os
4
  import shutil
5
+ import pydantic
 
 
 
 
 
6
  import fastapi
7
  import importlib
8
  import pathlib
 
13
  from lynxkite.core import workspace
14
  from . import crdt
15
 
16
+ if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
17
+ import cudf.pandas
18
+
19
+ cudf.pandas.install()
20
+
21
 
22
  def detect_plugins():
23
  plugins = {}
 
86
  CRDT_PATH = pathlib.Path(os.environ.get("LYNXKITE_CRDT_DATA", "lynxkite_crdt_data"))
87
 
88
 
89
+ class DirectoryEntry(pydantic.BaseModel):
 
90
  name: str
91
  type: str
92
 
lynxkite-app/uv.lock CHANGED
@@ -194,6 +194,15 @@ wheels = [
194
  { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
195
  ]
196
 
 
 
 
 
 
 
 
 
 
197
  [[package]]
198
  name = "jinja2"
199
  version = "3.1.5"
@@ -215,17 +224,23 @@ dependencies = [
215
  { name = "lynxkite-core" },
216
  { name = "orjson" },
217
  { name = "pycrdt-websocket" },
218
- { name = "pydantic-to-typescript" },
219
  { name = "sse-starlette" },
220
  ]
221
 
 
 
 
 
 
 
222
  [package.metadata]
223
  requires-dist = [
224
  { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
225
  { name = "lynxkite-core", virtual = "../lynxkite-core" },
226
  { name = "orjson", specifier = ">=3.10.13" },
227
  { name = "pycrdt-websocket", specifier = ">=0.15.3" },
228
- { name = "pydantic-to-typescript", specifier = ">=2.0.0" },
 
229
  { name = "sse-starlette", specifier = ">=2.2.1" },
230
  ]
231
 
@@ -234,6 +249,9 @@ name = "lynxkite-core"
234
  version = "0.1.0"
235
  source = { virtual = "../lynxkite-core" }
236
 
 
 
 
237
  [[package]]
238
  name = "markdown-it-py"
239
  version = "3.0.0"
@@ -346,6 +364,24 @@ wheels = [
346
  { url = "https://files.pythonhosted.org/packages/59/ac/5e96cad01083015f7bfdb02ccafa489da8e6caa7f4c519e215f04d2bd856/orjson-3.10.14-cp313-cp313-win_amd64.whl", hash = "sha256:eca04dfd792cedad53dc9a917da1a522486255360cb4e77619343a20d9f35364", size = 133388 },
347
  ]
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  [[package]]
350
  name = "pycrdt"
351
  version = "0.10.9"
@@ -486,6 +522,21 @@ wheels = [
486
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
487
  ]
488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  [[package]]
490
  name = "python-dotenv"
491
  version = "1.0.1"
 
194
  { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
195
  ]
196
 
197
+ [[package]]
198
+ name = "iniconfig"
199
+ version = "2.0.0"
200
+ source = { registry = "https://pypi.org/simple" }
201
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
202
+ wheels = [
203
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
204
+ ]
205
+
206
  [[package]]
207
  name = "jinja2"
208
  version = "3.1.5"
 
224
  { name = "lynxkite-core" },
225
  { name = "orjson" },
226
  { name = "pycrdt-websocket" },
 
227
  { name = "sse-starlette" },
228
  ]
229
 
230
+ [package.optional-dependencies]
231
+ dev = [
232
+ { name = "pydantic-to-typescript" },
233
+ { name = "pytest" },
234
+ ]
235
+
236
  [package.metadata]
237
  requires-dist = [
238
  { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
239
  { name = "lynxkite-core", virtual = "../lynxkite-core" },
240
  { name = "orjson", specifier = ">=3.10.13" },
241
  { name = "pycrdt-websocket", specifier = ">=0.15.3" },
242
+ { name = "pydantic-to-typescript", marker = "extra == 'dev'", specifier = ">=2.0.0" },
243
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" },
244
  { name = "sse-starlette", specifier = ">=2.2.1" },
245
  ]
246
 
 
249
  version = "0.1.0"
250
  source = { virtual = "../lynxkite-core" }
251
 
252
+ [package.metadata]
253
+ requires-dist = [{ name = "pytest", marker = "extra == 'dev'" }]
254
+
255
  [[package]]
256
  name = "markdown-it-py"
257
  version = "3.0.0"
 
364
  { url = "https://files.pythonhosted.org/packages/59/ac/5e96cad01083015f7bfdb02ccafa489da8e6caa7f4c519e215f04d2bd856/orjson-3.10.14-cp313-cp313-win_amd64.whl", hash = "sha256:eca04dfd792cedad53dc9a917da1a522486255360cb4e77619343a20d9f35364", size = 133388 },
365
  ]
366
 
367
+ [[package]]
368
+ name = "packaging"
369
+ version = "24.2"
370
+ source = { registry = "https://pypi.org/simple" }
371
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
372
+ wheels = [
373
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
374
+ ]
375
+
376
+ [[package]]
377
+ name = "pluggy"
378
+ version = "1.5.0"
379
+ source = { registry = "https://pypi.org/simple" }
380
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
381
+ wheels = [
382
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
383
+ ]
384
+
385
  [[package]]
386
  name = "pycrdt"
387
  version = "0.10.9"
 
522
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
523
  ]
524
 
525
+ [[package]]
526
+ name = "pytest"
527
+ version = "8.3.4"
528
+ source = { registry = "https://pypi.org/simple" }
529
+ dependencies = [
530
+ { name = "colorama", marker = "sys_platform == 'win32'" },
531
+ { name = "iniconfig" },
532
+ { name = "packaging" },
533
+ { name = "pluggy" },
534
+ ]
535
+ sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
536
+ wheels = [
537
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
538
+ ]
539
+
540
  [[package]]
541
  name = "python-dotenv"
542
  version = "1.0.1"
lynxkite-app/web/src/Directory.tsx CHANGED
@@ -1,172 +1,202 @@
1
- // The directory browser.
2
- import { useParams, useNavigate } from "react-router";
3
  import { useState } from "react";
4
- import useSWR from 'swr'
5
-
 
 
6
 
7
- import logo from './assets/logo.png';
8
  // @ts-ignore
9
- import Home from '~icons/tabler/home'
10
  // @ts-ignore
11
- import Folder from '~icons/tabler/folder'
12
  // @ts-ignore
13
- import FolderPlus from '~icons/tabler/folder-plus'
14
  // @ts-ignore
15
- import File from '~icons/tabler/file'
16
  // @ts-ignore
17
- import FilePlus from '~icons/tabler/file-plus'
18
  // @ts-ignore
19
- import Trash from '~icons/tabler/trash';
20
-
21
-
22
 
23
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
24
 
25
  export default function () {
26
- const { path } = useParams();
27
- const encodedPath = encodeURIComponent(path || '');
28
- const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher);
29
- const navigate = useNavigate();
30
- const [isCreatingDir, setIsCreatingDir] = useState(false);
31
- const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
32
-
33
-
34
- function link(item: any) {
35
- if (item.type === 'directory') {
36
- return `/dir/${item.name}`;
37
- } else {
38
- return `/edit/${item.name}`;
39
- }
40
- }
41
-
42
- function shortName(item: any) {
43
- return item.name.split('/').pop();
44
- }
45
-
46
- function newName(list: any[], baseName: string = "Untitled") {
47
- let i = 0;
48
- while (true) {
49
- const name = `${baseName}${i ? ` ${i}` : ''}`;
50
- if (!list.find(item => item.name === name)) {
51
- return name;
52
- }
53
- i++;
54
- }
55
- }
56
-
57
- function newWorkspaceIn(path: string, list: any[], workspaceName?: string) {
58
- const pathSlash = path ? `${path}/` : "";
59
- const name = workspaceName || newName(list);
60
- navigate(`/edit/${pathSlash}${name}`, { replace: true });
61
- }
62
-
63
-
64
- async function newFolderIn(path: string, list: any[], folderName?: string) {
65
- const name = folderName || newName(list, "New Folder");
66
- const pathSlash = path ? `${path}/` : "";
67
-
68
- const res = await fetch(`/api/dir/mkdir`, {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({ path: pathSlash + name }),
72
- });
73
- if (res.ok) {
74
- navigate(`/dir/${pathSlash}${name}`);
75
- } else {
76
- alert("Failed to create folder.");
77
- }
78
- }
79
-
80
- async function deleteItem(item: any) {
81
- if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
82
- const pathSlash = path ? `${path}/` : "";
83
-
84
- const apiPath = item.type === "directory" ? `/api/dir/delete` : `/api/delete`;
85
- await fetch(apiPath, {
86
- method: "POST",
87
- headers: { "Content-Type": "application/json" },
88
- body: JSON.stringify({ path: pathSlash + item.name }),
89
- });
90
- }
91
-
92
- return (
93
- <div className="directory">
94
- <div className="logo">
95
- <a href="https://lynxkite.com/">
96
- <img src={logo} className="logo-image" alt="LynxKite logo" />
97
- </a>
98
- <div className="tagline">The Complete Graph Data Science Platform</div>
99
- </div>
100
-
101
- <div className="entry-list">
102
- {list.error && <p className="error">{list.error.message}</p>}
103
- {list.isLoading && (
104
- <div className="loading spinner-border" role="status">
105
- <span className="visually-hidden">Loading...</span>
106
- </div>
107
- )}
108
-
109
- {list.data && (
110
- <>
111
- <div className="actions">
112
- <div className="new-workspace">
113
- {isCreatingWorkspace &&
114
- // @ts-ignore
115
- <form onSubmit={(e) => { e.preventDefault(); newWorkspaceIn(path || "", list.data, e.target.workspaceName.value.trim()) }}>
116
- <input
117
- type="text"
118
- name="workspaceName"
119
- defaultValue={newName(list.data)}
120
- placeholder={newName(list.data)}
121
- />
122
- </form>
123
- }
124
- <button type="button" onClick={() => setIsCreatingWorkspace(true)}>
125
- <FolderPlus /> New workspace
126
- </button>
127
- </div>
128
-
129
- <div className="new-folder">
130
- {isCreatingDir &&
131
- // @ts-ignore
132
- <form onSubmit={(e) => { e.preventDefault(); newFolderIn(path || "", list.data, e.target.folderName.value.trim()) }}>
133
- <input
134
- type="text"
135
- name="folderName"
136
- defaultValue={newName(list.data)}
137
- placeholder={newName(list.data)}
138
- />
139
- </form>
140
- }
141
- <button type="button" onClick={() => setIsCreatingDir(true)}>
142
- <FolderPlus /> New folder
143
- </button>
144
- </div>
145
- </div>
146
-
147
- {path && (
148
- <div className="breadcrumbs">
149
- <a href="/dir/">
150
- <Home />
151
- </a>{" "}
152
- <span className="current-folder">{path}</span>
153
- </div>
154
- )}
155
-
156
- {list.data.map((item: any) => (
157
- <div key={item.name} className="entry">
158
- <a key={link(item)} href={link(item)}>
159
- {item.type === 'directory' ? <Folder /> : <File />}
160
- {shortName(item)}
161
- </a>
162
- <button onClick={() => { deleteItem(item) }}>
163
- <Trash />
164
- </button>
165
- </div>
166
- ))}
167
- </>
168
- )}
169
- </div>
170
- </div>
171
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
 
 
 
1
  import { useState } from "react";
2
+ // The directory browser.
3
+ import { useNavigate, useParams } from "react-router";
4
+ import useSWR from "swr";
5
+ import type { DirectoryEntry } from "./apiTypes.ts";
6
 
 
7
  // @ts-ignore
8
+ import File from "~icons/tabler/file";
9
  // @ts-ignore
10
+ import FilePlus from "~icons/tabler/file-plus";
11
  // @ts-ignore
12
+ import Folder from "~icons/tabler/folder";
13
  // @ts-ignore
14
+ import FolderPlus from "~icons/tabler/folder-plus";
15
  // @ts-ignore
16
+ import Home from "~icons/tabler/home";
17
  // @ts-ignore
18
+ import Trash from "~icons/tabler/trash";
19
+ import logo from "./assets/logo.png";
 
20
 
21
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
22
 
23
  export default function () {
24
+ const { path } = useParams();
25
+ const encodedPath = encodeURIComponent(path || "");
26
+ const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher);
27
+ const navigate = useNavigate();
28
+ const [isCreatingDir, setIsCreatingDir] = useState(false);
29
+ const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
30
+
31
+ function link(item: DirectoryEntry) {
32
+ if (item.type === "directory") {
33
+ return `/dir/${item.name}`;
34
+ }
35
+ return `/edit/${item.name}`;
36
+ }
37
+
38
+ function shortName(item: DirectoryEntry) {
39
+ return item.name.split("/").pop();
40
+ }
41
+
42
+ function newName(list: DirectoryEntry[], baseName = "Untitled") {
43
+ let i = 0;
44
+ while (true) {
45
+ const name = `${baseName}${i ? ` ${i}` : ""}`;
46
+ if (!list.find((item) => item.name === name)) {
47
+ return name;
48
+ }
49
+ i++;
50
+ }
51
+ }
52
+
53
+ function newWorkspaceIn(
54
+ path: string,
55
+ list: DirectoryEntry[],
56
+ workspaceName?: string,
57
+ ) {
58
+ const pathSlash = path ? `${path}/` : "";
59
+ const name = workspaceName || newName(list);
60
+ navigate(`/edit/${pathSlash}${name}`, { replace: true });
61
+ }
62
+
63
+ async function newFolderIn(
64
+ path: string,
65
+ list: DirectoryEntry[],
66
+ folderName?: string,
67
+ ) {
68
+ const name = folderName || newName(list, "New Folder");
69
+ const pathSlash = path ? `${path}/` : "";
70
+
71
+ const res = await fetch("/api/dir/mkdir", {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: JSON.stringify({ path: pathSlash + name }),
75
+ });
76
+ if (res.ok) {
77
+ navigate(`/dir/${pathSlash}${name}`);
78
+ } else {
79
+ alert("Failed to create folder.");
80
+ }
81
+ }
82
+
83
+ async function deleteItem(item: DirectoryEntry) {
84
+ if (!window.confirm(`Are you sure you want to delete "${item.name}"?`))
85
+ return;
86
+ const pathSlash = path ? `${path}/` : "";
87
+
88
+ const apiPath =
89
+ item.type === "directory" ? "/api/dir/delete" : "/api/delete";
90
+ await fetch(apiPath, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ path: pathSlash + item.name }),
94
+ });
95
+ }
96
+
97
+ return (
98
+ <div className="directory">
99
+ <div className="logo">
100
+ <a href="https://lynxkite.com/">
101
+ <img src={logo} className="logo-image" alt="LynxKite logo" />
102
+ </a>
103
+ <div className="tagline">The Complete Graph Data Science Platform</div>
104
+ </div>
105
+ <div className="entry-list">
106
+ {list.error && <p className="error">{list.error.message}</p>}
107
+ {list.isLoading && (
108
+ <output className="loading spinner-border">
109
+ <span className="visually-hidden">Loading...</span>
110
+ </output>
111
+ )}
112
+
113
+ {list.data && (
114
+ <>
115
+ <div className="actions">
116
+ <div className="new-workspace">
117
+ {isCreatingWorkspace && (
118
+ // @ts-ignore
119
+ <form
120
+ onSubmit={(e) => {
121
+ e.preventDefault();
122
+ newWorkspaceIn(
123
+ path || "",
124
+ list.data,
125
+ e.target.workspaceName.value.trim(),
126
+ );
127
+ }}
128
+ >
129
+ <input
130
+ type="text"
131
+ name="workspaceName"
132
+ defaultValue={newName(list.data)}
133
+ placeholder={newName(list.data)}
134
+ />
135
+ </form>
136
+ )}
137
+ <button
138
+ type="button"
139
+ onClick={() => setIsCreatingWorkspace(true)}
140
+ >
141
+ <FolderPlus /> New workspace
142
+ </button>
143
+ </div>
144
+
145
+ <div className="new-folder">
146
+ {isCreatingDir && (
147
+ // @ts-ignore
148
+ <form
149
+ onSubmit={(e) => {
150
+ e.preventDefault();
151
+ newFolderIn(
152
+ path || "",
153
+ list.data,
154
+ e.target.folderName.value.trim(),
155
+ );
156
+ }}
157
+ >
158
+ <input
159
+ type="text"
160
+ name="folderName"
161
+ defaultValue={newName(list.data)}
162
+ placeholder={newName(list.data)}
163
+ />
164
+ </form>
165
+ )}
166
+ <button type="button" onClick={() => setIsCreatingDir(true)}>
167
+ <FolderPlus /> New folder
168
+ </button>
169
+ </div>
170
+ </div>
171
+
172
+ {path && (
173
+ <div className="breadcrumbs">
174
+ <a href="/dir/">
175
+ <Home />
176
+ </a>{" "}
177
+ <span className="current-folder">{path}</span>
178
+ </div>
179
+ )}
180
+
181
+ {list.data.map((item: DirectoryEntry) => (
182
+ <div key={item.name} className="entry">
183
+ <a key={link(item)} href={link(item)}>
184
+ {item.type === "directory" ? <Folder /> : <File />}
185
+ {shortName(item)}
186
+ </a>
187
+ <button
188
+ type="button"
189
+ onClick={() => {
190
+ deleteItem(item);
191
+ }}
192
+ >
193
+ <Trash />
194
+ </button>
195
+ </div>
196
+ ))}
197
+ </>
198
+ )}
199
+ </div>{" "}
200
+ </div>
201
+ );
202
  }
lynxkite-app/web/src/apiTypes.ts CHANGED
@@ -5,48 +5,54 @@
5
  /* Do not modify it by hand - just update the pydantic models and then re-run the script
6
  */
7
 
8
- /* eslint-disable */
9
- /**
10
- * This file was automatically generated by json-schema-to-typescript.
11
- * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
12
- * and run json-schema-to-typescript to regenerate this file.
13
- */
14
-
15
- export interface BaseConfig {
16
- [k: string]: unknown;
17
  }
18
- export interface Position {
19
- x: number;
20
- y: number;
21
- [k: string]: unknown;
22
  }
 
 
 
 
 
 
 
23
  export interface Workspace {
24
- env?: string;
25
- nodes?: WorkspaceNode[];
26
- edges?: WorkspaceEdge[];
27
- [k: string]: unknown;
28
  }
29
  export interface WorkspaceNode {
30
- id: string;
31
- type: string;
32
- data: WorkspaceNodeData;
33
- position: Position;
34
- [k: string]: unknown;
35
  }
36
  export interface WorkspaceNodeData {
37
- title: string;
38
- params: {
39
- [k: string]: unknown;
40
- };
41
- display?: unknown;
42
- error?: string | null;
43
- [k: string]: unknown;
 
 
 
 
 
44
  }
45
  export interface WorkspaceEdge {
46
- id: string;
47
- source: string;
48
- target: string;
49
- sourceHandle: string;
50
- targetHandle: string;
51
- [k: string]: unknown;
52
  }
 
5
  /* Do not modify it by hand - just update the pydantic models and then re-run the script
6
  */
7
 
8
+ export interface DirectoryEntry {
9
+ name: string;
10
+ type: string;
 
 
 
 
 
 
11
  }
12
+ export interface SaveRequest {
13
+ path: string;
14
+ ws: Workspace;
15
+ [k: string]: unknown;
16
  }
17
+ /**
18
+ * A workspace is a representation of a computational graph that consists of nodes and edges.
19
+ *
20
+ * Each node represents an operation or task, and the edges represent the flow of data between
21
+ * the nodes. Each workspace is associated with an environment, which determines the operations
22
+ * that can be performed in the workspace and the execution method for the operations.
23
+ */
24
  export interface Workspace {
25
+ env?: string;
26
+ nodes?: WorkspaceNode[];
27
+ edges?: WorkspaceEdge[];
28
+ [k: string]: unknown;
29
  }
30
  export interface WorkspaceNode {
31
+ id: string;
32
+ type: string;
33
+ data: WorkspaceNodeData;
34
+ position: Position;
35
+ [k: string]: unknown;
36
  }
37
  export interface WorkspaceNodeData {
38
+ title: string;
39
+ params: {
40
+ [k: string]: unknown;
41
+ };
42
+ display?: unknown;
43
+ error?: string | null;
44
+ [k: string]: unknown;
45
+ }
46
+ export interface Position {
47
+ x: number;
48
+ y: number;
49
+ [k: string]: unknown;
50
  }
51
  export interface WorkspaceEdge {
52
+ id: string;
53
+ source: string;
54
+ target: string;
55
+ sourceHandle: string;
56
+ targetHandle: string;
57
+ [k: string]: unknown;
58
  }