diff --git a/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx b/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx index 055c8b10..9bcae00a 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx @@ -2,7 +2,6 @@ import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import { Badge } from "@/components/ui/badge"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn, getShortenedNumberDisplayString } from "@/lib/utils"; import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon, CircleIcon } from "lucide-react"; import { usePathname } from "next/navigation"; diff --git a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx index cc5d5478..8e1889df 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useToast } from "@/components/hooks/use-toast"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; @@ -25,6 +26,7 @@ export const ProgressIndicator = ({ }: ProgressIndicatorProps) => { const domain = useDomain(); const router = useRouter(); + const { toast } = useToast(); if (numRepos === 0) { return null; @@ -51,6 +53,9 @@ export const ProgressIndicator = ({ className="h-6 w-6 text-muted-foreground" onClick={() => { router.refresh(); + toast({ + description: "Page refreshed", + }); }} > @@ -105,14 +110,13 @@ const RepoItem = ({ repo }: { repo: RepositoryQuery }) => { return ( - {repoIcon} {displayName} - + ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/repos/columns.tsx b/packages/web/src/app/[domain]/repos/columns.tsx index 5ad1657e..a1cd1bdc 100644 --- a/packages/web/src/app/[domain]/repos/columns.tsx +++ b/packages/web/src/app/[domain]/repos/columns.tsx @@ -2,72 +2,51 @@ import { Button } from "@/components/ui/button" import type { ColumnDef } from "@tanstack/react-table" -import { ArrowUpDown, Clock, Loader2, CheckCircle2, XCircle, Trash2, Check, ListFilter } from "lucide-react" +import { ArrowUpDown, Clock, Loader2, CheckCircle2, Check, ListFilter } from "lucide-react" import Image from "next/image" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { cn, getRepoImageSrc } from "@/lib/utils" -import { RepoIndexingStatus } from "@sourcebot/db"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import Link from "next/link" import { getBrowsePath } from "../browse/hooks/utils" +export type RepoStatus = 'syncing' | 'indexed' | 'not-indexed'; + export type RepositoryColumnInfo = { repoId: number repoName: string; repoDisplayName: string imageUrl?: string - repoIndexingStatus: RepoIndexingStatus + status: RepoStatus lastIndexed: string } -const statusLabels = { - [RepoIndexingStatus.NEW]: "Queued", - [RepoIndexingStatus.IN_INDEX_QUEUE]: "Queued", - [RepoIndexingStatus.INDEXING]: "Indexing", - [RepoIndexingStatus.INDEXED]: "Indexed", - [RepoIndexingStatus.FAILED]: "Failed", - [RepoIndexingStatus.IN_GC_QUEUE]: "Deleting", - [RepoIndexingStatus.GARBAGE_COLLECTING]: "Deleting", - [RepoIndexingStatus.GARBAGE_COLLECTION_FAILED]: "Deletion Failed" +const statusLabels: Record = { + 'syncing': "Syncing", + 'indexed': "Indexed", + 'not-indexed': "Pending", }; -const StatusIndicator = ({ status }: { status: RepoIndexingStatus }) => { +const StatusIndicator = ({ status }: { status: RepoStatus }) => { let icon = null let description = "" let className = "" switch (status) { - case RepoIndexingStatus.NEW: - case RepoIndexingStatus.IN_INDEX_QUEUE: - icon = - description = "Repository is queued for indexing" - className = "text-yellow-600 bg-yellow-50 dark:bg-yellow-900/20 dark:text-yellow-400" - break - case RepoIndexingStatus.INDEXING: + case 'syncing': icon = - description = "Repository is being indexed" + description = "Repository is currently syncing" className = "text-blue-600 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400" break - case RepoIndexingStatus.INDEXED: + case 'indexed': icon = - description = "Repository has been successfully indexed" + description = "Repository has been successfully indexed and is up to date" className = "text-green-600 bg-green-50 dark:bg-green-900/20 dark:text-green-400" break - case RepoIndexingStatus.FAILED: - icon = - description = "Repository indexing failed" - className = "text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400" - break - case RepoIndexingStatus.IN_GC_QUEUE: - case RepoIndexingStatus.GARBAGE_COLLECTING: - icon = - description = "Repository is being deleted" - className = "text-gray-600 bg-gray-50 dark:bg-gray-900/20 dark:text-gray-400" - break - case RepoIndexingStatus.GARBAGE_COLLECTION_FAILED: - icon = - description = "Repository deletion failed" - className = "text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400" + case 'not-indexed': + icon = + description = "Repository is pending initial sync" + className = "text-yellow-600 bg-yellow-50 dark:bg-yellow-900/20 dark:text-yellow-400" break } @@ -130,9 +109,9 @@ export const columns = (domain: string): ColumnDef[] => [ }, }, { - accessorKey: "repoIndexingStatus", + accessorKey: "status", header: ({ column }) => { - const uniqueLabels = Array.from(new Set(Object.values(statusLabels))); + const uniqueLabels = Object.values(statusLabels); const currentFilter = column.getFilterValue() as string | undefined; return ( @@ -173,12 +152,12 @@ export const columns = (domain: string): ColumnDef[] => [ ) }, cell: ({ row }) => { - return + return }, filterFn: (row, id, value) => { if (value === undefined) return true; - const status = row.getValue(id) as RepoIndexingStatus; + const status = row.getValue(id) as RepoStatus; return statusLabels[status] === value; }, }, @@ -191,14 +170,14 @@ export const columns = (domain: string): ColumnDef[] => [ onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} className="px-0 font-medium hover:bg-transparent focus:bg-transparent active:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" > - Last Indexed + Last Synced ), cell: ({ row }) => { if (!row.original.lastIndexed) { - return
-
; + return
Never
; } const date = new Date(row.original.lastIndexed) return ( diff --git a/packages/web/src/app/[domain]/repos/page.tsx b/packages/web/src/app/[domain]/repos/page.tsx index 4502dafc..60031811 100644 --- a/packages/web/src/app/[domain]/repos/page.tsx +++ b/packages/web/src/app/[domain]/repos/page.tsx @@ -1,8 +1,20 @@ -import { RepositoryTable } from "./repositoryTable"; -import { getOrgFromDomain } from "@/data/org"; -import { PageNotFound } from "../components/pageNotFound"; -import { Header } from "../components/header"; +import { auth } from "@/auth"; import { env } from "@/env.mjs"; +import { getPrismaClient } from "@/prisma"; +import { RepoJob } from "@sourcebot/db"; +import { Header } from "../components/header"; +import { RepoStatus } from "./columns"; +import { RepositoryTable } from "./repositoryTable"; + +function getRepoStatus(repo: { indexedAt: Date | null, jobs: RepoJob[] }): RepoStatus { + const latestJob = repo.jobs[0]; + + if (latestJob?.status === 'PENDING' || latestJob?.status === 'IN_PROGRESS') { + return 'syncing'; + } + + return repo.indexedAt ? 'indexed' : 'not-indexed'; +} export default async function ReposPage(props: { params: Promise<{ domain: string }> }) { const params = await props.params; @@ -11,22 +23,33 @@ export default async function ReposPage(props: { params: Promise<{ domain: strin domain } = params; - const org = await getOrgFromDomain(domain); - if (!org) { - return - } + const session = await auth(); + const prisma = getPrismaClient(session?.user?.id); + + const repos = await prisma.repo.findMany({ + include: { + jobs: true, + } + }); return (

Repositories

-
-
- -
+
+ ({ + repoId: repo.id, + repoName: repo.name, + repoDisplayName: repo.displayName ?? repo.name, + imageUrl: repo.imageUrl ?? undefined, + indexedAt: repo.indexedAt ?? undefined, + status: getRepoStatus(repo), + }))} + domain={domain} + isAddReposButtonVisible={env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED === 'true'} + />
) diff --git a/packages/web/src/app/[domain]/repos/repositoryTable.tsx b/packages/web/src/app/[domain]/repos/repositoryTable.tsx index 8d9dc0f1..9b67cca7 100644 --- a/packages/web/src/app/[domain]/repos/repositoryTable.tsx +++ b/packages/web/src/app/[domain]/repos/repositoryTable.tsx @@ -1,118 +1,81 @@ "use client"; -import { DataTable } from "@/components/ui/data-table"; -import { columns, RepositoryColumnInfo } from "./columns"; -import { unwrapServiceError } from "@/lib/utils"; -import { useQuery } from "@tanstack/react-query"; -import { useDomain } from "@/hooks/useDomain"; -import { RepoIndexingStatus } from "@sourcebot/db"; -import { useMemo } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { env } from "@/env.mjs"; +import { useToast } from "@/components/hooks/use-toast"; import { Button } from "@/components/ui/button"; -import { PlusIcon } from "lucide-react"; +import { DataTable } from "@/components/ui/data-table"; +import { PlusIcon, RefreshCwIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useMemo, useState } from "react"; +import { columns, RepositoryColumnInfo, RepoStatus } from "./columns"; import { AddRepositoryDialog } from "./components/addRepositoryDialog"; -import { useState } from "react"; -import { getRepos } from "@/app/api/(client)/client"; interface RepositoryTableProps { - isAddReposButtonVisible: boolean + repos: { + repoId: number; + repoName: string; + repoDisplayName: string; + imageUrl?: string; + indexedAt?: Date; + status: RepoStatus; + }[]; + domain: string; + isAddReposButtonVisible: boolean; } export const RepositoryTable = ({ + repos, + domain, isAddReposButtonVisible, }: RepositoryTableProps) => { - const domain = useDomain(); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - - const { data: repos, isLoading: reposLoading, error: reposError } = useQuery({ - queryKey: ['repos'], - queryFn: async () => { - return await unwrapServiceError(getRepos()); - }, - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - refetchIntervalInBackground: true, - }); + const router = useRouter(); + const { toast } = useToast(); const tableRepos = useMemo(() => { - if (reposLoading) return Array(4).fill(null).map(() => ({ - repoId: 0, - repoName: "", - repoDisplayName: "", - repoIndexingStatus: RepoIndexingStatus.NEW, - lastIndexed: "", - imageUrl: "", - })); - - if (!repos) return []; return repos.map((repo): RepositoryColumnInfo => ({ repoId: repo.repoId, repoName: repo.repoName, repoDisplayName: repo.repoDisplayName ?? repo.repoName, imageUrl: repo.imageUrl, - repoIndexingStatus: repo.repoIndexingStatus as RepoIndexingStatus, + status: repo.status, lastIndexed: repo.indexedAt?.toISOString() ?? "", })).sort((a, b) => { - const getPriorityFromStatus = (status: RepoIndexingStatus) => { + const getPriorityFromStatus = (status: RepoStatus) => { switch (status) { - case RepoIndexingStatus.IN_INDEX_QUEUE: - case RepoIndexingStatus.INDEXING: - return 0 // Highest priority - currently indexing - case RepoIndexingStatus.FAILED: - return 1 // Second priority - failed repos need attention - case RepoIndexingStatus.INDEXED: + case 'syncing': + return 0 // Highest priority - currently syncing + case 'not-indexed': + return 1 // Second priority - not yet indexed + case 'indexed': return 2 // Third priority - successfully indexed default: - return 3 // Lowest priority - other statuses (NEW, etc.) + return 3 } } // Sort by priority first - const aPriority = getPriorityFromStatus(a.repoIndexingStatus); - const bPriority = getPriorityFromStatus(b.repoIndexingStatus); - + const aPriority = getPriorityFromStatus(a.status); + const bPriority = getPriorityFromStatus(b.status); + if (aPriority !== bPriority) { - return aPriority - bPriority; // Lower priority number = higher precedence + return aPriority - bPriority; } - + // If same priority, sort by last indexed date (most recent first) - return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime(); + if (a.lastIndexed && b.lastIndexed) { + return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime(); + } + + // Put items without dates at the end + if (!a.lastIndexed) return 1; + if (!b.lastIndexed) return -1; + return 0; }); - }, [repos, reposLoading]); + }, [repos]); const tableColumns = useMemo(() => { - if (reposLoading) { - return columns(domain).map((column) => { - if ('accessorKey' in column && column.accessorKey === "name") { - return { - ...column, - cell: () => ( -
- {/* Avatar skeleton */} - {/* Repository name skeleton */} -
- ), - } - } - - return { - ...column, - cell: () => ( -
- -
- ), - } - }) - } - return columns(domain); - }, [reposLoading, domain]); - - - if (reposError) { - return
Error loading repositories
; - } + }, [domain]); return ( <> @@ -121,18 +84,35 @@ export const RepositoryTable = ({ data={tableRepos} searchKey="repoDisplayName" searchPlaceholder="Search repositories..." - headerActions={isAddReposButtonVisible && ( - + headerActions={( +
+ + {isAddReposButtonVisible && ( + + )} +
)} /> - + ({ return (
-
+
repo.repo.repoIndexingStatus === RepoIndexingStatus.FAILED).map(repo => repo.repo.id) ?? []; - if (failedRepos.length > 0) { - await prisma.repo.updateMany({ - where: { - id: { - in: failedRepos, - } - }, - data: { - repoIndexingStatus: RepoIndexingStatus.NEW, - } - }) - } } } diff --git a/packages/web/src/prisma.ts b/packages/web/src/prisma.ts index 1d4b7585..6bd9aff8 100644 --- a/packages/web/src/prisma.ts +++ b/packages/web/src/prisma.ts @@ -57,4 +57,8 @@ export const userScopedPrismaClientExtension = (userId?: string) => { } }) }) +} + +export const getPrismaClient = (userId?: string) => { + return prisma.$extends(userScopedPrismaClientExtension(userId)) as PrismaClient; } \ No newline at end of file diff --git a/packages/web/src/withAuthV2.ts b/packages/web/src/withAuthV2.ts index c6cbb8bb..1ffd541e 100644 --- a/packages/web/src/withAuthV2.ts +++ b/packages/web/src/withAuthV2.ts @@ -1,4 +1,4 @@ -import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma"; +import { getPrismaClient, prisma as __unsafePrisma } from "@/prisma"; import { hashSecret } from "@sourcebot/crypto"; import { ApiKey, Org, OrgRole, PrismaClient, User } from "@sourcebot/db"; import { headers } from "next/headers"; @@ -88,7 +88,7 @@ export const getAuthContext = async (): Promise