diff options
Diffstat (limited to 'makima/frontend/src/components/contracts/RepositoryPanel.tsx')
| -rw-r--r-- | makima/frontend/src/components/contracts/RepositoryPanel.tsx | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/RepositoryPanel.tsx b/makima/frontend/src/components/contracts/RepositoryPanel.tsx new file mode 100644 index 0000000..4170cfb --- /dev/null +++ b/makima/frontend/src/components/contracts/RepositoryPanel.tsx @@ -0,0 +1,260 @@ +import { useState, useEffect } from "react"; +import type { + ContractRepository, + RepositorySourceType, + RepositoryStatus, + DaemonDirectory, +} from "../../lib/api"; +import { getDaemonDirectories } from "../../lib/api"; +import { DirectoryInput } from "../mesh/DirectoryInput"; + +interface RepositoryPanelProps { + repositories: ContractRepository[]; + onAddRemote: (name: string, url: string, isPrimary: boolean) => void; + onAddLocal: (name: string, path: string, isPrimary: boolean) => void; + onCreateManaged: (name: string, isPrimary: boolean) => void; + onDelete: (repoId: string) => void; + onSetPrimary: (repoId: string) => void; +} + +type AddMode = "remote" | "local" | "managed" | null; + +const sourceTypeLabels: Record<RepositorySourceType, string> = { + remote: "Remote", + local: "Local", + managed: "Managed", +}; + +const sourceTypeIcons: Record<RepositorySourceType, string> = { + remote: "GH", + local: "FS", + managed: "MK", +}; + +const statusColors: Record<RepositoryStatus, string> = { + ready: "text-green-400", + pending: "text-yellow-400", + creating: "text-cyan-400", + failed: "text-red-400", +}; + +export function RepositoryPanel({ + repositories, + onAddRemote, + onAddLocal, + onCreateManaged, + onDelete, + onSetPrimary, +}: RepositoryPanelProps) { + const [addMode, setAddMode] = useState<AddMode>(null); + const [name, setName] = useState(""); + const [url, setUrl] = useState(""); + const [path, setPath] = useState(""); + const [isPrimary, setIsPrimary] = useState(false); + // Daemon directory suggestions for local repositories + const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]); + + // Fetch daemon directories when "local" mode is selected + useEffect(() => { + if (addMode === "local") { + getDaemonDirectories() + .then((res) => setSuggestedDirectories(res.directories)) + .catch(() => setSuggestedDirectories([])); + } + }, [addMode]); + + const handleAdd = () => { + if (!name.trim()) return; + + if (addMode === "remote" && url.trim()) { + onAddRemote(name.trim(), url.trim(), isPrimary); + } else if (addMode === "local" && path.trim()) { + onAddLocal(name.trim(), path.trim(), isPrimary); + } else if (addMode === "managed") { + onCreateManaged(name.trim(), isPrimary); + } + + // Reset form + setAddMode(null); + setName(""); + setUrl(""); + setPath(""); + setIsPrimary(false); + }; + + const handleCancel = () => { + setAddMode(null); + setName(""); + setUrl(""); + setPath(""); + setIsPrimary(false); + }; + + return ( + <div className="space-y-4"> + {/* Repository list */} + {repositories.length === 0 ? ( + <p className="font-mono text-xs text-[#555]"> + No repositories configured + </p> + ) : ( + <div className="space-y-2"> + {repositories.map((repo) => ( + <div + key={repo.id} + className="flex items-center gap-3 p-3 border border-[rgba(117,170,252,0.2)]" + > + {/* Type icon */} + <span className="font-mono text-[10px] text-[#555] uppercase w-6"> + {sourceTypeIcons[repo.sourceType]} + </span> + + {/* Name and details */} + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2"> + <span className="font-mono text-sm text-[#dbe7ff] truncate"> + {repo.name} + </span> + {repo.isPrimary && ( + <span className="px-1 py-0.5 text-[8px] font-mono uppercase bg-[rgba(117,170,252,0.1)] text-[#75aafc] border border-[rgba(117,170,252,0.3)]"> + Primary + </span> + )} + </div> + <div className="font-mono text-[10px] text-[#555] truncate"> + {repo.repositoryUrl || repo.localPath || "(pending creation)"} + </div> + </div> + + {/* Status */} + <span + className={`font-mono text-[10px] uppercase ${ + statusColors[repo.status] + }`} + > + {repo.status} + </span> + + {/* Actions */} + <div className="flex items-center gap-1"> + {!repo.isPrimary && repo.status === "ready" && ( + <button + onClick={() => onSetPrimary(repo.id)} + className="p-1 font-mono text-[10px] text-[#555] hover:text-[#75aafc] transition-colors" + title="Set as primary" + > + * + </button> + )} + <button + onClick={() => onDelete(repo.id)} + className="p-1 font-mono text-[10px] text-[#555] hover:text-red-400 transition-colors" + title="Remove" + > + x + </button> + </div> + </div> + ))} + </div> + )} + + {/* Add repository form */} + {addMode ? ( + <div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3"> + <div className="flex items-center gap-2 mb-2"> + <span className="font-mono text-xs text-[#75aafc] uppercase"> + Add {sourceTypeLabels[addMode]} Repository + </span> + </div> + + <input + type="text" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Repository name" + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" + /> + + {addMode === "remote" && ( + <input + type="text" + value={url} + onChange={(e) => setUrl(e.target.value)} + placeholder="https://github.com/owner/repo" + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" + /> + )} + + {addMode === "local" && ( + <DirectoryInput + value={path} + onChange={setPath} + suggestions={suggestedDirectories} + placeholder="/path/to/repository" + /> + )} + + {addMode === "managed" && ( + <p className="font-mono text-xs text-[#555]"> + Makima will create this repository via the daemon. + </p> + )} + + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="checkbox" + checked={isPrimary} + onChange={(e) => setIsPrimary(e.target.checked)} + className="w-3 h-3" + /> + <span className="font-mono text-xs text-[#9bc3ff]"> + Set as primary repository + </span> + </label> + + <div className="flex gap-2"> + <button + onClick={handleCancel} + className="px-3 py-1.5 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleAdd} + disabled={ + !name.trim() || + (addMode === "remote" && !url.trim()) || + (addMode === "local" && !path.trim()) + } + className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + Add Repository + </button> + </div> + </div> + ) : ( + <div className="flex gap-2"> + <button + onClick={() => setAddMode("remote")} + className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" + > + + Remote + </button> + <button + onClick={() => setAddMode("local")} + className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" + > + + Local + </button> + <button + onClick={() => setAddMode("managed")} + className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" + > + + Managed + </button> + </div> + )} + </div> + ); +} |
