diff options
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/chains.tsx | 496 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 4 |
2 files changed, 2 insertions, 498 deletions
diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx deleted file mode 100644 index 9b33304..0000000 --- a/makima/frontend/src/routes/chains.tsx +++ /dev/null @@ -1,496 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useParams, useNavigate } from "react-router"; -import { Masthead } from "../components/Masthead"; -import { ChainList } from "../components/chains/ChainList"; -import { ChainEditor } from "../components/chains/ChainEditor"; -import { useChains } from "../hooks/useChains"; -import { useAuth } from "../contexts/AuthContext"; -import type { - ChainSummary, - ChainWithContracts, - ChainGraphResponse, - CreateChainRequest, - AddChainRepositoryRequest, - RepositoryHistoryEntry, -} from "../lib/api"; -import { getRepositorySuggestions } from "../lib/api"; - -export default function ChainsPage() { - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - const navigate = useNavigate(); - - // Redirect to login if not authenticated (when auth is configured) - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - // Show loading while checking auth - if (authLoading) { - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading...</p> - </main> - </div> - ); - } - - // Don't render if not authenticated (will redirect) - if (isAuthConfigured && !isAuthenticated) { - return null; - } - - return <ChainsPageContent />; -} - -function ChainsPageContent() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const { - chains, - loading, - error, - createNewChain, - archiveExistingChain, - getChainById, - getGraph, - } = useChains(); - - const [chainDetail, setChainDetail] = useState<ChainWithContracts | null>(null); - const [chainGraph, setChainGraph] = useState<ChainGraphResponse | null>(null); - const [detailLoading, setDetailLoading] = useState(false); - const [isCreating, setIsCreating] = useState(false); - - // Load chain detail when ID changes - useEffect(() => { - if (id) { - setDetailLoading(true); - Promise.all([getChainById(id), getGraph(id)]).then(([chain, graph]) => { - setChainDetail(chain); - setChainGraph(graph); - setDetailLoading(false); - }); - } else { - setChainDetail(null); - setChainGraph(null); - } - }, [id, getChainById, getGraph]); - - const handleSelect = useCallback( - (chainId: string) => { - navigate(`/chains/${chainId}`); - }, - [navigate] - ); - - const handleBack = useCallback(() => { - navigate("/chains"); - }, [navigate]); - - const handleCreate = useCallback(() => { - setIsCreating(true); - }, []); - - const handleCreateSubmit = useCallback( - async (name: string, description: string, repositories: AddChainRepositoryRequest[]) => { - const data: CreateChainRequest = { - name: name.trim(), - description: description.trim() || undefined, - repositories: repositories.length > 0 ? repositories : undefined, - }; - - try { - const result = await createNewChain(data); - if (result) { - setIsCreating(false); - navigate(`/chains/${result.id}`); - } - } catch (err) { - console.error("Failed to create chain:", err); - } - }, - [createNewChain, navigate] - ); - - const handleCreateCancel = useCallback(() => { - setIsCreating(false); - }, []); - - const handleArchive = useCallback( - async (chain: ChainSummary) => { - if (confirm(`Are you sure you want to archive "${chain.name}"?`)) { - const success = await archiveExistingChain(chain.id); - if (success && chain.id === id) { - navigate("/chains"); - } - } - }, - [archiveExistingChain, id, navigate] - ); - - const handleRefresh = useCallback(async () => { - if (id) { - const [chain, graph] = await Promise.all([getChainById(id), getGraph(id)]); - setChainDetail(chain); - setChainGraph(graph); - } - }, [id, getChainById, getGraph]); - - const handleContractClick = useCallback( - (contractId: string) => { - navigate(`/contracts/${contractId}`); - }, - [navigate] - ); - - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden"> - {error && ( - <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm"> - {error} - </div> - )} - - {/* Create chain modal */} - {isCreating && ( - <CreateChainModal - onSubmit={handleCreateSubmit} - onCancel={handleCreateCancel} - /> - )} - - <div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0"> - {/* Chain list */} - <ChainList - chains={chains} - loading={loading} - onSelect={handleSelect} - onCreate={handleCreate} - selectedId={id} - onArchive={handleArchive} - /> - - {/* Chain detail/editor or empty state */} - {chainDetail ? ( - <ChainEditor - chain={chainDetail} - graph={chainGraph} - loading={detailLoading} - onBack={handleBack} - onRefresh={handleRefresh} - onContractClick={handleContractClick} - /> - ) : ( - <div className="panel h-full flex items-center justify-center"> - <div className="text-center"> - <p className="font-mono text-sm text-[#555] mb-4"> - Select a chain or create a new one - </p> - <button - onClick={handleCreate} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New Chain - </button> - </div> - </div> - )} - </div> - </main> - </div> - ); -} - -interface CreateChainModalProps { - onSubmit: (name: string, description: string, repositories: AddChainRepositoryRequest[]) => void; - onCancel: () => void; -} - -type RepoMode = "remote" | "local" | null; - -function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [repositories, setRepositories] = useState<AddChainRepositoryRequest[]>([]); - - // Repository input state - const [repoMode, setRepoMode] = useState<RepoMode>(null); - const [repoName, setRepoName] = useState(""); - const [repoUrl, setRepoUrl] = useState(""); - const [repoPath, setRepoPath] = useState(""); - - // Suggestions - const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]); - const [showSuggestions, setShowSuggestions] = useState(false); - - // Load suggestions when mode changes - useEffect(() => { - if (repoMode) { - getRepositorySuggestions(repoMode, undefined, 10) - .then((res) => { - setSuggestions(res.entries); - setShowSuggestions(res.entries.length > 0); - }) - .catch(() => { - setSuggestions([]); - setShowSuggestions(false); - }); - } else { - setSuggestions([]); - setShowSuggestions(false); - } - }, [repoMode]); - - const applySuggestion = (suggestion: RepositoryHistoryEntry) => { - setRepoName(suggestion.name); - if (suggestion.repositoryUrl) setRepoUrl(suggestion.repositoryUrl); - if (suggestion.localPath) setRepoPath(suggestion.localPath); - setShowSuggestions(false); - }; - - const handleAddRepo = () => { - if (!repoName.trim()) return; - if (repoMode === "remote" && !repoUrl.trim()) return; - if (repoMode === "local" && !repoPath.trim()) return; - - const newRepo: AddChainRepositoryRequest = { - name: repoName.trim(), - sourceType: repoMode || "remote", - isPrimary: repositories.length === 0, // First one is primary - ...(repoMode === "remote" ? { repositoryUrl: repoUrl.trim() } : { localPath: repoPath.trim() }), - }; - - setRepositories([...repositories, newRepo]); - setRepoMode(null); - setRepoName(""); - setRepoUrl(""); - setRepoPath(""); - }; - - const handleRemoveRepo = (index: number) => { - const newRepos = repositories.filter((_, i) => i !== index); - // If we removed the primary, make the first one primary - if (newRepos.length > 0 && repositories[index]?.isPrimary) { - newRepos[0].isPrimary = true; - } - setRepositories(newRepos); - }; - - const handleSubmit = () => { - if (name.trim()) { - onSubmit(name.trim(), description.trim(), repositories); - } - }; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> - <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - Create Chain - </h3> - - <div className="space-y-4"> - {/* Chain name */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Chain Name * - </label> - <input - type="text" - value={name} - onChange={(e) => setName(e.target.value)} - placeholder="e.g., Feature Implementation" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - autoFocus - /> - </div> - - {/* Description */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Description (optional) - </label> - <textarea - value={description} - onChange={(e) => setDescription(e.target.value)} - placeholder="Describe what this chain accomplishes..." - rows={2} - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none" - /> - </div> - - {/* Repositories */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2"> - Repositories - </label> - - {/* Added repositories */} - {repositories.length > 0 && ( - <div className="space-y-2 mb-3"> - {repositories.map((repo, index) => ( - <div - key={index} - className="flex items-center gap-2 px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)]" - > - <span className="font-mono text-[10px] text-[#556677] uppercase"> - {repo.sourceType === "remote" ? "URL" : "Local"} - </span> - <span className="font-mono text-xs text-[#dbe7ff] flex-1 truncate"> - {repo.name} - </span> - {repo.isPrimary && ( - <span className="font-mono text-[8px] text-[#75aafc] uppercase px-1 border border-[#75aafc]/30"> - primary - </span> - )} - <button - onClick={() => handleRemoveRepo(index)} - className="font-mono text-xs text-[#556677] hover:text-red-400" - > - ✕ - </button> - </div> - ))} - </div> - )} - - {/* Add repository form */} - {repoMode ? ( - <div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3"> - <div className="flex items-center justify-between"> - <span className="font-mono text-[10px] text-[#75aafc] uppercase"> - Add {repoMode === "remote" ? "Remote" : "Local"} Repository - </span> - {suggestions.length > 0 && ( - <button - onClick={() => setShowSuggestions(!showSuggestions)} - className="font-mono text-[10px] text-[#556677] hover:text-[#9bc3ff]" - > - {showSuggestions ? "Hide" : `${suggestions.length} suggestions`} - </button> - )} - </div> - - {/* Suggestions dropdown */} - {showSuggestions && suggestions.length > 0 && ( - <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-28 overflow-y-auto"> - {suggestions.map((s) => ( - <button - key={s.id} - onClick={() => applySuggestion(s)} - className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" - > - <div className="flex items-center justify-between"> - <span className="text-[#9bc3ff] truncate">{s.name}</span> - <span className="text-[10px] text-[#556677]">{s.useCount}×</span> - </div> - <div className="text-[10px] text-[#556677] truncate"> - {repoMode === "local" ? s.localPath : s.repositoryUrl} - </div> - </button> - ))} - </div> - )} - - <input - type="text" - value={repoName} - onChange={(e) => setRepoName(e.target.value)} - placeholder="Repository name" - className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" - /> - - {repoMode === "remote" ? ( - <input - type="text" - value={repoUrl} - onChange={(e) => setRepoUrl(e.target.value)} - placeholder="https://github.com/owner/repo" - className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" - /> - ) : ( - <input - type="text" - value={repoPath} - onChange={(e) => setRepoPath(e.target.value)} - placeholder="/path/to/repository" - className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" - /> - )} - - <div className="flex gap-2"> - <button - onClick={() => setRepoMode(null)} - className="px-3 py-1.5 font-mono text-xs text-[#556677] hover:text-[#9bc3ff]" - > - Cancel - </button> - <button - onClick={handleAddRepo} - disabled={ - !repoName.trim() || - (repoMode === "remote" && !repoUrl.trim()) || - (repoMode === "local" && !repoPath.trim()) - } - className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50" - > - Add - </button> - </div> - </div> - ) : ( - <div className="flex gap-2"> - <button - onClick={() => setRepoMode("remote")} - className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]" - > - + Remote - </button> - <button - onClick={() => setRepoMode("local")} - className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]" - > - + Local - </button> - </div> - )} - - {repositories.length === 0 && !repoMode && ( - <p className="font-mono text-[10px] text-[#556677] mt-2"> - Add repositories that contracts in this chain will work with - </p> - )} - </div> - - <p className="font-mono text-xs text-[#8b949e]"> - A chain links multiple contracts together in a DAG. Contracts depend on each - other and start automatically when dependencies complete. - </p> - - {/* Actions */} - <div className="flex gap-2 justify-end pt-2"> - <button - onClick={onCancel} - className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - Cancel - </button> - <button - onClick={handleSubmit} - disabled={!name.trim()} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - > - Create - </button> - </div> - </div> - </div> - </div> - ); -} diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index 51fd57a..35e5703 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -769,7 +769,7 @@ function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; grap // Build edges from dependencies const stepEdges: Edge[] = []; directive.steps.forEach((step) => { - step.dependsOn.forEach((depName) => { + (step.dependsOn ?? []).forEach((depName) => { const depStep = directive.steps.find((s) => s.name === depName); if (depStep) { stepEdges.push({ @@ -919,7 +919,7 @@ function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; grap {step.description && ( <p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p> )} - {step.dependsOn.length > 0 && ( + {step.dependsOn?.length > 0 && ( <div className="font-mono text-[10px] text-[#556677] mt-1"> Depends on: {step.dependsOn.join(", ")} </div> |
