Files
urapt/urapt-web/src/features/repos/RepoDetail.tsx
T
owen 39525bd5f0 Add urapt-web: web UI for urapt-server instances
A Vite + React 18 + TypeScript SPA, same-origin with the API (no CORS,
no Go server changes). Full management parity with the urapt CLI:

- First-run setup (admin bootstrap via needs_setup), login/logout
- Repositories: list, create, tabbed detail (overview edit/delete,
  members CRUD, distributions/components/architectures manager,
  package list + .deb upload + detail + delete, apt-config panel)
- API tokens: show-once creation + revoke
- Admin users: toggle admin / delete
- Settings + dark mode

Stack: Tailwind + shadcn/ui (New York), React Router v7 (lazy routes),
TanStack Query v5, Zustand, Vitest. Hand-written typed API client
mirroring shared/api + shared/models. Dev proxy forwards /api + /apt
to a local urapt-server; deploy behind nginx/Caddy (see urapt-web/README).
2026-06-28 18:42:46 -05:00

105 lines
3.6 KiB
TypeScript

import { useParams, Link } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PageHeader } from "@/components/common/page-header";
import { ErrorState } from "@/components/common/error-state";
import { Spinner } from "@/components/common/spinner";
import { VisibilityBadge } from "@/components/common/badges";
import { useRepository, useDeleteRepository } from "./repos-hooks";
import { useMembers } from "./repo-detail-hooks";
import { RepoProvider } from "./repo-context.tsx";
import { computeAccess, canWriteWith, canManageWith } from "./repo-context";
import { RepoOverview } from "./tabs/RepoOverview";
import { Members } from "./tabs/Members";
import { Structure } from "./tabs/Structure";
import { Packages } from "./tabs/Packages";
import { AptConfig } from "./tabs/AptConfig";
import { useMe } from "@/hooks/use-server";
import { useAuthStore } from "@/stores/auth";
export function RepoDetail() {
const { repo, tab } = useParams<{ repo: string; tab?: string }>();
const token = useAuthStore((s) => s.token);
const me = useMe(!!token);
const { data, isLoading, isError, error } = useRepository(repo ?? "");
const members = useMembers(repo ?? "");
const del = useDeleteRepository();
if (isLoading) {
return (
<div className="flex justify-center py-16">
<Spinner className="size-6" />
</div>
);
}
if (isError) {
return <ErrorState error={error} />;
}
if (!data) return null;
const myMember = members.data?.find((m) => m.user_id === me.data?.id);
const access = computeAccess(data, me.data ?? null, myMember?.access ?? null);
const canWrite = canWriteWith(access);
const canManage = canManageWith(access);
return (
<RepoProvider
value={{
repo: data,
me: me.data ?? null,
access,
canWrite,
canManage,
}}
>
<div className="space-y-6">
<Button asChild variant="ghost" size="sm" className="-ml-2">
<Link to="/">
<ArrowLeft className="size-4" /> Repositories
</Link>
</Button>
<PageHeader
title={data.name}
description={data.description || "No description"}
actions={<VisibilityBadge visibility={data.visibility} />}
/>
<Tabs value={tab ?? "overview"}>
<TabsList>
<TabsTrigger value="overview" asChild>
<Link to={`/repos/${data.name}`}>Overview</Link>
</TabsTrigger>
<TabsTrigger value="members" asChild>
<Link to={`/repos/${data.name}/members`}>Members</Link>
</TabsTrigger>
<TabsTrigger value="structure" asChild>
<Link to={`/repos/${data.name}/structure`}>Distributions</Link>
</TabsTrigger>
<TabsTrigger value="packages" asChild>
<Link to={`/repos/${data.name}/packages`}>Packages</Link>
</TabsTrigger>
<TabsTrigger value="apt-config" asChild>
<Link to={`/repos/${data.name}/apt-config`}>Apt config</Link>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="mt-2">
{tab === "members" ? (
<Members />
) : tab === "structure" ? (
<Structure />
) : tab === "packages" ? (
<Packages />
) : tab === "apt-config" ? (
<AptConfig />
) : (
<RepoOverview onDelete={del} />
)}
</div>
</div>
</RepoProvider>
);
}