lynxkite / lynxkite-app /web /src /workspace /nodes /GraphCreationNode.tsx
JMLizano's picture
47 smooth graph creation flow
45b3519 unverified
raw
history blame
9.07 kB
import { useReactFlow } from "@xyflow/react";
import { useState } from "react";
import React from "react";
import Markdown from "react-markdown";
// @ts-ignore
import Trash from "~icons/tabler/trash";
import LynxKiteNode from "./LynxKiteNode";
import Table from "./Table";
function toMD(v: any): string {
if (typeof v === "string") {
return v;
}
if (Array.isArray(v)) {
return v.map(toMD).join("\n\n");
}
return JSON.stringify(v);
}
function displayTable(name: string, df: any) {
if (df.data.length > 1) {
return (
<Table
key={`${name}-table`}
name={`${name}-table`}
columns={df.columns}
data={df.data}
/>
);
}
if (df.data.length) {
return (
<dl key={`${name}-dl`}>
{df.columns.map((c: string, i: number) => (
<React.Fragment key={`${name}-${c}`}>
<dt>{c}</dt>
<dd>
<Markdown>{toMD(df.data[0][i])}</Markdown>
</dd>
</React.Fragment>
))}
</dl>
);
}
return JSON.stringify(df.data);
}
function relationsToDict(relations: any[]) {
if (!relations) {
return {};
}
return Object.assign({}, ...relations.map((r: any) => ({ [r.name]: r })));
}
export type UpdateOptions = { delay?: number };
export default function NodeWithGraphCreationView(props: any) {
const reactFlow = useReactFlow();
const [open, setOpen] = useState({} as { [name: string]: boolean });
const display = props.data.display?.value;
const tables = display?.dataframes || {};
const singleTable = tables && Object.keys(tables).length === 1;
const [relations, setRelations] = useState(
relationsToDict(display?.relations) || {},
);
const singleRelation = relations && Object.keys(relations).length === 1;
function setParam(name: string, newValue: any, opts: UpdateOptions) {
reactFlow.updateNodeData(props.id, {
params: { ...props.data.params, [name]: newValue },
__execution_delay: opts.delay || 0,
});
}
function updateRelation(event: any, relation: any) {
event.preventDefault();
const updatedRelation = {
...relation,
...Object.fromEntries(new FormData(event.target).entries()),
};
// Avoid mutating React state directly
const newRelations = { ...relations };
if (relation.name !== updatedRelation.name) {
delete newRelations[relation.name];
}
newRelations[updatedRelation.name] = updatedRelation;
setRelations(newRelations);
// There is some issue with how Yjs handles complex objects (maps, arrays)
// so we need to serialize the relations object to a string
setParam("relations", JSON.stringify(newRelations), {});
}
const addRelation = () => {
const new_relation = {
name: "new_relation",
df: "",
source_column: "",
target_column: "",
source_table: "",
target_table: "",
source_key: "",
target_key: "",
};
setRelations({
...relations,
[new_relation.name]: new_relation,
});
setOpen({ ...open, [new_relation.name]: true });
};
const deleteRelation = (relation: any) => {
const newOpen = { ...open };
delete newOpen[relation.name];
setOpen(newOpen);
const newRelations = { ...relations };
delete newRelations[relation.name];
setRelations(newRelations);
// There is some issue with how Yjs handles complex objects (maps, arrays)
// so we need to serialize the relations object to a string
setParam("relations", JSON.stringify(newRelations), {});
};
function displayRelation(relation: any) {
// TODO: Dynamic autocomplete
return (
<form
className="graph-relation-attributes"
onSubmit={(e) => {
updateRelation(e, relation);
}}
>
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" defaultValue={relation.name} />
<label htmlFor="df">DataFrame:</label>
<input
type="text"
id="df"
name="df"
defaultValue={relation.df}
list="df-options"
required
/>
<label htmlFor="source_column">Source Column:</label>
<input
type="text"
id="source_column"
name="source_column"
defaultValue={relation.source_column}
list="edges-column-options"
required
/>
<label htmlFor="target_column">Target Column:</label>
<input
type="text"
id="target_column"
name="target_column"
defaultValue={relation.target_column}
list="edges-column-options"
required
/>
<label htmlFor="source_table">Source Table:</label>
<input
type="text"
id="source_table"
name="source_table"
defaultValue={relation.source_table}
list="df-options"
required
/>
<label htmlFor="target_table">Target Table:</label>
<input
type="text"
id="target_table"
name="target_table"
defaultValue={relation.target_table}
list="df-options"
required
/>
<label htmlFor="source_key">Source Key:</label>
<input
type="text"
id="source_key"
name="source_key"
defaultValue={relation.source_key}
list="source-node-column-options"
required
/>
<label htmlFor="target_key">Target Key:</label>
<input
type="text"
id="target_key"
name="target_key"
defaultValue={relation.target_key}
list="target-node-column-options"
required
/>
<datalist id="df-options">
{Object.keys(tables).map((name) => (
<option key={name} value={name} />
))}
</datalist>
<datalist id="edges-column-options">
{tables[relation.source_table] &&
tables[relation.df].columns.map((name: string) => (
<option key={name} value={name} />
))}
</datalist>
<datalist id="source-node-column-options">
{tables[relation.source_table] &&
tables[relation.source_table].columns.map((name: string) => (
<option key={name} value={name} />
))}
</datalist>
<datalist id="target-node-column-options">
{tables[relation.source_table] &&
tables[relation.target_table].columns.map((name: string) => (
<option key={name} value={name} />
))}
</datalist>
<button className="submit-relationship-button" type="submit">
Create
</button>
</form>
);
}
return (
<LynxKiteNode {...props}>
<div className="graph-creation-view">
<div className="graph-tables">
<div className="graph-table-header">Node Tables</div>
{display && [
Object.entries(tables).map(([name, df]: [string, any]) => (
<React.Fragment key={name}>
{!singleTable && (
<div
key={`${name}-header`}
className="df-head"
onClick={() => setOpen({ ...open, [name]: !open[name] })}
>
{name}
</div>
)}
{(singleTable || open[name]) && displayTable(name, df)}
</React.Fragment>
)),
Object.entries(display.others || {}).map(([name, o]) => (
<>
<div
key={name}
className="df-head"
onClick={() => setOpen({ ...open, [name]: !open[name] })}
>
{name}
</div>
{open[name] && <pre>{(o as any).toString()}</pre>}
</>
)),
]}
</div>
<div className="graph-relations">
<div className="graph-table-header">
Relationships
<button
className="add-relationship-button"
onClick={(_) => addRelation()}
>
+
</button>
</div>
{relations &&
Object.entries(relations).map(([name, relation]: [string, any]) => (
<React.Fragment key={name}>
<div
key={`${name}-header`}
className="df-head"
onClick={() => setOpen({ ...open, [name]: !open[name] })}
>
{name}
<button
onClick={() => {
deleteRelation(relation);
}}
>
<Trash />
</button>
</div>
{(singleRelation || open[name]) && displayRelation(relation)}
</React.Fragment>
))}
</div>
</div>
</LynxKiteNode>
);
}