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;
|