summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/RepositoryPanel.tsx
blob: 4170cfb39fd7e48361685e02a40046a2787d1e56 (plain) (tree)



































































































































































































































































                                                                                                                                                                                                
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>
  );
}