mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
repository table
This commit is contained in:
parent
cfb359351d
commit
c56433d8a9
10 changed files with 144 additions and 174 deletions
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { cn, getShortenedNumberDisplayString } from "@/lib/utils";
|
import { cn, getShortenedNumberDisplayString } from "@/lib/utils";
|
||||||
import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon, CircleIcon } from "lucide-react";
|
import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon, CircleIcon } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
@ -25,6 +26,7 @@ export const ProgressIndicator = ({
|
||||||
}: ProgressIndicatorProps) => {
|
}: ProgressIndicatorProps) => {
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
if (numRepos === 0) {
|
if (numRepos === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -51,6 +53,9 @@ export const ProgressIndicator = ({
|
||||||
className="h-6 w-6 text-muted-foreground"
|
className="h-6 w-6 text-muted-foreground"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
toast({
|
||||||
|
description: "Page refreshed",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className="w-3 h-3" />
|
<RefreshCwIcon className="w-3 h-3" />
|
||||||
|
|
@ -105,14 +110,13 @@ const RepoItem = ({ repo }: { repo: RepositoryQuery }) => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
href={'/'}
|
|
||||||
className={clsx("flex flex-row items-center gap-2 border rounded-md p-2 text-clip")}
|
className={clsx("flex flex-row items-center gap-2 border rounded-md p-2 text-clip")}
|
||||||
>
|
>
|
||||||
{repoIcon}
|
{repoIcon}
|
||||||
<span className="text-sm truncate">
|
<span className="text-sm truncate">
|
||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,72 +2,51 @@
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import type { ColumnDef } from "@tanstack/react-table"
|
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 Image from "next/image"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { cn, getRepoImageSrc } from "@/lib/utils"
|
import { cn, getRepoImageSrc } from "@/lib/utils"
|
||||||
import { RepoIndexingStatus } from "@sourcebot/db";
|
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { getBrowsePath } from "../browse/hooks/utils"
|
import { getBrowsePath } from "../browse/hooks/utils"
|
||||||
|
|
||||||
|
export type RepoStatus = 'syncing' | 'indexed' | 'not-indexed';
|
||||||
|
|
||||||
export type RepositoryColumnInfo = {
|
export type RepositoryColumnInfo = {
|
||||||
repoId: number
|
repoId: number
|
||||||
repoName: string;
|
repoName: string;
|
||||||
repoDisplayName: string
|
repoDisplayName: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
repoIndexingStatus: RepoIndexingStatus
|
status: RepoStatus
|
||||||
lastIndexed: string
|
lastIndexed: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels: Record<RepoStatus, string> = {
|
||||||
[RepoIndexingStatus.NEW]: "Queued",
|
'syncing': "Syncing",
|
||||||
[RepoIndexingStatus.IN_INDEX_QUEUE]: "Queued",
|
'indexed': "Indexed",
|
||||||
[RepoIndexingStatus.INDEXING]: "Indexing",
|
'not-indexed': "Pending",
|
||||||
[RepoIndexingStatus.INDEXED]: "Indexed",
|
|
||||||
[RepoIndexingStatus.FAILED]: "Failed",
|
|
||||||
[RepoIndexingStatus.IN_GC_QUEUE]: "Deleting",
|
|
||||||
[RepoIndexingStatus.GARBAGE_COLLECTING]: "Deleting",
|
|
||||||
[RepoIndexingStatus.GARBAGE_COLLECTION_FAILED]: "Deletion Failed"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusIndicator = ({ status }: { status: RepoIndexingStatus }) => {
|
const StatusIndicator = ({ status }: { status: RepoStatus }) => {
|
||||||
let icon = null
|
let icon = null
|
||||||
let description = ""
|
let description = ""
|
||||||
let className = ""
|
let className = ""
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case RepoIndexingStatus.NEW:
|
case 'syncing':
|
||||||
case RepoIndexingStatus.IN_INDEX_QUEUE:
|
|
||||||
icon = <Clock className="h-3.5 w-3.5" />
|
|
||||||
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:
|
|
||||||
icon = <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
icon = <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
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"
|
className = "text-blue-600 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400"
|
||||||
break
|
break
|
||||||
case RepoIndexingStatus.INDEXED:
|
case 'indexed':
|
||||||
icon = <CheckCircle2 className="h-3.5 w-3.5" />
|
icon = <CheckCircle2 className="h-3.5 w-3.5" />
|
||||||
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"
|
className = "text-green-600 bg-green-50 dark:bg-green-900/20 dark:text-green-400"
|
||||||
break
|
break
|
||||||
case RepoIndexingStatus.FAILED:
|
case 'not-indexed':
|
||||||
icon = <XCircle className="h-3.5 w-3.5" />
|
icon = <Clock className="h-3.5 w-3.5" />
|
||||||
description = "Repository indexing failed"
|
description = "Repository is pending initial sync"
|
||||||
className = "text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400"
|
className = "text-yellow-600 bg-yellow-50 dark:bg-yellow-900/20 dark:text-yellow-400"
|
||||||
break
|
|
||||||
case RepoIndexingStatus.IN_GC_QUEUE:
|
|
||||||
case RepoIndexingStatus.GARBAGE_COLLECTING:
|
|
||||||
icon = <Trash2 className="h-3.5 w-3.5" />
|
|
||||||
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 = <XCircle className="h-3.5 w-3.5" />
|
|
||||||
description = "Repository deletion failed"
|
|
||||||
className = "text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400"
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,9 +109,9 @@ export const columns = (domain: string): ColumnDef<RepositoryColumnInfo>[] => [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "repoIndexingStatus",
|
accessorKey: "status",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
const uniqueLabels = Array.from(new Set(Object.values(statusLabels)));
|
const uniqueLabels = Object.values(statusLabels);
|
||||||
const currentFilter = column.getFilterValue() as string | undefined;
|
const currentFilter = column.getFilterValue() as string | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -173,12 +152,12 @@ export const columns = (domain: string): ColumnDef<RepositoryColumnInfo>[] => [
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return <StatusIndicator status={row.original.repoIndexingStatus} />
|
return <StatusIndicator status={row.original.status} />
|
||||||
},
|
},
|
||||||
filterFn: (row, id, value) => {
|
filterFn: (row, id, value) => {
|
||||||
if (value === undefined) return true;
|
if (value === undefined) return true;
|
||||||
|
|
||||||
const status = row.getValue(id) as RepoIndexingStatus;
|
const status = row.getValue(id) as RepoStatus;
|
||||||
return statusLabels[status] === value;
|
return statusLabels[status] === value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -191,14 +170,14 @@ export const columns = (domain: string): ColumnDef<RepositoryColumnInfo>[] => [
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
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"
|
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
|
||||||
<ArrowUpDown className="ml-2 h-3.5 w-3.5" />
|
<ArrowUpDown className="ml-2 h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
if (!row.original.lastIndexed) {
|
if (!row.original.lastIndexed) {
|
||||||
return <div>-</div>;
|
return <div className="text-muted-foreground">Never</div>;
|
||||||
}
|
}
|
||||||
const date = new Date(row.original.lastIndexed)
|
const date = new Date(row.original.lastIndexed)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
import { RepositoryTable } from "./repositoryTable";
|
import { auth } from "@/auth";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
|
||||||
import { PageNotFound } from "../components/pageNotFound";
|
|
||||||
import { Header } from "../components/header";
|
|
||||||
import { env } from "@/env.mjs";
|
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 }> }) {
|
export default async function ReposPage(props: { params: Promise<{ domain: string }> }) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
@ -11,23 +23,34 @@ export default async function ReposPage(props: { params: Promise<{ domain: strin
|
||||||
domain
|
domain
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const org = await getOrgFromDomain(domain);
|
const session = await auth();
|
||||||
if (!org) {
|
const prisma = getPrismaClient(session?.user?.id);
|
||||||
return <PageNotFound />
|
|
||||||
|
const repos = await prisma.repo.findMany({
|
||||||
|
include: {
|
||||||
|
jobs: true,
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header>
|
<Header>
|
||||||
<h1 className="text-3xl">Repositories</h1>
|
<h1 className="text-3xl">Repositories</h1>
|
||||||
</Header>
|
</Header>
|
||||||
<div className="flex flex-col items-center">
|
<div className="px-6 py-6">
|
||||||
<div className="w-full">
|
|
||||||
<RepositoryTable
|
<RepositoryTable
|
||||||
|
repos={repos.map((repo) => ({
|
||||||
|
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'}
|
isAddReposButtonVisible={env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED === 'true'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,81 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DataTable } from "@/components/ui/data-table";
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
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 { Button } from "@/components/ui/button";
|
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 { AddRepositoryDialog } from "./components/addRepositoryDialog";
|
||||||
import { useState } from "react";
|
|
||||||
import { getRepos } from "@/app/api/(client)/client";
|
|
||||||
|
|
||||||
interface RepositoryTableProps {
|
interface RepositoryTableProps {
|
||||||
isAddReposButtonVisible: boolean
|
repos: {
|
||||||
|
repoId: number;
|
||||||
|
repoName: string;
|
||||||
|
repoDisplayName: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
indexedAt?: Date;
|
||||||
|
status: RepoStatus;
|
||||||
|
}[];
|
||||||
|
domain: string;
|
||||||
|
isAddReposButtonVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepositoryTable = ({
|
export const RepositoryTable = ({
|
||||||
|
repos,
|
||||||
|
domain,
|
||||||
isAddReposButtonVisible,
|
isAddReposButtonVisible,
|
||||||
}: RepositoryTableProps) => {
|
}: RepositoryTableProps) => {
|
||||||
const domain = useDomain();
|
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
const { data: repos, isLoading: reposLoading, error: reposError } = useQuery({
|
const { toast } = useToast();
|
||||||
queryKey: ['repos'],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await unwrapServiceError(getRepos());
|
|
||||||
},
|
|
||||||
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableRepos = useMemo(() => {
|
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 => ({
|
return repos.map((repo): RepositoryColumnInfo => ({
|
||||||
repoId: repo.repoId,
|
repoId: repo.repoId,
|
||||||
repoName: repo.repoName,
|
repoName: repo.repoName,
|
||||||
repoDisplayName: repo.repoDisplayName ?? repo.repoName,
|
repoDisplayName: repo.repoDisplayName ?? repo.repoName,
|
||||||
imageUrl: repo.imageUrl,
|
imageUrl: repo.imageUrl,
|
||||||
repoIndexingStatus: repo.repoIndexingStatus as RepoIndexingStatus,
|
status: repo.status,
|
||||||
lastIndexed: repo.indexedAt?.toISOString() ?? "",
|
lastIndexed: repo.indexedAt?.toISOString() ?? "",
|
||||||
})).sort((a, b) => {
|
})).sort((a, b) => {
|
||||||
const getPriorityFromStatus = (status: RepoIndexingStatus) => {
|
const getPriorityFromStatus = (status: RepoStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case RepoIndexingStatus.IN_INDEX_QUEUE:
|
case 'syncing':
|
||||||
case RepoIndexingStatus.INDEXING:
|
return 0 // Highest priority - currently syncing
|
||||||
return 0 // Highest priority - currently indexing
|
case 'not-indexed':
|
||||||
case RepoIndexingStatus.FAILED:
|
return 1 // Second priority - not yet indexed
|
||||||
return 1 // Second priority - failed repos need attention
|
case 'indexed':
|
||||||
case RepoIndexingStatus.INDEXED:
|
|
||||||
return 2 // Third priority - successfully indexed
|
return 2 // Third priority - successfully indexed
|
||||||
default:
|
default:
|
||||||
return 3 // Lowest priority - other statuses (NEW, etc.)
|
return 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by priority first
|
// Sort by priority first
|
||||||
const aPriority = getPriorityFromStatus(a.repoIndexingStatus);
|
const aPriority = getPriorityFromStatus(a.status);
|
||||||
const bPriority = getPriorityFromStatus(b.repoIndexingStatus);
|
const bPriority = getPriorityFromStatus(b.status);
|
||||||
|
|
||||||
if (aPriority !== bPriority) {
|
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)
|
// If same priority, sort by last indexed date (most recent first)
|
||||||
|
if (a.lastIndexed && b.lastIndexed) {
|
||||||
return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime();
|
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(() => {
|
const tableColumns = useMemo(() => {
|
||||||
if (reposLoading) {
|
|
||||||
return columns(domain).map((column) => {
|
|
||||||
if ('accessorKey' in column && column.accessorKey === "name") {
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
cell: () => (
|
|
||||||
<div className="flex flex-row items-center gap-3 py-2">
|
|
||||||
<Skeleton className="h-8 w-8 rounded-md" /> {/* Avatar skeleton */}
|
|
||||||
<Skeleton className="h-4 w-48" /> {/* Repository name skeleton */}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
cell: () => (
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
<Skeleton className="h-5 w-24 rounded-full" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return columns(domain);
|
return columns(domain);
|
||||||
}, [reposLoading, domain]);
|
}, [domain]);
|
||||||
|
|
||||||
|
|
||||||
if (reposError) {
|
|
||||||
return <div>Error loading repositories</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -121,7 +84,22 @@ export const RepositoryTable = ({
|
||||||
data={tableRepos}
|
data={tableRepos}
|
||||||
searchKey="repoDisplayName"
|
searchKey="repoDisplayName"
|
||||||
searchPlaceholder="Search repositories..."
|
searchPlaceholder="Search repositories..."
|
||||||
headerActions={isAddReposButtonVisible && (
|
headerActions={(
|
||||||
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
router.refresh();
|
||||||
|
toast({
|
||||||
|
description: "Page refreshed",
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<RefreshCwIcon className="w-4 h-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
{isAddReposButtonVisible && (
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="default"
|
size="default"
|
||||||
|
|
@ -131,6 +109,8 @@ export const RepositoryTable = ({
|
||||||
Add repository
|
Add repository
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddRepositoryDialog
|
<AddRepositoryDialog
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,6 @@ export default async function SettingsLayout(
|
||||||
),
|
),
|
||||||
href: `/${domain}/settings/members`,
|
href: `/${domain}/settings/members`,
|
||||||
}] : []),
|
}] : []),
|
||||||
...(userRoleInOrg === OrgRole.OWNER ? [{
|
|
||||||
title: "Connections",
|
|
||||||
href: `/${domain}/connections`,
|
|
||||||
}] : []),
|
|
||||||
{
|
{
|
||||||
title: "Secrets",
|
title: "Secrets",
|
||||||
href: `/${domain}/settings/secrets`,
|
href: `/${domain}/settings/secrets`,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function DataTable<TData, TValue>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between py-4">
|
<div className="flex items-center py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
|
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
|
||||||
|
|
|
||||||
|
|
@ -64,21 +64,6 @@ const syncConnections = async (connections?: { [key: string]: ConnectionConfig }
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Upserted connection with name '${key}'. Connection ID: ${connectionDb.id}`);
|
logger.info(`Upserted connection with name '${key}'. Connection ID: ${connectionDb.id}`);
|
||||||
|
|
||||||
// Re-try any repos that failed to index.
|
|
||||||
const failedRepos = currentConnection?.repos.filter(repo => 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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,7 @@ export const userScopedPrismaClientExtension = (userId?: string) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPrismaClient = (userId?: string) => {
|
||||||
|
return prisma.$extends(userScopedPrismaClientExtension(userId)) as PrismaClient;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma";
|
import { getPrismaClient, prisma as __unsafePrisma } from "@/prisma";
|
||||||
import { hashSecret } from "@sourcebot/crypto";
|
import { hashSecret } from "@sourcebot/crypto";
|
||||||
import { ApiKey, Org, OrgRole, PrismaClient, User } from "@sourcebot/db";
|
import { ApiKey, Org, OrgRole, PrismaClient, User } from "@sourcebot/db";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
@ -88,7 +88,7 @@ export const getAuthContext = async (): Promise<OptionalAuthContext | ServiceErr
|
||||||
},
|
},
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(user?.id)) as PrismaClient;
|
const prisma = getPrismaClient(user?.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user ?? undefined,
|
user: user ?? undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue