import { useEffect, useRef, useState } from "react"; import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { LuPencil, LuPlus, LuTrash2 } from "react-icons/lu"; import { errorMessage } from "../api/client.js"; import { useDeleteProfile, useProfileUsage, useProfiles } from "../api/hooks.js"; import { Modal } from "../components/Modal.jsx"; import { ProfileEditorModal } from "../components/ProfileEditorModal.jsx"; import { ScriptIcon } from "../components/ScriptIcon.jsx"; import { useRouteDrivenModal } from "../hooks/useRouteDrivenModal.js"; import { formatTime } from "../lib/format"; import { DEFAULT_OWNER } from "../lib/tenant.js"; function previewConfig(config) { if (!config || typeof config !== "object") return ""; if (typeof config.url === "string" && config.url) return config.url; const first = Object.entries(config).find( ([, v]) => v != null && (typeof v === "string" || typeof v === "number" || typeof v === "boolean"), ); if (!first) return ""; return `${first[0]}=${String(first[1])}`; } function formatUsage(usages) { const names = (usages ?? []).map((u) => u.name); if (names.length === 0) return ""; if (names.length === 1) return `${names[0]} uses this.`; if (names.length === 2) return `${names[0]} and ${names[1]} use this.`; const rest = names.length - 2; return `${names[0]}, ${names[1]}, and ${rest} other workflow${rest === 1 ? "" : "s"} use this.`; } export function ProfilesPage() { const navigate = useNavigate(); const location = useLocation(); const { owner: routeOwner, name: routeName } = useParams(); const [params] = useSearchParams(); const isNewRoute = /\/profiles\/new\/?$/.test(location.pathname); const isEditRoute = Boolean(routeOwner && routeName); const { data: profiles = [], isLoading } = useProfiles(); const del = useDeleteProfile(); const [confirmDelete, setConfirmDelete] = useState(null); const [highlightName, setHighlightName] = useState(() => routeName || ""); const highlightRef = useRef(null); const listPath = "/profiles"; const { editor, closeEditor } = useRouteDrivenModal({ isNewRoute, isEditRoute, listPath, newRouteKey: () => `new:${params.get("name") || ""}:${params.get("script") || ""}`, canOpenNew: () => true, buildNewEditor: () => ({ mode: "add", initial: { owner: DEFAULT_OWNER, name: params.get("name") || "", script: params.get("script") || "", config: {}, description: "", }, }), editRouteKey: () => `edit:${routeOwner}/${routeName}`, canOpenEdit: () => !isLoading, onOpenEdit: () => setHighlightName(routeName), buildEditEditor: () => { const row = profiles.find((p) => p.owner === routeOwner && p.name === routeName); if (row) { return { mode: "edit", initial: { owner: row.owner, name: row.name, script: row.script, config: row.config, description: row.description, }, usageCount: row.usageCount ?? 0, }; } return { mode: "add", initial: { owner: routeOwner || DEFAULT_OWNER, name: routeName, script: "", config: {}, description: "", }, }; }, newDeps: [params], editDeps: [routeOwner, routeName, isLoading, profiles], }); useEffect(() => { if (!highlightName || isLoading) return; highlightRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" }); }, [highlightName, isLoading, profiles]); return (

Profiles

A profile is saved config for one script. Workflow steps can use it as initial config; local keys override. Changing a profile updates every workflow that references it.

{isLoading ? ( ) : profiles.length === 0 ? (

No profiles yet.

) : (
{profiles.map((row) => { const highlighted = highlightName && row.name === highlightName && (!routeOwner || row.owner === routeOwner); const preview = previewConfig(row.config); return (

{row.name}

{row.script}

{row.description ? (

{row.description}

) : null} {preview ? (

{preview}

) : null}

{row.usageCount ? `${row.usageCount} workflow${row.usageCount === 1 ? "" : "s"}` : "unused"} {row.updated_at ? ` · ${formatTime(row.updated_at)}` : ""}

); })}
)} {editor ? ( { setHighlightName(saved?.name || editor.initial.name); }} /> ) : null} {confirmDelete ? ( { del.reset(); setConfirmDelete(null); }} /> ) : null}
); } function DeleteProfileDialog({ profile, del, onClose }) { const [force, setForce] = useState(false); const usage = useProfileUsage(profile.id, true); const usages = usage.data ?? []; const used = usages.length > 0; const title = `Delete ${profile.name}?`; return (

{title}

{usage.isLoading ? (

Checking workflow usage…

) : used ? ( <>

{formatUsage(usages)}

{force ? (

Workflows that still reference this profile will fail until you fix them.

) : null} ) : (

This cannot be undone.

)} {del.isError ? (

{errorMessage(del.error)}

) : null}
{used && !force ? ( ) : ( )}
); }