sourcebot/packages/web/src/app/[domain]/repos/[id]/page.tsx

206 lines
8.8 KiB
TypeScript
Raw Normal View History

import { sew } from "@/actions"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { env } from "@/env.mjs"
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
import { ServiceErrorException } from "@/lib/serviceError"
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"
import { withOptionalAuthV2 } from "@/withAuthV2"
import { getConfigSettings, repoMetadataSchema } from "@sourcebot/shared"
import { ExternalLink, Info } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { BackButton } from "../../components/backButton"
import { DisplayDate } from "../../components/DisplayDate"
import { RepoBranchesTable } from "../components/repoBranchesTable"
import { RepoJobsTable } from "../components/repoJobsTable"
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const repo = await getRepoWithJobs(Number.parseInt(id))
if (isServiceError(repo)) {
throw new ServiceErrorException(repo);
}
const codeHostInfo = getCodeHostInfoForRepo({
codeHostType: repo.external_codeHostType,
name: repo.name,
displayName: repo.displayName ?? undefined,
webUrl: repo.webUrl ?? undefined,
});
const configSettings = await getConfigSettings(env.CONFIG_PATH);
const nextIndexAttempt = (() => {
const latestJob = repo.jobs.length > 0 ? repo.jobs[0] : null;
if (!latestJob) {
return undefined;
}
if (latestJob.completedAt) {
return new Date(latestJob.completedAt.getTime() + configSettings.reindexIntervalMs);
}
return undefined;
})();
const repoMetadata = repoMetadataSchema.parse(repo.metadata);
return (
<>
<div className="mb-6">
<BackButton
href={`/${SINGLE_TENANT_ORG_DOMAIN}/repos`}
name="Back to repositories"
className="mb-2"
/>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-semibold">{repo.displayName || repo.name}</h1>
<p className="text-muted-foreground mt-2">{repo.name}</p>
</div>
{(codeHostInfo && codeHostInfo.repoLink) && (
<Button variant="outline" asChild>
<Link href={codeHostInfo.repoLink} target="_blank" rel="noopener noreferrer" className="flex items-center">
<Image
src={codeHostInfo.icon}
alt={codeHostInfo.codeHostName}
className={cn("w-4 h-4 flex-shrink-0", codeHostInfo.iconClassName)}
/>
Open in {codeHostInfo.codeHostName}
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
)}
</div>
<div className="flex gap-2 mt-4">
{repo.isArchived && <Badge variant="secondary">Archived</Badge>}
{repo.isPublic && <Badge variant="outline">Public</Badge>}
</div>
</div>
<div className="grid gap-4 md:grid-cols-3 mb-8">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-1.5">
Created
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>When this repository was first added to Sourcebot</p>
</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent>
<span className="text-2xl font-semibold"><DisplayDate date={repo.createdAt} /></span>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-1.5">
Last indexed
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>The last time this repository was successfully indexed</p>
</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent>
<span className="text-2xl font-semibold">{repo.indexedAt ? <DisplayDate date={repo.indexedAt} /> : "Never"}</span>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-1.5">
Scheduled
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>When the next indexing job is scheduled to run</p>
</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent>
<span className="text-2xl font-semibold">{nextIndexAttempt ? <DisplayDate date={nextIndexAttempt} /> : "-"}</span>
</CardContent>
</Card>
</div>
{repoMetadata.indexedRevisions && (
<Card className="mb-8">
<CardHeader>
<div className="flex items-center gap-2">
<CardTitle>Indexed Branches</CardTitle>
</div>
<CardDescription>Branches that have been indexed for this repository. <Link href="https://docs.sourcebot.dev/docs/features/search/multi-branch-indexing" target="_blank" className="text-link hover:underline">Docs</Link></CardDescription>
</CardHeader>
<CardContent>
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
<RepoBranchesTable
indexRevisions={repoMetadata.indexedRevisions}
repoWebUrl={repo.webUrl}
repoCodeHostType={repo.external_codeHostType}
/>
</Suspense>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Indexing History</CardTitle>
<CardDescription>History of all indexing and cleanup jobs for this repository.</CardDescription>
</CardHeader>
<CardContent>
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
<RepoJobsTable data={repo.jobs} />
</Suspense>
</CardContent>
</Card>
</>
)
}
const getRepoWithJobs = async (repoId: number) => sew(() =>
withOptionalAuthV2(async ({ prisma, org }) => {
const repo = await prisma.repo.findUnique({
where: {
id: repoId,
orgId: org.id,
},
include: {
jobs: {
orderBy: {
createdAt: 'desc'
},
}
},
});
if (!repo) {
return notFound();
}
return repo;
})
);