Spaces:
Running
Running
Delete open-source packages, just depend on them. Move bio stuff into lynxkite-bio.
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .pre-commit-config.yaml +4 -14
- lynxkite-app/.gitignore +0 -5
- lynxkite-app/MANIFEST.in +0 -2
- lynxkite-app/README.md +0 -31
- lynxkite-app/pyproject.toml +0 -60
- lynxkite-app/src/build_frontend.py +0 -26
- lynxkite-app/src/lynxkite_app/__init__.py +0 -0
- lynxkite-app/src/lynxkite_app/__main__.py +0 -20
- lynxkite-app/src/lynxkite_app/crdt.py +0 -342
- lynxkite-app/src/lynxkite_app/main.py +0 -165
- lynxkite-app/src/lynxkite_app/web_assets/__init__.py +0 -1
- lynxkite-app/src/lynxkite_app/web_assets/assets/__init__.py +0 -1
- lynxkite-app/tests/test_crdt.py +0 -72
- lynxkite-app/tests/test_main.py +0 -57
- lynxkite-app/uv.lock +0 -0
- lynxkite-app/web/.gitignore +0 -24
- lynxkite-app/web/README.md +0 -7
- lynxkite-app/web/index.html +0 -12
- lynxkite-app/web/package-lock.json +0 -0
- lynxkite-app/web/package.json +0 -61
- lynxkite-app/web/playwright.config.ts +0 -30
- lynxkite-app/web/postcss.config.js +0 -6
- lynxkite-app/web/src/Code.tsx +0 -119
- lynxkite-app/web/src/Directory.tsx +0 -243
- lynxkite-app/web/src/Tooltip.tsx +0 -21
- lynxkite-app/web/src/apiTypes.ts +0 -65
- lynxkite-app/web/src/assets/favicon.ico +0 -0
- lynxkite-app/web/src/assets/logo.png +0 -0
- lynxkite-app/web/src/code-theme.ts +0 -38
- lynxkite-app/web/src/common.ts +0 -16
- lynxkite-app/web/src/index.css +0 -727
- lynxkite-app/web/src/main.tsx +0 -53
- lynxkite-app/web/src/vite-env.d.ts +0 -1
- lynxkite-app/web/src/workspace/EnvironmentSelector.tsx +0 -22
- lynxkite-app/web/src/workspace/LynxKiteEdge.tsx +0 -32
- lynxkite-app/web/src/workspace/LynxKiteState.ts +0 -4
- lynxkite-app/web/src/workspace/NodeSearch.tsx +0 -93
- lynxkite-app/web/src/workspace/Workspace.tsx +0 -655
- lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx +0 -296
- lynxkite-app/web/src/workspace/nodes/Group.tsx +0 -64
- lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +0 -209
- lynxkite-app/web/src/workspace/nodes/ModelMappingParameter.tsx +0 -169
- lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx +0 -48
- lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +0 -147
- lynxkite-app/web/src/workspace/nodes/NodeWithComment.tsx +0 -58
- lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx +0 -12
- lynxkite-app/web/src/workspace/nodes/NodeWithMolecule.tsx +0 -68
- lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +0 -44
- lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +0 -82
- lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx +0 -43
.pre-commit-config.yaml
CHANGED
|
@@ -11,10 +11,6 @@ repos:
|
|
| 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
|
| 18 |
# https://github.com/astral-sh/ty/issues/269
|
| 19 |
- repo: local
|
| 20 |
hooks:
|
|
@@ -29,14 +25,8 @@ repos:
|
|
| 29 |
rev: "0.23.0"
|
| 30 |
hooks:
|
| 31 |
- id: deptry
|
| 32 |
-
name: deptry for lynxkite-
|
| 33 |
-
entry: bash -c 'cd lynxkite-
|
| 34 |
-
- id: deptry
|
| 35 |
-
name: deptry for lynxkite-core
|
| 36 |
-
entry: bash -c 'cd lynxkite-core && deptry .'
|
| 37 |
-
- id: deptry
|
| 38 |
-
name: deptry for lynxkite-graph-analytics
|
| 39 |
-
entry: bash -c 'cd lynxkite-graph-analytics && deptry .'
|
| 40 |
- id: deptry
|
| 41 |
-
name: deptry for lynxkite-
|
| 42 |
-
entry: bash -c 'cd lynxkite-
|
|
|
|
| 11 |
- id: ruff
|
| 12 |
args: [ --fix ]
|
| 13 |
- id: ruff-format
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# https://github.com/astral-sh/ty/issues/269
|
| 15 |
- repo: local
|
| 16 |
hooks:
|
|
|
|
| 25 |
rev: "0.23.0"
|
| 26 |
hooks:
|
| 27 |
- id: deptry
|
| 28 |
+
name: deptry for lynxkite-bio
|
| 29 |
+
entry: bash -c 'cd lynxkite-bio && deptry .'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
- id: deptry
|
| 31 |
+
name: deptry for lynxkite-lynxscribe
|
| 32 |
+
entry: bash -c 'cd lynxkite-lynxscribe && deptry .'
|
lynxkite-app/.gitignore
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
/src/lynxkite_app/web_assets
|
| 2 |
-
!/src/lynxkite_app/web_assets/__init__.py
|
| 3 |
-
!/src/lynxkite_app/web_assets/assets/__init__.py
|
| 4 |
-
data/
|
| 5 |
-
!/web/tests/data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/MANIFEST.in
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
graft web
|
| 2 |
-
prune web/node_modules
|
|
|
|
|
|
|
|
|
lynxkite-app/README.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
# LynxKite MM
|
| 2 |
-
|
| 3 |
-
This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite). It is not compatible with the
|
| 4 |
-
original LynxKite. The primary goals of this rewrite are:
|
| 5 |
-
|
| 6 |
-
- Target GPU clusters instead of Hadoop clusters. We use Python instead of Scala, RAPIDS instead of Apache Spark.
|
| 7 |
-
- More extensible backend. Make it easy to add new LynxKite boxes. Make it easy to use our frontend for other purposes,
|
| 8 |
-
configuring and executing other pipelines.
|
| 9 |
-
|
| 10 |
-
## Development
|
| 11 |
-
|
| 12 |
-
To run the backend:
|
| 13 |
-
|
| 14 |
-
```bash
|
| 15 |
-
uv pip install -e .
|
| 16 |
-
cd ../examples && LYNXKITE_RELOAD=1 lynxkite
|
| 17 |
-
```
|
| 18 |
-
|
| 19 |
-
To run the frontend:
|
| 20 |
-
|
| 21 |
-
```bash
|
| 22 |
-
cd web
|
| 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
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
[project]
|
| 2 |
-
name = "lynxkite"
|
| 3 |
-
version = "0.1.0"
|
| 4 |
-
description = "The LynxKite application, with web server and UI"
|
| 5 |
-
readme = "README.md"
|
| 6 |
-
requires-python = ">=3.11"
|
| 7 |
-
dependencies = [
|
| 8 |
-
"fastapi[standard]>=0.115.6",
|
| 9 |
-
"griffe>=1.7.3",
|
| 10 |
-
"joblib>=1.5.1",
|
| 11 |
-
"lynxkite-core",
|
| 12 |
-
"pycrdt-websocket>=0.16",
|
| 13 |
-
"pycrdt>=0.12.26",
|
| 14 |
-
"pydantic>=2.11.7",
|
| 15 |
-
"sse-starlette>=2.2.1",
|
| 16 |
-
"uvicorn>=0.35.0",
|
| 17 |
-
]
|
| 18 |
-
classifiers = ["Private :: Do Not Upload"]
|
| 19 |
-
|
| 20 |
-
[project.urls]
|
| 21 |
-
Homepage = "https://github.com/lynxkite/lynxkite-2000/"
|
| 22 |
-
|
| 23 |
-
[dependency-groups]
|
| 24 |
-
dev = [
|
| 25 |
-
"pydantic-to-typescript>=2.0.0",
|
| 26 |
-
"setuptools>=80.9.0",
|
| 27 |
-
]
|
| 28 |
-
|
| 29 |
-
[tool.uv.sources]
|
| 30 |
-
lynxkite-core = { workspace = true }
|
| 31 |
-
|
| 32 |
-
[build-system]
|
| 33 |
-
requires = ["setuptools", "wheel", "setuptools-scm"]
|
| 34 |
-
build-backend = "setuptools.build_meta"
|
| 35 |
-
|
| 36 |
-
[tool.setuptools.packages.find]
|
| 37 |
-
namespaces = true
|
| 38 |
-
where = ["src"]
|
| 39 |
-
|
| 40 |
-
[tool.setuptools.package-data]
|
| 41 |
-
"lynxkite_app.web_assets" = ["*"]
|
| 42 |
-
"lynxkite_app.web_assets.assets" = ["*"]
|
| 43 |
-
|
| 44 |
-
[tool.setuptools]
|
| 45 |
-
py-modules = ["build_frontend"]
|
| 46 |
-
include-package-data = true
|
| 47 |
-
|
| 48 |
-
[tool.setuptools.cmdclass]
|
| 49 |
-
build_py = "build_frontend.build_py"
|
| 50 |
-
|
| 51 |
-
[project.scripts]
|
| 52 |
-
lynxkite = "lynxkite_app.__main__:main"
|
| 53 |
-
|
| 54 |
-
[tool.deptry.package_module_name_map]
|
| 55 |
-
lynxkite-core = "lynxkite"
|
| 56 |
-
sse-starlette = "starlette"
|
| 57 |
-
|
| 58 |
-
[tool.deptry.per_rule_ignores]
|
| 59 |
-
DEP002 = ["pycrdt-websocket", "griffe"]
|
| 60 |
-
DEP004 = ["setuptools"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/build_frontend.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
"""Customized build process for setuptools."""
|
| 2 |
-
|
| 3 |
-
import subprocess
|
| 4 |
-
from setuptools.command.build_py import build_py as _build_py
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import shutil
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class build_py(_build_py):
|
| 10 |
-
def run(self):
|
| 11 |
-
print("\n\nBuilding frontend...", __file__)
|
| 12 |
-
here = Path(__file__).parent.parent
|
| 13 |
-
frontend_dir = here / "web"
|
| 14 |
-
package_dir = here / "src" / "lynxkite_app" / "web_assets"
|
| 15 |
-
subprocess.check_call(["npm", "install"], cwd=frontend_dir)
|
| 16 |
-
subprocess.check_call(["npm", "run", "build"], cwd=frontend_dir)
|
| 17 |
-
print("files in", frontend_dir / "dist")
|
| 18 |
-
for file in (frontend_dir / "dist").iterdir():
|
| 19 |
-
print(file)
|
| 20 |
-
# shutil.rmtree(package_dir)
|
| 21 |
-
shutil.copytree(frontend_dir / "dist", package_dir, dirs_exist_ok=True)
|
| 22 |
-
# (frontend_dir / "dist").rename(package_dir)
|
| 23 |
-
print("files in", package_dir)
|
| 24 |
-
for file in package_dir.iterdir():
|
| 25 |
-
print(file)
|
| 26 |
-
super().run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/__init__.py
DELETED
|
File without changes
|
lynxkite-app/src/lynxkite_app/__main__.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import uvicorn
|
| 2 |
-
from .main import app # noqa: F401
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
def main():
|
| 7 |
-
port = int(os.environ.get("PORT", "8000"))
|
| 8 |
-
reload = bool(os.environ.get("LYNXKITE_RELOAD", ""))
|
| 9 |
-
uvicorn.run(
|
| 10 |
-
"lynxkite_app.main:app",
|
| 11 |
-
host="0.0.0.0",
|
| 12 |
-
port=port,
|
| 13 |
-
reload=reload,
|
| 14 |
-
loop="asyncio",
|
| 15 |
-
proxy_headers=True,
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
if __name__ == "__main__":
|
| 20 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/crdt.py
DELETED
|
@@ -1,342 +0,0 @@
|
|
| 1 |
-
"""CRDT is used to synchronize workspace state for backend and frontend(s)."""
|
| 2 |
-
|
| 3 |
-
import asyncio
|
| 4 |
-
import contextlib
|
| 5 |
-
import enum
|
| 6 |
-
import pathlib
|
| 7 |
-
import fastapi
|
| 8 |
-
import os.path
|
| 9 |
-
import pycrdt.websocket
|
| 10 |
-
import pycrdt.store.file
|
| 11 |
-
import uvicorn.protocols.utils
|
| 12 |
-
import builtins
|
| 13 |
-
from lynxkite.core import workspace, ops
|
| 14 |
-
|
| 15 |
-
router = fastapi.APIRouter()
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def ws_exception_handler(exception, log):
|
| 19 |
-
if isinstance(exception, builtins.ExceptionGroup):
|
| 20 |
-
for ex in exception.exceptions:
|
| 21 |
-
if not isinstance(ex, uvicorn.protocols.utils.ClientDisconnected):
|
| 22 |
-
log.exception(ex)
|
| 23 |
-
else:
|
| 24 |
-
log.exception(exception)
|
| 25 |
-
return True
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class WorkspaceWebsocketServer(pycrdt.websocket.WebsocketServer):
|
| 29 |
-
async def init_room(self, name: str) -> pycrdt.websocket.YRoom:
|
| 30 |
-
"""Initialize a room for the workspace with the given name.
|
| 31 |
-
|
| 32 |
-
The workspace is loaded from ".crdt" if it exists there, or from a JSON file, or a new workspace is created.
|
| 33 |
-
"""
|
| 34 |
-
crdt_path = pathlib.Path(".crdt")
|
| 35 |
-
path = crdt_path / f"{name}.crdt"
|
| 36 |
-
assert path.is_relative_to(crdt_path), f"Path '{path}' is invalid"
|
| 37 |
-
ystore = pycrdt.store.file.FileYStore(path)
|
| 38 |
-
ydoc = pycrdt.Doc()
|
| 39 |
-
ydoc["workspace"] = ws = pycrdt.Map()
|
| 40 |
-
# Replay updates from the store.
|
| 41 |
-
try:
|
| 42 |
-
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
| 43 |
-
ydoc.apply_update(update)
|
| 44 |
-
except pycrdt.store.YDocNotFound:
|
| 45 |
-
pass
|
| 46 |
-
if "nodes" not in ws:
|
| 47 |
-
ws["nodes"] = pycrdt.Array()
|
| 48 |
-
if "edges" not in ws:
|
| 49 |
-
ws["edges"] = pycrdt.Array()
|
| 50 |
-
if "env" not in ws:
|
| 51 |
-
ws["env"] = next(iter(ops.CATALOGS), "unset")
|
| 52 |
-
# We have two possible sources of truth for the workspaces, the YStore and the JSON files.
|
| 53 |
-
# In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
|
| 54 |
-
try_to_load_workspace(ws, name)
|
| 55 |
-
ws_simple = workspace.Workspace.model_validate(ws.to_py())
|
| 56 |
-
clean_input(ws_simple)
|
| 57 |
-
# Set the last known version to the current state, so we don't trigger a change event.
|
| 58 |
-
last_known_versions[name] = ws_simple
|
| 59 |
-
room = pycrdt.websocket.YRoom(
|
| 60 |
-
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 61 |
-
)
|
| 62 |
-
# We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
|
| 63 |
-
room.ws = ws # ty: ignore[unresolved-attribute]
|
| 64 |
-
|
| 65 |
-
def on_change(changes):
|
| 66 |
-
task = asyncio.create_task(workspace_changed(name, changes, ws))
|
| 67 |
-
# We have no way to await workspace_changed(). The best we can do is to
|
| 68 |
-
# dereference its result after it's done, so exceptions are logged normally.
|
| 69 |
-
task.add_done_callback(lambda t: t.result())
|
| 70 |
-
|
| 71 |
-
ws.observe_deep(on_change)
|
| 72 |
-
return room
|
| 73 |
-
|
| 74 |
-
async def get_room(self, name: str) -> pycrdt.websocket.YRoom:
|
| 75 |
-
"""Get a room by name.
|
| 76 |
-
|
| 77 |
-
This method overrides the parent get_room method. The original creates an empty room,
|
| 78 |
-
with no associated Ydoc. Instead, we want to initialize the the room with a Workspace
|
| 79 |
-
object.
|
| 80 |
-
"""
|
| 81 |
-
if name not in self.rooms:
|
| 82 |
-
self.rooms[name] = await self.init_room(name)
|
| 83 |
-
room = self.rooms[name]
|
| 84 |
-
await self.start_room(room)
|
| 85 |
-
return room
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
class CodeWebsocketServer(WorkspaceWebsocketServer):
|
| 89 |
-
async def init_room(self, name: str) -> pycrdt.websocket.YRoom:
|
| 90 |
-
"""Initialize a room for a text document with the given name."""
|
| 91 |
-
crdt_path = pathlib.Path(".crdt")
|
| 92 |
-
path = crdt_path / f"{name}.crdt"
|
| 93 |
-
assert path.is_relative_to(crdt_path), f"Path '{path}' is invalid"
|
| 94 |
-
ystore = pycrdt.store.file.FileYStore(path)
|
| 95 |
-
ydoc = pycrdt.Doc()
|
| 96 |
-
ydoc["text"] = text = pycrdt.Text()
|
| 97 |
-
# Replay updates from the store.
|
| 98 |
-
try:
|
| 99 |
-
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
| 100 |
-
ydoc.apply_update(update)
|
| 101 |
-
except pycrdt.store.YDocNotFound:
|
| 102 |
-
pass
|
| 103 |
-
if len(text) == 0:
|
| 104 |
-
if os.path.exists(name):
|
| 105 |
-
with open(name, encoding="utf-8") as f:
|
| 106 |
-
text += f.read().replace("\r\n", "\n")
|
| 107 |
-
room = pycrdt.websocket.YRoom(
|
| 108 |
-
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 109 |
-
)
|
| 110 |
-
# We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
|
| 111 |
-
room.text = text # ty: ignore[unresolved-attribute]
|
| 112 |
-
|
| 113 |
-
def on_change(changes):
|
| 114 |
-
asyncio.create_task(code_changed(name, changes, text))
|
| 115 |
-
|
| 116 |
-
text.observe(on_change)
|
| 117 |
-
return room
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
last_ws_input = None
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def clean_input(ws_pyd):
|
| 124 |
-
"""Delete everything that we want to ignore for the purposes of change detection."""
|
| 125 |
-
for node in ws_pyd.nodes:
|
| 126 |
-
node.data.display = None
|
| 127 |
-
node.data.input_metadata = None
|
| 128 |
-
node.data.error = None
|
| 129 |
-
node.data.status = workspace.NodeStatus.done
|
| 130 |
-
for p in list(node.data.params):
|
| 131 |
-
if p.startswith("_"):
|
| 132 |
-
del node.data.params[p]
|
| 133 |
-
if node.data.op_id == "Comment":
|
| 134 |
-
node.data.params = {}
|
| 135 |
-
node.position.x = 0
|
| 136 |
-
node.position.y = 0
|
| 137 |
-
node.width = 0
|
| 138 |
-
node.height = 0
|
| 139 |
-
if node.model_extra:
|
| 140 |
-
for key in list(node.model_extra.keys()):
|
| 141 |
-
delattr(node, key)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def crdt_update(
|
| 145 |
-
crdt_obj: pycrdt.Map | pycrdt.Array,
|
| 146 |
-
python_obj: dict | list,
|
| 147 |
-
non_collaborative_fields: set[str] = set(),
|
| 148 |
-
):
|
| 149 |
-
"""Update a CRDT object to match a Python object.
|
| 150 |
-
|
| 151 |
-
The types between the CRDT object and the Python object must match. If the Python object
|
| 152 |
-
is a dict, the CRDT object must be a Map. If the Python object is a list, the CRDT object
|
| 153 |
-
must be an Array.
|
| 154 |
-
|
| 155 |
-
Args:
|
| 156 |
-
crdt_obj: The CRDT object, that will be updated to match the Python object.
|
| 157 |
-
python_obj: The Python object to update with.
|
| 158 |
-
non_collaborative_fields: List of fields to treat as a black box. Black boxes are
|
| 159 |
-
updated as a whole, instead of having a fine-grained data structure to edit
|
| 160 |
-
collaboratively. Useful for complex fields that contain auto-generated data or
|
| 161 |
-
metadata.
|
| 162 |
-
The default is an empty set.
|
| 163 |
-
|
| 164 |
-
Raises:
|
| 165 |
-
ValueError: If the Python object provided is not a dict or list.
|
| 166 |
-
"""
|
| 167 |
-
if isinstance(python_obj, dict):
|
| 168 |
-
assert isinstance(crdt_obj, pycrdt.Map), "CRDT object must be a Map for a dict input"
|
| 169 |
-
for key, value in python_obj.items():
|
| 170 |
-
if key in non_collaborative_fields:
|
| 171 |
-
crdt_obj[key] = value
|
| 172 |
-
elif isinstance(value, dict):
|
| 173 |
-
if crdt_obj.get(key) is None:
|
| 174 |
-
crdt_obj[key] = pycrdt.Map()
|
| 175 |
-
crdt_update(crdt_obj[key], value, non_collaborative_fields)
|
| 176 |
-
elif isinstance(value, list):
|
| 177 |
-
if crdt_obj.get(key) is None:
|
| 178 |
-
crdt_obj[key] = pycrdt.Array()
|
| 179 |
-
crdt_update(crdt_obj[key], value, non_collaborative_fields)
|
| 180 |
-
elif isinstance(value, enum.Enum):
|
| 181 |
-
crdt_obj[key] = str(value.value)
|
| 182 |
-
else:
|
| 183 |
-
crdt_obj[key] = value
|
| 184 |
-
elif isinstance(python_obj, list):
|
| 185 |
-
assert isinstance(crdt_obj, pycrdt.Array), "CRDT object must be an Array for a list input"
|
| 186 |
-
for i, value in enumerate(python_obj):
|
| 187 |
-
if isinstance(value, dict):
|
| 188 |
-
if i >= len(crdt_obj):
|
| 189 |
-
crdt_obj.append(pycrdt.Map())
|
| 190 |
-
crdt_update(crdt_obj[i], value, non_collaborative_fields)
|
| 191 |
-
elif isinstance(value, list):
|
| 192 |
-
if i >= len(crdt_obj):
|
| 193 |
-
crdt_obj.append(pycrdt.Array())
|
| 194 |
-
crdt_update(crdt_obj[i], value, non_collaborative_fields)
|
| 195 |
-
else:
|
| 196 |
-
if isinstance(value, enum.Enum):
|
| 197 |
-
value = str(value.value)
|
| 198 |
-
if i >= len(crdt_obj):
|
| 199 |
-
crdt_obj.append(value)
|
| 200 |
-
else:
|
| 201 |
-
crdt_obj[i] = value
|
| 202 |
-
else:
|
| 203 |
-
raise ValueError("Invalid type:", python_obj)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def try_to_load_workspace(ws: pycrdt.Map, name: str):
|
| 207 |
-
"""Load the workspace `name`, if it exists, and update the `ws` CRDT object to match its contents.
|
| 208 |
-
|
| 209 |
-
Args:
|
| 210 |
-
ws: CRDT object to udpate with the workspace contents.
|
| 211 |
-
name: Name of the workspace to load.
|
| 212 |
-
"""
|
| 213 |
-
if os.path.exists(name):
|
| 214 |
-
ws_pyd = workspace.Workspace.load(name)
|
| 215 |
-
crdt_update(
|
| 216 |
-
ws,
|
| 217 |
-
ws_pyd.model_dump(),
|
| 218 |
-
# We treat some fields as black boxes. They are not edited on the frontend.
|
| 219 |
-
non_collaborative_fields={"display", "input_metadata", "meta"},
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
last_known_versions = {}
|
| 224 |
-
delayed_executions = {}
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
async def workspace_changed(name: str, changes: list[pycrdt.MapEvent], ws_crdt: pycrdt.Map):
|
| 228 |
-
"""Callback to react to changes in the workspace.
|
| 229 |
-
|
| 230 |
-
Args:
|
| 231 |
-
name: Name of the workspace.
|
| 232 |
-
changes: Changes performed to the workspace.
|
| 233 |
-
ws_crdt: CRDT object representing the workspace.
|
| 234 |
-
"""
|
| 235 |
-
ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
|
| 236 |
-
# Do not trigger execution for superficial changes.
|
| 237 |
-
# This is a quick solution until we build proper caching.
|
| 238 |
-
ws_simple = ws_pyd.model_copy(deep=True)
|
| 239 |
-
clean_input(ws_simple)
|
| 240 |
-
if ws_simple == last_known_versions.get(name):
|
| 241 |
-
return
|
| 242 |
-
last_known_versions[name] = ws_simple
|
| 243 |
-
# Frontend changes that result from typing are delayed to avoid
|
| 244 |
-
# rerunning the workspace for every keystroke.
|
| 245 |
-
if name in delayed_executions:
|
| 246 |
-
delayed_executions[name].cancel()
|
| 247 |
-
delay = min(
|
| 248 |
-
getattr(change, "keys", {}).get("__execution_delay", {}).get("newValue", 0)
|
| 249 |
-
for change in changes
|
| 250 |
-
)
|
| 251 |
-
# Check if workspace is paused - if so, skip automatic execution
|
| 252 |
-
if getattr(ws_pyd, "paused", False):
|
| 253 |
-
print(f"Skipping automatic execution for {name} in {ws_pyd.env} - workspace is paused")
|
| 254 |
-
return
|
| 255 |
-
if delay:
|
| 256 |
-
task = asyncio.create_task(execute(name, ws_crdt, ws_pyd, delay))
|
| 257 |
-
delayed_executions[name] = task
|
| 258 |
-
else:
|
| 259 |
-
await execute(name, ws_crdt, ws_pyd)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0):
|
| 263 |
-
"""Execute the workspace and update the CRDT object with the results.
|
| 264 |
-
|
| 265 |
-
Args:
|
| 266 |
-
name: Name of the workspace.
|
| 267 |
-
ws_crdt: CRDT object representing the workspace.
|
| 268 |
-
ws_pyd: Workspace object to execute.
|
| 269 |
-
delay: Wait time before executing the workspace. The default is 0.
|
| 270 |
-
"""
|
| 271 |
-
if delay:
|
| 272 |
-
try:
|
| 273 |
-
await asyncio.sleep(delay)
|
| 274 |
-
except asyncio.CancelledError:
|
| 275 |
-
return
|
| 276 |
-
print(f"Running {name} in {ws_pyd.env}...")
|
| 277 |
-
cwd = pathlib.Path()
|
| 278 |
-
path = cwd / name
|
| 279 |
-
assert path.is_relative_to(cwd), f"Path '{path}' is invalid"
|
| 280 |
-
# Save user changes before executing, in case the execution fails.
|
| 281 |
-
ws_pyd.save(path)
|
| 282 |
-
ops.load_user_scripts(name)
|
| 283 |
-
ws_pyd.connect_crdt(ws_crdt)
|
| 284 |
-
ws_pyd.update_metadata()
|
| 285 |
-
if not ws_pyd.has_executor():
|
| 286 |
-
return
|
| 287 |
-
with ws_crdt.doc.transaction():
|
| 288 |
-
for nc in ws_crdt["nodes"]:
|
| 289 |
-
nc["data"]["status"] = "planned"
|
| 290 |
-
ws_pyd.normalize()
|
| 291 |
-
await ws_pyd.execute()
|
| 292 |
-
ws_pyd.save(path)
|
| 293 |
-
print(f"Finished running {name} in {ws_pyd.env}.")
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
|
| 297 |
-
contents = str(text).strip() + "\n"
|
| 298 |
-
with open(name, "w", encoding="utf-8") as f:
|
| 299 |
-
f.write(contents)
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
ws_websocket_server: WorkspaceWebsocketServer
|
| 303 |
-
code_websocket_server: CodeWebsocketServer
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
def get_room(name):
|
| 307 |
-
return ws_websocket_server.get_room(name)
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
@contextlib.asynccontextmanager
|
| 311 |
-
async def lifespan(app):
|
| 312 |
-
global ws_websocket_server
|
| 313 |
-
global code_websocket_server
|
| 314 |
-
ws_websocket_server = WorkspaceWebsocketServer(auto_clean_rooms=False)
|
| 315 |
-
code_websocket_server = CodeWebsocketServer(auto_clean_rooms=False)
|
| 316 |
-
async with ws_websocket_server:
|
| 317 |
-
async with code_websocket_server:
|
| 318 |
-
yield
|
| 319 |
-
print("closing websocket server")
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
def delete_room(name: str):
|
| 323 |
-
if name in ws_websocket_server.rooms:
|
| 324 |
-
del ws_websocket_server.rooms[name]
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def sanitize_path(path):
|
| 328 |
-
return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
@router.websocket("/ws/crdt/{room_name:path}")
|
| 332 |
-
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
| 333 |
-
room_name = sanitize_path(room_name)
|
| 334 |
-
server = pycrdt.websocket.ASGIServer(ws_websocket_server)
|
| 335 |
-
await server({"path": room_name, "type": "websocket"}, websocket._receive, websocket._send)
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
@router.websocket("/ws/code/crdt/{room_name:path}")
|
| 339 |
-
async def code_crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
| 340 |
-
room_name = sanitize_path(room_name)
|
| 341 |
-
server = pycrdt.websocket.ASGIServer(code_websocket_server)
|
| 342 |
-
await server({"path": room_name, "type": "websocket"}, websocket._receive, websocket._send)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/main.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
"""The FastAPI server for serving the LynxKite application."""
|
| 2 |
-
|
| 3 |
-
import shutil
|
| 4 |
-
import pydantic
|
| 5 |
-
import fastapi
|
| 6 |
-
import importlib
|
| 7 |
-
import joblib
|
| 8 |
-
import pathlib
|
| 9 |
-
import pkgutil
|
| 10 |
-
from fastapi.staticfiles import StaticFiles
|
| 11 |
-
from fastapi.middleware.gzip import GZipMiddleware
|
| 12 |
-
import starlette.exceptions
|
| 13 |
-
from lynxkite.core import ops
|
| 14 |
-
from lynxkite.core import workspace
|
| 15 |
-
from . import crdt
|
| 16 |
-
|
| 17 |
-
mem = joblib.Memory(".joblib-cache")
|
| 18 |
-
ops.CACHE_WRAPPER = mem.cache
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def detect_plugins():
|
| 22 |
-
plugins = {}
|
| 23 |
-
for _, name, _ in pkgutil.iter_modules():
|
| 24 |
-
if name.startswith("lynxkite_") and name != "lynxkite_app":
|
| 25 |
-
print(f"Importing {name}")
|
| 26 |
-
plugins[name] = importlib.import_module(name)
|
| 27 |
-
if not plugins:
|
| 28 |
-
print("No LynxKite plugins found. Be sure to install some!")
|
| 29 |
-
return plugins
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
lynxkite_plugins = detect_plugins()
|
| 33 |
-
ops.save_catalogs("plugins loaded")
|
| 34 |
-
|
| 35 |
-
app = fastapi.FastAPI(lifespan=crdt.lifespan)
|
| 36 |
-
app.include_router(crdt.router)
|
| 37 |
-
app.add_middleware(GZipMiddleware)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _get_ops(env: str):
|
| 41 |
-
catalog = ops.CATALOGS[env]
|
| 42 |
-
res = {op.name: op.model_dump() for op in catalog.values()}
|
| 43 |
-
res.setdefault("Comment", ops.COMMENT_OP.model_dump())
|
| 44 |
-
return res
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
@app.get("/api/catalog")
|
| 48 |
-
def get_catalog(workspace: str):
|
| 49 |
-
ops.load_user_scripts(workspace)
|
| 50 |
-
return {env: _get_ops(env) for env in ops.CATALOGS}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
data_path = pathlib.Path()
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
@app.post("/api/delete")
|
| 57 |
-
async def delete_workspace(req: dict):
|
| 58 |
-
json_path: pathlib.Path = data_path / req["path"]
|
| 59 |
-
crdt_path: pathlib.Path = data_path / ".crdt" / f"{req['path']}.crdt"
|
| 60 |
-
assert json_path.is_relative_to(data_path), f"Path '{json_path}' is invalid"
|
| 61 |
-
json_path.unlink()
|
| 62 |
-
crdt_path.unlink()
|
| 63 |
-
crdt.delete_room(req["path"])
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
class DirectoryEntry(pydantic.BaseModel):
|
| 67 |
-
name: str
|
| 68 |
-
type: str
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _get_path_type(path: pathlib.Path) -> str:
|
| 72 |
-
if path.is_dir():
|
| 73 |
-
return "directory"
|
| 74 |
-
elif path.suffixes[-2:] == [".lynxkite", ".json"]:
|
| 75 |
-
return "workspace"
|
| 76 |
-
else:
|
| 77 |
-
return "file"
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
@app.get("/api/dir/list")
|
| 81 |
-
def list_dir(path: str):
|
| 82 |
-
path = data_path / path
|
| 83 |
-
assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
|
| 84 |
-
return sorted(
|
| 85 |
-
[
|
| 86 |
-
DirectoryEntry(
|
| 87 |
-
name=str(p.relative_to(data_path)),
|
| 88 |
-
type=_get_path_type(p),
|
| 89 |
-
)
|
| 90 |
-
for p in path.iterdir()
|
| 91 |
-
if not p.name.startswith(".")
|
| 92 |
-
],
|
| 93 |
-
key=lambda x: (x.type != "directory", x.name.lower()),
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
@app.post("/api/dir/mkdir")
|
| 98 |
-
def make_dir(req: dict):
|
| 99 |
-
path = data_path / req["path"]
|
| 100 |
-
assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
|
| 101 |
-
assert not path.exists(), f"{path} already exists"
|
| 102 |
-
path.mkdir()
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
@app.post("/api/dir/delete")
|
| 106 |
-
def delete_dir(req: dict):
|
| 107 |
-
path: pathlib.Path = data_path / req["path"]
|
| 108 |
-
assert all([path.is_relative_to(data_path), path.exists(), path.is_dir()]), (
|
| 109 |
-
f"Path '{path}' is invalid"
|
| 110 |
-
)
|
| 111 |
-
shutil.rmtree(path)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
@app.get("/api/service/{module_path:path}")
|
| 115 |
-
async def service_get(req: fastapi.Request, module_path: str):
|
| 116 |
-
"""Executors can provide extra HTTP APIs through the /api/service endpoint."""
|
| 117 |
-
module = lynxkite_plugins[module_path.split("/")[0]]
|
| 118 |
-
return await module.api_service_get(req)
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
@app.post("/api/service/{module_path:path}")
|
| 122 |
-
async def service_post(req: fastapi.Request, module_path: str):
|
| 123 |
-
"""Executors can provide extra HTTP APIs through the /api/service endpoint."""
|
| 124 |
-
module = lynxkite_plugins[module_path.split("/")[0]]
|
| 125 |
-
return await module.api_service_post(req)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
@app.post("/api/upload")
|
| 129 |
-
async def upload(req: fastapi.Request):
|
| 130 |
-
"""Receives file uploads and stores them in DATA_PATH."""
|
| 131 |
-
form = await req.form()
|
| 132 |
-
for file in form.values():
|
| 133 |
-
file_path = data_path / "uploads" / file.filename
|
| 134 |
-
assert file_path.is_relative_to(data_path), f"Path '{file_path}' is invalid"
|
| 135 |
-
with file_path.open("wb") as buffer:
|
| 136 |
-
shutil.copyfileobj(file.file, buffer)
|
| 137 |
-
return {"status": "ok"}
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
@app.post("/api/execute_workspace")
|
| 141 |
-
async def execute_workspace(name: str):
|
| 142 |
-
"""Trigger and await the execution of a workspace."""
|
| 143 |
-
room = await crdt.get_room(name)
|
| 144 |
-
ws_pyd = workspace.Workspace.model_validate(room.ws.to_py())
|
| 145 |
-
await crdt.execute(name, room.ws, ws_pyd)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
class SPAStaticFiles(StaticFiles):
|
| 149 |
-
"""Route everything to index.html. https://stackoverflow.com/a/73552966/3318517"""
|
| 150 |
-
|
| 151 |
-
async def get_response(self, path: str, scope):
|
| 152 |
-
try:
|
| 153 |
-
return await super().get_response(path, scope)
|
| 154 |
-
except (
|
| 155 |
-
fastapi.HTTPException,
|
| 156 |
-
starlette.exceptions.HTTPException,
|
| 157 |
-
) as ex:
|
| 158 |
-
if ex.status_code == 404:
|
| 159 |
-
return await super().get_response(".", scope)
|
| 160 |
-
else:
|
| 161 |
-
raise ex
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
static_dir = SPAStaticFiles(packages=[("lynxkite_app", "web_assets")], html=True)
|
| 165 |
-
app.mount("/", static_dir, name="web_assets")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/web_assets/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""The build process (uv build) puts the frontend build artifacts here."""
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/web_assets/assets/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""More frontend build artifacts."""
|
|
|
|
|
|
lynxkite-app/tests/test_crdt.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
from enum import Enum
|
| 2 |
-
import pycrdt
|
| 3 |
-
import pytest
|
| 4 |
-
from lynxkite_app.crdt import crdt_update
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
@pytest.fixture
|
| 8 |
-
def empty_dict_workspace():
|
| 9 |
-
ydoc = pycrdt.Doc()
|
| 10 |
-
ydoc["workspace"] = ws = pycrdt.Map()
|
| 11 |
-
yield ws
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
@pytest.fixture
|
| 15 |
-
def empty_list_workspace():
|
| 16 |
-
ydoc = pycrdt.Doc()
|
| 17 |
-
ydoc["workspace"] = ws = pycrdt.Array()
|
| 18 |
-
yield ws
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class MyEnum(int, Enum):
|
| 22 |
-
VALUE = 1
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@pytest.mark.parametrize(
|
| 26 |
-
"python_obj,expected",
|
| 27 |
-
[
|
| 28 |
-
(
|
| 29 |
-
{
|
| 30 |
-
"key1": "value1",
|
| 31 |
-
"key2": {
|
| 32 |
-
"nested_key1": "nested_value1",
|
| 33 |
-
"nested_key2": ["nested_value2"],
|
| 34 |
-
"nested_key3": MyEnum.VALUE,
|
| 35 |
-
},
|
| 36 |
-
},
|
| 37 |
-
{
|
| 38 |
-
"key1": "value1",
|
| 39 |
-
"key2": {
|
| 40 |
-
"nested_key1": "nested_value1",
|
| 41 |
-
"nested_key2": ["nested_value2"],
|
| 42 |
-
"nested_key3": "1",
|
| 43 |
-
},
|
| 44 |
-
},
|
| 45 |
-
)
|
| 46 |
-
],
|
| 47 |
-
)
|
| 48 |
-
def test_crdt_update_with_dict(empty_dict_workspace, python_obj, expected):
|
| 49 |
-
crdt_update(empty_dict_workspace, python_obj)
|
| 50 |
-
assert empty_dict_workspace.to_py() == expected
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
@pytest.mark.parametrize(
|
| 54 |
-
"python_obj,expected",
|
| 55 |
-
[
|
| 56 |
-
(
|
| 57 |
-
[
|
| 58 |
-
"value1",
|
| 59 |
-
{"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
|
| 60 |
-
MyEnum.VALUE,
|
| 61 |
-
],
|
| 62 |
-
[
|
| 63 |
-
"value1",
|
| 64 |
-
{"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
|
| 65 |
-
"1",
|
| 66 |
-
],
|
| 67 |
-
),
|
| 68 |
-
],
|
| 69 |
-
)
|
| 70 |
-
def test_crdt_update_with_list(empty_list_workspace, python_obj, expected):
|
| 71 |
-
crdt_update(empty_list_workspace, python_obj)
|
| 72 |
-
assert empty_list_workspace.to_py() == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/tests/test_main.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
import pathlib
|
| 2 |
-
import uuid
|
| 3 |
-
from fastapi.testclient import TestClient
|
| 4 |
-
from lynxkite_app.main import app, detect_plugins
|
| 5 |
-
from lynxkite.core import ops
|
| 6 |
-
import os
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
ops.user_script_root = None
|
| 10 |
-
client = TestClient(app)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def test_detect_plugins_with_plugins():
|
| 14 |
-
# This test assumes that these plugins are installed as part of the testing process.
|
| 15 |
-
plugins = detect_plugins()
|
| 16 |
-
assert all(
|
| 17 |
-
plugin in plugins.keys()
|
| 18 |
-
for plugin in [
|
| 19 |
-
"lynxkite_graph_analytics",
|
| 20 |
-
"lynxkite_lynxscribe",
|
| 21 |
-
"lynxkite_pillow_example",
|
| 22 |
-
]
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def test_get_catalog():
|
| 27 |
-
response = client.get("/api/catalog?workspace=test")
|
| 28 |
-
assert response.status_code == 200
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def test_list_dir():
|
| 32 |
-
test_dir = pathlib.Path() / str(uuid.uuid4())
|
| 33 |
-
test_dir.mkdir(parents=True, exist_ok=True)
|
| 34 |
-
dir = test_dir / "test_dir"
|
| 35 |
-
dir.mkdir(exist_ok=True)
|
| 36 |
-
file = test_dir / "test_file.txt"
|
| 37 |
-
file.touch()
|
| 38 |
-
ws = test_dir / "test_workspace.lynxkite.json"
|
| 39 |
-
ws.touch()
|
| 40 |
-
response = client.get(f"/api/dir/list?path={str(test_dir)}")
|
| 41 |
-
assert response.status_code == 200
|
| 42 |
-
assert response.json() == [
|
| 43 |
-
{"name": f"{test_dir}/test_dir", "type": "directory"},
|
| 44 |
-
{"name": f"{test_dir}/test_file.txt", "type": "file"},
|
| 45 |
-
{"name": f"{test_dir}/test_workspace.lynxkite.json", "type": "workspace"},
|
| 46 |
-
]
|
| 47 |
-
file.unlink()
|
| 48 |
-
ws.unlink()
|
| 49 |
-
dir.rmdir()
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def test_make_dir():
|
| 53 |
-
dir_name = str(uuid.uuid4())
|
| 54 |
-
response = client.post("/api/dir/mkdir", json={"path": dir_name})
|
| 55 |
-
assert response.status_code == 200
|
| 56 |
-
assert os.path.exists(dir_name)
|
| 57 |
-
os.rmdir(dir_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/uv.lock
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lynxkite-app/web/.gitignore
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
# Logs
|
| 2 |
-
logs
|
| 3 |
-
*.log
|
| 4 |
-
npm-debug.log*
|
| 5 |
-
yarn-debug.log*
|
| 6 |
-
yarn-error.log*
|
| 7 |
-
pnpm-debug.log*
|
| 8 |
-
lerna-debug.log*
|
| 9 |
-
|
| 10 |
-
node_modules
|
| 11 |
-
dist
|
| 12 |
-
dist-ssr
|
| 13 |
-
*.local
|
| 14 |
-
|
| 15 |
-
# Editor directories and files
|
| 16 |
-
.vscode/*
|
| 17 |
-
!.vscode/extensions.json
|
| 18 |
-
.idea
|
| 19 |
-
.DS_Store
|
| 20 |
-
*.suo
|
| 21 |
-
*.ntvs*
|
| 22 |
-
*.njsproj
|
| 23 |
-
*.sln
|
| 24 |
-
*.sw?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/README.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
To set up:
|
| 2 |
-
|
| 3 |
-
npm i
|
| 4 |
-
|
| 5 |
-
To start dev server:
|
| 6 |
-
|
| 7 |
-
npm run dev
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/index.html
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.ico" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
</head>
|
| 8 |
-
<body>
|
| 9 |
-
<div id="root"></div>
|
| 10 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
-
</body>
|
| 12 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lynxkite-app/web/package.json
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "lynxkite",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "0.0.0",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"test": "playwright test",
|
| 8 |
-
"dev": "npx vite",
|
| 9 |
-
"build": "npx tsc -b && npx vite build",
|
| 10 |
-
"preview": "npx vite preview"
|
| 11 |
-
},
|
| 12 |
-
"dependencies": {
|
| 13 |
-
"@esbuild/linux-x64": "^0.25.0",
|
| 14 |
-
"@iconify-json/tabler": "^1.2.10",
|
| 15 |
-
"@monaco-editor/react": "^4.7.0",
|
| 16 |
-
"@svgr/core": "^8.1.0",
|
| 17 |
-
"@svgr/plugin-jsx": "^8.1.0",
|
| 18 |
-
"@swc/core": "^1.10.1",
|
| 19 |
-
"@syncedstore/core": "^0.6.0",
|
| 20 |
-
"@syncedstore/react": "^0.6.0",
|
| 21 |
-
"@types/node": "^22.10.1",
|
| 22 |
-
"@xyflow/react": "^12.6.0",
|
| 23 |
-
"3dmol": "^2.4.2",
|
| 24 |
-
"axios": "^1.8.2",
|
| 25 |
-
"daisyui": "^4.12.20",
|
| 26 |
-
"echarts": "^5.5.1",
|
| 27 |
-
"fuse.js": "^7.0.0",
|
| 28 |
-
"jmespath": "^0.16.0",
|
| 29 |
-
"json-schema-to-typescript": "^15.0.3",
|
| 30 |
-
"monaco-editor": "^0.52.2",
|
| 31 |
-
"react": "^18.3.1",
|
| 32 |
-
"react-dom": "^18.3.1",
|
| 33 |
-
"react-error-boundary": "^5.0.0",
|
| 34 |
-
"react-markdown": "^9.0.1",
|
| 35 |
-
"react-router-dom": "^7.5.2",
|
| 36 |
-
"react-tooltip": "^5.28.1",
|
| 37 |
-
"swr": "^2.2.5",
|
| 38 |
-
"unplugin-icons": "^0.21.0",
|
| 39 |
-
"y-monaco": "^0.1.6",
|
| 40 |
-
"y-websocket": "^2.0.4",
|
| 41 |
-
"yjs": "^13.6.20"
|
| 42 |
-
},
|
| 43 |
-
"devDependencies": {
|
| 44 |
-
"@playwright/test": "^1.50.1",
|
| 45 |
-
"@tailwindcss/typography": "^0.5.16",
|
| 46 |
-
"@types/jmespath": "^0.15.2",
|
| 47 |
-
"@types/node": "^22.13.1",
|
| 48 |
-
"@types/react": "^18.3.14",
|
| 49 |
-
"@types/react-dom": "^18.3.2",
|
| 50 |
-
"@vitejs/plugin-react-swc": "^3.5.0",
|
| 51 |
-
"autoprefixer": "^10.4.20",
|
| 52 |
-
"globals": "^15.12.0",
|
| 53 |
-
"postcss": "^8.4.49",
|
| 54 |
-
"tailwindcss": "^3.4.16",
|
| 55 |
-
"typescript": "~5.6.2",
|
| 56 |
-
"vite": "^6.3.4"
|
| 57 |
-
},
|
| 58 |
-
"optionalDependencies": {
|
| 59 |
-
"@rollup/rollup-linux-x64-gnu": "^4.28.1"
|
| 60 |
-
}
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/playwright.config.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
import { defineConfig, devices } from "@playwright/test";
|
| 2 |
-
|
| 3 |
-
export default defineConfig({
|
| 4 |
-
testDir: "./tests",
|
| 5 |
-
timeout: process.env.CI ? 30000 : 10000,
|
| 6 |
-
fullyParallel: false,
|
| 7 |
-
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
| 8 |
-
forbidOnly: !!process.env.CI,
|
| 9 |
-
retries: 0,
|
| 10 |
-
maxFailures: 3,
|
| 11 |
-
workers: 1,
|
| 12 |
-
reporter: process.env.CI ? [["github"], ["html"]] : "html",
|
| 13 |
-
use: {
|
| 14 |
-
/* Base URL to use in actions like `await page.goto('/')`. */
|
| 15 |
-
baseURL: "http://127.0.0.1:8000",
|
| 16 |
-
trace: "on",
|
| 17 |
-
testIdAttribute: "data-nodeid", // Useful for easily selecting nodes using getByTestId
|
| 18 |
-
},
|
| 19 |
-
projects: [
|
| 20 |
-
{
|
| 21 |
-
name: "chromium",
|
| 22 |
-
use: { ...devices["Desktop Chrome"] },
|
| 23 |
-
},
|
| 24 |
-
],
|
| 25 |
-
webServer: {
|
| 26 |
-
command: "cd ../../examples && LYNXKITE_SUPPRESS_OP_ERRORS=1 lynxkite",
|
| 27 |
-
port: 8000,
|
| 28 |
-
reuseExistingServer: false,
|
| 29 |
-
},
|
| 30 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/postcss.config.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
export default {
|
| 2 |
-
plugins: {
|
| 3 |
-
tailwindcss: {},
|
| 4 |
-
autoprefixer: {},
|
| 5 |
-
},
|
| 6 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/Code.tsx
DELETED
|
@@ -1,119 +0,0 @@
|
|
| 1 |
-
// Full-page editor for code files.
|
| 2 |
-
|
| 3 |
-
import Editor, { type Monaco } from "@monaco-editor/react";
|
| 4 |
-
import type { editor } from "monaco-editor";
|
| 5 |
-
import { useEffect, useRef } from "react";
|
| 6 |
-
import { Link } from "react-router";
|
| 7 |
-
import { WebsocketProvider } from "y-websocket";
|
| 8 |
-
import * as Y from "yjs";
|
| 9 |
-
// @ts-ignore
|
| 10 |
-
import Atom from "~icons/tabler/atom.jsx";
|
| 11 |
-
// @ts-ignore
|
| 12 |
-
import Backspace from "~icons/tabler/backspace.jsx";
|
| 13 |
-
// @ts-ignore
|
| 14 |
-
import Close from "~icons/tabler/x.jsx";
|
| 15 |
-
import favicon from "./assets/favicon.ico";
|
| 16 |
-
import theme from "./code-theme.ts";
|
| 17 |
-
import { usePath } from "./common.ts";
|
| 18 |
-
|
| 19 |
-
export default function Code() {
|
| 20 |
-
const path = usePath().replace(/^[/]code[/]/, "");
|
| 21 |
-
const parentDir = path!.split("/").slice(0, -1).join("/");
|
| 22 |
-
const yDocRef = useRef<any>();
|
| 23 |
-
const wsProviderRef = useRef<any>();
|
| 24 |
-
const monacoBindingRef = useRef<any>();
|
| 25 |
-
const yMonacoRef = useRef<any>();
|
| 26 |
-
const yMonacoLoadingRef = useRef(false);
|
| 27 |
-
const editorRef = useRef<any>();
|
| 28 |
-
useEffect(() => {
|
| 29 |
-
const loadMonaco = async () => {
|
| 30 |
-
if (yMonacoLoadingRef.current) return;
|
| 31 |
-
yMonacoLoadingRef.current = true;
|
| 32 |
-
// y-monaco is gigantic. The other Monaco packages are small.
|
| 33 |
-
yMonacoRef.current = await import("y-monaco");
|
| 34 |
-
initCRDT();
|
| 35 |
-
};
|
| 36 |
-
loadMonaco();
|
| 37 |
-
}, []);
|
| 38 |
-
function beforeMount(monaco: Monaco) {
|
| 39 |
-
monaco.editor.defineTheme("lynxkite", theme);
|
| 40 |
-
}
|
| 41 |
-
function onMount(_editor: editor.IStandaloneCodeEditor, monaco: Monaco) {
|
| 42 |
-
// Do nothing on Ctrl+S. We save after every keypress anyway.
|
| 43 |
-
_editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {});
|
| 44 |
-
editorRef.current = _editor;
|
| 45 |
-
initCRDT();
|
| 46 |
-
}
|
| 47 |
-
function initCRDT() {
|
| 48 |
-
if (!yMonacoRef.current || !editorRef.current) return;
|
| 49 |
-
if (yDocRef.current) return;
|
| 50 |
-
yDocRef.current = new Y.Doc();
|
| 51 |
-
const text = yDocRef.current.getText("text");
|
| 52 |
-
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
| 53 |
-
const encodedPath = path!
|
| 54 |
-
.split("/")
|
| 55 |
-
.map((segment) => encodeURIComponent(segment))
|
| 56 |
-
.join("/");
|
| 57 |
-
wsProviderRef.current = new WebsocketProvider(
|
| 58 |
-
`${proto}//${location.host}/ws/code/crdt`,
|
| 59 |
-
encodedPath!,
|
| 60 |
-
yDocRef.current,
|
| 61 |
-
);
|
| 62 |
-
editorRef.current.getModel()!.setEOL(0); // https://github.com/yjs/y-monaco/issues/6
|
| 63 |
-
monacoBindingRef.current = new yMonacoRef.current.MonacoBinding(
|
| 64 |
-
text,
|
| 65 |
-
editorRef.current.getModel()!,
|
| 66 |
-
new Set([editorRef.current]),
|
| 67 |
-
wsProviderRef.current.awareness,
|
| 68 |
-
);
|
| 69 |
-
}
|
| 70 |
-
useEffect(() => {
|
| 71 |
-
return () => {
|
| 72 |
-
yDocRef.current?.destroy();
|
| 73 |
-
wsProviderRef.current?.destroy();
|
| 74 |
-
monacoBindingRef.current?.destroy();
|
| 75 |
-
};
|
| 76 |
-
});
|
| 77 |
-
return (
|
| 78 |
-
<div className="workspace">
|
| 79 |
-
<div className="top-bar bg-neutral">
|
| 80 |
-
<Link className="logo" to="/">
|
| 81 |
-
<img alt="" src={favicon} />
|
| 82 |
-
</Link>
|
| 83 |
-
<div className="ws-name">{path}</div>
|
| 84 |
-
<title>{path}</title>
|
| 85 |
-
<div className="tools text-secondary">
|
| 86 |
-
<button className="btn btn-link">
|
| 87 |
-
<Atom />
|
| 88 |
-
</button>
|
| 89 |
-
<button className="btn btn-link">
|
| 90 |
-
<Backspace />
|
| 91 |
-
</button>
|
| 92 |
-
<Link
|
| 93 |
-
to={`/dir/${parentDir
|
| 94 |
-
.split("/")
|
| 95 |
-
.map((segment) => encodeURIComponent(segment))
|
| 96 |
-
.join("/")}`}
|
| 97 |
-
className="btn btn-link"
|
| 98 |
-
>
|
| 99 |
-
<Close />
|
| 100 |
-
</Link>
|
| 101 |
-
</div>
|
| 102 |
-
</div>
|
| 103 |
-
<Editor
|
| 104 |
-
defaultLanguage="python"
|
| 105 |
-
theme="lynxkite"
|
| 106 |
-
path={path}
|
| 107 |
-
beforeMount={beforeMount}
|
| 108 |
-
onMount={onMount}
|
| 109 |
-
loading={null}
|
| 110 |
-
options={{
|
| 111 |
-
cursorStyle: "block",
|
| 112 |
-
cursorBlinking: "solid",
|
| 113 |
-
minimap: { enabled: false },
|
| 114 |
-
renderLineHighlight: "none",
|
| 115 |
-
}}
|
| 116 |
-
/>
|
| 117 |
-
</div>
|
| 118 |
-
);
|
| 119 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/Directory.tsx
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
// The directory browser.
|
| 3 |
-
import { Link, useNavigate } from "react-router";
|
| 4 |
-
import useSWR from "swr";
|
| 5 |
-
import type { DirectoryEntry } from "./apiTypes.ts";
|
| 6 |
-
import { usePath } from "./common.ts";
|
| 7 |
-
|
| 8 |
-
// @ts-ignore
|
| 9 |
-
import File from "~icons/tabler/file";
|
| 10 |
-
// @ts-ignore
|
| 11 |
-
import FilePlus from "~icons/tabler/file-plus";
|
| 12 |
-
// @ts-ignore
|
| 13 |
-
import Folder from "~icons/tabler/folder";
|
| 14 |
-
// @ts-ignore
|
| 15 |
-
import FolderPlus from "~icons/tabler/folder-plus";
|
| 16 |
-
// @ts-ignore
|
| 17 |
-
import Home from "~icons/tabler/home";
|
| 18 |
-
// @ts-ignore
|
| 19 |
-
import LayoutGrid from "~icons/tabler/layout-grid";
|
| 20 |
-
// @ts-ignore
|
| 21 |
-
import LayoutGridAdd from "~icons/tabler/layout-grid-add";
|
| 22 |
-
// @ts-ignore
|
| 23 |
-
import Trash from "~icons/tabler/trash";
|
| 24 |
-
import logo from "./assets/logo.png";
|
| 25 |
-
|
| 26 |
-
function EntryCreator(props: {
|
| 27 |
-
label: string;
|
| 28 |
-
icon: JSX.Element;
|
| 29 |
-
onCreate: (name: string) => void;
|
| 30 |
-
}) {
|
| 31 |
-
const [isCreating, setIsCreating] = useState(false);
|
| 32 |
-
const [nameValidationError, setNameValidationError] = useState("");
|
| 33 |
-
|
| 34 |
-
function validateName(name: string): boolean {
|
| 35 |
-
if (name.includes("/")) {
|
| 36 |
-
setNameValidationError("Name cannot contain '/' characters");
|
| 37 |
-
return false;
|
| 38 |
-
}
|
| 39 |
-
if (name.trim() === "") {
|
| 40 |
-
setNameValidationError("Name cannot be empty");
|
| 41 |
-
return false;
|
| 42 |
-
}
|
| 43 |
-
setNameValidationError("");
|
| 44 |
-
return true;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
return (
|
| 48 |
-
<>
|
| 49 |
-
{isCreating ? (
|
| 50 |
-
<form
|
| 51 |
-
onSubmit={(e) => {
|
| 52 |
-
e.preventDefault();
|
| 53 |
-
const name = (e.target as HTMLFormElement).entryName.value.trim();
|
| 54 |
-
if (validateName(name)) {
|
| 55 |
-
props.onCreate(name);
|
| 56 |
-
setIsCreating(false);
|
| 57 |
-
}
|
| 58 |
-
}}
|
| 59 |
-
>
|
| 60 |
-
<input
|
| 61 |
-
className={`input input-ghost w-full ${nameValidationError ? "input-error" : ""}`}
|
| 62 |
-
autoFocus
|
| 63 |
-
type="text"
|
| 64 |
-
name="entryName"
|
| 65 |
-
onBlur={() => setIsCreating(false)}
|
| 66 |
-
onChange={(e) => validateName(e.target.value)}
|
| 67 |
-
placeholder={`${props.label} name`}
|
| 68 |
-
/>
|
| 69 |
-
{nameValidationError && (
|
| 70 |
-
<div
|
| 71 |
-
className="error-message"
|
| 72 |
-
role="alert"
|
| 73 |
-
style={{ position: "absolute", zIndex: 10 }}
|
| 74 |
-
>
|
| 75 |
-
<span className="error-icon" aria-hidden="true">
|
| 76 |
-
⚠️
|
| 77 |
-
</span>
|
| 78 |
-
<span className="error-text">{nameValidationError}</span>
|
| 79 |
-
</div>
|
| 80 |
-
)}
|
| 81 |
-
</form>
|
| 82 |
-
) : (
|
| 83 |
-
<button type="button" onClick={() => setIsCreating(true)}>
|
| 84 |
-
{props.icon} {props.label}
|
| 85 |
-
</button>
|
| 86 |
-
)}
|
| 87 |
-
</>
|
| 88 |
-
);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
| 92 |
-
|
| 93 |
-
export default function Directory() {
|
| 94 |
-
const path = usePath().replace(/^[/]$|^[/]dir$|^[/]dir[/]/, "");
|
| 95 |
-
const encodedPath = encodeURIComponent(path || "");
|
| 96 |
-
const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher, {
|
| 97 |
-
dedupingInterval: 0,
|
| 98 |
-
});
|
| 99 |
-
const navigate = useNavigate();
|
| 100 |
-
|
| 101 |
-
function link(item: DirectoryEntry) {
|
| 102 |
-
const encodedName = encodePathSegments(item.name);
|
| 103 |
-
if (item.type === "directory") {
|
| 104 |
-
return `/dir/${encodedName}`;
|
| 105 |
-
}
|
| 106 |
-
if (item.type === "workspace") {
|
| 107 |
-
return `/edit/${encodedName}`;
|
| 108 |
-
}
|
| 109 |
-
return `/code/${encodedName}`;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
function shortName(item: DirectoryEntry) {
|
| 113 |
-
return item.name
|
| 114 |
-
.split("/")
|
| 115 |
-
.pop()
|
| 116 |
-
?.replace(/[.]lynxkite[.]json$/, "");
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
function encodePathSegments(path: string): string {
|
| 120 |
-
const segments = path.split("/");
|
| 121 |
-
return segments.map((segment) => encodeURIComponent(segment)).join("/");
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
function newWorkspaceIn(path: string, workspaceName: string) {
|
| 125 |
-
const pathSlash = path ? `${encodePathSegments(path)}/` : "";
|
| 126 |
-
navigate(`/edit/${pathSlash}${encodeURIComponent(workspaceName)}.lynxkite.json`, {
|
| 127 |
-
replace: true,
|
| 128 |
-
});
|
| 129 |
-
}
|
| 130 |
-
function newCodeFile(path: string, name: string) {
|
| 131 |
-
const pathSlash = path ? `${encodePathSegments(path)}/` : "";
|
| 132 |
-
navigate(`/code/${pathSlash}${encodeURIComponent(name)}`, { replace: true });
|
| 133 |
-
}
|
| 134 |
-
async function newFolderIn(path: string, folderName: string) {
|
| 135 |
-
const pathSlash = path ? `${path}/` : "";
|
| 136 |
-
const res = await fetch("/api/dir/mkdir", {
|
| 137 |
-
method: "POST",
|
| 138 |
-
headers: { "Content-Type": "application/json" },
|
| 139 |
-
body: JSON.stringify({ path: pathSlash + folderName }),
|
| 140 |
-
});
|
| 141 |
-
if (res.ok) {
|
| 142 |
-
const pathSlash = path ? `${encodePathSegments(path)}/` : "";
|
| 143 |
-
navigate(`/dir/${pathSlash}${encodeURIComponent(folderName)}`);
|
| 144 |
-
} else {
|
| 145 |
-
alert("Failed to create folder.");
|
| 146 |
-
}
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
async function deleteItem(item: DirectoryEntry) {
|
| 150 |
-
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
|
| 151 |
-
const apiPath = item.type === "directory" ? "/api/dir/delete" : "/api/delete";
|
| 152 |
-
await fetch(apiPath, {
|
| 153 |
-
method: "POST",
|
| 154 |
-
headers: { "Content-Type": "application/json" },
|
| 155 |
-
body: JSON.stringify({ path: item.name }),
|
| 156 |
-
});
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
return (
|
| 160 |
-
<div className="directory">
|
| 161 |
-
<div className="logo">
|
| 162 |
-
<a href="https://lynxkite.com/">
|
| 163 |
-
<img src={logo} className="logo-image" alt="LynxKite logo" />
|
| 164 |
-
</a>
|
| 165 |
-
<div className="tagline">The Complete Graph Data Science Platform</div>
|
| 166 |
-
</div>
|
| 167 |
-
<div className="entry-list">
|
| 168 |
-
{list.error && <p className="error">{list.error.message}</p>}
|
| 169 |
-
{list.isLoading && (
|
| 170 |
-
<output className="loading spinner-border">
|
| 171 |
-
<span className="visually-hidden">Loading...</span>
|
| 172 |
-
</output>
|
| 173 |
-
)}
|
| 174 |
-
|
| 175 |
-
{list.data && (
|
| 176 |
-
<>
|
| 177 |
-
<div className="actions">
|
| 178 |
-
<EntryCreator
|
| 179 |
-
onCreate={(name) => {
|
| 180 |
-
newWorkspaceIn(path || "", name);
|
| 181 |
-
}}
|
| 182 |
-
icon={<LayoutGridAdd />}
|
| 183 |
-
label="New workspace"
|
| 184 |
-
/>
|
| 185 |
-
<EntryCreator
|
| 186 |
-
onCreate={(name) => {
|
| 187 |
-
newCodeFile(path || "", name);
|
| 188 |
-
}}
|
| 189 |
-
icon={<FilePlus />}
|
| 190 |
-
label="New code file"
|
| 191 |
-
/>
|
| 192 |
-
<EntryCreator
|
| 193 |
-
onCreate={(name: string) => {
|
| 194 |
-
newFolderIn(path || "", name);
|
| 195 |
-
}}
|
| 196 |
-
icon={<FolderPlus />}
|
| 197 |
-
label="New folder"
|
| 198 |
-
/>
|
| 199 |
-
</div>
|
| 200 |
-
|
| 201 |
-
{path ? (
|
| 202 |
-
<div className="breadcrumbs">
|
| 203 |
-
<Link to="/dir/" aria-label="home">
|
| 204 |
-
<Home />
|
| 205 |
-
</Link>{" "}
|
| 206 |
-
<span className="current-folder">{path}</span>
|
| 207 |
-
<title>{path}</title>
|
| 208 |
-
</div>
|
| 209 |
-
) : (
|
| 210 |
-
<title>LynxKite 2000:MM</title>
|
| 211 |
-
)}
|
| 212 |
-
|
| 213 |
-
{list.data.map(
|
| 214 |
-
(item: DirectoryEntry) =>
|
| 215 |
-
!shortName(item)?.startsWith("__") && (
|
| 216 |
-
<div key={item.name} className="entry">
|
| 217 |
-
<Link key={link(item)} to={link(item)}>
|
| 218 |
-
{item.type === "directory" ? (
|
| 219 |
-
<Folder />
|
| 220 |
-
) : item.type === "workspace" ? (
|
| 221 |
-
<LayoutGrid />
|
| 222 |
-
) : (
|
| 223 |
-
<File />
|
| 224 |
-
)}
|
| 225 |
-
<span className="entry-name">{shortName(item)}</span>
|
| 226 |
-
</Link>
|
| 227 |
-
<button
|
| 228 |
-
type="button"
|
| 229 |
-
onClick={() => {
|
| 230 |
-
deleteItem(item);
|
| 231 |
-
}}
|
| 232 |
-
>
|
| 233 |
-
<Trash />
|
| 234 |
-
</button>
|
| 235 |
-
</div>
|
| 236 |
-
),
|
| 237 |
-
)}
|
| 238 |
-
</>
|
| 239 |
-
)}
|
| 240 |
-
</div>{" "}
|
| 241 |
-
</div>
|
| 242 |
-
);
|
| 243 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/Tooltip.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import { useId } from "react";
|
| 2 |
-
import Markdown from "react-markdown";
|
| 3 |
-
import { Tooltip as ReactTooltip } from "react-tooltip";
|
| 4 |
-
|
| 5 |
-
export default function Tooltip(props: any) {
|
| 6 |
-
const id = useId();
|
| 7 |
-
if (!props.doc) return null;
|
| 8 |
-
return (
|
| 9 |
-
<>
|
| 10 |
-
<span data-tooltip-id={id} tabIndex={0}>
|
| 11 |
-
{props.children}
|
| 12 |
-
</span>
|
| 13 |
-
<ReactTooltip id={id} className="tooltip prose" place="top-end">
|
| 14 |
-
{props.doc.map?.(
|
| 15 |
-
(section: any, i: number) =>
|
| 16 |
-
section.kind === "text" && <Markdown key={i}>{section.value}</Markdown>,
|
| 17 |
-
) ?? <Markdown>{props.doc}</Markdown>}
|
| 18 |
-
</ReactTooltip>
|
| 19 |
-
</>
|
| 20 |
-
);
|
| 21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/apiTypes.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
/* tslint:disable */
|
| 2 |
-
/* eslint-disable */
|
| 3 |
-
/**
|
| 4 |
-
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
| 5 |
-
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
export type NodeStatus = "planned" | "active" | "done";
|
| 9 |
-
|
| 10 |
-
export interface DirectoryEntry {
|
| 11 |
-
name: string;
|
| 12 |
-
type: string;
|
| 13 |
-
}
|
| 14 |
-
export interface SaveRequest {
|
| 15 |
-
path: string;
|
| 16 |
-
ws: Workspace;
|
| 17 |
-
[k: string]: unknown;
|
| 18 |
-
}
|
| 19 |
-
/**
|
| 20 |
-
* A workspace is a representation of a computational graph that consists of nodes and edges.
|
| 21 |
-
*
|
| 22 |
-
* Each node represents an operation or task, and the edges represent the flow of data between
|
| 23 |
-
* the nodes. Each workspace is associated with an environment, which determines the operations
|
| 24 |
-
* that can be performed in the workspace and the execution method for the operations.
|
| 25 |
-
*/
|
| 26 |
-
export interface Workspace {
|
| 27 |
-
env?: string;
|
| 28 |
-
paused?: boolean;
|
| 29 |
-
nodes?: WorkspaceNode[];
|
| 30 |
-
edges?: WorkspaceEdge[];
|
| 31 |
-
[k: string]: unknown;
|
| 32 |
-
}
|
| 33 |
-
export interface WorkspaceNode {
|
| 34 |
-
id: string;
|
| 35 |
-
type: string;
|
| 36 |
-
data: WorkspaceNodeData;
|
| 37 |
-
position: Position;
|
| 38 |
-
width: number;
|
| 39 |
-
height: number;
|
| 40 |
-
[k: string]: unknown;
|
| 41 |
-
}
|
| 42 |
-
export interface WorkspaceNodeData {
|
| 43 |
-
title: string;
|
| 44 |
-
params: {
|
| 45 |
-
[k: string]: unknown;
|
| 46 |
-
};
|
| 47 |
-
display?: unknown;
|
| 48 |
-
input_metadata?: unknown;
|
| 49 |
-
error?: string | null;
|
| 50 |
-
status?: NodeStatus;
|
| 51 |
-
[k: string]: unknown;
|
| 52 |
-
}
|
| 53 |
-
export interface Position {
|
| 54 |
-
x: number;
|
| 55 |
-
y: number;
|
| 56 |
-
[k: string]: unknown;
|
| 57 |
-
}
|
| 58 |
-
export interface WorkspaceEdge {
|
| 59 |
-
id: string;
|
| 60 |
-
source: string;
|
| 61 |
-
target: string;
|
| 62 |
-
sourceHandle: string;
|
| 63 |
-
targetHandle: string;
|
| 64 |
-
[k: string]: unknown;
|
| 65 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/assets/favicon.ico
DELETED
|
Binary file (4.28 kB)
|
|
|
lynxkite-app/web/src/assets/logo.png
DELETED
|
Binary file (23.4 kB)
|
|
|
lynxkite-app/web/src/code-theme.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
// A simple theme using the LynxKite colors.
|
| 2 |
-
|
| 3 |
-
import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
| 4 |
-
|
| 5 |
-
const theme: editor.IStandaloneThemeData = {
|
| 6 |
-
base: "vs-dark",
|
| 7 |
-
inherit: true,
|
| 8 |
-
rules: [
|
| 9 |
-
{
|
| 10 |
-
foreground: "ff8800",
|
| 11 |
-
token: "keyword",
|
| 12 |
-
},
|
| 13 |
-
{
|
| 14 |
-
foreground: "0088ff",
|
| 15 |
-
fontStyle: "italic",
|
| 16 |
-
token: "comment",
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
foreground: "39bcf3",
|
| 20 |
-
token: "string",
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
foreground: "ffc600",
|
| 24 |
-
token: "",
|
| 25 |
-
},
|
| 26 |
-
],
|
| 27 |
-
colors: {
|
| 28 |
-
"editor.foreground": "#FFFFFF",
|
| 29 |
-
"editor.background": "#002a4c",
|
| 30 |
-
"editor.selectionBackground": "#0050a4",
|
| 31 |
-
"editor.lineHighlightBackground": "#1f4662",
|
| 32 |
-
"editorCursor.foreground": "#ffc600",
|
| 33 |
-
"editorWhitespace.foreground": "#7f7f7fb2",
|
| 34 |
-
"editorIndentGuide.background": "#3b5364",
|
| 35 |
-
"editorIndentGuide.activeBackground": "#ffc600",
|
| 36 |
-
},
|
| 37 |
-
};
|
| 38 |
-
export default theme;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/common.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
import { useLocation } from "react-router";
|
| 2 |
-
|
| 3 |
-
export function usePath() {
|
| 4 |
-
// Decode special characters. Drop trailing slash. (Some clients add it, e.g. Playwright.)
|
| 5 |
-
const path = decodeURIComponent(useLocation().pathname).replace(/[/]$/, "");
|
| 6 |
-
return path;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
export const COLORS: { [key: string]: string } = {
|
| 10 |
-
gray: "oklch(95% 0 0)",
|
| 11 |
-
pink: "oklch(75% 0.2 0)",
|
| 12 |
-
orange: "oklch(75% 0.2 55)",
|
| 13 |
-
green: "oklch(75% 0.2 150)",
|
| 14 |
-
blue: "oklch(75% 0.2 230)",
|
| 15 |
-
purple: "oklch(75% 0.2 290)",
|
| 16 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/index.css
DELETED
|
@@ -1,727 +0,0 @@
|
|
| 1 |
-
@tailwind base;
|
| 2 |
-
@tailwind components;
|
| 3 |
-
@tailwind utilities;
|
| 4 |
-
|
| 5 |
-
:root {
|
| 6 |
-
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 7 |
-
line-height: 1.5;
|
| 8 |
-
font-weight: 400;
|
| 9 |
-
|
| 10 |
-
font-synthesis: none;
|
| 11 |
-
text-rendering: optimizeLegibility;
|
| 12 |
-
-webkit-font-smoothing: antialiased;
|
| 13 |
-
-moz-osx-font-smoothing: grayscale;
|
| 14 |
-
|
| 15 |
-
background: #002a4c;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
img,
|
| 19 |
-
svg {
|
| 20 |
-
display: inline-block;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
body {
|
| 24 |
-
color: var(--foreground);
|
| 25 |
-
background: var(--background);
|
| 26 |
-
font-family: Arial, Helvetica, sans-serif;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
.workspace {
|
| 30 |
-
background: white;
|
| 31 |
-
display: flex;
|
| 32 |
-
flex-direction: column;
|
| 33 |
-
height: 100vh;
|
| 34 |
-
|
| 35 |
-
.top-bar {
|
| 36 |
-
display: flex;
|
| 37 |
-
justify-content: space-between;
|
| 38 |
-
align-items: center;
|
| 39 |
-
background: #002a4c;
|
| 40 |
-
|
| 41 |
-
.ws-name {
|
| 42 |
-
font-size: 1.5em;
|
| 43 |
-
flex: 1;
|
| 44 |
-
color: white;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
.logo img {
|
| 48 |
-
height: 2em;
|
| 49 |
-
vertical-align: middle;
|
| 50 |
-
margin: 4px;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
.tools {
|
| 54 |
-
display: flex;
|
| 55 |
-
align-items: center;
|
| 56 |
-
|
| 57 |
-
.btn {
|
| 58 |
-
color: oklch(75% 0.13 230);
|
| 59 |
-
font-size: 1.5em;
|
| 60 |
-
padding: 0 10px;
|
| 61 |
-
}
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.error {
|
| 66 |
-
background: #ffdddd;
|
| 67 |
-
padding: 8px;
|
| 68 |
-
font-size: 12px;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
.node-container {
|
| 72 |
-
padding: 8px;
|
| 73 |
-
position: relative;
|
| 74 |
-
}
|
| 75 |
-
|
| 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;
|
| 83 |
-
|
| 84 |
-
> :not(.title, .react-flow__handle) {
|
| 85 |
-
user-select: text;
|
| 86 |
-
cursor: default;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
.node-content {
|
| 90 |
-
flex: 1;
|
| 91 |
-
overflow: auto;
|
| 92 |
-
display: flex;
|
| 93 |
-
flex-direction: column;
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
.in-group .lynxkite-node {
|
| 98 |
-
box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.3);
|
| 99 |
-
opacity: 0.3;
|
| 100 |
-
transition: opacity 0.3s;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.in-group .lynxkite-node:hover {
|
| 104 |
-
opacity: 1;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
.node-group {
|
| 108 |
-
box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.3);
|
| 109 |
-
border-radius: 20px;
|
| 110 |
-
border: none;
|
| 111 |
-
background-color: white;
|
| 112 |
-
opacity: 0.9;
|
| 113 |
-
display: flex;
|
| 114 |
-
flex-direction: column;
|
| 115 |
-
align-items: end;
|
| 116 |
-
padding: 10px 20px;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.node-group.in-group {
|
| 120 |
-
opacity: 0.5;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
.node-group-color-picker-icon {
|
| 124 |
-
font-size: 30px;
|
| 125 |
-
opacity: 0.1;
|
| 126 |
-
transition: opacity 0.3s;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
.node-group:hover .node-group-color-picker-icon {
|
| 130 |
-
opacity: 1;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
.color-picker-button {
|
| 134 |
-
font-size: 30px;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
.tooltip {
|
| 138 |
-
padding: 8px;
|
| 139 |
-
border-radius: 4px;
|
| 140 |
-
opacity: 1;
|
| 141 |
-
text-align: left;
|
| 142 |
-
background: #fffa;
|
| 143 |
-
color: black;
|
| 144 |
-
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.1);
|
| 145 |
-
backdrop-filter: blur(10px);
|
| 146 |
-
font-size: 16px;
|
| 147 |
-
font-weight: initial;
|
| 148 |
-
max-width: 300px;
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
.prose p {
|
| 152 |
-
margin-bottom: 0;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.expanded .lynxkite-node {
|
| 156 |
-
height: 100%;
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
.lynxkite-node .title {
|
| 160 |
-
font-weight: bold;
|
| 161 |
-
padding: 8px;
|
| 162 |
-
background-image: linear-gradient(
|
| 163 |
-
to right,
|
| 164 |
-
var(--status-color-1),
|
| 165 |
-
var(--status-color-2) 40%,
|
| 166 |
-
var(--status-color-2) 60%,
|
| 167 |
-
var(--status-color-3)
|
| 168 |
-
);
|
| 169 |
-
background-blend-mode: luminosity;
|
| 170 |
-
background-size: 180% 180%;
|
| 171 |
-
--status-color-1: #0000;
|
| 172 |
-
--status-color-2: #0000;
|
| 173 |
-
--status-color-3: #0000;
|
| 174 |
-
transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
|
| 175 |
-
display: flex;
|
| 176 |
-
flex-direction: row;
|
| 177 |
-
gap: 10px;
|
| 178 |
-
|
| 179 |
-
.title-title {
|
| 180 |
-
flex: 1;
|
| 181 |
-
}
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
.lynxkite-node .title.active {
|
| 185 |
-
--status-color-1: #0000;
|
| 186 |
-
--status-color-2: #fff4;
|
| 187 |
-
--status-color-3: #888f;
|
| 188 |
-
animation: active-node-gradient-animation 1.2s alternate ease-in-out infinite;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
.lynxkite-node .title.planned {
|
| 192 |
-
--status-color-1: #888f;
|
| 193 |
-
--status-color-2: #888f;
|
| 194 |
-
--status-color-3: #888f;
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
.handle-name {
|
| 198 |
-
font-size: 10px;
|
| 199 |
-
color: black;
|
| 200 |
-
letter-spacing: 0.05em;
|
| 201 |
-
text-align: right;
|
| 202 |
-
white-space: nowrap;
|
| 203 |
-
position: absolute;
|
| 204 |
-
top: -5px;
|
| 205 |
-
backdrop-filter: blur(10px);
|
| 206 |
-
padding: 2px 8px;
|
| 207 |
-
border-radius: 4px;
|
| 208 |
-
visibility: hidden;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.react-flow__handle-left {
|
| 212 |
-
left: -5px;
|
| 213 |
-
|
| 214 |
-
.handle-name {
|
| 215 |
-
right: 30px;
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
.react-flow__handle-right {
|
| 220 |
-
right: -5px;
|
| 221 |
-
|
| 222 |
-
.handle-name {
|
| 223 |
-
left: 30px;
|
| 224 |
-
}
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
.react-flow__handle-top {
|
| 228 |
-
top: -5px;
|
| 229 |
-
|
| 230 |
-
.handle-name {
|
| 231 |
-
top: -3px;
|
| 232 |
-
left: 13px;
|
| 233 |
-
backdrop-filter: none;
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
.react-flow__handle-bottom {
|
| 238 |
-
bottom: -5px;
|
| 239 |
-
|
| 240 |
-
.handle-name {
|
| 241 |
-
top: 0px;
|
| 242 |
-
left: 13px;
|
| 243 |
-
backdrop-filter: none;
|
| 244 |
-
}
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
.node-container:hover .handle-name {
|
| 248 |
-
visibility: visible;
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
.node-resizer {
|
| 252 |
-
position: absolute;
|
| 253 |
-
bottom: 8px;
|
| 254 |
-
right: 8px;
|
| 255 |
-
cursor: nwse-resize;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
.lynxkite-node {
|
| 259 |
-
.param {
|
| 260 |
-
padding: 4px 8px 4px 8px;
|
| 261 |
-
display: block;
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
.param-name-row {
|
| 265 |
-
display: flex;
|
| 266 |
-
flex-direction: row;
|
| 267 |
-
justify-content: space-between;
|
| 268 |
-
align-items: end;
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
.param-name {
|
| 272 |
-
display: block;
|
| 273 |
-
font-size: 10px;
|
| 274 |
-
letter-spacing: 0.05em;
|
| 275 |
-
margin-left: 10px;
|
| 276 |
-
width: fit-content;
|
| 277 |
-
padding: 2px 8px;
|
| 278 |
-
border-radius: 4px 4px 0 0;
|
| 279 |
-
}
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
.node-search {
|
| 283 |
-
position: fixed;
|
| 284 |
-
width: 300px;
|
| 285 |
-
z-index: 5;
|
| 286 |
-
padding: 4px;
|
| 287 |
-
border-radius: 4px;
|
| 288 |
-
border: 1px solid #888;
|
| 289 |
-
background-color: white;
|
| 290 |
-
max-height: -webkit-fill-available;
|
| 291 |
-
max-height: -moz-available;
|
| 292 |
-
display: flex;
|
| 293 |
-
flex-direction: column;
|
| 294 |
-
|
| 295 |
-
input {
|
| 296 |
-
width: calc(100% - 26px);
|
| 297 |
-
font-size: 20px;
|
| 298 |
-
padding: 8px;
|
| 299 |
-
border-radius: 4px;
|
| 300 |
-
border: 1px solid #eee;
|
| 301 |
-
margin: 4px;
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
.search-result {
|
| 305 |
-
padding: 4px;
|
| 306 |
-
cursor: pointer;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
.search-result.selected {
|
| 310 |
-
background-color: oklch(75% 0.2 55);
|
| 311 |
-
border-radius: 4px;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
.matches {
|
| 315 |
-
overflow-y: auto;
|
| 316 |
-
}
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
.react-flow__node-table_view {
|
| 320 |
-
.df-head {
|
| 321 |
-
font-weight: bold;
|
| 322 |
-
padding: 8px;
|
| 323 |
-
background: #f0f0f0;
|
| 324 |
-
cursor: pointer;
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
dl {
|
| 328 |
-
margin: 10px;
|
| 329 |
-
}
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
.react-flow__node-comment {
|
| 333 |
-
width: auto !important;
|
| 334 |
-
height: auto !important;
|
| 335 |
-
max-width: 400px;
|
| 336 |
-
|
| 337 |
-
.comment-view {
|
| 338 |
-
border-radius: 4px;
|
| 339 |
-
padding: 5px 10px;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
.comment-editor {
|
| 343 |
-
width: 400px;
|
| 344 |
-
box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.3);
|
| 345 |
-
border-radius: 4px;
|
| 346 |
-
padding: 5px 10px;
|
| 347 |
-
border: 1px solid #ccc;
|
| 348 |
-
overflow-y: hidden;
|
| 349 |
-
}
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
.env-select {
|
| 353 |
-
background: transparent;
|
| 354 |
-
color: #39bcf3;
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
.workspace-message {
|
| 358 |
-
position: absolute;
|
| 359 |
-
left: 50%;
|
| 360 |
-
bottom: 20px;
|
| 361 |
-
transform: translateX(-50%);
|
| 362 |
-
box-shadow: 0 5px 50px 0px #8008;
|
| 363 |
-
padding: 10px 40px 10px 20px;
|
| 364 |
-
border-radius: 5px;
|
| 365 |
-
|
| 366 |
-
.close {
|
| 367 |
-
position: absolute;
|
| 368 |
-
right: 10px;
|
| 369 |
-
cursor: pointer;
|
| 370 |
-
}
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
.model-mapping-param {
|
| 374 |
-
border: 1px solid var(--fallback-bc, oklch(var(--bc) / 0.2));
|
| 375 |
-
border-collapse: separate;
|
| 376 |
-
border-radius: 5px;
|
| 377 |
-
padding: 5px 10px;
|
| 378 |
-
width: 100%;
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
.table-viewer {
|
| 382 |
-
td {
|
| 383 |
-
padding: 5px 10px;
|
| 384 |
-
vertical-align: top;
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
.image-in-table {
|
| 388 |
-
max-height: 100px;
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
.sort-indicator {
|
| 392 |
-
display: inline-block;
|
| 393 |
-
width: 0;
|
| 394 |
-
font-size: 10px;
|
| 395 |
-
color: #0006;
|
| 396 |
-
transform: translate(5px, -2px);
|
| 397 |
-
}
|
| 398 |
-
}
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
.params-expander {
|
| 402 |
-
font-size: 15px;
|
| 403 |
-
padding: 4px;
|
| 404 |
-
color: #000a;
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
.flippy {
|
| 408 |
-
transition: transform 0.5s;
|
| 409 |
-
}
|
| 410 |
-
|
| 411 |
-
.flippy.flippy-90 {
|
| 412 |
-
transform: rotate(-90deg);
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
.directory {
|
| 416 |
-
.entry-list {
|
| 417 |
-
width: 100%;
|
| 418 |
-
margin: 10px auto;
|
| 419 |
-
background-color: white;
|
| 420 |
-
border-radius: 10px;
|
| 421 |
-
box-shadow: 0px 2px 4px;
|
| 422 |
-
padding: 0 0 10px 0;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
@media (min-width: 768px) {
|
| 426 |
-
.entry-list {
|
| 427 |
-
width: 768px;
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
@media (min-width: 960px) {
|
| 432 |
-
.entry-list {
|
| 433 |
-
width: 80%;
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
.logo {
|
| 438 |
-
margin: 0;
|
| 439 |
-
padding-top: 50px;
|
| 440 |
-
text-align: center;
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
.logo-image {
|
| 444 |
-
max-width: 50%;
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
.tagline {
|
| 448 |
-
color: #39bcf3;
|
| 449 |
-
font-size: 14px;
|
| 450 |
-
font-weight: 500;
|
| 451 |
-
}
|
| 452 |
-
|
| 453 |
-
@media (min-width: 1400px) {
|
| 454 |
-
.tagline {
|
| 455 |
-
font-size: 18px;
|
| 456 |
-
}
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
.actions {
|
| 460 |
-
display: flex;
|
| 461 |
-
justify-content: space-evenly;
|
| 462 |
-
align-items: center;
|
| 463 |
-
height: 50px;
|
| 464 |
-
padding: 5px;
|
| 465 |
-
|
| 466 |
-
form,
|
| 467 |
-
button {
|
| 468 |
-
flex: 1;
|
| 469 |
-
}
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
.actions a {
|
| 473 |
-
padding: 2px 10px;
|
| 474 |
-
border-radius: 5px;
|
| 475 |
-
}
|
| 476 |
-
|
| 477 |
-
.actions a:hover {
|
| 478 |
-
background: #39bcf3;
|
| 479 |
-
color: white;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
.breadcrumbs {
|
| 483 |
-
padding-left: 10px;
|
| 484 |
-
font-size: 20px;
|
| 485 |
-
background: #002a4c20;
|
| 486 |
-
}
|
| 487 |
-
|
| 488 |
-
.breadcrumbs a:hover {
|
| 489 |
-
color: #39bcf3;
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
.entry-list .entry {
|
| 493 |
-
display: flex;
|
| 494 |
-
border-bottom: 1px solid whitesmoke;
|
| 495 |
-
color: #004165;
|
| 496 |
-
cursor: pointer;
|
| 497 |
-
user-select: none;
|
| 498 |
-
|
| 499 |
-
a {
|
| 500 |
-
text-decoration: none;
|
| 501 |
-
flex: 1;
|
| 502 |
-
padding-left: 10px;
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
.entry-name {
|
| 506 |
-
padding-left: 10px;
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
button {
|
| 510 |
-
padding-right: 10px;
|
| 511 |
-
}
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
.entry-list .open .entry,
|
| 515 |
-
.entry-list .entry:hover,
|
| 516 |
-
.entry-list .entry:focus {
|
| 517 |
-
background: #39bcf3;
|
| 518 |
-
color: white;
|
| 519 |
-
}
|
| 520 |
-
|
| 521 |
-
.entry-list .entry:last-child {
|
| 522 |
-
border-bottom: none;
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
a {
|
| 526 |
-
text-decoration: none;
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
.loading {
|
| 530 |
-
color: #39bcf3;
|
| 531 |
-
margin: 10px;
|
| 532 |
-
}
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
@keyframes active-node-gradient-animation {
|
| 536 |
-
to {
|
| 537 |
-
background-position-x: 100%;
|
| 538 |
-
}
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
@property --status-color-1 {
|
| 542 |
-
syntax: "<color>";
|
| 543 |
-
initial-value: red;
|
| 544 |
-
inherits: false;
|
| 545 |
-
}
|
| 546 |
-
|
| 547 |
-
@property --status-color-2 {
|
| 548 |
-
syntax: "<color>";
|
| 549 |
-
initial-value: red;
|
| 550 |
-
inherits: false;
|
| 551 |
-
}
|
| 552 |
-
|
| 553 |
-
@property --status-color-3 {
|
| 554 |
-
syntax: "<color>";
|
| 555 |
-
initial-value: red;
|
| 556 |
-
inherits: false;
|
| 557 |
-
}
|
| 558 |
-
|
| 559 |
-
.react-flow__edge.selected path.react-flow__edge-path {
|
| 560 |
-
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
| 561 |
-
outline-offset: 10px;
|
| 562 |
-
border-radius: 1px;
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
.react-flow__handle {
|
| 566 |
-
border-color: black;
|
| 567 |
-
background: white;
|
| 568 |
-
width: 20px;
|
| 569 |
-
height: 20px;
|
| 570 |
-
border-width: 2px;
|
| 571 |
-
}
|
| 572 |
-
|
| 573 |
-
.react-flow__arrowhead * {
|
| 574 |
-
stroke: none;
|
| 575 |
-
fill: black;
|
| 576 |
-
}
|
| 577 |
-
|
| 578 |
-
.react-flow__node-area {
|
| 579 |
-
z-index: -10 !important;
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
.selected .node-group,
|
| 583 |
-
.selected .comment-view,
|
| 584 |
-
.selected .lynxkite-node {
|
| 585 |
-
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
| 586 |
-
outline-offset: 7.5px;
|
| 587 |
-
}
|
| 588 |
-
|
| 589 |
-
.selected .node-group {
|
| 590 |
-
outline-offset: 20px;
|
| 591 |
-
}
|
| 592 |
-
|
| 593 |
-
.graph-creation-view {
|
| 594 |
-
display: flex;
|
| 595 |
-
width: 100%;
|
| 596 |
-
margin-top: 10px;
|
| 597 |
-
}
|
| 598 |
-
|
| 599 |
-
.graph-tables,
|
| 600 |
-
.graph-relations {
|
| 601 |
-
flex: 1;
|
| 602 |
-
padding-left: 10px;
|
| 603 |
-
padding-right: 10px;
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
.graph-table-header {
|
| 607 |
-
display: flex;
|
| 608 |
-
justify-content: space-between;
|
| 609 |
-
font-weight: bold;
|
| 610 |
-
text-align: left;
|
| 611 |
-
background-color: #333;
|
| 612 |
-
color: white;
|
| 613 |
-
padding: 10px;
|
| 614 |
-
border-bottom: 2px solid #222;
|
| 615 |
-
font-size: 16px;
|
| 616 |
-
}
|
| 617 |
-
|
| 618 |
-
.graph-creation-view .df-head {
|
| 619 |
-
font-weight: bold;
|
| 620 |
-
display: flex;
|
| 621 |
-
justify-content: space-between;
|
| 622 |
-
padding: 8px 12px;
|
| 623 |
-
/* Adds a separator between rows */
|
| 624 |
-
border-bottom: 1px solid #ccc;
|
| 625 |
-
}
|
| 626 |
-
|
| 627 |
-
/* Alternating background colors for table-like effect */
|
| 628 |
-
.graph-creation-view .df-head:nth-child(odd) {
|
| 629 |
-
background-color: #f9f9f9;
|
| 630 |
-
}
|
| 631 |
-
|
| 632 |
-
.graph-creation-view .df-head:nth-child(even) {
|
| 633 |
-
background-color: #e0e0e0;
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
.graph-relation-attributes {
|
| 637 |
-
display: flex;
|
| 638 |
-
flex-direction: column;
|
| 639 |
-
/* Adds space between each label-input pair */
|
| 640 |
-
gap: 10px;
|
| 641 |
-
width: 100%;
|
| 642 |
-
}
|
| 643 |
-
|
| 644 |
-
.graph-relation-attributes label {
|
| 645 |
-
font-size: 12px;
|
| 646 |
-
font-weight: bold;
|
| 647 |
-
display: block;
|
| 648 |
-
margin-bottom: 2px;
|
| 649 |
-
/* Lighter text for labels */
|
| 650 |
-
color: #666;
|
| 651 |
-
}
|
| 652 |
-
|
| 653 |
-
.graph-relation-attributes input {
|
| 654 |
-
width: 100%;
|
| 655 |
-
padding: 8px;
|
| 656 |
-
font-size: 14px;
|
| 657 |
-
border: 1px solid #ccc;
|
| 658 |
-
border-radius: 4px;
|
| 659 |
-
outline: none;
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
.graph-relation-attributes input:focus {
|
| 663 |
-
/* Highlight input on focus */
|
| 664 |
-
border-color: #007bff;
|
| 665 |
-
}
|
| 666 |
-
|
| 667 |
-
.add-relationship-button {
|
| 668 |
-
background-color: #28a745;
|
| 669 |
-
color: white;
|
| 670 |
-
border: none;
|
| 671 |
-
font-size: 16px;
|
| 672 |
-
cursor: pointer;
|
| 673 |
-
padding: 4px 10px;
|
| 674 |
-
border-radius: 4px;
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
.add-relationship-button:hover {
|
| 678 |
-
background-color: #218838;
|
| 679 |
-
}
|
| 680 |
-
|
| 681 |
-
.yRemoteSelection {
|
| 682 |
-
background-color: rgb(250, 129, 0, 0.5);
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
.yRemoteSelectionHead {
|
| 686 |
-
position: absolute;
|
| 687 |
-
border-left: #ff8800 solid 2px;
|
| 688 |
-
border-top: #ff8800 solid 2px;
|
| 689 |
-
border-bottom: #ff8800 solid 2px;
|
| 690 |
-
height: 100%;
|
| 691 |
-
box-sizing: border-box;
|
| 692 |
-
}
|
| 693 |
-
|
| 694 |
-
.yRemoteSelectionHead::after {
|
| 695 |
-
position: absolute;
|
| 696 |
-
content: " ";
|
| 697 |
-
border: 3px solid #ff8800;
|
| 698 |
-
border-radius: 4px;
|
| 699 |
-
left: -4px;
|
| 700 |
-
top: -5px;
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
.error-message {
|
| 704 |
-
display: flex;
|
| 705 |
-
align-items: center;
|
| 706 |
-
gap: 0.5rem;
|
| 707 |
-
margin-top: 0.25rem;
|
| 708 |
-
padding: 0.5rem;
|
| 709 |
-
background-color: #fee2e2;
|
| 710 |
-
border: 1px solid #fecaca;
|
| 711 |
-
border-radius: 0.375rem;
|
| 712 |
-
color: #dc2626;
|
| 713 |
-
font-size: 0.875rem;
|
| 714 |
-
}
|
| 715 |
-
|
| 716 |
-
.error-icon {
|
| 717 |
-
flex-shrink: 0;
|
| 718 |
-
}
|
| 719 |
-
|
| 720 |
-
.error-text {
|
| 721 |
-
line-height: 1.4;
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
.input-error {
|
| 725 |
-
border-color: #dc2626;
|
| 726 |
-
box-shadow: 0 0 0 1px #dc2626;
|
| 727 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/main.tsx
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
import { StrictMode } from "react";
|
| 2 |
-
import { createRoot } from "react-dom/client";
|
| 3 |
-
import "@xyflow/react/dist/style.css";
|
| 4 |
-
import "./index.css";
|
| 5 |
-
import {
|
| 6 |
-
Link,
|
| 7 |
-
Route,
|
| 8 |
-
RouterProvider,
|
| 9 |
-
createBrowserRouter,
|
| 10 |
-
createRoutesFromElements,
|
| 11 |
-
useRouteError,
|
| 12 |
-
} from "react-router";
|
| 13 |
-
import Code from "./Code.tsx";
|
| 14 |
-
import Directory from "./Directory.tsx";
|
| 15 |
-
import Workspace from "./workspace/Workspace.tsx";
|
| 16 |
-
|
| 17 |
-
function WorkspaceError() {
|
| 18 |
-
const error = useRouteError();
|
| 19 |
-
const stack = error instanceof Error ? error.stack : null;
|
| 20 |
-
return (
|
| 21 |
-
<div className="hero min-h-screen">
|
| 22 |
-
<div className="card bg-base-100 shadow-sm">
|
| 23 |
-
<div className="card-body">
|
| 24 |
-
<h2 className="card-title">Something went wrong...</h2>
|
| 25 |
-
<pre>{stack || "Unknown error."}</pre>
|
| 26 |
-
<div className="card-actions justify-end">
|
| 27 |
-
<Link to="/" className="btn btn-primary">
|
| 28 |
-
Close workspace
|
| 29 |
-
</Link>
|
| 30 |
-
</div>
|
| 31 |
-
</div>
|
| 32 |
-
</div>
|
| 33 |
-
</div>
|
| 34 |
-
);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
const router = createBrowserRouter(
|
| 38 |
-
createRoutesFromElements(
|
| 39 |
-
<>
|
| 40 |
-
<Route path="/" element={<Directory />} />
|
| 41 |
-
<Route path="/dir" element={<Directory />} />
|
| 42 |
-
<Route path="/dir/*" element={<Directory />} />
|
| 43 |
-
<Route path="/edit/*" element={<Workspace />} errorElement={<WorkspaceError />} />
|
| 44 |
-
<Route path="/code/*" element={<Code />} />
|
| 45 |
-
</>,
|
| 46 |
-
),
|
| 47 |
-
);
|
| 48 |
-
|
| 49 |
-
createRoot(document.getElementById("root")!).render(
|
| 50 |
-
<StrictMode>
|
| 51 |
-
<RouterProvider router={router} />
|
| 52 |
-
</StrictMode>,
|
| 53 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
/// <reference types="vite/client" />
|
|
|
|
|
|
lynxkite-app/web/src/workspace/EnvironmentSelector.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
export default function EnvironmentSelector(props: {
|
| 2 |
-
options: string[];
|
| 3 |
-
value: string;
|
| 4 |
-
onChange: (val: string) => void;
|
| 5 |
-
}) {
|
| 6 |
-
return (
|
| 7 |
-
<>
|
| 8 |
-
<select
|
| 9 |
-
className="env-select select w-full max-w-xs"
|
| 10 |
-
name="workspace-env"
|
| 11 |
-
value={props.value}
|
| 12 |
-
onChange={(evt) => props.onChange(evt.currentTarget.value)}
|
| 13 |
-
>
|
| 14 |
-
{props.options.map((option) => (
|
| 15 |
-
<option key={option} value={option}>
|
| 16 |
-
{option}
|
| 17 |
-
</option>
|
| 18 |
-
))}
|
| 19 |
-
</select>
|
| 20 |
-
</>
|
| 21 |
-
);
|
| 22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/LynxKiteEdge.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import { BaseEdge, Position } from "@xyflow/react";
|
| 2 |
-
|
| 3 |
-
function addOffset(x: number, y: number, p: Position, offset: number) {
|
| 4 |
-
if (p === Position.Top) return `${x},${y - offset}`;
|
| 5 |
-
if (p === Position.Bottom) return `${x},${y + offset}`;
|
| 6 |
-
if (p === Position.Left) return `${x - offset},${y}`;
|
| 7 |
-
return `${x + offset},${y}`;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export default function LynxKiteEdge(props: any) {
|
| 11 |
-
const offset = 0.3 * Math.hypot(props.targetX - props.sourceX, props.targetY - props.sourceY);
|
| 12 |
-
const s = addOffset(props.sourceX, props.sourceY, props.sourcePosition, 0);
|
| 13 |
-
const sc = addOffset(props.sourceX, props.sourceY, props.sourcePosition, offset);
|
| 14 |
-
const tc = addOffset(props.targetX, props.targetY, props.targetPosition, offset);
|
| 15 |
-
const t = addOffset(props.targetX, props.targetY, props.targetPosition, 0);
|
| 16 |
-
const path = `M${s} C${sc} ${tc} ${t}`;
|
| 17 |
-
return (
|
| 18 |
-
<>
|
| 19 |
-
<BaseEdge
|
| 20 |
-
path={path}
|
| 21 |
-
labelX={props.labelX}
|
| 22 |
-
labelY={props.labelY}
|
| 23 |
-
markerStart={props.markerStart}
|
| 24 |
-
markerEnd={props.markerEnd}
|
| 25 |
-
style={{
|
| 26 |
-
strokeWidth: 2,
|
| 27 |
-
stroke: "black",
|
| 28 |
-
}}
|
| 29 |
-
/>
|
| 30 |
-
</>
|
| 31 |
-
);
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/LynxKiteState.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
import { createContext } from "react";
|
| 2 |
-
import type { Workspace } from "../apiTypes.ts";
|
| 3 |
-
|
| 4 |
-
export const LynxKiteState = createContext({ workspace: {} as Workspace });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/NodeSearch.tsx
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
import Fuse from "fuse.js";
|
| 2 |
-
import { useEffect, useMemo, useRef, useState } from "react";
|
| 3 |
-
|
| 4 |
-
export type OpsOp = {
|
| 5 |
-
name: string;
|
| 6 |
-
id: string;
|
| 7 |
-
categories: string[];
|
| 8 |
-
type: string;
|
| 9 |
-
position: { x: number; y: number };
|
| 10 |
-
params: { name: string; default: any }[];
|
| 11 |
-
};
|
| 12 |
-
export type Catalog = { [op: string]: OpsOp };
|
| 13 |
-
export type Catalogs = { [env: string]: Catalog };
|
| 14 |
-
|
| 15 |
-
export default function NodeSearch(props: {
|
| 16 |
-
boxes: Catalog;
|
| 17 |
-
onCancel: any;
|
| 18 |
-
onAdd: any;
|
| 19 |
-
pos: { x: number; y: number };
|
| 20 |
-
}) {
|
| 21 |
-
const searchBox = useRef(null as unknown as HTMLInputElement);
|
| 22 |
-
const [searchText, setSearchText] = useState("");
|
| 23 |
-
const fuse = useMemo(
|
| 24 |
-
() =>
|
| 25 |
-
new Fuse(Object.values(props.boxes), {
|
| 26 |
-
keys: ["name"],
|
| 27 |
-
}),
|
| 28 |
-
[props.boxes],
|
| 29 |
-
);
|
| 30 |
-
const allOps = useMemo(() => {
|
| 31 |
-
const boxes = Object.values(props.boxes).map((box) => ({ item: box }));
|
| 32 |
-
boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
|
| 33 |
-
return boxes;
|
| 34 |
-
}, [props.boxes]);
|
| 35 |
-
const hits: { item: OpsOp }[] = searchText ? fuse.search<OpsOp>(searchText) : allOps;
|
| 36 |
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
| 37 |
-
useEffect(() => searchBox.current.focus());
|
| 38 |
-
function typed(text: string) {
|
| 39 |
-
setSearchText(text);
|
| 40 |
-
setSelectedIndex(Math.max(0, Math.min(selectedIndex, hits.length - 1)));
|
| 41 |
-
}
|
| 42 |
-
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
| 43 |
-
if (e.key === "ArrowDown") {
|
| 44 |
-
e.preventDefault();
|
| 45 |
-
setSelectedIndex(Math.min(selectedIndex + 1, hits.length - 1));
|
| 46 |
-
} else if (e.key === "ArrowUp") {
|
| 47 |
-
e.preventDefault();
|
| 48 |
-
setSelectedIndex(Math.max(selectedIndex - 1, 0));
|
| 49 |
-
} else if (e.key === "Enter") {
|
| 50 |
-
addSelected();
|
| 51 |
-
} else if (e.key === "Escape") {
|
| 52 |
-
props.onCancel();
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
function addSelected() {
|
| 56 |
-
const node = { ...hits[selectedIndex].item };
|
| 57 |
-
node.position = props.pos;
|
| 58 |
-
props.onAdd(node);
|
| 59 |
-
}
|
| 60 |
-
async function lostFocus(e: any) {
|
| 61 |
-
// If it's a click on a result, let the click handler handle it.
|
| 62 |
-
if (e.relatedTarget?.closest(".node-search")) return;
|
| 63 |
-
props.onCancel();
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
return (
|
| 67 |
-
<div className="node-search" style={{ top: props.pos.y, left: props.pos.x }}>
|
| 68 |
-
<input
|
| 69 |
-
ref={searchBox}
|
| 70 |
-
value={searchText}
|
| 71 |
-
onChange={(event) => typed(event.target.value)}
|
| 72 |
-
onKeyDown={onKeyDown}
|
| 73 |
-
onBlur={lostFocus}
|
| 74 |
-
placeholder="Search for box"
|
| 75 |
-
/>
|
| 76 |
-
<div className="matches">
|
| 77 |
-
{hits.map((box, index) => (
|
| 78 |
-
<div
|
| 79 |
-
key={box.item.name}
|
| 80 |
-
tabIndex={0}
|
| 81 |
-
onFocus={() => setSelectedIndex(index)}
|
| 82 |
-
onMouseEnter={() => setSelectedIndex(index)}
|
| 83 |
-
onClick={addSelected}
|
| 84 |
-
className={`search-result ${index === selectedIndex ? "selected" : ""}`}
|
| 85 |
-
>
|
| 86 |
-
{box.item.categories.map((category) => `${category}\u00A0›\u00A0`)}
|
| 87 |
-
{box.item.name}
|
| 88 |
-
</div>
|
| 89 |
-
))}
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
);
|
| 93 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/Workspace.tsx
DELETED
|
@@ -1,655 +0,0 @@
|
|
| 1 |
-
// The LynxKite workspace editor.
|
| 2 |
-
|
| 3 |
-
import { getYjsDoc, syncedStore } from "@syncedstore/core";
|
| 4 |
-
import {
|
| 5 |
-
type Connection,
|
| 6 |
-
Controls,
|
| 7 |
-
type Edge,
|
| 8 |
-
MarkerType,
|
| 9 |
-
type Node,
|
| 10 |
-
ReactFlow,
|
| 11 |
-
ReactFlowProvider,
|
| 12 |
-
type XYPosition,
|
| 13 |
-
applyEdgeChanges,
|
| 14 |
-
applyNodeChanges,
|
| 15 |
-
useReactFlow,
|
| 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";
|
| 23 |
-
// @ts-ignore
|
| 24 |
-
import Atom from "~icons/tabler/atom.jsx";
|
| 25 |
-
// @ts-ignore
|
| 26 |
-
import Backspace from "~icons/tabler/backspace.jsx";
|
| 27 |
-
// @ts-ignore
|
| 28 |
-
import UngroupIcon from "~icons/tabler/library-minus.jsx";
|
| 29 |
-
// @ts-ignore
|
| 30 |
-
import GroupIcon from "~icons/tabler/library-plus.jsx";
|
| 31 |
-
// @ts-ignore
|
| 32 |
-
import Pause from "~icons/tabler/player-pause.jsx";
|
| 33 |
-
// @ts-ignore
|
| 34 |
-
import Play from "~icons/tabler/player-play.jsx";
|
| 35 |
-
// @ts-ignore
|
| 36 |
-
import Restart from "~icons/tabler/rotate-clockwise.jsx";
|
| 37 |
-
// @ts-ignore
|
| 38 |
-
import Close from "~icons/tabler/x.jsx";
|
| 39 |
-
import Tooltip from "../Tooltip.tsx";
|
| 40 |
-
import type { WorkspaceNode, Workspace as WorkspaceType } from "../apiTypes.ts";
|
| 41 |
-
import favicon from "../assets/favicon.ico";
|
| 42 |
-
import { usePath } from "../common.ts";
|
| 43 |
-
// import NodeWithTableView from './NodeWithTableView';
|
| 44 |
-
import EnvironmentSelector from "./EnvironmentSelector";
|
| 45 |
-
import LynxKiteEdge from "./LynxKiteEdge.tsx";
|
| 46 |
-
import { LynxKiteState } from "./LynxKiteState";
|
| 47 |
-
import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
|
| 48 |
-
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
| 49 |
-
import Group from "./nodes/Group.tsx";
|
| 50 |
-
import NodeWithComment from "./nodes/NodeWithComment.tsx";
|
| 51 |
-
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
| 52 |
-
import NodeWithMolecule from "./nodes/NodeWithMolecule.tsx";
|
| 53 |
-
import NodeWithParams from "./nodes/NodeWithParams";
|
| 54 |
-
import NodeWithTableView from "./nodes/NodeWithTableView.tsx";
|
| 55 |
-
import NodeWithVisualization from "./nodes/NodeWithVisualization.tsx";
|
| 56 |
-
|
| 57 |
-
export default function Workspace(props: any) {
|
| 58 |
-
return (
|
| 59 |
-
<ReactFlowProvider>
|
| 60 |
-
<LynxKiteFlow {...props} />
|
| 61 |
-
</ReactFlowProvider>
|
| 62 |
-
);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
function LynxKiteFlow() {
|
| 66 |
-
const updateNodeInternals = useUpdateNodeInternals();
|
| 67 |
-
const reactFlow = useReactFlow();
|
| 68 |
-
const reactFlowContainer = useRef<HTMLDivElement>(null);
|
| 69 |
-
const [nodes, setNodes] = useState([] as Node[]);
|
| 70 |
-
const [edges, setEdges] = useState([] as Edge[]);
|
| 71 |
-
const path = usePath().replace(/^[/]edit[/]/, "");
|
| 72 |
-
const shortPath = path!
|
| 73 |
-
.split("/")
|
| 74 |
-
.pop()!
|
| 75 |
-
.replace(/[.]lynxkite[.]json$/, "");
|
| 76 |
-
const [state, setState] = useState({ workspace: {} as WorkspaceType });
|
| 77 |
-
const [message, setMessage] = useState(null as string | null);
|
| 78 |
-
const [pausedUIState, setPausedUIState] = useState(false);
|
| 79 |
-
useEffect(() => {
|
| 80 |
-
const state = syncedStore({ workspace: {} as WorkspaceType });
|
| 81 |
-
setState(state);
|
| 82 |
-
const doc = getYjsDoc(state);
|
| 83 |
-
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
| 84 |
-
const encodedPath = path!
|
| 85 |
-
.split("/")
|
| 86 |
-
.map((segment) => encodeURIComponent(segment))
|
| 87 |
-
.join("/");
|
| 88 |
-
const wsProvider = new WebsocketProvider(
|
| 89 |
-
`${proto}//${location.host}/ws/crdt`,
|
| 90 |
-
encodedPath,
|
| 91 |
-
doc,
|
| 92 |
-
);
|
| 93 |
-
if (state.workspace && typeof state.workspace.paused === "undefined") {
|
| 94 |
-
state.workspace.paused = false;
|
| 95 |
-
}
|
| 96 |
-
const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
|
| 97 |
-
if (origin === wsProvider) {
|
| 98 |
-
// An update from the CRDT. Apply it to the local state.
|
| 99 |
-
// This is only necessary because ReactFlow keeps secret internal copies of our stuff.
|
| 100 |
-
if (!state.workspace) return;
|
| 101 |
-
if (!state.workspace.nodes) return;
|
| 102 |
-
if (!state.workspace.edges) return;
|
| 103 |
-
for (const n of state.workspace.nodes) {
|
| 104 |
-
if (n.type !== "node_group" && n.dragHandle !== ".drag-handle") {
|
| 105 |
-
n.dragHandle = ".drag-handle";
|
| 106 |
-
}
|
| 107 |
-
}
|
| 108 |
-
const nodes = reactFlow.getNodes();
|
| 109 |
-
const selection = nodes.filter((n) => n.selected).map((n) => n.id);
|
| 110 |
-
const newNodes = state.workspace.nodes.map((n) =>
|
| 111 |
-
selection.includes(n.id) ? { ...n, selected: true } : n,
|
| 112 |
-
);
|
| 113 |
-
setNodes([...newNodes] as Node[]);
|
| 114 |
-
setEdges([...state.workspace.edges] as Edge[]);
|
| 115 |
-
for (const node of state.workspace.nodes) {
|
| 116 |
-
// Make sure the internal copies are updated.
|
| 117 |
-
updateNodeInternals(node.id);
|
| 118 |
-
}
|
| 119 |
-
setPausedUIState(state.workspace.paused || false);
|
| 120 |
-
}
|
| 121 |
-
};
|
| 122 |
-
doc.on("update", onChange);
|
| 123 |
-
return () => {
|
| 124 |
-
doc.destroy();
|
| 125 |
-
wsProvider.destroy();
|
| 126 |
-
};
|
| 127 |
-
}, [path, updateNodeInternals]);
|
| 128 |
-
|
| 129 |
-
const onNodesChange = useCallback(
|
| 130 |
-
(changes: any[]) => {
|
| 131 |
-
// An update from the UI. Apply it to the local state...
|
| 132 |
-
setNodes((nds) => applyNodeChanges(changes, nds));
|
| 133 |
-
// ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
|
| 134 |
-
const wnodes = state.workspace?.nodes;
|
| 135 |
-
if (!wnodes) return;
|
| 136 |
-
for (const ch of changes) {
|
| 137 |
-
const nodeIndex = wnodes.findIndex((n) => n.id === ch.id);
|
| 138 |
-
if (nodeIndex === -1) continue;
|
| 139 |
-
const node = wnodes[nodeIndex];
|
| 140 |
-
if (!node) continue;
|
| 141 |
-
// Position events sometimes come with NaN values. Ignore them.
|
| 142 |
-
if (
|
| 143 |
-
ch.type === "position" &&
|
| 144 |
-
!Number.isNaN(ch.position.x) &&
|
| 145 |
-
!Number.isNaN(ch.position.y)
|
| 146 |
-
) {
|
| 147 |
-
getYjsDoc(state).transact(() => {
|
| 148 |
-
node.position.x = ch.position.x;
|
| 149 |
-
node.position.y = ch.position.y;
|
| 150 |
-
});
|
| 151 |
-
// Update edge positions.
|
| 152 |
-
updateNodeInternals(ch.id);
|
| 153 |
-
} else if (ch.type === "select") {
|
| 154 |
-
} else if (ch.type === "dimensions") {
|
| 155 |
-
getYjsDoc(state).transact(() => {
|
| 156 |
-
node.width = ch.dimensions.width;
|
| 157 |
-
node.height = ch.dimensions.height;
|
| 158 |
-
});
|
| 159 |
-
// Update edge positions when node size changes.
|
| 160 |
-
updateNodeInternals(ch.id);
|
| 161 |
-
} else if (ch.type === "remove") {
|
| 162 |
-
wnodes.splice(nodeIndex, 1);
|
| 163 |
-
} else if (ch.type === "replace") {
|
| 164 |
-
// Ideally we would only update the parameter that changed. But ReactFlow does not give us that detail.
|
| 165 |
-
getYjsDoc(state).transact(() => {
|
| 166 |
-
if (node.data.collapsed !== ch.item.data.collapsed) {
|
| 167 |
-
node.data.collapsed = ch.item.data.collapsed;
|
| 168 |
-
// Update edge positions when node collapses/expands.
|
| 169 |
-
setTimeout(() => updateNodeInternals(ch.id), 0);
|
| 170 |
-
}
|
| 171 |
-
if (node.data.__execution_delay !== ch.item.data.__execution_delay) {
|
| 172 |
-
node.data.__execution_delay = ch.item.data.__execution_delay;
|
| 173 |
-
}
|
| 174 |
-
for (const [key, value] of Object.entries(ch.item.data.params)) {
|
| 175 |
-
if (node.data.params[key] !== value) {
|
| 176 |
-
node.data.params[key] = value;
|
| 177 |
-
}
|
| 178 |
-
}
|
| 179 |
-
});
|
| 180 |
-
} else {
|
| 181 |
-
console.log("Unknown node change", ch);
|
| 182 |
-
}
|
| 183 |
-
}
|
| 184 |
-
},
|
| 185 |
-
[state, updateNodeInternals],
|
| 186 |
-
);
|
| 187 |
-
const onEdgesChange = useCallback(
|
| 188 |
-
(changes: any[]) => {
|
| 189 |
-
setEdges((eds) => applyEdgeChanges(changes, eds));
|
| 190 |
-
const wedges = state.workspace?.edges;
|
| 191 |
-
if (!wedges) return;
|
| 192 |
-
for (const ch of changes) {
|
| 193 |
-
const edgeIndex = wedges.findIndex((e) => e.id === ch.id);
|
| 194 |
-
if (ch.type === "remove") {
|
| 195 |
-
wedges.splice(edgeIndex, 1);
|
| 196 |
-
} else if (ch.type === "select") {
|
| 197 |
-
} else {
|
| 198 |
-
console.log("Unknown edge change", ch);
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
},
|
| 202 |
-
[state],
|
| 203 |
-
);
|
| 204 |
-
|
| 205 |
-
const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) =>
|
| 206 |
-
fetch(resource, init).then((res) => res.json());
|
| 207 |
-
const encodedPathForAPI = path!
|
| 208 |
-
.split("/")
|
| 209 |
-
.map((segment) => encodeURIComponent(segment))
|
| 210 |
-
.join("/");
|
| 211 |
-
const catalog = useSWR(`/api/catalog?workspace=${encodedPathForAPI}`, fetcher);
|
| 212 |
-
const [suppressSearchUntil, setSuppressSearchUntil] = useState(0);
|
| 213 |
-
const [nodeSearchSettings, setNodeSearchSettings] = useState(
|
| 214 |
-
undefined as
|
| 215 |
-
| {
|
| 216 |
-
pos: XYPosition;
|
| 217 |
-
boxes: Catalog;
|
| 218 |
-
}
|
| 219 |
-
| undefined,
|
| 220 |
-
);
|
| 221 |
-
const nodeTypes = useMemo(
|
| 222 |
-
() => ({
|
| 223 |
-
basic: NodeWithParams,
|
| 224 |
-
visualization: NodeWithVisualization,
|
| 225 |
-
image: NodeWithImage,
|
| 226 |
-
table_view: NodeWithTableView,
|
| 227 |
-
graph_creation_view: NodeWithGraphCreationView,
|
| 228 |
-
molecule: NodeWithMolecule,
|
| 229 |
-
comment: NodeWithComment,
|
| 230 |
-
node_group: Group,
|
| 231 |
-
}),
|
| 232 |
-
[],
|
| 233 |
-
);
|
| 234 |
-
const edgeTypes = useMemo(
|
| 235 |
-
() => ({
|
| 236 |
-
default: LynxKiteEdge,
|
| 237 |
-
}),
|
| 238 |
-
[],
|
| 239 |
-
);
|
| 240 |
-
|
| 241 |
-
// Global keyboard shortcuts.
|
| 242 |
-
useEffect(() => {
|
| 243 |
-
const handleKeyDown = (event: KeyboardEvent) => {
|
| 244 |
-
// Show the node search dialog on "/".
|
| 245 |
-
if (nodeSearchSettings || isTypingInFormElement()) return;
|
| 246 |
-
if (event.key === "/") {
|
| 247 |
-
event.preventDefault();
|
| 248 |
-
setNodeSearchSettings({
|
| 249 |
-
pos: getBestPosition(),
|
| 250 |
-
boxes: catalog.data![state.workspace.env!],
|
| 251 |
-
});
|
| 252 |
-
} else if (event.key === "r") {
|
| 253 |
-
event.preventDefault();
|
| 254 |
-
executeWorkspace();
|
| 255 |
-
}
|
| 256 |
-
};
|
| 257 |
-
// TODO: Switch to keydown once https://github.com/xyflow/xyflow/pull/5055 is merged.
|
| 258 |
-
document.addEventListener("keyup", handleKeyDown);
|
| 259 |
-
return () => {
|
| 260 |
-
document.removeEventListener("keyup", handleKeyDown);
|
| 261 |
-
};
|
| 262 |
-
}, [catalog.data, nodeSearchSettings, state.workspace.env]);
|
| 263 |
-
|
| 264 |
-
function getBestPosition() {
|
| 265 |
-
const W = reactFlowContainer.current!.clientWidth;
|
| 266 |
-
const H = reactFlowContainer.current!.clientHeight;
|
| 267 |
-
const w = 200;
|
| 268 |
-
const h = 200;
|
| 269 |
-
const SPEED = 20;
|
| 270 |
-
const GAP = 50;
|
| 271 |
-
const pos = { x: 100, y: 100 };
|
| 272 |
-
while (pos.y < H) {
|
| 273 |
-
// Find a position that is not occupied by a node.
|
| 274 |
-
const fpos = reactFlow.screenToFlowPosition(pos);
|
| 275 |
-
const occupied = state.workspace.nodes!.some((n) => {
|
| 276 |
-
const np = n.position;
|
| 277 |
-
return (
|
| 278 |
-
np.x < fpos.x + w + GAP &&
|
| 279 |
-
np.x + n.width + GAP > fpos.x &&
|
| 280 |
-
np.y < fpos.y + h + GAP &&
|
| 281 |
-
np.y + n.height + GAP > fpos.y
|
| 282 |
-
);
|
| 283 |
-
});
|
| 284 |
-
if (!occupied) {
|
| 285 |
-
return pos;
|
| 286 |
-
}
|
| 287 |
-
// Move the position to the right and down until we find a free spot.
|
| 288 |
-
pos.x += SPEED;
|
| 289 |
-
if (pos.x + w > W) {
|
| 290 |
-
pos.x = 100;
|
| 291 |
-
pos.y += SPEED;
|
| 292 |
-
}
|
| 293 |
-
}
|
| 294 |
-
return { x: 100, y: 100 };
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
function isTypingInFormElement() {
|
| 298 |
-
const activeElement = document.activeElement;
|
| 299 |
-
return (
|
| 300 |
-
activeElement &&
|
| 301 |
-
(activeElement.tagName === "INPUT" ||
|
| 302 |
-
activeElement.tagName === "TEXTAREA" ||
|
| 303 |
-
(activeElement as HTMLElement).isContentEditable)
|
| 304 |
-
);
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
const closeNodeSearch = useCallback(() => {
|
| 308 |
-
setNodeSearchSettings(undefined);
|
| 309 |
-
setSuppressSearchUntil(Date.now() + 200);
|
| 310 |
-
}, []);
|
| 311 |
-
const toggleNodeSearch = useCallback(
|
| 312 |
-
(event: MouseEvent) => {
|
| 313 |
-
if (suppressSearchUntil > Date.now()) return;
|
| 314 |
-
if (nodeSearchSettings) {
|
| 315 |
-
closeNodeSearch();
|
| 316 |
-
return;
|
| 317 |
-
}
|
| 318 |
-
event.preventDefault();
|
| 319 |
-
setNodeSearchSettings({
|
| 320 |
-
pos: { x: event.clientX, y: event.clientY },
|
| 321 |
-
boxes: catalog.data![state.workspace.env!],
|
| 322 |
-
});
|
| 323 |
-
},
|
| 324 |
-
[catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
|
| 325 |
-
);
|
| 326 |
-
function findFreeId(prefix: string) {
|
| 327 |
-
let i = 1;
|
| 328 |
-
let id = `${prefix} ${i}`;
|
| 329 |
-
const used = new Set(state.workspace.nodes!.map((n) => n.id));
|
| 330 |
-
while (used.has(id)) {
|
| 331 |
-
i += 1;
|
| 332 |
-
id = `${prefix} ${i}`;
|
| 333 |
-
}
|
| 334 |
-
return id;
|
| 335 |
-
}
|
| 336 |
-
function addNode(node: Partial<WorkspaceNode>) {
|
| 337 |
-
state.workspace.nodes!.push(node as WorkspaceNode);
|
| 338 |
-
setNodes([...nodes, node as WorkspaceNode]);
|
| 339 |
-
}
|
| 340 |
-
function nodeFromMeta(meta: OpsOp): Partial<WorkspaceNode> {
|
| 341 |
-
const node: Partial<WorkspaceNode> = {
|
| 342 |
-
type: meta.type,
|
| 343 |
-
data: {
|
| 344 |
-
meta: { value: meta },
|
| 345 |
-
title: meta.name,
|
| 346 |
-
op_id: meta.id,
|
| 347 |
-
params: Object.fromEntries(meta.params.map((p) => [p.name, p.default])),
|
| 348 |
-
},
|
| 349 |
-
};
|
| 350 |
-
return node;
|
| 351 |
-
}
|
| 352 |
-
const addNodeFromSearch = useCallback(
|
| 353 |
-
(meta: OpsOp) => {
|
| 354 |
-
const node = nodeFromMeta(meta);
|
| 355 |
-
const nss = nodeSearchSettings!;
|
| 356 |
-
node.position = reactFlow.screenToFlowPosition({
|
| 357 |
-
x: nss.pos.x,
|
| 358 |
-
y: nss.pos.y,
|
| 359 |
-
});
|
| 360 |
-
node.id = findFreeId(node.data!.title);
|
| 361 |
-
addNode(node);
|
| 362 |
-
closeNodeSearch();
|
| 363 |
-
},
|
| 364 |
-
[nodeSearchSettings, state, reactFlow, nodes, closeNodeSearch],
|
| 365 |
-
);
|
| 366 |
-
|
| 367 |
-
const onConnect = useCallback(
|
| 368 |
-
(connection: Connection) => {
|
| 369 |
-
setSuppressSearchUntil(Date.now() + 200);
|
| 370 |
-
const edge = {
|
| 371 |
-
id: `${connection.source} ${connection.sourceHandle} ${connection.target} ${connection.targetHandle}`,
|
| 372 |
-
source: connection.source,
|
| 373 |
-
sourceHandle: connection.sourceHandle!,
|
| 374 |
-
target: connection.target,
|
| 375 |
-
targetHandle: connection.targetHandle!,
|
| 376 |
-
};
|
| 377 |
-
state.workspace.edges!.push(edge);
|
| 378 |
-
setEdges((oldEdges) => [...oldEdges, edge]);
|
| 379 |
-
},
|
| 380 |
-
[state],
|
| 381 |
-
);
|
| 382 |
-
const parentDir = path!.split("/").slice(0, -1).join("/");
|
| 383 |
-
function onDragOver(e: React.DragEvent<HTMLDivElement>) {
|
| 384 |
-
e.stopPropagation();
|
| 385 |
-
e.preventDefault();
|
| 386 |
-
}
|
| 387 |
-
async function onDrop(e: React.DragEvent<HTMLDivElement>) {
|
| 388 |
-
e.stopPropagation();
|
| 389 |
-
e.preventDefault();
|
| 390 |
-
const file = e.dataTransfer.files[0];
|
| 391 |
-
const formData = new FormData();
|
| 392 |
-
formData.append("file", file);
|
| 393 |
-
try {
|
| 394 |
-
await axios.post("/api/upload", formData, {
|
| 395 |
-
onUploadProgress: (progressEvent) => {
|
| 396 |
-
const percentCompleted = Math.round((100 * progressEvent.loaded) / progressEvent.total!);
|
| 397 |
-
if (percentCompleted === 100) setMessage("Processing file...");
|
| 398 |
-
else setMessage(`Uploading ${percentCompleted}%`);
|
| 399 |
-
},
|
| 400 |
-
});
|
| 401 |
-
setMessage(null);
|
| 402 |
-
const cat = catalog.data![state.workspace.env!];
|
| 403 |
-
const node = nodeFromMeta(cat["Import file"]);
|
| 404 |
-
node.id = findFreeId(node.data!.title);
|
| 405 |
-
node.position = reactFlow.screenToFlowPosition({
|
| 406 |
-
x: e.clientX,
|
| 407 |
-
y: e.clientY,
|
| 408 |
-
});
|
| 409 |
-
node.data!.params.file_path = `uploads/${file.name}`;
|
| 410 |
-
if (file.name.includes(".csv")) {
|
| 411 |
-
node.data!.params.file_format = "csv";
|
| 412 |
-
} else if (file.name.includes(".parquet")) {
|
| 413 |
-
node.data!.params.file_format = "parquet";
|
| 414 |
-
} else if (file.name.includes(".json")) {
|
| 415 |
-
node.data!.params.file_format = "json";
|
| 416 |
-
} else if (file.name.includes(".xls")) {
|
| 417 |
-
node.data!.params.file_format = "excel";
|
| 418 |
-
}
|
| 419 |
-
addNode(node);
|
| 420 |
-
} catch (error) {
|
| 421 |
-
setMessage("File upload failed.");
|
| 422 |
-
}
|
| 423 |
-
}
|
| 424 |
-
async function executeWorkspace() {
|
| 425 |
-
const response = await axios.post(`/api/execute_workspace?name=${encodeURIComponent(path)}`);
|
| 426 |
-
if (response.status !== 200) {
|
| 427 |
-
setMessage("Workspace execution failed.");
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
function togglePause() {
|
| 431 |
-
state.workspace.paused = !state.workspace.paused;
|
| 432 |
-
setPausedUIState(state.workspace.paused);
|
| 433 |
-
}
|
| 434 |
-
function deleteSelection() {
|
| 435 |
-
const selectedNodes = nodes.filter((n) => n.selected);
|
| 436 |
-
const selectedEdges = edges.filter((e) => e.selected);
|
| 437 |
-
reactFlow.deleteElements({ nodes: selectedNodes, edges: selectedEdges });
|
| 438 |
-
}
|
| 439 |
-
function groupSelection() {
|
| 440 |
-
const selectedNodes = nodes.filter((n) => n.selected && !n.parentId);
|
| 441 |
-
const groupNode = {
|
| 442 |
-
id: findFreeId("Group"),
|
| 443 |
-
type: "node_group",
|
| 444 |
-
position: { x: 0, y: 0 },
|
| 445 |
-
width: 0,
|
| 446 |
-
height: 0,
|
| 447 |
-
data: { title: "Group", params: {} },
|
| 448 |
-
};
|
| 449 |
-
let top = Number.POSITIVE_INFINITY;
|
| 450 |
-
let left = Number.POSITIVE_INFINITY;
|
| 451 |
-
let bottom = Number.NEGATIVE_INFINITY;
|
| 452 |
-
let right = Number.NEGATIVE_INFINITY;
|
| 453 |
-
const PAD = 10;
|
| 454 |
-
for (const node of selectedNodes) {
|
| 455 |
-
if (node.position.y - PAD < top) top = node.position.y - PAD;
|
| 456 |
-
if (node.position.x - PAD < left) left = node.position.x - PAD;
|
| 457 |
-
if (node.position.y + PAD + node.height! > bottom)
|
| 458 |
-
bottom = node.position.y + PAD + node.height!;
|
| 459 |
-
if (node.position.x + PAD + node.width! > right) right = node.position.x + PAD + node.width!;
|
| 460 |
-
}
|
| 461 |
-
groupNode.position = {
|
| 462 |
-
x: left,
|
| 463 |
-
y: top,
|
| 464 |
-
};
|
| 465 |
-
groupNode.width = right - left;
|
| 466 |
-
groupNode.height = bottom - top;
|
| 467 |
-
setNodes([
|
| 468 |
-
{ ...(groupNode as WorkspaceNode), selected: true },
|
| 469 |
-
...nodes.map((n) =>
|
| 470 |
-
n.selected
|
| 471 |
-
? {
|
| 472 |
-
...n,
|
| 473 |
-
position: { x: n.position.x - left, y: n.position.y - top },
|
| 474 |
-
parentId: groupNode.id,
|
| 475 |
-
extent: "parent" as const,
|
| 476 |
-
selected: false,
|
| 477 |
-
}
|
| 478 |
-
: n,
|
| 479 |
-
),
|
| 480 |
-
]);
|
| 481 |
-
getYjsDoc(state).transact(() => {
|
| 482 |
-
state.workspace.nodes!.unshift(groupNode as WorkspaceNode);
|
| 483 |
-
const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));
|
| 484 |
-
for (const node of state.workspace.nodes!) {
|
| 485 |
-
if (selectedNodeIds.has(node.id)) {
|
| 486 |
-
node.position.x -= left;
|
| 487 |
-
node.position.y -= top;
|
| 488 |
-
node.parentId = groupNode.id;
|
| 489 |
-
node.extent = "parent";
|
| 490 |
-
node.selected = false;
|
| 491 |
-
}
|
| 492 |
-
}
|
| 493 |
-
});
|
| 494 |
-
}
|
| 495 |
-
function ungroupSelection() {
|
| 496 |
-
const groups = Object.fromEntries(
|
| 497 |
-
nodes
|
| 498 |
-
.filter((n) => n.selected && n.type === "node_group" && !n.parentId)
|
| 499 |
-
.map((n) => [n.id, n]),
|
| 500 |
-
);
|
| 501 |
-
setNodes(
|
| 502 |
-
nodes
|
| 503 |
-
.filter((n) => !groups[n.id])
|
| 504 |
-
.map((n) => {
|
| 505 |
-
const g = groups[n.parentId!];
|
| 506 |
-
if (!g) return n;
|
| 507 |
-
return {
|
| 508 |
-
...n,
|
| 509 |
-
position: { x: n.position.x + g.position.x, y: n.position.y + g.position.y },
|
| 510 |
-
parentId: undefined,
|
| 511 |
-
extent: undefined,
|
| 512 |
-
selected: true,
|
| 513 |
-
};
|
| 514 |
-
}),
|
| 515 |
-
);
|
| 516 |
-
getYjsDoc(state).transact(() => {
|
| 517 |
-
const wnodes = state.workspace.nodes!;
|
| 518 |
-
for (const node of state.workspace.nodes!) {
|
| 519 |
-
const g = groups[node.parentId as string];
|
| 520 |
-
if (!g) continue;
|
| 521 |
-
node.position.x += g.position.x;
|
| 522 |
-
node.position.y += g.position.y;
|
| 523 |
-
node.parentId = undefined;
|
| 524 |
-
node.extent = undefined;
|
| 525 |
-
}
|
| 526 |
-
for (const groupId in groups) {
|
| 527 |
-
const groupIdx = wnodes.findIndex((n) => n.id === groupId);
|
| 528 |
-
wnodes.splice(groupIdx, 1);
|
| 529 |
-
}
|
| 530 |
-
});
|
| 531 |
-
}
|
| 532 |
-
const areMultipleNodesSelected = nodes.filter((n) => n.selected).length > 1;
|
| 533 |
-
const isAnyGroupSelected = nodes.some((n) => n.selected && n.type === "node_group");
|
| 534 |
-
return (
|
| 535 |
-
<div className="workspace">
|
| 536 |
-
<div className="top-bar bg-neutral">
|
| 537 |
-
<Link className="logo" to="/">
|
| 538 |
-
<img alt="" src={favicon} />
|
| 539 |
-
</Link>
|
| 540 |
-
<div className="ws-name">{shortPath}</div>
|
| 541 |
-
<title>{shortPath}</title>
|
| 542 |
-
<EnvironmentSelector
|
| 543 |
-
options={Object.keys(catalog.data || {})}
|
| 544 |
-
value={state.workspace.env!}
|
| 545 |
-
onChange={(env) => {
|
| 546 |
-
state.workspace.env = env;
|
| 547 |
-
}}
|
| 548 |
-
/>
|
| 549 |
-
<div className="tools text-secondary">
|
| 550 |
-
{areMultipleNodesSelected && (
|
| 551 |
-
<Tooltip doc="Group selected nodes">
|
| 552 |
-
<button className="btn btn-link" onClick={groupSelection}>
|
| 553 |
-
<GroupIcon />
|
| 554 |
-
</button>
|
| 555 |
-
</Tooltip>
|
| 556 |
-
)}
|
| 557 |
-
{isAnyGroupSelected && (
|
| 558 |
-
<Tooltip doc="Ungroup selected nodes">
|
| 559 |
-
<button className="btn btn-link" onClick={ungroupSelection}>
|
| 560 |
-
<UngroupIcon />
|
| 561 |
-
</button>
|
| 562 |
-
</Tooltip>
|
| 563 |
-
)}
|
| 564 |
-
<Tooltip doc="Delete selected nodes and edges">
|
| 565 |
-
<button className="btn btn-link" onClick={deleteSelection}>
|
| 566 |
-
<Backspace />
|
| 567 |
-
</button>
|
| 568 |
-
</Tooltip>
|
| 569 |
-
<Tooltip doc={pausedUIState ? "Resume automatic execution" : "Pause automatic execution"}>
|
| 570 |
-
<button className="btn btn-link" onClick={togglePause}>
|
| 571 |
-
{pausedUIState ? <Play /> : <Pause />}
|
| 572 |
-
</button>
|
| 573 |
-
</Tooltip>
|
| 574 |
-
<Tooltip doc="Re-run the workspace">
|
| 575 |
-
<button className="btn btn-link" onClick={executeWorkspace}>
|
| 576 |
-
<Restart />
|
| 577 |
-
</button>
|
| 578 |
-
</Tooltip>
|
| 579 |
-
<Tooltip doc="Close workspace">
|
| 580 |
-
<Link
|
| 581 |
-
className="btn btn-link"
|
| 582 |
-
to={`/dir/${parentDir
|
| 583 |
-
.split("/")
|
| 584 |
-
.map((segment) => encodeURIComponent(segment))
|
| 585 |
-
.join("/")}`}
|
| 586 |
-
aria-label="close"
|
| 587 |
-
>
|
| 588 |
-
<Close />
|
| 589 |
-
</Link>
|
| 590 |
-
</Tooltip>
|
| 591 |
-
</div>
|
| 592 |
-
</div>
|
| 593 |
-
<div
|
| 594 |
-
style={{ height: "100%", width: "100vw" }}
|
| 595 |
-
onDragOver={onDragOver}
|
| 596 |
-
onDrop={onDrop}
|
| 597 |
-
ref={reactFlowContainer}
|
| 598 |
-
>
|
| 599 |
-
<LynxKiteState.Provider value={state}>
|
| 600 |
-
<ReactFlow
|
| 601 |
-
nodes={nodes}
|
| 602 |
-
edges={edges}
|
| 603 |
-
nodeTypes={nodeTypes}
|
| 604 |
-
edgeTypes={edgeTypes}
|
| 605 |
-
fitView
|
| 606 |
-
onNodesChange={onNodesChange}
|
| 607 |
-
onEdgesChange={onEdgesChange}
|
| 608 |
-
onPaneClick={toggleNodeSearch}
|
| 609 |
-
onConnect={onConnect}
|
| 610 |
-
proOptions={{ hideAttribution: true }}
|
| 611 |
-
maxZoom={10}
|
| 612 |
-
minZoom={0.2}
|
| 613 |
-
zoomOnScroll={false}
|
| 614 |
-
panOnScroll={true}
|
| 615 |
-
panOnDrag={false}
|
| 616 |
-
selectionOnDrag={true}
|
| 617 |
-
panOnScrollSpeed={1}
|
| 618 |
-
preventScrolling={false}
|
| 619 |
-
defaultEdgeOptions={{
|
| 620 |
-
markerEnd: {
|
| 621 |
-
type: MarkerType.ArrowClosed,
|
| 622 |
-
color: "black",
|
| 623 |
-
width: 15,
|
| 624 |
-
height: 15,
|
| 625 |
-
},
|
| 626 |
-
style: {
|
| 627 |
-
strokeWidth: 2,
|
| 628 |
-
stroke: "black",
|
| 629 |
-
},
|
| 630 |
-
}}
|
| 631 |
-
fitViewOptions={{ maxZoom: 1 }}
|
| 632 |
-
>
|
| 633 |
-
<Controls />
|
| 634 |
-
{nodeSearchSettings && (
|
| 635 |
-
<NodeSearch
|
| 636 |
-
pos={nodeSearchSettings.pos}
|
| 637 |
-
boxes={nodeSearchSettings.boxes}
|
| 638 |
-
onCancel={closeNodeSearch}
|
| 639 |
-
onAdd={addNodeFromSearch}
|
| 640 |
-
/>
|
| 641 |
-
)}
|
| 642 |
-
</ReactFlow>
|
| 643 |
-
</LynxKiteState.Provider>
|
| 644 |
-
{message && (
|
| 645 |
-
<div className="workspace-message">
|
| 646 |
-
<span className="close" onClick={() => setMessage(null)}>
|
| 647 |
-
<Close />
|
| 648 |
-
</span>
|
| 649 |
-
{message}
|
| 650 |
-
</div>
|
| 651 |
-
)}
|
| 652 |
-
</div>
|
| 653 |
-
</div>
|
| 654 |
-
);
|
| 655 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx
DELETED
|
@@ -1,296 +0,0 @@
|
|
| 1 |
-
import { useReactFlow } from "@xyflow/react";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
import React from "react";
|
| 4 |
-
import Markdown from "react-markdown";
|
| 5 |
-
// @ts-ignore
|
| 6 |
-
import Trash from "~icons/tabler/trash";
|
| 7 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 8 |
-
import Table from "./Table";
|
| 9 |
-
|
| 10 |
-
function toMD(v: any): string {
|
| 11 |
-
if (typeof v === "string") {
|
| 12 |
-
return v;
|
| 13 |
-
}
|
| 14 |
-
if (Array.isArray(v)) {
|
| 15 |
-
return v.map(toMD).join("\n\n");
|
| 16 |
-
}
|
| 17 |
-
return JSON.stringify(v);
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
function displayTable(name: string, df: any) {
|
| 21 |
-
if (df.data.length > 1) {
|
| 22 |
-
return (
|
| 23 |
-
<Table key={`${name}-table`} name={`${name}-table`} columns={df.columns} data={df.data} />
|
| 24 |
-
);
|
| 25 |
-
}
|
| 26 |
-
if (df.data.length) {
|
| 27 |
-
return (
|
| 28 |
-
<dl key={`${name}-dl`}>
|
| 29 |
-
{df.columns.map((c: string, i: number) => (
|
| 30 |
-
<React.Fragment key={`${name}-${c}`}>
|
| 31 |
-
<dt>{c}</dt>
|
| 32 |
-
<dd>
|
| 33 |
-
<Markdown>{toMD(df.data[0][i])}</Markdown>
|
| 34 |
-
</dd>
|
| 35 |
-
</React.Fragment>
|
| 36 |
-
))}
|
| 37 |
-
</dl>
|
| 38 |
-
);
|
| 39 |
-
}
|
| 40 |
-
return JSON.stringify(df.data);
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
function relationsToDict(relations: any[]) {
|
| 44 |
-
if (!relations) {
|
| 45 |
-
return {};
|
| 46 |
-
}
|
| 47 |
-
return Object.assign({}, ...relations.map((r: any) => ({ [r.name]: r })));
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
export type UpdateOptions = { delay?: number };
|
| 51 |
-
|
| 52 |
-
function NodeWithGraphCreationView(props: any) {
|
| 53 |
-
const reactFlow = useReactFlow();
|
| 54 |
-
const [open, setOpen] = useState({} as { [name: string]: boolean });
|
| 55 |
-
const display = props.data.display?.value;
|
| 56 |
-
const tables = display?.dataframes || {};
|
| 57 |
-
const singleTable = tables && Object.keys(tables).length === 1;
|
| 58 |
-
const [relations, setRelations] = useState(relationsToDict(display?.relations) || {});
|
| 59 |
-
const singleRelation = relations && Object.keys(relations).length === 1;
|
| 60 |
-
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
| 61 |
-
reactFlow.updateNodeData(props.id, {
|
| 62 |
-
params: { ...props.data.params, [name]: newValue },
|
| 63 |
-
__execution_delay: opts.delay || 0,
|
| 64 |
-
});
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
function updateRelation(event: any, relation: any) {
|
| 68 |
-
event.preventDefault();
|
| 69 |
-
|
| 70 |
-
const updatedRelation = {
|
| 71 |
-
...relation,
|
| 72 |
-
...Object.fromEntries(new FormData(event.target).entries()),
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
// Avoid mutating React state directly
|
| 76 |
-
const newRelations = { ...relations };
|
| 77 |
-
if (relation.name !== updatedRelation.name) {
|
| 78 |
-
delete newRelations[relation.name];
|
| 79 |
-
}
|
| 80 |
-
newRelations[updatedRelation.name] = updatedRelation;
|
| 81 |
-
setRelations(newRelations);
|
| 82 |
-
// There is some issue with how Yjs handles complex objects (maps, arrays)
|
| 83 |
-
// so we need to serialize the relations object to a string
|
| 84 |
-
setParam("relations", JSON.stringify(newRelations), {});
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
const addRelation = () => {
|
| 88 |
-
const new_relation = {
|
| 89 |
-
name: "new_relation",
|
| 90 |
-
df: "",
|
| 91 |
-
source_column: "",
|
| 92 |
-
target_column: "",
|
| 93 |
-
source_table: "",
|
| 94 |
-
target_table: "",
|
| 95 |
-
source_key: "",
|
| 96 |
-
target_key: "",
|
| 97 |
-
};
|
| 98 |
-
setRelations({
|
| 99 |
-
...relations,
|
| 100 |
-
[new_relation.name]: new_relation,
|
| 101 |
-
});
|
| 102 |
-
setOpen({ ...open, [new_relation.name]: true });
|
| 103 |
-
};
|
| 104 |
-
|
| 105 |
-
const deleteRelation = (relation: any) => {
|
| 106 |
-
const newOpen = { ...open };
|
| 107 |
-
delete newOpen[relation.name];
|
| 108 |
-
setOpen(newOpen);
|
| 109 |
-
const newRelations = { ...relations };
|
| 110 |
-
delete newRelations[relation.name];
|
| 111 |
-
setRelations(newRelations);
|
| 112 |
-
// There is some issue with how Yjs handles complex objects (maps, arrays)
|
| 113 |
-
// so we need to serialize the relations object to a string
|
| 114 |
-
setParam("relations", JSON.stringify(newRelations), {});
|
| 115 |
-
};
|
| 116 |
-
|
| 117 |
-
function displayRelation(relation: any) {
|
| 118 |
-
// TODO: Dynamic autocomplete
|
| 119 |
-
return (
|
| 120 |
-
<form
|
| 121 |
-
className="graph-relation-attributes"
|
| 122 |
-
onSubmit={(e) => {
|
| 123 |
-
updateRelation(e, relation);
|
| 124 |
-
}}
|
| 125 |
-
>
|
| 126 |
-
<label htmlFor="name">Name:</label>
|
| 127 |
-
<input type="text" id="name" name="name" defaultValue={relation.name} />
|
| 128 |
-
|
| 129 |
-
<label htmlFor="df">DataFrame:</label>
|
| 130 |
-
<input
|
| 131 |
-
type="text"
|
| 132 |
-
id="df"
|
| 133 |
-
name="df"
|
| 134 |
-
defaultValue={relation.df}
|
| 135 |
-
list="df-options"
|
| 136 |
-
required
|
| 137 |
-
/>
|
| 138 |
-
|
| 139 |
-
<label htmlFor="source_column">Source Column:</label>
|
| 140 |
-
<input
|
| 141 |
-
type="text"
|
| 142 |
-
id="source_column"
|
| 143 |
-
name="source_column"
|
| 144 |
-
defaultValue={relation.source_column}
|
| 145 |
-
list="edges-column-options"
|
| 146 |
-
required
|
| 147 |
-
/>
|
| 148 |
-
|
| 149 |
-
<label htmlFor="target_column">Target Column:</label>
|
| 150 |
-
<input
|
| 151 |
-
type="text"
|
| 152 |
-
id="target_column"
|
| 153 |
-
name="target_column"
|
| 154 |
-
defaultValue={relation.target_column}
|
| 155 |
-
list="edges-column-options"
|
| 156 |
-
required
|
| 157 |
-
/>
|
| 158 |
-
|
| 159 |
-
<label htmlFor="source_table">Source Table:</label>
|
| 160 |
-
<input
|
| 161 |
-
type="text"
|
| 162 |
-
id="source_table"
|
| 163 |
-
name="source_table"
|
| 164 |
-
defaultValue={relation.source_table}
|
| 165 |
-
list="df-options"
|
| 166 |
-
required
|
| 167 |
-
/>
|
| 168 |
-
|
| 169 |
-
<label htmlFor="target_table">Target Table:</label>
|
| 170 |
-
<input
|
| 171 |
-
type="text"
|
| 172 |
-
id="target_table"
|
| 173 |
-
name="target_table"
|
| 174 |
-
defaultValue={relation.target_table}
|
| 175 |
-
list="df-options"
|
| 176 |
-
required
|
| 177 |
-
/>
|
| 178 |
-
|
| 179 |
-
<label htmlFor="source_key">Source Key:</label>
|
| 180 |
-
<input
|
| 181 |
-
type="text"
|
| 182 |
-
id="source_key"
|
| 183 |
-
name="source_key"
|
| 184 |
-
defaultValue={relation.source_key}
|
| 185 |
-
list="source-node-column-options"
|
| 186 |
-
required
|
| 187 |
-
/>
|
| 188 |
-
|
| 189 |
-
<label htmlFor="target_key">Target Key:</label>
|
| 190 |
-
<input
|
| 191 |
-
type="text"
|
| 192 |
-
id="target_key"
|
| 193 |
-
name="target_key"
|
| 194 |
-
defaultValue={relation.target_key}
|
| 195 |
-
list="target-node-column-options"
|
| 196 |
-
required
|
| 197 |
-
/>
|
| 198 |
-
|
| 199 |
-
<datalist id="df-options">
|
| 200 |
-
{Object.keys(tables).map((name) => (
|
| 201 |
-
<option key={name} value={name} />
|
| 202 |
-
))}
|
| 203 |
-
</datalist>
|
| 204 |
-
|
| 205 |
-
<datalist id="edges-column-options">
|
| 206 |
-
{tables[relation.source_table] &&
|
| 207 |
-
tables[relation.df].columns.map((name: string) => <option key={name} value={name} />)}
|
| 208 |
-
</datalist>
|
| 209 |
-
|
| 210 |
-
<datalist id="source-node-column-options">
|
| 211 |
-
{tables[relation.source_table] &&
|
| 212 |
-
tables[relation.source_table].columns.map((name: string) => (
|
| 213 |
-
<option key={name} value={name} />
|
| 214 |
-
))}
|
| 215 |
-
</datalist>
|
| 216 |
-
|
| 217 |
-
<datalist id="target-node-column-options">
|
| 218 |
-
{tables[relation.source_table] &&
|
| 219 |
-
tables[relation.target_table].columns.map((name: string) => (
|
| 220 |
-
<option key={name} value={name} />
|
| 221 |
-
))}
|
| 222 |
-
</datalist>
|
| 223 |
-
|
| 224 |
-
<button className="submit-relationship-button" type="submit">
|
| 225 |
-
Create
|
| 226 |
-
</button>
|
| 227 |
-
</form>
|
| 228 |
-
);
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
return (
|
| 232 |
-
<div className="graph-creation-view">
|
| 233 |
-
<div className="graph-tables">
|
| 234 |
-
<div className="graph-table-header">Node Tables</div>
|
| 235 |
-
{display && [
|
| 236 |
-
Object.entries(tables).map(([name, df]: [string, any]) => (
|
| 237 |
-
<React.Fragment key={name}>
|
| 238 |
-
{!singleTable && (
|
| 239 |
-
<div
|
| 240 |
-
key={`${name}-header`}
|
| 241 |
-
className="df-head"
|
| 242 |
-
onClick={() => setOpen({ ...open, [name]: !open[name] })}
|
| 243 |
-
>
|
| 244 |
-
{name}
|
| 245 |
-
</div>
|
| 246 |
-
)}
|
| 247 |
-
{(singleTable || open[name]) && displayTable(name, df)}
|
| 248 |
-
</React.Fragment>
|
| 249 |
-
)),
|
| 250 |
-
Object.entries(display.others || {}).map(([name, o]) => (
|
| 251 |
-
<>
|
| 252 |
-
<div
|
| 253 |
-
key={name}
|
| 254 |
-
className="df-head"
|
| 255 |
-
onClick={() => setOpen({ ...open, [name]: !open[name] })}
|
| 256 |
-
>
|
| 257 |
-
{name}
|
| 258 |
-
</div>
|
| 259 |
-
{open[name] && <pre>{(o as any).toString()}</pre>}
|
| 260 |
-
</>
|
| 261 |
-
)),
|
| 262 |
-
]}
|
| 263 |
-
</div>
|
| 264 |
-
<div className="graph-relations">
|
| 265 |
-
<div className="graph-table-header">
|
| 266 |
-
Relationships
|
| 267 |
-
<button className="add-relationship-button" onClick={(_) => addRelation()}>
|
| 268 |
-
+
|
| 269 |
-
</button>
|
| 270 |
-
</div>
|
| 271 |
-
{relations &&
|
| 272 |
-
Object.entries(relations).map(([name, relation]: [string, any]) => (
|
| 273 |
-
<React.Fragment key={name}>
|
| 274 |
-
<div
|
| 275 |
-
key={`${name}-header`}
|
| 276 |
-
className="df-head"
|
| 277 |
-
onClick={() => setOpen({ ...open, [name]: !open[name] })}
|
| 278 |
-
>
|
| 279 |
-
{name}
|
| 280 |
-
<button
|
| 281 |
-
onClick={() => {
|
| 282 |
-
deleteRelation(relation);
|
| 283 |
-
}}
|
| 284 |
-
>
|
| 285 |
-
<Trash />
|
| 286 |
-
</button>
|
| 287 |
-
</div>
|
| 288 |
-
{(singleRelation || open[name]) && displayRelation(relation)}
|
| 289 |
-
</React.Fragment>
|
| 290 |
-
))}
|
| 291 |
-
</div>
|
| 292 |
-
</div>
|
| 293 |
-
);
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
export default LynxKiteNode(NodeWithGraphCreationView);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/Group.tsx
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
import { useReactFlow } from "@xyflow/react";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
// @ts-ignore
|
| 4 |
-
import Palette from "~icons/tabler/palette-filled.jsx";
|
| 5 |
-
// @ts-ignore
|
| 6 |
-
import Square from "~icons/tabler/square-filled.jsx";
|
| 7 |
-
import Tooltip from "../../Tooltip.tsx";
|
| 8 |
-
import { COLORS } from "../../common.ts";
|
| 9 |
-
|
| 10 |
-
export default function Group(props: any) {
|
| 11 |
-
const reactFlow = useReactFlow();
|
| 12 |
-
const [displayingColorPicker, setDisplayingColorPicker] = useState(false);
|
| 13 |
-
function setColor(newValue: string) {
|
| 14 |
-
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
| 15 |
-
...prevData,
|
| 16 |
-
params: { color: newValue },
|
| 17 |
-
}));
|
| 18 |
-
setDisplayingColorPicker(false);
|
| 19 |
-
}
|
| 20 |
-
function toggleColorPicker(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
| 21 |
-
e.stopPropagation();
|
| 22 |
-
setDisplayingColorPicker(!displayingColorPicker);
|
| 23 |
-
}
|
| 24 |
-
const currentColor = props.data?.params?.color || "gray";
|
| 25 |
-
return (
|
| 26 |
-
<div
|
| 27 |
-
className={`node-group ${props.parentId ? "in-group" : ""}`}
|
| 28 |
-
style={{
|
| 29 |
-
width: props.width,
|
| 30 |
-
height: props.height,
|
| 31 |
-
backgroundColor: COLORS[currentColor],
|
| 32 |
-
}}
|
| 33 |
-
>
|
| 34 |
-
<button
|
| 35 |
-
className="node-group-color-picker-icon"
|
| 36 |
-
onClick={toggleColorPicker}
|
| 37 |
-
aria-label="Change group color"
|
| 38 |
-
>
|
| 39 |
-
<Tooltip doc="Change color">
|
| 40 |
-
<Palette />
|
| 41 |
-
</Tooltip>
|
| 42 |
-
</button>
|
| 43 |
-
{displayingColorPicker && <ColorPicker currentColor={currentColor} onPick={setColor} />}
|
| 44 |
-
</div>
|
| 45 |
-
);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function ColorPicker(props: { currentColor: string; onPick: (color: string) => void }) {
|
| 49 |
-
const colors = Object.keys(COLORS).filter((color) => color !== props.currentColor);
|
| 50 |
-
return (
|
| 51 |
-
<div className="color-picker">
|
| 52 |
-
{colors.map((color) => (
|
| 53 |
-
<button
|
| 54 |
-
key={color}
|
| 55 |
-
className="color-picker-button"
|
| 56 |
-
style={{ color: COLORS[color] }}
|
| 57 |
-
onClick={() => props.onPick(color)}
|
| 58 |
-
>
|
| 59 |
-
<Square />
|
| 60 |
-
</button>
|
| 61 |
-
))}
|
| 62 |
-
</div>
|
| 63 |
-
);
|
| 64 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx
DELETED
|
@@ -1,209 +0,0 @@
|
|
| 1 |
-
import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
|
| 2 |
-
import React from "react";
|
| 3 |
-
import { ErrorBoundary } from "react-error-boundary";
|
| 4 |
-
// @ts-ignore
|
| 5 |
-
import AlertTriangle from "~icons/tabler/alert-triangle-filled.jsx";
|
| 6 |
-
// @ts-ignore
|
| 7 |
-
import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
|
| 8 |
-
// @ts-ignore
|
| 9 |
-
import Dots from "~icons/tabler/dots.jsx";
|
| 10 |
-
// @ts-ignore
|
| 11 |
-
import Help from "~icons/tabler/question-mark.jsx";
|
| 12 |
-
// @ts-ignore
|
| 13 |
-
import Skull from "~icons/tabler/skull.jsx";
|
| 14 |
-
import Tooltip from "../../Tooltip";
|
| 15 |
-
import { COLORS } from "../../common.ts";
|
| 16 |
-
|
| 17 |
-
interface LynxKiteNodeProps {
|
| 18 |
-
id: string;
|
| 19 |
-
width: number;
|
| 20 |
-
height: number;
|
| 21 |
-
nodeStyle: any;
|
| 22 |
-
data: any;
|
| 23 |
-
children: any;
|
| 24 |
-
parentId?: string;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
function getHandles(inputs: any[], outputs: any[]) {
|
| 28 |
-
const handles: {
|
| 29 |
-
position: "top" | "bottom" | "left" | "right";
|
| 30 |
-
name: string;
|
| 31 |
-
index: number;
|
| 32 |
-
offsetPercentage: number;
|
| 33 |
-
showLabel: boolean;
|
| 34 |
-
type: "source" | "target";
|
| 35 |
-
}[] = [];
|
| 36 |
-
for (const e of inputs) {
|
| 37 |
-
handles.push({ ...e, type: "target" });
|
| 38 |
-
}
|
| 39 |
-
for (const e of outputs) {
|
| 40 |
-
handles.push({ ...e, type: "source" });
|
| 41 |
-
}
|
| 42 |
-
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
| 43 |
-
for (const e of handles) {
|
| 44 |
-
e.index = counts[e.position];
|
| 45 |
-
counts[e.position]++;
|
| 46 |
-
}
|
| 47 |
-
const simpleHorizontal =
|
| 48 |
-
counts.top === 0 && counts.bottom === 0 && counts.left <= 1 && counts.right <= 1;
|
| 49 |
-
const simpleVertical =
|
| 50 |
-
counts.left === 0 && counts.right === 0 && counts.top <= 1 && counts.bottom <= 1;
|
| 51 |
-
for (const e of handles) {
|
| 52 |
-
e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
|
| 53 |
-
e.showLabel = !simpleHorizontal && !simpleVertical;
|
| 54 |
-
}
|
| 55 |
-
return handles;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
function canScrollX(element: HTMLElement) {
|
| 59 |
-
const style = getComputedStyle(element);
|
| 60 |
-
return style.overflowX === "auto" || style.overflow === "auto";
|
| 61 |
-
}
|
| 62 |
-
function canScrollY(element: HTMLElement) {
|
| 63 |
-
const style = getComputedStyle(element);
|
| 64 |
-
return style.overflowY === "auto" || style.overflow === "auto";
|
| 65 |
-
}
|
| 66 |
-
function canScrollUp(e: HTMLElement) {
|
| 67 |
-
return canScrollY(e) && e.scrollTop > 0;
|
| 68 |
-
}
|
| 69 |
-
function canScrollDown(e: HTMLElement) {
|
| 70 |
-
return canScrollY(e) && e.scrollTop < e.scrollHeight - e.clientHeight - 1;
|
| 71 |
-
}
|
| 72 |
-
function canScrollLeft(e: HTMLElement) {
|
| 73 |
-
return canScrollX(e) && e.scrollLeft > 0;
|
| 74 |
-
}
|
| 75 |
-
function canScrollRight(e: HTMLElement) {
|
| 76 |
-
return canScrollX(e) && e.scrollLeft < e.scrollWidth - e.clientWidth - 1;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
function onWheel(e: WheelEvent) {
|
| 80 |
-
if (e.ctrlKey) return; // Zoom, not scroll.
|
| 81 |
-
let t = e.target as HTMLElement;
|
| 82 |
-
// If we find an element inside the node container that can apply this scroll event, we stop propagation.
|
| 83 |
-
// Otherwise ReactFlow can have it and pan the workspace.
|
| 84 |
-
while (t && !t.classList.contains("node-container")) {
|
| 85 |
-
if (
|
| 86 |
-
(e.deltaX < 0 && canScrollLeft(t)) ||
|
| 87 |
-
(e.deltaX > 0 && canScrollRight(t)) ||
|
| 88 |
-
(e.deltaY < 0 && canScrollUp(t)) ||
|
| 89 |
-
(e.deltaY > 0 && canScrollDown(t))
|
| 90 |
-
) {
|
| 91 |
-
e.stopPropagation();
|
| 92 |
-
return;
|
| 93 |
-
}
|
| 94 |
-
t = t.parentElement as HTMLElement;
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
|
| 99 |
-
const reactFlow = useReactFlow();
|
| 100 |
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
| 101 |
-
const data = props.data;
|
| 102 |
-
const expanded = !data.collapsed;
|
| 103 |
-
const handles = getHandles(data.meta?.value?.inputs || [], data.meta?.value?.outputs || []);
|
| 104 |
-
React.useEffect(() => {
|
| 105 |
-
// ReactFlow handles wheel events to zoom/pan and this would prevent scrolling inside the node.
|
| 106 |
-
// To stop the event from reaching ReactFlow, we stop propagation on the wheel event.
|
| 107 |
-
// This must be done with a "passive: false" listener, which we can only register like this.
|
| 108 |
-
containerRef.current?.addEventListener("wheel", onWheel, {
|
| 109 |
-
passive: false,
|
| 110 |
-
});
|
| 111 |
-
return () => {
|
| 112 |
-
containerRef.current?.removeEventListener("wheel", onWheel);
|
| 113 |
-
};
|
| 114 |
-
}, [containerRef]);
|
| 115 |
-
function titleClicked() {
|
| 116 |
-
reactFlow.updateNodeData(props.id, { collapsed: expanded });
|
| 117 |
-
}
|
| 118 |
-
const handleOffsetDirection = {
|
| 119 |
-
top: "left",
|
| 120 |
-
bottom: "left",
|
| 121 |
-
left: "top",
|
| 122 |
-
right: "top",
|
| 123 |
-
};
|
| 124 |
-
const titleStyle: { backgroundColor?: string } = {};
|
| 125 |
-
if (data.meta?.value?.color) {
|
| 126 |
-
titleStyle.backgroundColor = COLORS[data.meta.value.color] || data.meta.value.color;
|
| 127 |
-
}
|
| 128 |
-
return (
|
| 129 |
-
<div
|
| 130 |
-
className={`node-container ${expanded ? "expanded" : "collapsed"} ${props.parentId ? "in-group" : ""}`}
|
| 131 |
-
style={{
|
| 132 |
-
width: props.width || 200,
|
| 133 |
-
height: expanded ? props.height || 200 : undefined,
|
| 134 |
-
}}
|
| 135 |
-
ref={containerRef}
|
| 136 |
-
>
|
| 137 |
-
<div className="lynxkite-node" style={props.nodeStyle}>
|
| 138 |
-
<div
|
| 139 |
-
className={`title bg-primary drag-handle ${data.status}`}
|
| 140 |
-
style={titleStyle}
|
| 141 |
-
onClick={titleClicked}
|
| 142 |
-
>
|
| 143 |
-
<span className="title-title">{data.title}</span>
|
| 144 |
-
{data.error && (
|
| 145 |
-
<Tooltip doc={`Error: ${data.error}`}>
|
| 146 |
-
<AlertTriangle />
|
| 147 |
-
</Tooltip>
|
| 148 |
-
)}
|
| 149 |
-
{expanded || (
|
| 150 |
-
<Tooltip doc="Click to expand node">
|
| 151 |
-
<Dots />
|
| 152 |
-
</Tooltip>
|
| 153 |
-
)}
|
| 154 |
-
<Tooltip doc={data.meta?.value?.doc}>
|
| 155 |
-
<Help />
|
| 156 |
-
</Tooltip>
|
| 157 |
-
</div>
|
| 158 |
-
{expanded && (
|
| 159 |
-
<>
|
| 160 |
-
{data.error && <div className="error">{data.error}</div>}
|
| 161 |
-
<ErrorBoundary
|
| 162 |
-
resetKeys={[props]}
|
| 163 |
-
fallback={
|
| 164 |
-
<p className="error" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
| 165 |
-
<Skull style={{ fontSize: 20 }} />
|
| 166 |
-
Failed to display this node.
|
| 167 |
-
</p>
|
| 168 |
-
}
|
| 169 |
-
>
|
| 170 |
-
<div className="node-content">{props.children}</div>
|
| 171 |
-
</ErrorBoundary>
|
| 172 |
-
<NodeResizeControl
|
| 173 |
-
minWidth={100}
|
| 174 |
-
minHeight={50}
|
| 175 |
-
style={{ background: "transparent", border: "none" }}
|
| 176 |
-
>
|
| 177 |
-
<ChevronDownRight className="node-resizer" />
|
| 178 |
-
</NodeResizeControl>
|
| 179 |
-
</>
|
| 180 |
-
)}
|
| 181 |
-
{handles.map((handle) => (
|
| 182 |
-
<Handle
|
| 183 |
-
key={`${handle.name} on ${handle.position}`}
|
| 184 |
-
id={handle.name}
|
| 185 |
-
type={handle.type}
|
| 186 |
-
position={handle.position as Position}
|
| 187 |
-
style={{
|
| 188 |
-
[handleOffsetDirection[handle.position]]: `${handle.offsetPercentage}% `,
|
| 189 |
-
}}
|
| 190 |
-
>
|
| 191 |
-
{handle.showLabel && (
|
| 192 |
-
<span className="handle-name">{handle.name.replace(/_/g, " ")}</span>
|
| 193 |
-
)}
|
| 194 |
-
</Handle>
|
| 195 |
-
))}
|
| 196 |
-
</div>
|
| 197 |
-
</div>
|
| 198 |
-
);
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
export default function LynxKiteNode(Component: React.ComponentType<any>) {
|
| 202 |
-
return (props: any) => {
|
| 203 |
-
return (
|
| 204 |
-
<LynxKiteNodeComponent {...props}>
|
| 205 |
-
<Component {...props} />
|
| 206 |
-
</LynxKiteNodeComponent>
|
| 207 |
-
);
|
| 208 |
-
};
|
| 209 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/ModelMappingParameter.tsx
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
import { useRef } from "react";
|
| 2 |
-
// @ts-ignore
|
| 3 |
-
import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
|
| 4 |
-
// @ts-ignore
|
| 5 |
-
import Help from "~icons/tabler/question-mark.jsx";
|
| 6 |
-
import ParameterInput from "./ParameterInput";
|
| 7 |
-
|
| 8 |
-
type Bindings = {
|
| 9 |
-
[key: string]: {
|
| 10 |
-
df: string;
|
| 11 |
-
column: string;
|
| 12 |
-
};
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
type NamedId = {
|
| 16 |
-
name: string;
|
| 17 |
-
id: string;
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
function getModelBindings(
|
| 21 |
-
data: any,
|
| 22 |
-
variant: "training input" | "inference input" | "output",
|
| 23 |
-
): NamedId[] {
|
| 24 |
-
function bindingsOfModel(m: any): string[] {
|
| 25 |
-
switch (variant) {
|
| 26 |
-
case "training input":
|
| 27 |
-
return [
|
| 28 |
-
...m.model_inputs,
|
| 29 |
-
...m.loss_inputs.filter((i: string) => !m.model_outputs.includes(i)),
|
| 30 |
-
];
|
| 31 |
-
case "inference input":
|
| 32 |
-
return m.model_inputs;
|
| 33 |
-
case "output":
|
| 34 |
-
return m.model_outputs;
|
| 35 |
-
}
|
| 36 |
-
}
|
| 37 |
-
const bindings = new Set<NamedId>();
|
| 38 |
-
const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
|
| 39 |
-
for (const input of inputs) {
|
| 40 |
-
const other = input.other ?? {};
|
| 41 |
-
for (const e of Object.values(other) as any[]) {
|
| 42 |
-
if (e.type === "model") {
|
| 43 |
-
for (const id of bindingsOfModel(e.model)) {
|
| 44 |
-
bindings.add({ id, name: e.model.input_output_names[id] ?? id });
|
| 45 |
-
}
|
| 46 |
-
}
|
| 47 |
-
}
|
| 48 |
-
}
|
| 49 |
-
const list = [...bindings];
|
| 50 |
-
list.sort((a, b) => {
|
| 51 |
-
if (a.name < b.name) return -1;
|
| 52 |
-
if (a.name > b.name) return 1;
|
| 53 |
-
return 0;
|
| 54 |
-
});
|
| 55 |
-
return list;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
function parseJsonOrEmpty(json: string): object {
|
| 59 |
-
try {
|
| 60 |
-
const j = JSON.parse(json);
|
| 61 |
-
if (j !== null && typeof j === "object") {
|
| 62 |
-
return j;
|
| 63 |
-
}
|
| 64 |
-
} catch (e) {}
|
| 65 |
-
return {};
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
export default function ModelMapping({ value, onChange, data, variant }: any) {
|
| 69 |
-
const dfsRef = useRef({} as { [binding: string]: HTMLSelectElement | null });
|
| 70 |
-
const columnsRef = useRef(
|
| 71 |
-
{} as { [binding: string]: HTMLSelectElement | HTMLInputElement | null },
|
| 72 |
-
);
|
| 73 |
-
const v: any = parseJsonOrEmpty(value);
|
| 74 |
-
v.map ??= {};
|
| 75 |
-
const dfs: { [df: string]: string[] } = {};
|
| 76 |
-
const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
|
| 77 |
-
for (const input of inputs) {
|
| 78 |
-
if (!input.dataframes) continue;
|
| 79 |
-
const dataframes = input.dataframes as {
|
| 80 |
-
[df: string]: { columns: string[] };
|
| 81 |
-
};
|
| 82 |
-
for (const [df, { columns }] of Object.entries(dataframes)) {
|
| 83 |
-
dfs[df] = columns;
|
| 84 |
-
}
|
| 85 |
-
}
|
| 86 |
-
const bindings = getModelBindings(data, variant);
|
| 87 |
-
function getMap() {
|
| 88 |
-
const map: Bindings = {};
|
| 89 |
-
for (const binding of bindings) {
|
| 90 |
-
const df = dfsRef.current[binding.id]?.value ?? "";
|
| 91 |
-
const column = columnsRef.current[binding.id]?.value ?? "";
|
| 92 |
-
if (df.length || column.length) {
|
| 93 |
-
map[binding.id] = { df, column };
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
return map;
|
| 97 |
-
}
|
| 98 |
-
return (
|
| 99 |
-
<table className="model-mapping-param">
|
| 100 |
-
<tbody>
|
| 101 |
-
{bindings.length > 0 ? (
|
| 102 |
-
bindings.map((binding: NamedId) => (
|
| 103 |
-
<tr key={binding.id}>
|
| 104 |
-
<td>{binding.name}</td>
|
| 105 |
-
<td>
|
| 106 |
-
<ArrowsHorizontal />
|
| 107 |
-
</td>
|
| 108 |
-
<td>
|
| 109 |
-
<select
|
| 110 |
-
className="select select-ghost"
|
| 111 |
-
value={v.map?.[binding.id]?.df}
|
| 112 |
-
ref={(el) => {
|
| 113 |
-
dfsRef.current[binding.id] = el;
|
| 114 |
-
}}
|
| 115 |
-
onChange={() => onChange(JSON.stringify({ map: getMap() }))}
|
| 116 |
-
>
|
| 117 |
-
<option key="" value="" />
|
| 118 |
-
{Object.keys(dfs).map((df: string) => (
|
| 119 |
-
<option key={df} value={df}>
|
| 120 |
-
{df}
|
| 121 |
-
</option>
|
| 122 |
-
))}
|
| 123 |
-
</select>
|
| 124 |
-
</td>
|
| 125 |
-
<td>
|
| 126 |
-
{variant === "output" ? (
|
| 127 |
-
<ParameterInput
|
| 128 |
-
inputRef={(el) => {
|
| 129 |
-
columnsRef.current[binding.id] = el;
|
| 130 |
-
}}
|
| 131 |
-
value={v.map?.[binding.id]?.column}
|
| 132 |
-
onChange={(column, options) => {
|
| 133 |
-
const map = getMap();
|
| 134 |
-
// At this point the <input> has not been updated yet. We use the value from the event.
|
| 135 |
-
const df = dfsRef.current[binding.id]?.value ?? "";
|
| 136 |
-
map[binding.id] ??= { df, column };
|
| 137 |
-
map[binding.id].column = column;
|
| 138 |
-
onChange(JSON.stringify({ map }), options);
|
| 139 |
-
}}
|
| 140 |
-
/>
|
| 141 |
-
) : (
|
| 142 |
-
<select
|
| 143 |
-
className="select select-ghost"
|
| 144 |
-
value={v.map?.[binding.id]?.column}
|
| 145 |
-
ref={(el) => {
|
| 146 |
-
columnsRef.current[binding.id] = el;
|
| 147 |
-
}}
|
| 148 |
-
onChange={() => onChange(JSON.stringify({ map: getMap() }))}
|
| 149 |
-
>
|
| 150 |
-
<option key="" value="" />
|
| 151 |
-
{dfs[v.map?.[binding.id]?.df]?.map((col: string) => (
|
| 152 |
-
<option key={col} value={col}>
|
| 153 |
-
{col}
|
| 154 |
-
</option>
|
| 155 |
-
))}
|
| 156 |
-
</select>
|
| 157 |
-
)}
|
| 158 |
-
</td>
|
| 159 |
-
</tr>
|
| 160 |
-
))
|
| 161 |
-
) : (
|
| 162 |
-
<tr>
|
| 163 |
-
<td>no bindings</td>
|
| 164 |
-
</tr>
|
| 165 |
-
)}
|
| 166 |
-
</tbody>
|
| 167 |
-
</table>
|
| 168 |
-
);
|
| 169 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
import NodeParameter, { type UpdateOptions } from "./NodeParameter";
|
| 2 |
-
|
| 3 |
-
interface SelectorType {
|
| 4 |
-
name: string;
|
| 5 |
-
default: string;
|
| 6 |
-
type: {
|
| 7 |
-
enum: string[];
|
| 8 |
-
};
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
interface ParameterType {
|
| 12 |
-
name: string;
|
| 13 |
-
default: string;
|
| 14 |
-
type: {
|
| 15 |
-
type: string;
|
| 16 |
-
};
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
interface GroupsType {
|
| 20 |
-
[key: string]: ParameterType[];
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
interface NodeGroupParameterProps {
|
| 24 |
-
meta: { selector: SelectorType; groups: GroupsType };
|
| 25 |
-
data: any;
|
| 26 |
-
setParam: (name: string, value: any, options: UpdateOptions) => void;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export default function NodeGroupParameter({ meta, data, setParam }: NodeGroupParameterProps) {
|
| 30 |
-
const selector = meta.selector;
|
| 31 |
-
const selectorValue = data.params[selector.name] || selector.default;
|
| 32 |
-
const group = meta.groups[selectorValue] || [];
|
| 33 |
-
|
| 34 |
-
return (
|
| 35 |
-
<>
|
| 36 |
-
{group.map((meta: any) => (
|
| 37 |
-
<NodeParameter
|
| 38 |
-
name={meta.name}
|
| 39 |
-
key={meta.name}
|
| 40 |
-
value={data.params[meta.name] ?? meta.default}
|
| 41 |
-
data={data}
|
| 42 |
-
meta={meta}
|
| 43 |
-
setParam={setParam}
|
| 44 |
-
/>
|
| 45 |
-
))}
|
| 46 |
-
</>
|
| 47 |
-
);
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
import jmespath from "jmespath";
|
| 2 |
-
// @ts-ignore
|
| 3 |
-
import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
|
| 4 |
-
// @ts-ignore
|
| 5 |
-
import Help from "~icons/tabler/question-mark.jsx";
|
| 6 |
-
import Tooltip from "../../Tooltip";
|
| 7 |
-
import ModelMapping from "./ModelMappingParameter";
|
| 8 |
-
import NodeGroupParameter from "./NodeGroupParameter";
|
| 9 |
-
import ParameterInput from "./ParameterInput";
|
| 10 |
-
|
| 11 |
-
const BOOLEAN = "<class 'bool'>";
|
| 12 |
-
const MODEL_TRAINING_INPUT_MAPPING =
|
| 13 |
-
"<class 'lynxkite_graph_analytics.ml_ops.ModelTrainingInputMapping'>";
|
| 14 |
-
const MODEL_INFERENCE_INPUT_MAPPING =
|
| 15 |
-
"<class 'lynxkite_graph_analytics.ml_ops.ModelInferenceInputMapping'>";
|
| 16 |
-
const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.ml_ops.ModelOutputMapping'>";
|
| 17 |
-
|
| 18 |
-
function ParamName({ name, doc }: { name: string; doc: string }) {
|
| 19 |
-
const help = doc && (
|
| 20 |
-
<Tooltip doc={doc} width={200}>
|
| 21 |
-
<Help />
|
| 22 |
-
</Tooltip>
|
| 23 |
-
);
|
| 24 |
-
return (
|
| 25 |
-
<div className="param-name-row">
|
| 26 |
-
<span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
|
| 27 |
-
{help}
|
| 28 |
-
</div>
|
| 29 |
-
);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
interface NodeParameterProps {
|
| 33 |
-
name: string;
|
| 34 |
-
value: any;
|
| 35 |
-
meta: any;
|
| 36 |
-
data: any;
|
| 37 |
-
setParam: (name: string, value: any, options: UpdateOptions) => void;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
export type UpdateOptions = { delay?: number };
|
| 41 |
-
|
| 42 |
-
function findDocs(docs: any, parameter: string) {
|
| 43 |
-
for (const sec of docs) {
|
| 44 |
-
if (sec.kind === "parameters") {
|
| 45 |
-
for (const p of sec.value) {
|
| 46 |
-
if (p.name === parameter) {
|
| 47 |
-
return p.description;
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
export default function NodeParameter({ name, value, meta, data, setParam }: NodeParameterProps) {
|
| 55 |
-
const doc = findDocs(data.meta?.value?.doc ?? [], name);
|
| 56 |
-
function onChange(value: any, opts?: UpdateOptions) {
|
| 57 |
-
setParam(meta.name, value, opts || {});
|
| 58 |
-
}
|
| 59 |
-
return meta?.type?.format === "textarea" ? (
|
| 60 |
-
<label className="param">
|
| 61 |
-
<ParamName name={name} doc={doc} />
|
| 62 |
-
<textarea
|
| 63 |
-
className="textarea textarea-bordered w-full"
|
| 64 |
-
rows={(value ?? "").split("\n").length}
|
| 65 |
-
value={value ?? ""}
|
| 66 |
-
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
| 67 |
-
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
| 68 |
-
/>
|
| 69 |
-
</label>
|
| 70 |
-
) : meta?.type?.format === "dropdown" ? (
|
| 71 |
-
<label className="param">
|
| 72 |
-
<ParamName name={name} doc={doc} />
|
| 73 |
-
<select
|
| 74 |
-
className="select select-bordered w-full"
|
| 75 |
-
value={value ?? ""}
|
| 76 |
-
onChange={(evt) => onChange(evt.currentTarget.value)}
|
| 77 |
-
>
|
| 78 |
-
{getDropDownValues(data, meta).map((option: string) => (
|
| 79 |
-
<option key={option} value={option}>
|
| 80 |
-
{option}
|
| 81 |
-
</option>
|
| 82 |
-
))}
|
| 83 |
-
</select>
|
| 84 |
-
</label>
|
| 85 |
-
) : meta?.type === "group" ? (
|
| 86 |
-
<NodeGroupParameter meta={meta} data={data} setParam={setParam} />
|
| 87 |
-
) : meta?.type?.enum ? (
|
| 88 |
-
<label className="param">
|
| 89 |
-
<ParamName name={name} doc={doc} />
|
| 90 |
-
<select
|
| 91 |
-
className="select select-bordered w-full"
|
| 92 |
-
value={value || meta.type.enum[0]}
|
| 93 |
-
onChange={(evt) => onChange(evt.currentTarget.value)}
|
| 94 |
-
>
|
| 95 |
-
{meta.type.enum.map((option: string) => (
|
| 96 |
-
<option key={option} value={option}>
|
| 97 |
-
{option}
|
| 98 |
-
</option>
|
| 99 |
-
))}
|
| 100 |
-
</select>
|
| 101 |
-
</label>
|
| 102 |
-
) : meta?.type?.type === BOOLEAN ? (
|
| 103 |
-
<div className="form-control">
|
| 104 |
-
<label className="label cursor-pointer">
|
| 105 |
-
{name.replace(/_/g, " ")}
|
| 106 |
-
<input
|
| 107 |
-
className="checkbox"
|
| 108 |
-
type="checkbox"
|
| 109 |
-
checked={value}
|
| 110 |
-
onChange={(evt) => onChange(evt.currentTarget.checked)}
|
| 111 |
-
/>
|
| 112 |
-
</label>
|
| 113 |
-
</div>
|
| 114 |
-
) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
|
| 115 |
-
<label className="param">
|
| 116 |
-
<ParamName name={name} doc={doc} />
|
| 117 |
-
<ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
|
| 118 |
-
</label>
|
| 119 |
-
) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
|
| 120 |
-
<label className="param">
|
| 121 |
-
<ParamName name={name} doc={doc} />
|
| 122 |
-
<ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
|
| 123 |
-
</label>
|
| 124 |
-
) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
|
| 125 |
-
<label className="param">
|
| 126 |
-
<ParamName name={name} doc={doc} />
|
| 127 |
-
<ModelMapping value={value} data={data} variant="output" onChange={onChange} />
|
| 128 |
-
</label>
|
| 129 |
-
) : (
|
| 130 |
-
<label className="param">
|
| 131 |
-
<ParamName name={name} doc={doc} />
|
| 132 |
-
<ParameterInput value={value} onChange={onChange} />
|
| 133 |
-
</label>
|
| 134 |
-
);
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
function getDropDownValues(data: any, meta: any): string[] {
|
| 138 |
-
const metadata = data.input_metadata.value;
|
| 139 |
-
let query = meta.type.metadata_query;
|
| 140 |
-
// Substitute parameters in the query.
|
| 141 |
-
for (const p in data.params) {
|
| 142 |
-
query = query.replace(`<${p}>`, data.params[p]);
|
| 143 |
-
}
|
| 144 |
-
const res = ["", ...jmespath.search(metadata, query)];
|
| 145 |
-
res.sort();
|
| 146 |
-
return res;
|
| 147 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithComment.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
import { useReactFlow } from "@xyflow/react";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
import Markdown from "react-markdown";
|
| 4 |
-
import type { UpdateOptions } from "./NodeParameter";
|
| 5 |
-
|
| 6 |
-
export default function NodeWithComment(props: any) {
|
| 7 |
-
const reactFlow = useReactFlow();
|
| 8 |
-
const [editing, setEditing] = useState(false);
|
| 9 |
-
function setComment(newValue: string, opts?: UpdateOptions) {
|
| 10 |
-
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
| 11 |
-
...prevData,
|
| 12 |
-
params: { text: newValue },
|
| 13 |
-
__execution_delay: opts?.delay || 0,
|
| 14 |
-
}));
|
| 15 |
-
}
|
| 16 |
-
function onClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
| 17 |
-
// Start editing on double-click.
|
| 18 |
-
if (e.detail === 2) {
|
| 19 |
-
setEditing(true);
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
function finishEditing(el: HTMLTextAreaElement) {
|
| 23 |
-
setComment(el.value);
|
| 24 |
-
setEditing(false);
|
| 25 |
-
}
|
| 26 |
-
function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
| 27 |
-
if (e.key === "Escape") {
|
| 28 |
-
finishEditing(e.currentTarget);
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
function onInput(el: HTMLTextAreaElement | null) {
|
| 32 |
-
if (!el) return;
|
| 33 |
-
el.focus();
|
| 34 |
-
// Resize the textarea to the content.
|
| 35 |
-
el.style.height = "auto";
|
| 36 |
-
el.style.height = `${el.scrollHeight}px`;
|
| 37 |
-
}
|
| 38 |
-
if (editing) {
|
| 39 |
-
return (
|
| 40 |
-
<textarea
|
| 41 |
-
className="comment-editor"
|
| 42 |
-
onBlur={(e) => finishEditing(e.currentTarget)}
|
| 43 |
-
onKeyDown={onKeyDown}
|
| 44 |
-
onInput={(e) => onInput(e.currentTarget)}
|
| 45 |
-
ref={(el) => onInput(el)}
|
| 46 |
-
defaultValue={props.data.params.text}
|
| 47 |
-
onClick={(e) => e.stopPropagation()}
|
| 48 |
-
placeholder="Enter workspace comment"
|
| 49 |
-
/>
|
| 50 |
-
);
|
| 51 |
-
}
|
| 52 |
-
const text = props.data.params.text || "_double-click to edit_";
|
| 53 |
-
return (
|
| 54 |
-
<div className="comment-view drag-handle prose" onClick={onClick}>
|
| 55 |
-
<Markdown>{text}</Markdown>
|
| 56 |
-
</div>
|
| 57 |
-
);
|
| 58 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 2 |
-
import { NodeWithParams } from "./NodeWithParams";
|
| 3 |
-
|
| 4 |
-
const NodeWithImage = (props: any) => {
|
| 5 |
-
return (
|
| 6 |
-
<NodeWithParams collapsed {...props}>
|
| 7 |
-
{props.data.display && <img src={props.data.display} alt="Node Display" />}
|
| 8 |
-
</NodeWithParams>
|
| 9 |
-
);
|
| 10 |
-
};
|
| 11 |
-
|
| 12 |
-
export default LynxKiteNode(NodeWithImage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithMolecule.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
import React, { useEffect, type CSSProperties } from "react";
|
| 2 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 3 |
-
import { NodeWithParams } from "./NodeWithParams";
|
| 4 |
-
|
| 5 |
-
const NodeWithMolecule = (props: any) => {
|
| 6 |
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
| 7 |
-
const viewerRef = React.useRef<any>(null);
|
| 8 |
-
|
| 9 |
-
useEffect(() => {
|
| 10 |
-
const config = props.data?.display?.value;
|
| 11 |
-
if (!config || !containerRef.current) return;
|
| 12 |
-
async function run() {
|
| 13 |
-
const $3Dmol = await import("3dmol");
|
| 14 |
-
|
| 15 |
-
try {
|
| 16 |
-
// Initialize viewer only once
|
| 17 |
-
if (!viewerRef.current) {
|
| 18 |
-
viewerRef.current = $3Dmol.createViewer(containerRef.current, {
|
| 19 |
-
backgroundColor: "white",
|
| 20 |
-
});
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const viewer = viewerRef.current;
|
| 24 |
-
|
| 25 |
-
// Clear previous models
|
| 26 |
-
viewer.clear();
|
| 27 |
-
|
| 28 |
-
// Add new model and style it
|
| 29 |
-
viewer.addModel(config.data, config.format);
|
| 30 |
-
viewer.setStyle({}, { stick: {} });
|
| 31 |
-
viewer.zoomTo();
|
| 32 |
-
viewer.render();
|
| 33 |
-
} catch (error) {
|
| 34 |
-
console.error("Error rendering 3D molecule:", error);
|
| 35 |
-
}
|
| 36 |
-
}
|
| 37 |
-
run();
|
| 38 |
-
const resizeObserver = new ResizeObserver(() => {
|
| 39 |
-
viewerRef.current?.resize();
|
| 40 |
-
});
|
| 41 |
-
|
| 42 |
-
const observed = containerRef.current;
|
| 43 |
-
resizeObserver.observe(observed);
|
| 44 |
-
return () => {
|
| 45 |
-
resizeObserver.unobserve(observed);
|
| 46 |
-
if (viewerRef.current) {
|
| 47 |
-
viewerRef.current.clear();
|
| 48 |
-
}
|
| 49 |
-
};
|
| 50 |
-
}, [props.data?.display?.value]);
|
| 51 |
-
|
| 52 |
-
const vizStyle: CSSProperties = {
|
| 53 |
-
flex: 1,
|
| 54 |
-
minHeight: "300px",
|
| 55 |
-
border: "1px solid #ddd",
|
| 56 |
-
borderRadius: "4px",
|
| 57 |
-
overflow: "hidden",
|
| 58 |
-
position: "relative",
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
return (
|
| 62 |
-
<NodeWithParams collapsed {...props}>
|
| 63 |
-
<div style={vizStyle} ref={containerRef} />
|
| 64 |
-
</NodeWithParams>
|
| 65 |
-
);
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
export default LynxKiteNode(NodeWithMolecule);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
import { useReactFlow } from "@xyflow/react";
|
| 2 |
-
import React from "react";
|
| 3 |
-
// @ts-ignore
|
| 4 |
-
import Triangle from "~icons/tabler/triangle-inverted-filled.jsx";
|
| 5 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 6 |
-
import NodeParameter, { type UpdateOptions } from "./NodeParameter";
|
| 7 |
-
|
| 8 |
-
export function NodeWithParams(props: any) {
|
| 9 |
-
const reactFlow = useReactFlow();
|
| 10 |
-
const metaParams = props.data.meta?.value?.params ?? [];
|
| 11 |
-
const [collapsed, setCollapsed] = React.useState(props.collapsed);
|
| 12 |
-
|
| 13 |
-
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
| 14 |
-
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
| 15 |
-
...prevData,
|
| 16 |
-
params: { ...prevData.data.params, [name]: newValue },
|
| 17 |
-
__execution_delay: opts.delay || 0,
|
| 18 |
-
}));
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
return (
|
| 22 |
-
<>
|
| 23 |
-
{props.collapsed && metaParams.length > 0 && (
|
| 24 |
-
<div className="params-expander" onClick={() => setCollapsed(!collapsed)}>
|
| 25 |
-
<Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
|
| 26 |
-
</div>
|
| 27 |
-
)}
|
| 28 |
-
{!collapsed &&
|
| 29 |
-
metaParams.map((meta: any) => (
|
| 30 |
-
<NodeParameter
|
| 31 |
-
name={meta.name}
|
| 32 |
-
key={meta.name}
|
| 33 |
-
value={props.data.params[meta.name]}
|
| 34 |
-
data={props.data}
|
| 35 |
-
meta={meta}
|
| 36 |
-
setParam={setParam}
|
| 37 |
-
/>
|
| 38 |
-
))}
|
| 39 |
-
{props.children}
|
| 40 |
-
</>
|
| 41 |
-
);
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
export default LynxKiteNode(NodeWithParams);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
import { useReactFlow } from "@xyflow/react";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
import React from "react";
|
| 4 |
-
import Markdown from "react-markdown";
|
| 5 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 6 |
-
import Table from "./Table";
|
| 7 |
-
|
| 8 |
-
function toMD(v: any): string {
|
| 9 |
-
if (typeof v === "string") {
|
| 10 |
-
return v;
|
| 11 |
-
}
|
| 12 |
-
if (Array.isArray(v)) {
|
| 13 |
-
return v.map(toMD).join("\n\n");
|
| 14 |
-
}
|
| 15 |
-
return JSON.stringify(v);
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
type OpenState = { [name: string]: boolean };
|
| 19 |
-
|
| 20 |
-
function NodeWithTableView(props: any) {
|
| 21 |
-
const reactFlow = useReactFlow();
|
| 22 |
-
const [open, setOpen] = useState((props.data?.params?._tables_open ?? {}) as OpenState);
|
| 23 |
-
const display = props.data.display?.value;
|
| 24 |
-
const single = display?.dataframes && Object.keys(display?.dataframes).length === 1;
|
| 25 |
-
const dfs = Object.entries(display?.dataframes || {});
|
| 26 |
-
dfs.sort();
|
| 27 |
-
function setParam(name: string, newValue: any) {
|
| 28 |
-
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
| 29 |
-
...prevData,
|
| 30 |
-
params: { ...prevData.data.params, [name]: newValue },
|
| 31 |
-
}));
|
| 32 |
-
}
|
| 33 |
-
function toggleTable(name: string) {
|
| 34 |
-
setOpen((prevOpen: OpenState) => {
|
| 35 |
-
const newOpen = { ...prevOpen, [name]: !prevOpen[name] };
|
| 36 |
-
setParam("_tables_open", newOpen);
|
| 37 |
-
return newOpen;
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
return (
|
| 41 |
-
<>
|
| 42 |
-
{display && [
|
| 43 |
-
dfs.map(([name, df]: [string, any]) => (
|
| 44 |
-
<React.Fragment key={name}>
|
| 45 |
-
{!single && (
|
| 46 |
-
<div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
|
| 47 |
-
{name}
|
| 48 |
-
</div>
|
| 49 |
-
)}
|
| 50 |
-
{(single || open[name]) &&
|
| 51 |
-
(df.data.length > 1 ? (
|
| 52 |
-
<Table key={`${name}-table`} columns={df.columns} data={df.data} />
|
| 53 |
-
) : df.data.length ? (
|
| 54 |
-
<dl key={`${name}-dl`}>
|
| 55 |
-
{df.columns.map((c: string, i: number) => (
|
| 56 |
-
<React.Fragment key={`${name}-${c}`}>
|
| 57 |
-
{df.columns.length > 1 && <dt>{c}</dt>}
|
| 58 |
-
<dd className="prose">
|
| 59 |
-
<Markdown>{toMD(df.data[0][i])}</Markdown>
|
| 60 |
-
</dd>
|
| 61 |
-
</React.Fragment>
|
| 62 |
-
))}
|
| 63 |
-
</dl>
|
| 64 |
-
) : (
|
| 65 |
-
JSON.stringify(df.data)
|
| 66 |
-
))}
|
| 67 |
-
</React.Fragment>
|
| 68 |
-
)),
|
| 69 |
-
Object.entries(display.others || {}).map(([name, o]) => (
|
| 70 |
-
<>
|
| 71 |
-
<div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
|
| 72 |
-
{name}
|
| 73 |
-
</div>
|
| 74 |
-
{open[name] && <pre>{(o as any).toString()}</pre>}
|
| 75 |
-
</>
|
| 76 |
-
)),
|
| 77 |
-
]}
|
| 78 |
-
</>
|
| 79 |
-
);
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
export default LynxKiteNode(NodeWithTableView);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
import React, { useEffect } from "react";
|
| 2 |
-
import LynxKiteNode from "./LynxKiteNode";
|
| 3 |
-
import { NodeWithParams } from "./NodeWithParams";
|
| 4 |
-
const echarts = await import("echarts");
|
| 5 |
-
|
| 6 |
-
function NodeWithVisualization(props: any) {
|
| 7 |
-
const chartsRef = React.useRef<HTMLDivElement>(null);
|
| 8 |
-
const chartsInstanceRef = React.useRef<echarts.ECharts>();
|
| 9 |
-
useEffect(() => {
|
| 10 |
-
const opts = props.data?.display?.value;
|
| 11 |
-
if (!opts || !chartsRef.current) return;
|
| 12 |
-
if (opts.tooltip?.formatter === "GET_THIRD_VALUE") {
|
| 13 |
-
// We can't pass a function from the backend, and can't get good tooltips otherwise.
|
| 14 |
-
opts.tooltip.formatter = (params: any) => params.value[2];
|
| 15 |
-
}
|
| 16 |
-
chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
|
| 17 |
-
renderer: "canvas",
|
| 18 |
-
width: "auto",
|
| 19 |
-
height: "auto",
|
| 20 |
-
});
|
| 21 |
-
chartsInstanceRef.current.setOption(opts);
|
| 22 |
-
const resizeObserver = new ResizeObserver(() => {
|
| 23 |
-
const e = chartsRef.current;
|
| 24 |
-
if (!e) return;
|
| 25 |
-
e.style.padding = "1px";
|
| 26 |
-
chartsInstanceRef.current?.resize();
|
| 27 |
-
e.style.padding = "0";
|
| 28 |
-
});
|
| 29 |
-
const observed = chartsRef.current;
|
| 30 |
-
resizeObserver.observe(observed);
|
| 31 |
-
return () => {
|
| 32 |
-
resizeObserver.unobserve(observed);
|
| 33 |
-
chartsInstanceRef.current?.dispose();
|
| 34 |
-
};
|
| 35 |
-
}, [props.data?.display?.value]);
|
| 36 |
-
return (
|
| 37 |
-
<NodeWithParams collapsed {...props}>
|
| 38 |
-
<div style={{ flex: 1 }} ref={chartsRef} />
|
| 39 |
-
</NodeWithParams>
|
| 40 |
-
);
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
export default LynxKiteNode(NodeWithVisualization);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|