feat: enhance project structure with server and workflow management
- Introduced a new server package with Fastify for handling API requests and workflows. - Implemented user authentication and authorization with JWT and cookie management. - Added endpoints for managing workflows, runs, and user accounts. - Established a dashboard for monitoring workflow status and statistics. - Included a script sandbox for executing user-defined scripts securely. - Updated README with setup instructions and API documentation. - Configured database migrations for user management. - Enhanced logging capabilities for better traceability.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { useTheme } from "../theme.jsx";
|
||||
|
||||
export function CodeEditor({ language, value, onChange, height = "50vh" }) {
|
||||
const { theme } = useTheme();
|
||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-box border border-base-300">
|
||||
<Editor
|
||||
height={height}
|
||||
language={language}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v ?? "")}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
options={{
|
||||
minimap: { enabled: !isMobile },
|
||||
lineNumbers: "on",
|
||||
tabSize: 2,
|
||||
insertSpaces: true,
|
||||
autoIndent: "full",
|
||||
automaticLayout: true,
|
||||
wordWrap: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 13,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
LuActivity,
|
||||
LuCode,
|
||||
LuGitBranch,
|
||||
LuHouse,
|
||||
LuLogOut,
|
||||
LuMenu,
|
||||
LuMoon,
|
||||
LuSun,
|
||||
LuUsers,
|
||||
} from "react-icons/lu";
|
||||
import { useLogout } from "../api/hooks.js";
|
||||
import { useTheme } from "../theme.jsx";
|
||||
|
||||
const links = [
|
||||
{ to: "/", label: "Home", icon: LuHouse, end: true },
|
||||
{ to: "/scripts", label: "Scripts", icon: LuCode },
|
||||
{ to: "/workflows", label: "Workflows", icon: LuGitBranch },
|
||||
{ to: "/events", label: "Events", icon: LuActivity },
|
||||
];
|
||||
|
||||
export function Layout({ user, children }) {
|
||||
const { theme, toggle } = useTheme();
|
||||
const logout = useLogout();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const navItems = [
|
||||
...links,
|
||||
...(user.role === "admin"
|
||||
? [{ to: "/users", label: "Users", icon: LuUsers }]
|
||||
: []),
|
||||
];
|
||||
|
||||
function closeDrawer() {
|
||||
const el = document.getElementById("nav-drawer");
|
||||
if (el) el.checked = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="drawer lg:drawer-open">
|
||||
<input id="nav-drawer" type="checkbox" className="drawer-toggle" />
|
||||
<div className="drawer-content flex min-h-screen flex-col">
|
||||
<div className="navbar bg-base-100 border-base-300 sticky top-0 z-10 border-b">
|
||||
<div className="flex-none lg:hidden">
|
||||
<label htmlFor="nav-drawer" className="btn btn-square btn-ghost" aria-label="Open menu">
|
||||
<LuMenu className="size-5" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex-1 px-2 text-lg font-semibold">scrunner</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-square"
|
||||
onClick={toggle}
|
||||
title={theme === "dark" ? "Light" : "Dark"}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? <LuSun className="size-5" /> : <LuMoon className="size-5" />}
|
||||
</button>
|
||||
<span className="hidden sm:inline text-sm opacity-70 px-2">{user.username}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-square"
|
||||
title="Log out"
|
||||
aria-label="Log out"
|
||||
onClick={() =>
|
||||
logout.mutate(undefined, { onSuccess: () => navigate("/") })
|
||||
}
|
||||
>
|
||||
<LuLogOut className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main className="flex-1 p-4">{children}</main>
|
||||
</div>
|
||||
<div className="drawer-side z-20">
|
||||
<label htmlFor="nav-drawer" aria-label="Close menu" className="drawer-overlay" />
|
||||
<ul className="menu bg-base-200 min-h-full w-64 p-4">
|
||||
<li className="menu-title">scrunner</li>
|
||||
{navItems.map(({ to, label, icon: Icon, end }) => (
|
||||
<li key={to}>
|
||||
<NavLink to={to} end={end} onClick={closeDrawer}>
|
||||
<Icon className="size-4" />
|
||||
{label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useId, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import mermaid from "mermaid";
|
||||
import { useTheme } from "../theme.jsx";
|
||||
|
||||
function findNode(root, id) {
|
||||
return (
|
||||
root.querySelector(`#${CSS.escape(id)}`) ||
|
||||
root.querySelector(`[id="${CSS.escape(id)}"]`) ||
|
||||
root.querySelector(`[id*="${CSS.escape(id)}"]`)
|
||||
);
|
||||
}
|
||||
|
||||
export function MermaidDiagram({ chart, scriptIds = {} }) {
|
||||
const { theme } = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef(null);
|
||||
const reactId = useId().replace(/:/g, "");
|
||||
|
||||
const scriptKey = JSON.stringify(scriptIds);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chart || !ref.current) {
|
||||
if (ref.current) ref.current.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose",
|
||||
suppressErrorRendering: true,
|
||||
theme: theme === "dark" ? "dark" : "default",
|
||||
});
|
||||
|
||||
const ids = JSON.parse(scriptKey);
|
||||
const renderId = `mmd-${reactId}-${Date.now()}`;
|
||||
mermaid
|
||||
.render(renderId, chart)
|
||||
.then(({ svg }) => {
|
||||
if (cancelled || !ref.current) return;
|
||||
ref.current.innerHTML = svg;
|
||||
for (const [nodeId, script] of Object.entries(ids)) {
|
||||
const el = findNode(ref.current, nodeId);
|
||||
if (!el) continue;
|
||||
el.style.cursor = "pointer";
|
||||
el.setAttribute("title", script);
|
||||
el.addEventListener("click", () => {
|
||||
navigate(`/scripts?edit=${encodeURIComponent(script)}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled || !ref.current) return;
|
||||
ref.current.innerHTML = "";
|
||||
const p = document.createElement("p");
|
||||
p.className = "text-error text-sm";
|
||||
p.textContent = "Could not render diagram";
|
||||
ref.current.appendChild(p);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [chart, scriptKey, theme, navigate, reactId]);
|
||||
|
||||
if (!chart) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="mermaid-clickable overflow-x-auto rounded-box border border-base-300 bg-base-200 p-3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user