public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import { IconHierarchy2 } from "@tabler/icons-react";
import { ReactFlowProvider } from "@xyflow/react";
import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
import { TbSettings } from "react-icons/tb";
import { useLocation } from "wouter";
import { useBknd } from "../../client/BkndProvider";
import { useFlows } from "../../client/schema/flows/use-flows";
import { IconButton } from "../../components/buttons/IconButton";
import { Empty } from "../../components/display/Empty";
import { SearchInput } from "../../components/form/SearchInput";
import { Link } from "../../components/wouter/Link";
import { useBrowserTitle } from "../../hooks/use-browser-title";
import * as AppShell from "../../layouts/AppShell/AppShell";
export function FlowsRoot({ children }) {
return <ReactFlowProvider>{children}</ReactFlowProvider>;
}
export function FlowsEmpty() {
const { app } = useBknd();
useBrowserTitle(["Flows"]);
const [, navigate] = useLocation();
const { flows } = useFlows();
return (
<>
<AppShell.Sidebar>
<AppShell.SectionHeader
right={
<Link href={app.getSettingsPath(["flows"])}>
<IconButton Icon={TbSettings} />
</Link>
}
>
Flows
</AppShell.SectionHeader>
<AppShell.Scrollable initialOffset={96}>
<div className="flex flex-col flex-grow p-3 gap-3">
<div>
<SearchInput placeholder="Search flows" />
</div>
<nav className="flex flex-col flex-1 gap-1">
{flows.map((flow) => (
<AppShell.SidebarLink key={flow.name} as={Link} href={`/flow/${flow.name}`}>
{ucFirstAllSnakeToPascalWithSpaces(flow.name)}
</AppShell.SidebarLink>
))}
</nav>
</div>
</AppShell.Scrollable>
</AppShell.Sidebar>
<main className="flex flex-col flex-grow">
<Empty
Icon={IconHierarchy2}
title="No flow selected"
description="Please select a flow from the left sidebar or create a new one
to continue."
buttonText="Create Flow"
buttonOnClick={() => navigate(app.getSettingsPath(["flows"]))}
/>
</main>
</>
);
}

View File

@@ -0,0 +1,202 @@
import { type Edge, type Node, useOnSelectionChange } from "@xyflow/react";
import { type Execution, ExecutionState, type Flow, type Task } from "flows";
import { useEffect, useState } from "react";
import {
TbArrowLeft,
TbChevronDown,
TbChevronUp,
TbDots,
TbPlayerPlayFilled,
TbSettings
} from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import FlowCanvas from "ui/modules/flows/components/FlowCanvas";
import { TaskForm } from "ui/modules/flows/components/form/TaskForm";
import { useLocation } from "wouter";
import { useBknd } from "../../client/BkndProvider";
import { Button } from "../../components/buttons/Button";
import { IconButton } from "../../components/buttons/IconButton";
import { Dropdown } from "../../components/overlay/Dropdown";
import { useFlow } from "../../container/use-flows";
import * as AppShell from "../../layouts/AppShell/AppShell";
import { SectionHeader } from "../../layouts/AppShell/AppShell";
export function FlowEdit({ params }) {
const { app } = useBknd();
const { color_scheme: theme } = app.getAdminConfig();
const { basepath } = app.getAdminConfig();
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const [location, navigate] = useLocation();
const [execution, setExecution] = useState<Execution>();
const [selectedNodes, setSelectedNodes] = useState<Node[]>([]);
const [selectedEdges, setSelectedEdges] = useState<Edge[]>([]);
console.log("key", params, params.flow);
const { flow } = useFlow(params.flow);
console.log("--flow", flow);
async function handleRun() {
console.log("Running flow", flow);
const execution = flow.createExecution();
setExecution(execution);
// delay a bit before starting
execution.emgr.onEvent(
ExecutionState,
async (event) => {
if (event.params.state === "started") {
await new Promise((resolve) => setTimeout(resolve, 100));
}
},
"sync"
);
execution.subscribe(async (event) => {
console.log("[event]", event);
});
await new Promise((resolve) => setTimeout(resolve, 100));
execution.start();
}
function goBack(state?: Record<string, any>) {
window.history.go(-1);
}
useOnSelectionChange({
onChange: ({ nodes, edges }) => {
setSelectedNodes(nodes);
setSelectedEdges(edges);
}
});
return (
<>
<AppShell.Sidebar>
<AppShell.SectionHeader
right={
<a href="#" className="link p-1 rounded-md hover:bg-primary/5 flex items-center">
<TbSettings size={20} />
</a>
}
>
Tasks
</AppShell.SectionHeader>
<AppShell.Scrollable initialOffset={96}>
<Sidebar edges={selectedEdges} nodes={selectedNodes} flow={flow} />
</AppShell.Scrollable>
</AppShell.Sidebar>
<main className="flex flex-col flex-grow">
<SectionHeader
right={
<>
<Dropdown
items={[
{
label: "Settings",
onClick: () => navigate(`${prefix}/flows/flows/${flow.name}`)
}
]}
position="bottom-end"
>
<IconButton Icon={TbDots} />
</Dropdown>
<Button variant="primary" IconLeft={TbPlayerPlayFilled} onClick={handleRun}>
Run
</Button>
</>
}
className="pl-3"
>
<AppShell.SectionHeaderTitle className="flex flex-row items-center gap-2">
<IconButton
onClick={goBack}
Icon={TbArrowLeft}
variant="default"
size="lg"
className="mr-1"
/>
<div className="truncate">
<span className="text-primary/60">Flow / </span>
{flow.name}
</div>
</AppShell.SectionHeaderTitle>
</SectionHeader>
<div className="w-full h-full">
<FlowCanvas flow={flow} execution={execution} options={{ theme }} key={theme} />
</div>
</main>
</>
);
}
function Sidebar({ nodes, edges, flow }: { flow: Flow; nodes: Node[]; edges: Edge[] }) {
const selectedNode = nodes?.[0];
// @ts-ignore
const selectedTask: Task | undefined = selectedNode?.data?.task;
useEffect(() => {
console.log("-- selected", selectedTask);
}, [selectedTask]);
const tasks = flow.getSequence().flat();
const Header = ({ onClick, opened, task }) => (
<div
className={twMerge(
"flex flex-row pl-5 pr-3 py-3 border-muted border-b cursor-pointer justify-between items-center font-bold",
opened && "bg-primary/5"
)}
onClick={onClick}
>
{task.name}
{opened ? <TbChevronUp size={18} /> : <TbChevronDown size={18} />}
</div>
);
return (
<div className="flex flex-col flex-grow">
{tasks.map((task) => {
const open = task.name === selectedTask?.name;
return (
<Collapsible
key={task.name}
className="flex flex-col"
header={(props) => <Header {...props} task={task} />}
open={open}
>
<div className="flex flex-col pl-5 pr-3 py-3">
<TaskForm task={task} onChange={console.log} />
</div>
</Collapsible>
);
})}
</div>
);
}
type CollapsibleProps = {
header: (props: any) => any;
className?: string;
children: React.ReactNode;
open?: boolean;
};
function Collapsible({ header, children, open = false, className }: CollapsibleProps) {
const [opened, setOpened] = useState(open);
function toggle() {
setOpened((prev) => !prev);
}
useEffect(() => {
setOpened(open);
}, [open]);
return (
<div className={twMerge("flex flex-col", className)}>
{header?.({ onClick: toggle, opened })}
{opened && children}
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { Route } from "wouter";
import { FlowsEmpty, FlowsRoot } from "./_flows.root";
import { FlowEdit } from "./flow.$key";
export default function FlowRoutes() {
return (
<FlowsRoot>
<Route path="/" component={FlowsEmpty} />
<Route path="/flow/:flow" component={FlowEdit} />
</FlowsRoot>
);
}