Spaces:
Running
Running
Missed these files. Workspace in React.
Browse files
web/app/workspace/LynxKiteState.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { createContext } from "react";
|
2 |
+
|
3 |
+
export const LynxKiteState = createContext({ workspace: {} as any });
|
web/app/workspace/Workspace.tsx
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
import useSWR from 'swr';
|
3 |
+
import { useMemo } from "react";
|
4 |
+
import { useSearchParams } from 'next/navigation';
|
5 |
+
import {
|
6 |
+
ReactFlow,
|
7 |
+
useNodesState,
|
8 |
+
useEdgesState,
|
9 |
+
Controls,
|
10 |
+
MiniMap,
|
11 |
+
MarkerType,
|
12 |
+
useReactFlow,
|
13 |
+
type XYPosition,
|
14 |
+
type Node,
|
15 |
+
type Edge,
|
16 |
+
type Connection,
|
17 |
+
type NodeTypes,
|
18 |
+
} from '@xyflow/react';
|
19 |
+
// @ts-ignore
|
20 |
+
import ArrowBack from '~icons/tabler/arrow-back.jsx';
|
21 |
+
// @ts-ignore
|
22 |
+
import Backspace from '~icons/tabler/backspace.jsx';
|
23 |
+
// @ts-ignore
|
24 |
+
import Atom from '~icons/tabler/atom.jsx';
|
25 |
+
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
26 |
+
import { useSyncedStore } from "@syncedstore/react";
|
27 |
+
import { WebsocketProvider } from "y-websocket";
|
28 |
+
import NodeWithParams from './nodes/NodeWithParams';
|
29 |
+
// import NodeWithVisualization from './NodeWithVisualization';
|
30 |
+
// import NodeWithImage from './NodeWithImage';
|
31 |
+
// import NodeWithTableView from './NodeWithTableView';
|
32 |
+
// import NodeWithSubFlow from './NodeWithSubFlow';
|
33 |
+
// import NodeWithArea from './NodeWithArea';
|
34 |
+
// import NodeSearch from './NodeSearch';
|
35 |
+
import EnvironmentSelector from './EnvironmentSelector';
|
36 |
+
import { LynxKiteState } from './LynxKiteState';
|
37 |
+
import '@xyflow/react/dist/style.css';
|
38 |
+
|
39 |
+
export default function Workspace() {
|
40 |
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
41 |
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
42 |
+
const searchParams = useSearchParams();
|
43 |
+
|
44 |
+
let path = searchParams.get('path');
|
45 |
+
const sstore = syncedStore({ workspace: {} });
|
46 |
+
const doc = getYjsDoc(sstore);
|
47 |
+
const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path, doc);
|
48 |
+
const state = useSyncedStore(sstore);
|
49 |
+
|
50 |
+
const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
|
51 |
+
const catalog = useSWR('/api/catalog', fetcher);
|
52 |
+
|
53 |
+
const nodeTypes = useMemo(() => ({
|
54 |
+
basic: NodeWithParams,
|
55 |
+
table_view: NodeWithParams,
|
56 |
+
}), []);
|
57 |
+
return (
|
58 |
+
<div className="workspace">
|
59 |
+
<div className="top-bar bg-neutral">
|
60 |
+
<a className="logo" href=""><img src="/favicon.ico" /></a>
|
61 |
+
<div className="ws-name">
|
62 |
+
{path}
|
63 |
+
</div>
|
64 |
+
<EnvironmentSelector
|
65 |
+
options={Object.keys(catalog.data || {})}
|
66 |
+
value={state.workspace?.env}
|
67 |
+
onChange={(env) => state.workspace.env = env}
|
68 |
+
/>
|
69 |
+
<div className="tools text-secondary">
|
70 |
+
<a href=""><Atom /></a>
|
71 |
+
<a href=""><Backspace /></a>
|
72 |
+
<a href="#dir?path={parentDir}"><ArrowBack /></a>
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
<div style={{ height: "100%", width: '100vw' }}>
|
76 |
+
<LynxKiteState.Provider value={state}>
|
77 |
+
<ReactFlow nodes={state.workspace?.nodes} edges={state.workspace?.edges} nodeTypes={nodeTypes} fitView
|
78 |
+
proOptions={{ hideAttribution: true }}
|
79 |
+
maxZoom={3}
|
80 |
+
minZoom={0.3}
|
81 |
+
defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
|
82 |
+
>
|
83 |
+
<Controls />
|
84 |
+
<MiniMap />
|
85 |
+
{/* {#if nodeSearchSettings}
|
86 |
+
<NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
|
87 |
+
{/if} */}
|
88 |
+
</ReactFlow>
|
89 |
+
</LynxKiteState.Provider>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
|
93 |
+
);
|
94 |
+
}
|
web/app/workspace/nodes/LynxKiteNode.tsx
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useContext } from 'react';
|
2 |
+
import { LynxKiteState } from '../LynxKiteState';
|
3 |
+
import { Handle, NodeResizeControl } from '@xyflow/react';
|
4 |
+
// @ts-ignore
|
5 |
+
import ChevronDownRight from '~icons/tabler/chevron-down-right.jsx';
|
6 |
+
|
7 |
+
interface LynxKiteNodeProps {
|
8 |
+
width: number;
|
9 |
+
height: number;
|
10 |
+
nodeStyle: any;
|
11 |
+
data: any;
|
12 |
+
children: any;
|
13 |
+
}
|
14 |
+
|
15 |
+
function getHandles(inputs, outputs) {
|
16 |
+
const handles: {
|
17 |
+
position: 'top' | 'bottom' | 'left' | 'right',
|
18 |
+
name: string,
|
19 |
+
index: number,
|
20 |
+
offsetPercentage: number,
|
21 |
+
showLabel: boolean,
|
22 |
+
}[] = [];
|
23 |
+
for (const e of Object.values(inputs)) {
|
24 |
+
handles.push({ ...e, type: 'target' });
|
25 |
+
}
|
26 |
+
for (const e of Object.values(outputs)) {
|
27 |
+
handles.push({ ...e, type: 'source' });
|
28 |
+
}
|
29 |
+
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
30 |
+
for (const e of handles) {
|
31 |
+
e.index = counts[e.position];
|
32 |
+
counts[e.position]++;
|
33 |
+
}
|
34 |
+
for (const e of handles) {
|
35 |
+
e.offsetPercentage = 100 * (e.index + 1) / (counts[e.position] + 1);
|
36 |
+
const simpleHorizontal = counts.top === 0 && counts.bottom === 0 && handles.length <= 2;
|
37 |
+
const simpleVertical = counts.left === 0 && counts.right === 0 && handles.length <= 2;
|
38 |
+
e.showLabel = !simpleHorizontal && !simpleVertical;
|
39 |
+
}
|
40 |
+
return handles;
|
41 |
+
}
|
42 |
+
|
43 |
+
export default function LynxKiteNode(props: LynxKiteNodeProps) {
|
44 |
+
const data = props.data;
|
45 |
+
const state = useContext(LynxKiteState);
|
46 |
+
const expanded = true;
|
47 |
+
const handles = getHandles(data.meta?.inputs || {}, data.meta?.outputs || {});
|
48 |
+
function asPx(n: number | undefined) {
|
49 |
+
return (n ? n + 'px' : undefined) || '200px';
|
50 |
+
}
|
51 |
+
function titleClicked() { }
|
52 |
+
function updateNodeData() { }
|
53 |
+
const handleOffsetDirection = { top: 'left', bottom: 'left', left: 'top', right: 'top' };
|
54 |
+
|
55 |
+
return (
|
56 |
+
<div className={'node-container ' + (expanded ? 'expanded' : 'collapsed')}
|
57 |
+
style={{ width: asPx(props.width), height: asPx(expanded ? props.height : undefined) }}>
|
58 |
+
<div className="lynxkite-node" style={props.nodeStyle}>
|
59 |
+
<div className="title bg-primary" onClick={titleClicked}>
|
60 |
+
{data.title}
|
61 |
+
{data.error && <span className="title-icon">⚠️</span>}
|
62 |
+
{expanded || <span className="title-icon">⋯</span>}
|
63 |
+
</div>
|
64 |
+
{expanded && <>
|
65 |
+
{data.error &&
|
66 |
+
<div className="error">{data.error}</div>
|
67 |
+
}
|
68 |
+
{props.children}
|
69 |
+
{handles.map(handle => (
|
70 |
+
<Handle
|
71 |
+
key={handle.name}
|
72 |
+
id={handle.name} type={handle.type} position={handle.position}
|
73 |
+
style={{ [handleOffsetDirection[handle.position]]: handle.offsetPercentage + '%' }}>
|
74 |
+
{handle.showLabel && <span className="handle-name">{handle.name.replace(/_/g, " ")}</span>}
|
75 |
+
</Handle>
|
76 |
+
))}
|
77 |
+
<NodeResizeControl
|
78 |
+
minWidth={100}
|
79 |
+
minHeight={50}
|
80 |
+
style={{ 'background': 'transparent', 'border': 'none' }}
|
81 |
+
onResizeStart={() => updateNodeData(id, { beingResized: true })}
|
82 |
+
onResizeEnd={() => updateNodeData(id, { beingResized: false })}
|
83 |
+
>
|
84 |
+
<ChevronDownRight className="node-resizer" />
|
85 |
+
</NodeResizeControl>
|
86 |
+
</>}
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
);
|
90 |
+
}
|
web/app/workspace/nodes/NodeParameter.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BOOLEAN = "<class 'bool'>";
|
2 |
+
|
3 |
+
function ParamName({ name }) {
|
4 |
+
return <span className="param-name bg-base-200">{name.replace(/_/g, ' ')}</span>;
|
5 |
+
}
|
6 |
+
|
7 |
+
export default function NodeParameter({ name, value, meta, onChange }) {
|
8 |
+
return (
|
9 |
+
<label className="param">
|
10 |
+
{meta?.type?.format === 'collapsed' ? <>
|
11 |
+
<ParamName name={name} />
|
12 |
+
<button className="collapsed-param">
|
13 |
+
⋯
|
14 |
+
</button>
|
15 |
+
</> : meta?.type?.format === 'textarea' ? <>
|
16 |
+
<ParamName name={name} />
|
17 |
+
<textarea className="textarea textarea-bordered w-full max-w-xs"
|
18 |
+
rows={6}
|
19 |
+
value={value}
|
20 |
+
onChange={(evt) => onChange(evt.currentTarget.value)}
|
21 |
+
/>
|
22 |
+
</> : meta?.type?.enum ? <>
|
23 |
+
<ParamName name={name} />
|
24 |
+
<select className="select select-bordered w-full max-w-xs"
|
25 |
+
value={value || meta.type.enum[0]}
|
26 |
+
onChange={(evt) => onChange(evt.currentTarget.value)}
|
27 |
+
>
|
28 |
+
{meta.type.enum.map(option =>
|
29 |
+
<option key={option} value={option}>{option}</option>
|
30 |
+
)}
|
31 |
+
</select>
|
32 |
+
</> : meta?.type?.type === BOOLEAN ? <div className="form-control">
|
33 |
+
<label className="label cursor-pointer">
|
34 |
+
<input className="checkbox"
|
35 |
+
type="checkbox"
|
36 |
+
checked={value}
|
37 |
+
onChange={(evt) => onChange(evt.currentTarget.checked)}
|
38 |
+
/>
|
39 |
+
{name.replace(/_/g, ' ')}
|
40 |
+
</label>
|
41 |
+
</div> : <>
|
42 |
+
<ParamName name={name} />
|
43 |
+
<input className="input input-bordered w-full max-w-xs"
|
44 |
+
value={value || ""}
|
45 |
+
onChange={(evt) => onChange(evt.currentTarget.value)}
|
46 |
+
/>
|
47 |
+
</>
|
48 |
+
}
|
49 |
+
</label >
|
50 |
+
);
|
51 |
+
}
|
web/app/workspace/nodes/NodeWithParams.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useContext } from 'react';
|
2 |
+
import { LynxKiteState } from '../LynxKiteState';
|
3 |
+
import LynxKiteNode from './LynxKiteNode';
|
4 |
+
import { useNodesState } from '@xyflow/react';
|
5 |
+
import NodeParameter from './NodeParameter';
|
6 |
+
|
7 |
+
function NodeWithParams(props) {
|
8 |
+
const metaParams = props.data.meta?.params;
|
9 |
+
const state = useContext(LynxKiteState);
|
10 |
+
function setParam(name, newValue) {
|
11 |
+
const i = state.workspace.nodes.findIndex((n) => n.id === props.id);
|
12 |
+
state.workspace.nodes[i].data.params[name] = newValue;
|
13 |
+
}
|
14 |
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
15 |
+
const params = nodes && props.data?.params ? Object.entries(props.data.params) : [];
|
16 |
+
|
17 |
+
return (
|
18 |
+
<LynxKiteNode {...props}>
|
19 |
+
{params.map(([name, value]) =>
|
20 |
+
<NodeParameter
|
21 |
+
name={name}
|
22 |
+
key={name}
|
23 |
+
value={value}
|
24 |
+
meta={metaParams?.[name]}
|
25 |
+
onChange={(value) => setParam(name, value)}
|
26 |
+
/>
|
27 |
+
)}
|
28 |
+
</LynxKiteNode >
|
29 |
+
);
|
30 |
+
}
|
31 |
+
|
32 |
+
export default NodeWithParams;
|