Spaces:
Running
Running
Some nodes are now visible.
Browse files- server/crdt.py +44 -21
- web/app/globals.css +86 -17
- web/app/layout.tsx +3 -15
- web/app/workspace/EnvironmentSelector.tsx +11 -8
- web/app/workspace/page.tsx +4 -221
- web/package-lock.json +105 -0
- web/tailwind.config.js +15 -1
- web/tsconfig.json +1 -0
server/crdt.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
-
|
|
|
2 |
import asyncio
|
3 |
import contextlib
|
|
|
4 |
import fastapi
|
5 |
import os.path
|
6 |
import pycrdt
|
@@ -10,33 +12,39 @@ import pycrdt_websocket.ystore
|
|
10 |
|
11 |
router = fastapi.APIRouter()
|
12 |
|
|
|
13 |
def ws_exception_handler(exception, log):
|
14 |
-
print(
|
15 |
log.exception(exception)
|
16 |
return True
|
17 |
|
|
|
18 |
class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
19 |
async def init_room(self, name):
|
20 |
-
ystore = pycrdt_websocket.ystore.FileYStore(f
|
21 |
ydoc = pycrdt.Doc()
|
22 |
-
ydoc[
|
23 |
# Replay updates from the store.
|
24 |
try:
|
25 |
-
for update, timestamp in [
|
|
|
|
|
26 |
ydoc.apply_update(update)
|
27 |
except pycrdt_websocket.ystore.YDocNotFound:
|
28 |
pass
|
29 |
-
if
|
30 |
-
ws[
|
31 |
-
if
|
32 |
-
ws[
|
33 |
-
if
|
34 |
-
ws[
|
35 |
try_to_load_workspace(ws, name)
|
36 |
room = pycrdt_websocket.YRoom(ystore=ystore, ydoc=ydoc)
|
37 |
room.ws = ws
|
|
|
38 |
def on_change(changes):
|
39 |
asyncio.create_task(workspace_changed(changes, ws))
|
|
|
40 |
ws.observe_deep(on_change)
|
41 |
return room
|
42 |
|
@@ -47,10 +55,16 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
47 |
await self.start_room(room)
|
48 |
return room
|
49 |
|
50 |
-
|
|
|
|
|
|
|
|
|
51 |
asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
|
52 |
|
53 |
last_ws_input = None
|
|
|
|
|
54 |
def clean_input(ws_pyd):
|
55 |
for node in ws_pyd.nodes:
|
56 |
node.data.display = None
|
@@ -60,6 +74,7 @@ def clean_input(ws_pyd):
|
|
60 |
for key in list(node.model_extra.keys()):
|
61 |
delattr(node, key)
|
62 |
|
|
|
63 |
def crdt_update(crdt_obj, python_obj, boxes=set()):
|
64 |
if isinstance(python_obj, dict):
|
65 |
for key, value in python_obj.items():
|
@@ -73,6 +88,8 @@ def crdt_update(crdt_obj, python_obj, boxes=set()):
|
|
73 |
if crdt_obj.get(key) is None:
|
74 |
crdt_obj[key] = pycrdt.Array()
|
75 |
crdt_update(crdt_obj[key], value, boxes)
|
|
|
|
|
76 |
else:
|
77 |
crdt_obj[key] = value
|
78 |
elif isinstance(python_obj, list):
|
@@ -91,41 +108,47 @@ def crdt_update(crdt_obj, python_obj, boxes=set()):
|
|
91 |
else:
|
92 |
crdt_obj[i] = value
|
93 |
else:
|
94 |
-
raise ValueError(
|
95 |
|
96 |
|
97 |
def try_to_load_workspace(ws, name):
|
98 |
from . import workspace
|
99 |
-
|
|
|
100 |
if os.path.exists(json_path):
|
101 |
ws_pyd = workspace.load(json_path)
|
102 |
-
crdt_update(ws, ws_pyd.model_dump(), boxes={
|
|
|
103 |
|
104 |
async def workspace_changed(e, ws_crdt):
|
105 |
global last_ws_input
|
106 |
from . import workspace
|
|
|
107 |
ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
|
108 |
clean_input(ws_pyd)
|
109 |
if ws_pyd == last_ws_input:
|
110 |
return
|
111 |
last_ws_input = ws_pyd.model_copy(deep=True)
|
112 |
await workspace.execute(ws_pyd)
|
113 |
-
for nc, np in zip(ws_crdt[
|
114 |
-
if
|
115 |
-
nc[
|
116 |
# Display is added as an opaque Box.
|
117 |
-
nc[
|
118 |
-
nc[
|
|
|
119 |
|
120 |
@contextlib.asynccontextmanager
|
121 |
async def lifespan(app):
|
122 |
async with websocket_server:
|
123 |
yield
|
124 |
|
|
|
125 |
def sanitize_path(path):
|
126 |
return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
|
127 |
|
|
|
128 |
@router.websocket("/ws/crdt/{room_name}")
|
129 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
130 |
room_name = sanitize_path(room_name)
|
131 |
-
await asgi_server({
|
|
|
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 fastapi
|
7 |
import os.path
|
8 |
import pycrdt
|
|
|
12 |
|
13 |
router = fastapi.APIRouter()
|
14 |
|
15 |
+
|
16 |
def ws_exception_handler(exception, log):
|
17 |
+
print("exception", exception)
|
18 |
log.exception(exception)
|
19 |
return True
|
20 |
|
21 |
+
|
22 |
class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
23 |
async def init_room(self, name):
|
24 |
+
ystore = pycrdt_websocket.ystore.FileYStore(f"crdt_data/{name}.crdt")
|
25 |
ydoc = pycrdt.Doc()
|
26 |
+
ydoc["workspace"] = ws = pycrdt.Map()
|
27 |
# Replay updates from the store.
|
28 |
try:
|
29 |
+
for update, timestamp in [
|
30 |
+
(item[0], item[-1]) async for item in ystore.read()
|
31 |
+
]:
|
32 |
ydoc.apply_update(update)
|
33 |
except pycrdt_websocket.ystore.YDocNotFound:
|
34 |
pass
|
35 |
+
if "nodes" not in ws:
|
36 |
+
ws["nodes"] = pycrdt.Array()
|
37 |
+
if "edges" not in ws:
|
38 |
+
ws["edges"] = pycrdt.Array()
|
39 |
+
if "env" not in ws:
|
40 |
+
ws["env"] = "unset"
|
41 |
try_to_load_workspace(ws, name)
|
42 |
room = pycrdt_websocket.YRoom(ystore=ystore, ydoc=ydoc)
|
43 |
room.ws = ws
|
44 |
+
|
45 |
def on_change(changes):
|
46 |
asyncio.create_task(workspace_changed(changes, ws))
|
47 |
+
|
48 |
ws.observe_deep(on_change)
|
49 |
return room
|
50 |
|
|
|
55 |
await self.start_room(room)
|
56 |
return room
|
57 |
|
58 |
+
|
59 |
+
websocket_server = WebsocketServer(
|
60 |
+
# exception_handler=ws_exception_handler,
|
61 |
+
auto_clean_rooms=False,
|
62 |
+
)
|
63 |
asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
|
64 |
|
65 |
last_ws_input = None
|
66 |
+
|
67 |
+
|
68 |
def clean_input(ws_pyd):
|
69 |
for node in ws_pyd.nodes:
|
70 |
node.data.display = None
|
|
|
74 |
for key in list(node.model_extra.keys()):
|
75 |
delattr(node, key)
|
76 |
|
77 |
+
|
78 |
def crdt_update(crdt_obj, python_obj, boxes=set()):
|
79 |
if isinstance(python_obj, dict):
|
80 |
for key, value in python_obj.items():
|
|
|
88 |
if crdt_obj.get(key) is None:
|
89 |
crdt_obj[key] = pycrdt.Array()
|
90 |
crdt_update(crdt_obj[key], value, boxes)
|
91 |
+
elif isinstance(value, enum.Enum):
|
92 |
+
crdt_obj[key] = str(value)
|
93 |
else:
|
94 |
crdt_obj[key] = value
|
95 |
elif isinstance(python_obj, list):
|
|
|
108 |
else:
|
109 |
crdt_obj[i] = value
|
110 |
else:
|
111 |
+
raise ValueError("Invalid type:", python_obj)
|
112 |
|
113 |
|
114 |
def try_to_load_workspace(ws, name):
|
115 |
from . import workspace
|
116 |
+
|
117 |
+
json_path = f"data/{name}"
|
118 |
if os.path.exists(json_path):
|
119 |
ws_pyd = workspace.load(json_path)
|
120 |
+
crdt_update(ws, ws_pyd.model_dump(), boxes={"display"})
|
121 |
+
|
122 |
|
123 |
async def workspace_changed(e, ws_crdt):
|
124 |
global last_ws_input
|
125 |
from . import workspace
|
126 |
+
|
127 |
ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
|
128 |
clean_input(ws_pyd)
|
129 |
if ws_pyd == last_ws_input:
|
130 |
return
|
131 |
last_ws_input = ws_pyd.model_copy(deep=True)
|
132 |
await workspace.execute(ws_pyd)
|
133 |
+
for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
|
134 |
+
if "data" not in nc:
|
135 |
+
nc["data"] = pycrdt.Map()
|
136 |
# Display is added as an opaque Box.
|
137 |
+
nc["data"]["display"] = np.data.display
|
138 |
+
nc["data"]["error"] = np.data.error
|
139 |
+
|
140 |
|
141 |
@contextlib.asynccontextmanager
|
142 |
async def lifespan(app):
|
143 |
async with websocket_server:
|
144 |
yield
|
145 |
|
146 |
+
|
147 |
def sanitize_path(path):
|
148 |
return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
|
149 |
|
150 |
+
|
151 |
@router.websocket("/ws/crdt/{room_name}")
|
152 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
153 |
room_name = sanitize_path(room_name)
|
154 |
+
await asgi_server({"path": room_name}, websocket._receive, websocket._send)
|
web/app/globals.css
CHANGED
@@ -2,18 +2,6 @@
|
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
4 |
|
5 |
-
:root {
|
6 |
-
--background: #ffffff;
|
7 |
-
--foreground: #171717;
|
8 |
-
}
|
9 |
-
|
10 |
-
/* @media (prefers-color-scheme: dark) {
|
11 |
-
:root {
|
12 |
-
--background: #0a0a0a;
|
13 |
-
--foreground: #ededed;
|
14 |
-
}
|
15 |
-
} */
|
16 |
-
|
17 |
body {
|
18 |
color: var(--foreground);
|
19 |
background: var(--background);
|
@@ -23,14 +11,15 @@ body {
|
|
23 |
.top-bar {
|
24 |
display: flex;
|
25 |
justify-content: space-between;
|
26 |
-
|
27 |
-
color: white;
|
28 |
}
|
29 |
.ws-name {
|
30 |
font-size: 1.5em;
|
|
|
|
|
31 |
}
|
32 |
-
.
|
33 |
-
height:
|
34 |
vertical-align: middle;
|
35 |
margin: 4px;
|
36 |
}
|
@@ -45,7 +34,87 @@ body {
|
|
45 |
align-items: center;
|
46 |
}
|
47 |
.tools a {
|
48 |
-
color: oklch(75% 0.13 230);
|
49 |
font-size: 1.5em;
|
50 |
padding: 0 10px;
|
51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
body {
|
6 |
color: var(--foreground);
|
7 |
background: var(--background);
|
|
|
11 |
.top-bar {
|
12 |
display: flex;
|
13 |
justify-content: space-between;
|
14 |
+
align-items: center;
|
|
|
15 |
}
|
16 |
.ws-name {
|
17 |
font-size: 1.5em;
|
18 |
+
flex: 1;
|
19 |
+
color: white;
|
20 |
}
|
21 |
+
.top-bar .logo img {
|
22 |
+
height: 2em;
|
23 |
vertical-align: middle;
|
24 |
margin: 4px;
|
25 |
}
|
|
|
34 |
align-items: center;
|
35 |
}
|
36 |
.tools a {
|
|
|
37 |
font-size: 1.5em;
|
38 |
padding: 0 10px;
|
39 |
}
|
40 |
+
.error {
|
41 |
+
background: #ffdddd;
|
42 |
+
padding: 8px;
|
43 |
+
font-size: 12px;
|
44 |
+
}
|
45 |
+
.title-icon {
|
46 |
+
margin-left: 5px;
|
47 |
+
float: right;
|
48 |
+
}
|
49 |
+
.node-container {
|
50 |
+
padding: 8px;
|
51 |
+
position: relative;
|
52 |
+
}
|
53 |
+
.lynxkite-node {
|
54 |
+
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
55 |
+
border-radius: 4px;
|
56 |
+
background: white;
|
57 |
+
}
|
58 |
+
.expanded .lynxkite-node {
|
59 |
+
overflow-y: auto;
|
60 |
+
height: 100%;
|
61 |
+
}
|
62 |
+
.lynxkite-node .title {
|
63 |
+
/* background: oklch(75% 0.2 55); */
|
64 |
+
font-weight: bold;
|
65 |
+
padding: 8px;
|
66 |
+
}
|
67 |
+
.handle-name {
|
68 |
+
font-size: 10px;
|
69 |
+
color: black;
|
70 |
+
letter-spacing: 0.05em;
|
71 |
+
text-align: right;
|
72 |
+
white-space: nowrap;
|
73 |
+
position: absolute;
|
74 |
+
top: -5px;
|
75 |
+
backdrop-filter: blur(10px);
|
76 |
+
padding: 2px 8px;
|
77 |
+
border-radius: 4px;
|
78 |
+
visibility: hidden;
|
79 |
+
}
|
80 |
+
.left .handle-name {
|
81 |
+
right: 20px;
|
82 |
+
}
|
83 |
+
.right .handle-name {
|
84 |
+
left: 20px;
|
85 |
+
}
|
86 |
+
.top .handle-name,
|
87 |
+
.bottom .handle-name {
|
88 |
+
top: -5px;
|
89 |
+
left: 5px;
|
90 |
+
backdrop-filter: none;
|
91 |
+
}
|
92 |
+
.node-container:hover .handle-name {
|
93 |
+
visibility: visible;
|
94 |
+
}
|
95 |
+
.node-resizer {
|
96 |
+
position: absolute;
|
97 |
+
bottom: 8px;
|
98 |
+
right: 8px;
|
99 |
+
cursor: nwse-resize;
|
100 |
+
}
|
101 |
+
.lynxkite-node {
|
102 |
+
.param {
|
103 |
+
padding: 4px 8px 4px 8px;
|
104 |
+
display: block;
|
105 |
+
}
|
106 |
+
.param-name {
|
107 |
+
display: block;
|
108 |
+
font-size: 10px;
|
109 |
+
letter-spacing: 0.05em;
|
110 |
+
margin-left: 10px;
|
111 |
+
width: fit-content;
|
112 |
+
padding: 2px 8px;
|
113 |
+
border-radius: 4px 4px 0 0;
|
114 |
+
;
|
115 |
+
}
|
116 |
+
.collapsed-param {
|
117 |
+
min-height: 20px;
|
118 |
+
line-height: 10px;
|
119 |
+
}
|
120 |
+
}
|
web/app/layout.tsx
CHANGED
@@ -1,21 +1,9 @@
|
|
1 |
import type { Metadata } from "next";
|
2 |
-
import localFont from "next/font/local";
|
3 |
import "./globals.css";
|
4 |
|
5 |
-
const geistSans = localFont({
|
6 |
-
src: "./fonts/GeistVF.woff",
|
7 |
-
variable: "--font-geist-sans",
|
8 |
-
weight: "100 900",
|
9 |
-
});
|
10 |
-
const geistMono = localFont({
|
11 |
-
src: "./fonts/GeistMonoVF.woff",
|
12 |
-
variable: "--font-geist-mono",
|
13 |
-
weight: "100 900",
|
14 |
-
});
|
15 |
-
|
16 |
export const metadata: Metadata = {
|
17 |
-
title: "
|
18 |
-
description: "
|
19 |
};
|
20 |
|
21 |
export default function RootLayout({
|
@@ -26,7 +14,7 @@ export default function RootLayout({
|
|
26 |
return (
|
27 |
<html lang="en">
|
28 |
<body
|
29 |
-
className={
|
30 |
>
|
31 |
{children}
|
32 |
</body>
|
|
|
1 |
import type { Metadata } from "next";
|
|
|
2 |
import "./globals.css";
|
3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
export const metadata: Metadata = {
|
5 |
+
title: "LynxKite MM",
|
6 |
+
description: "From Lynx Analytics",
|
7 |
};
|
8 |
|
9 |
export default function RootLayout({
|
|
|
14 |
return (
|
15 |
<html lang="en">
|
16 |
<body
|
17 |
+
className={`antialiased`}
|
18 |
>
|
19 |
{children}
|
20 |
</body>
|
web/app/workspace/EnvironmentSelector.tsx
CHANGED
@@ -1,12 +1,15 @@
|
|
1 |
export default function EnvironmentSelector(props: { options: string[], value: string, onChange: (val: string) => void }) {
|
2 |
return (
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
11 |
);
|
12 |
}
|
|
|
1 |
export default function EnvironmentSelector(props: { options: string[], value: string, onChange: (val: string) => void }) {
|
2 |
return (
|
3 |
+
<>
|
4 |
+
<select className="select w-full max-w-xs"
|
5 |
+
name="workspace-env"
|
6 |
+
value={props.value}
|
7 |
+
onChange={(evt) => props.onChange(evt.currentTarget.value)}
|
8 |
+
>
|
9 |
+
{props.options.map(option =>
|
10 |
+
<option key={option} value={option}>{option}</option>
|
11 |
+
)}
|
12 |
+
</select>
|
13 |
+
</>
|
14 |
);
|
15 |
}
|
web/app/workspace/page.tsx
CHANGED
@@ -1,224 +1,7 @@
|
|
1 |
'use client';
|
2 |
-
import useSWR from 'swr';
|
3 |
import { useMemo } from "react";
|
4 |
-
import
|
5 |
-
ReactFlow,
|
6 |
-
useNodesState,
|
7 |
-
useEdgesState,
|
8 |
-
Controls,
|
9 |
-
MiniMap,
|
10 |
-
MarkerType,
|
11 |
-
useReactFlow,
|
12 |
-
type XYPosition,
|
13 |
-
type Node,
|
14 |
-
type Edge,
|
15 |
-
type Connection,
|
16 |
-
type NodeTypes,
|
17 |
-
} from '@xyflow/React';
|
18 |
-
// @ts-ignore
|
19 |
-
import ArrowBack from '~icons/tabler/arrow-back.jsx';
|
20 |
-
// @ts-ignore
|
21 |
-
import Backspace from '~icons/tabler/backspace.jsx';
|
22 |
-
// @ts-ignore
|
23 |
-
import Atom from '~icons/tabler/atom.jsx';
|
24 |
-
// import NodeWithParams from './NodeWithParams';
|
25 |
-
// import NodeWithVisualization from './NodeWithVisualization';
|
26 |
-
// import NodeWithImage from './NodeWithImage';
|
27 |
-
// import NodeWithTableView from './NodeWithTableView';
|
28 |
-
// import NodeWithSubFlow from './NodeWithSubFlow';
|
29 |
-
// import NodeWithArea from './NodeWithArea';
|
30 |
-
// import NodeSearch from './NodeSearch';
|
31 |
-
import EnvironmentSelector from './EnvironmentSelector';
|
32 |
-
import '@xyflow/react/dist/style.css';
|
33 |
|
34 |
-
export default
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
let path = '';
|
39 |
-
|
40 |
-
// const { screenToFlowPosition } = useReactFlow();
|
41 |
-
// const queryClient = useQueryClient();
|
42 |
-
// const backendWorkspace = useQuery(['workspace', path], async () => {
|
43 |
-
// const res = await fetch(`/api/load?path=${path}`);
|
44 |
-
// return res.json();
|
45 |
-
// }, { staleTime: 10000, retry: false });
|
46 |
-
// const mutation = useMutation(async (update) => {
|
47 |
-
// const res = await fetch('/api/save', {
|
48 |
-
// method: 'POST',
|
49 |
-
// headers: {
|
50 |
-
// 'Content-Type': 'application/json',
|
51 |
-
// },
|
52 |
-
// body: JSON.stringify(update),
|
53 |
-
// });
|
54 |
-
// return await res.json();
|
55 |
-
// }, {
|
56 |
-
// onSuccess: data => queryClient.setQueryData(['workspace', path], data),
|
57 |
-
// });
|
58 |
-
|
59 |
-
// const nodeTypes: NodeTypes = {
|
60 |
-
// basic: NodeWithParams,
|
61 |
-
// visualization: NodeWithVisualization,
|
62 |
-
// image: NodeWithImage,
|
63 |
-
// table_view: NodeWithTableView,
|
64 |
-
// sub_flow: NodeWithSubFlow,
|
65 |
-
// area: NodeWithArea,
|
66 |
-
// };
|
67 |
-
|
68 |
-
// const nodes = writable<Node[]>([]);
|
69 |
-
// const edges = writable<Edge[]>([]);
|
70 |
-
// let doNotSave = true;
|
71 |
-
// $: if ($backendWorkspace.isSuccess) {
|
72 |
-
// doNotSave = true; // Change is coming from the backend.
|
73 |
-
// nodes.set(JSON.parse(JSON.stringify($backendWorkspace.data?.nodes || [])));
|
74 |
-
// edges.set(JSON.parse(JSON.stringify($backendWorkspace.data?.edges || [])));
|
75 |
-
// doNotSave = false;
|
76 |
-
// }
|
77 |
-
|
78 |
-
// function closeNodeSearch() {
|
79 |
-
// nodeSearchSettings = undefined;
|
80 |
-
// }
|
81 |
-
// function toggleNodeSearch({ detail: { event } }) {
|
82 |
-
// if (nodeSearchSettings) {
|
83 |
-
// closeNodeSearch();
|
84 |
-
// return;
|
85 |
-
// }
|
86 |
-
// event.preventDefault();
|
87 |
-
// nodeSearchSettings = {
|
88 |
-
// pos: { x: event.clientX, y: event.clientY },
|
89 |
-
// boxes: $catalog.data[$backendWorkspace.data?.env],
|
90 |
-
// };
|
91 |
-
// }
|
92 |
-
// function addNode(e) {
|
93 |
-
// const meta = { ...e.detail };
|
94 |
-
// nodes.update((n) => {
|
95 |
-
// const node = {
|
96 |
-
// type: meta.type,
|
97 |
-
// data: {
|
98 |
-
// meta: meta,
|
99 |
-
// title: meta.name,
|
100 |
-
// params: Object.fromEntries(
|
101 |
-
// Object.values(meta.params).map((p) => [p.name, p.default])),
|
102 |
-
// },
|
103 |
-
// };
|
104 |
-
// node.position = screenToFlowPosition({ x: nodeSearchSettings.pos.x, y: nodeSearchSettings.pos.y });
|
105 |
-
// const title = node.data.title;
|
106 |
-
// let i = 1;
|
107 |
-
// node.id = `${title} ${i}`;
|
108 |
-
// while (n.find((x) => x.id === node.id)) {
|
109 |
-
// i += 1;
|
110 |
-
// node.id = `${title} ${i}`;
|
111 |
-
// }
|
112 |
-
// node.parentId = nodeSearchSettings.parentId;
|
113 |
-
// if (node.parentId) {
|
114 |
-
// node.extent = 'parent';
|
115 |
-
// const parent = n.find((x) => x.id === node.parentId);
|
116 |
-
// node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
117 |
-
// }
|
118 |
-
// return [...n, node]
|
119 |
-
// });
|
120 |
-
// closeNodeSearch();
|
121 |
-
// }
|
122 |
-
const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
|
123 |
-
const catalog = useSWR('/api/catalog', fetcher);
|
124 |
-
|
125 |
-
// let nodeSearchSettings: {
|
126 |
-
// pos: XYPosition,
|
127 |
-
// boxes: any[],
|
128 |
-
// parentId: string,
|
129 |
-
// };
|
130 |
-
|
131 |
-
// const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
|
132 |
-
// // Like JSON.stringify, but with keys sorted.
|
133 |
-
// function orderedJSON(obj: any) {
|
134 |
-
// const allKeys = new Set();
|
135 |
-
// JSON.stringify(obj, (key, value) => (allKeys.add(key), value));
|
136 |
-
// return JSON.stringify(obj, Array.from(allKeys).sort());
|
137 |
-
// }
|
138 |
-
// graph.subscribe(async (g) => {
|
139 |
-
// if (doNotSave) return;
|
140 |
-
// const dragging = g.nodes.find((n) => n.dragging);
|
141 |
-
// if (dragging) return;
|
142 |
-
// const resizing = g.nodes.find((n) => n.data?.beingResized);
|
143 |
-
// if (resizing) return;
|
144 |
-
// scheduleSave(g);
|
145 |
-
// });
|
146 |
-
// let saveTimeout;
|
147 |
-
// function scheduleSave(g) {
|
148 |
-
// // A slight delay, so we don't send a million requests when a node is resized, for example.
|
149 |
-
// clearTimeout(saveTimeout);
|
150 |
-
// saveTimeout = setTimeout(() => save(g), 500);
|
151 |
-
// }
|
152 |
-
// function save(g) {
|
153 |
-
// g = JSON.parse(JSON.stringify(g));
|
154 |
-
// for (const node of g.nodes) {
|
155 |
-
// delete node.measured;
|
156 |
-
// delete node.selected;
|
157 |
-
// delete node.dragging;
|
158 |
-
// delete node.beingResized;
|
159 |
-
// }
|
160 |
-
// for (const node of g.edges) {
|
161 |
-
// delete node.markerEnd;
|
162 |
-
// delete node.selected;
|
163 |
-
// }
|
164 |
-
// g.env = $backendWorkspace.data?.env;
|
165 |
-
// const ws = orderedJSON(g);
|
166 |
-
// const bd = orderedJSON($backendWorkspace.data);
|
167 |
-
// if (ws === bd) return;
|
168 |
-
// console.log('changed', JSON.stringify(diff(g, $backendWorkspace.data), null, 2));
|
169 |
-
// $mutation.mutate({ path, ws: g });
|
170 |
-
// }
|
171 |
-
// function nodeClick(e) {
|
172 |
-
// const node = e.detail.node;
|
173 |
-
// const meta = node.data.meta;
|
174 |
-
// if (!meta) return;
|
175 |
-
// const sub_nodes = meta.sub_nodes;
|
176 |
-
// if (!sub_nodes) return;
|
177 |
-
// const event = e.detail.event;
|
178 |
-
// if (event.target.classList.contains('title')) return;
|
179 |
-
// nodeSearchSettings = {
|
180 |
-
// pos: { x: event.clientX, y: event.clientY },
|
181 |
-
// boxes: sub_nodes,
|
182 |
-
// parentId: node.id,
|
183 |
-
// };
|
184 |
-
// }
|
185 |
-
// $: parentDir = path.split('/').slice(0, -1).join('/');
|
186 |
-
|
187 |
-
const nodeTypes = useMemo(() => ({}), []);
|
188 |
-
return (
|
189 |
-
|
190 |
-
<div className="page">
|
191 |
-
<div className="top-bar">
|
192 |
-
<div className="ws-name">
|
193 |
-
<a href=""><img src="/favicon.ico" /></a>
|
194 |
-
{path}
|
195 |
-
</div>
|
196 |
-
<div className="tools">
|
197 |
-
<EnvironmentSelector
|
198 |
-
options={Object.keys(catalog.data || {})}
|
199 |
-
value={'asd'}
|
200 |
-
onChange={(env) => 1}
|
201 |
-
/>
|
202 |
-
<a href=""><Atom /></a>
|
203 |
-
<a href=""><Backspace /></a>
|
204 |
-
<a href="#dir?path={parentDir}"><ArrowBack /></a>
|
205 |
-
</div>
|
206 |
-
</div>
|
207 |
-
<div style={{ height: "100%", width: '100vw' }}>
|
208 |
-
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView
|
209 |
-
proOptions={{ hideAttribution: true }}
|
210 |
-
maxZoom={3}
|
211 |
-
minZoom={0.3}
|
212 |
-
defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
|
213 |
-
>
|
214 |
-
<Controls />
|
215 |
-
<MiniMap />
|
216 |
-
{/* {#if nodeSearchSettings}
|
217 |
-
<NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
|
218 |
-
{/if} */}
|
219 |
-
</ReactFlow>
|
220 |
-
</div>
|
221 |
-
</div>
|
222 |
-
|
223 |
-
);
|
224 |
-
}
|
|
|
1 |
'use client';
|
|
|
2 |
import { useMemo } from "react";
|
3 |
+
import dynamic from 'next/dynamic';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
+
export default dynamic(() => import('./Workspace'), {
|
6 |
+
ssr: false,
|
7 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/package-lock.json
CHANGED
@@ -6932,6 +6932,111 @@
|
|
6932 |
"version": "2.3.0",
|
6933 |
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
6934 |
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6935 |
}
|
6936 |
}
|
6937 |
}
|
|
|
6932 |
"version": "2.3.0",
|
6933 |
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
6934 |
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
6935 |
+
},
|
6936 |
+
"node_modules/@next/swc-darwin-x64": {
|
6937 |
+
"version": "15.0.3",
|
6938 |
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3.tgz",
|
6939 |
+
"integrity": "sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==",
|
6940 |
+
"cpu": [
|
6941 |
+
"x64"
|
6942 |
+
],
|
6943 |
+
"optional": true,
|
6944 |
+
"os": [
|
6945 |
+
"darwin"
|
6946 |
+
],
|
6947 |
+
"engines": {
|
6948 |
+
"node": ">= 10"
|
6949 |
+
}
|
6950 |
+
},
|
6951 |
+
"node_modules/@next/swc-linux-arm64-gnu": {
|
6952 |
+
"version": "15.0.3",
|
6953 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3.tgz",
|
6954 |
+
"integrity": "sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==",
|
6955 |
+
"cpu": [
|
6956 |
+
"arm64"
|
6957 |
+
],
|
6958 |
+
"optional": true,
|
6959 |
+
"os": [
|
6960 |
+
"linux"
|
6961 |
+
],
|
6962 |
+
"engines": {
|
6963 |
+
"node": ">= 10"
|
6964 |
+
}
|
6965 |
+
},
|
6966 |
+
"node_modules/@next/swc-linux-arm64-musl": {
|
6967 |
+
"version": "15.0.3",
|
6968 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3.tgz",
|
6969 |
+
"integrity": "sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==",
|
6970 |
+
"cpu": [
|
6971 |
+
"arm64"
|
6972 |
+
],
|
6973 |
+
"optional": true,
|
6974 |
+
"os": [
|
6975 |
+
"linux"
|
6976 |
+
],
|
6977 |
+
"engines": {
|
6978 |
+
"node": ">= 10"
|
6979 |
+
}
|
6980 |
+
},
|
6981 |
+
"node_modules/@next/swc-linux-x64-gnu": {
|
6982 |
+
"version": "15.0.3",
|
6983 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3.tgz",
|
6984 |
+
"integrity": "sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==",
|
6985 |
+
"cpu": [
|
6986 |
+
"x64"
|
6987 |
+
],
|
6988 |
+
"optional": true,
|
6989 |
+
"os": [
|
6990 |
+
"linux"
|
6991 |
+
],
|
6992 |
+
"engines": {
|
6993 |
+
"node": ">= 10"
|
6994 |
+
}
|
6995 |
+
},
|
6996 |
+
"node_modules/@next/swc-linux-x64-musl": {
|
6997 |
+
"version": "15.0.3",
|
6998 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3.tgz",
|
6999 |
+
"integrity": "sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==",
|
7000 |
+
"cpu": [
|
7001 |
+
"x64"
|
7002 |
+
],
|
7003 |
+
"optional": true,
|
7004 |
+
"os": [
|
7005 |
+
"linux"
|
7006 |
+
],
|
7007 |
+
"engines": {
|
7008 |
+
"node": ">= 10"
|
7009 |
+
}
|
7010 |
+
},
|
7011 |
+
"node_modules/@next/swc-win32-arm64-msvc": {
|
7012 |
+
"version": "15.0.3",
|
7013 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3.tgz",
|
7014 |
+
"integrity": "sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==",
|
7015 |
+
"cpu": [
|
7016 |
+
"arm64"
|
7017 |
+
],
|
7018 |
+
"optional": true,
|
7019 |
+
"os": [
|
7020 |
+
"win32"
|
7021 |
+
],
|
7022 |
+
"engines": {
|
7023 |
+
"node": ">= 10"
|
7024 |
+
}
|
7025 |
+
},
|
7026 |
+
"node_modules/@next/swc-win32-x64-msvc": {
|
7027 |
+
"version": "15.0.3",
|
7028 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3.tgz",
|
7029 |
+
"integrity": "sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==",
|
7030 |
+
"cpu": [
|
7031 |
+
"x64"
|
7032 |
+
],
|
7033 |
+
"optional": true,
|
7034 |
+
"os": [
|
7035 |
+
"win32"
|
7036 |
+
],
|
7037 |
+
"engines": {
|
7038 |
+
"node": ">= 10"
|
7039 |
+
}
|
7040 |
}
|
7041 |
}
|
7042 |
}
|
web/tailwind.config.js
CHANGED
@@ -1,8 +1,22 @@
|
|
1 |
/** @type {import('tailwindcss').Config} */
|
2 |
module.exports = {
|
3 |
-
|
|
|
4 |
theme: {
|
5 |
extend: {},
|
6 |
},
|
7 |
plugins: [require('daisyui')],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
};
|
|
|
1 |
/** @type {import('tailwindcss').Config} */
|
2 |
module.exports = {
|
3 |
+
darkMode: 'selector',
|
4 |
+
content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'],
|
5 |
theme: {
|
6 |
extend: {},
|
7 |
},
|
8 |
plugins: [require('daisyui')],
|
9 |
+
daisyui: {
|
10 |
+
themes: [
|
11 |
+
{
|
12 |
+
lynxkite: {
|
13 |
+
primary: 'oklch(75% 0.2 55)',
|
14 |
+
secondary: 'oklch(75% 0.13 230)',
|
15 |
+
accent: 'oklch(55% 0.25 320)',
|
16 |
+
neutral: 'oklch(35% 0.1 240)',
|
17 |
+
'base-100': '#ffffff',
|
18 |
+
},
|
19 |
+
},
|
20 |
+
],
|
21 |
+
},
|
22 |
};
|
web/tsconfig.json
CHANGED
@@ -13,6 +13,7 @@
|
|
13 |
"isolatedModules": true,
|
14 |
"jsx": "preserve",
|
15 |
"incremental": true,
|
|
|
16 |
"plugins": [
|
17 |
{
|
18 |
"name": "next"
|
|
|
13 |
"isolatedModules": true,
|
14 |
"jsx": "preserve",
|
15 |
"incremental": true,
|
16 |
+
"noImplicitAny": false,
|
17 |
"plugins": [
|
18 |
{
|
19 |
"name": "next"
|