mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 13:06:45 +03:00
Updates to Repository and Org Pages for Responsive Layouts
This commit is contained in:
@@ -3,11 +3,17 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|||||||
import type { MirrorJob } from '@/lib/db/schema';
|
import type { MirrorJob } from '@/lib/db/schema';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw, Check, X, Loader2, Import } from 'lucide-react';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { formatDate, getStatusColor } from '@/lib/utils';
|
import { formatDate, getStatusColor } from '@/lib/utils';
|
||||||
import { Skeleton } from '../ui/skeleton';
|
import { Skeleton } from '../ui/skeleton';
|
||||||
import type { FilterParams } from '@/types/filter';
|
import type { FilterParams } from '@/types/filter';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '../ui/tooltip';
|
||||||
|
|
||||||
type MirrorJobWithKey = MirrorJob & { _rowKey: string };
|
type MirrorJobWithKey = MirrorJob & { _rowKey: string };
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@ export default function ActivityList({
|
|||||||
count: filteredActivities.length,
|
count: filteredActivities.length,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: (idx) =>
|
estimateSize: (idx) =>
|
||||||
expandedItems.has(filteredActivities[idx]._rowKey) ? 217 : 120,
|
expandedItems.has(filteredActivities[idx]._rowKey) ? 217 : 100,
|
||||||
overscan: 5,
|
overscan: 5,
|
||||||
measureElement: (el) => el.getBoundingClientRect().height + 8,
|
measureElement: (el) => el.getBoundingClientRect().height + 8,
|
||||||
});
|
});
|
||||||
@@ -155,8 +161,8 @@ export default function ActivityList({
|
|||||||
}}
|
}}
|
||||||
className='border-b px-4 pt-4'
|
className='border-b px-4 pt-4'
|
||||||
>
|
>
|
||||||
<div className='flex items-start gap-4'>
|
<div className='flex items-start gap-3 sm:gap-4'>
|
||||||
<div className='relative mt-2'>
|
<div className='relative mt-2 flex-shrink-0'>
|
||||||
<div
|
<div
|
||||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
className={`h-2 w-2 rounded-full ${getStatusColor(
|
||||||
activity.status,
|
activity.status,
|
||||||
@@ -164,25 +170,112 @@ export default function ActivityList({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1'>
|
<div className='flex-1 min-w-0'>
|
||||||
<div className='mb-1 flex flex-col sm:flex-row sm:items-center sm:justify-between'>
|
<div className='mb-1 flex items-start justify-between gap-2'>
|
||||||
<p className='font-medium'>{activity.message}</p>
|
<div className='flex-1 min-w-0'>
|
||||||
<p className='text-sm text-muted-foreground'>
|
{/* Mobile: Show simplified status-based message */}
|
||||||
|
<div className='block sm:hidden'>
|
||||||
|
<p className='font-medium flex items-center gap-1.5'>
|
||||||
|
{activity.status === 'synced' ? (
|
||||||
|
<>
|
||||||
|
<Check className='h-4 w-4 text-teal-600 dark:text-teal-400' />
|
||||||
|
<span className='text-teal-600 dark:text-teal-400'>Sync successful</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'mirrored' ? (
|
||||||
|
<>
|
||||||
|
<Check className='h-4 w-4 text-emerald-600 dark:text-emerald-400' />
|
||||||
|
<span className='text-emerald-600 dark:text-emerald-400'>Mirror successful</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'failed' ? (
|
||||||
|
<>
|
||||||
|
<X className='h-4 w-4 text-rose-600 dark:text-rose-400' />
|
||||||
|
<span className='text-rose-600 dark:text-rose-400'>Operation failed</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'syncing' ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className='h-4 w-4 text-indigo-600 dark:text-indigo-400 animate-spin' />
|
||||||
|
<span className='text-indigo-600 dark:text-indigo-400'>Syncing in progress</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'mirroring' ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className='h-4 w-4 text-yellow-600 dark:text-yellow-400 animate-spin' />
|
||||||
|
<span className='text-yellow-600 dark:text-yellow-400'>Mirroring in progress</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'imported' ? (
|
||||||
|
<>
|
||||||
|
<Import className='h-4 w-4 text-blue-600 dark:text-blue-400' />
|
||||||
|
<span className='text-blue-600 dark:text-blue-400'>Imported</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{activity.message}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Desktop: Show status with icon and full message in tooltip */}
|
||||||
|
<div className='hidden sm:block'>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<p className='font-medium flex items-center gap-1.5 cursor-help'>
|
||||||
|
{activity.status === 'synced' ? (
|
||||||
|
<>
|
||||||
|
<Check className='h-4 w-4 text-teal-600 dark:text-teal-400 flex-shrink-0' />
|
||||||
|
<span className='text-teal-600 dark:text-teal-400'>Sync successful</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'mirrored' ? (
|
||||||
|
<>
|
||||||
|
<Check className='h-4 w-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0' />
|
||||||
|
<span className='text-emerald-600 dark:text-emerald-400'>Mirror successful</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'failed' ? (
|
||||||
|
<>
|
||||||
|
<X className='h-4 w-4 text-rose-600 dark:text-rose-400 flex-shrink-0' />
|
||||||
|
<span className='text-rose-600 dark:text-rose-400'>Operation failed</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'syncing' ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className='h-4 w-4 text-indigo-600 dark:text-indigo-400 animate-spin flex-shrink-0' />
|
||||||
|
<span className='text-indigo-600 dark:text-indigo-400'>Syncing in progress</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'mirroring' ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className='h-4 w-4 text-yellow-600 dark:text-yellow-400 animate-spin flex-shrink-0' />
|
||||||
|
<span className='text-yellow-600 dark:text-yellow-400'>Mirroring in progress</span>
|
||||||
|
</>
|
||||||
|
) : activity.status === 'imported' ? (
|
||||||
|
<>
|
||||||
|
<Import className='h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0' />
|
||||||
|
<span className='text-blue-600 dark:text-blue-400'>Imported</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className='truncate'>{activity.message}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="start" className="max-w-[400px]">
|
||||||
|
<p className="whitespace-pre-wrap break-words">{activity.message}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='text-sm text-muted-foreground whitespace-nowrap flex-shrink-0 ml-2'>
|
||||||
{formatDate(activity.timestamp)}
|
{formatDate(activity.timestamp)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activity.repositoryName && (
|
<div className='flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3'>
|
||||||
<p className='mb-2 text-sm text-muted-foreground'>
|
{activity.repositoryName && (
|
||||||
Repository: {activity.repositoryName}
|
<p className='text-sm text-muted-foreground truncate'>
|
||||||
</p>
|
<span className='font-medium'>Repo:</span> {activity.repositoryName}
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
{activity.organizationName && (
|
{activity.organizationName && (
|
||||||
<p className='mb-2 text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground truncate'>
|
||||||
Organization: {activity.organizationName}
|
<span className='font-medium'>Org:</span> {activity.organizationName}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activity.details && (
|
{activity.details && (
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
@@ -199,7 +292,7 @@ export default function ActivityList({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isExpanded ? 'Hide Details' : 'Show Details'}
|
{isExpanded ? 'Hide Details' : activity.status === 'failed' ? 'Show Error Details' : 'Show Details'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
|
|||||||
@@ -69,19 +69,19 @@ export function MirrorDestinationEditor({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center gap-2", className)}>
|
<div className={cn("flex items-center gap-2 w-full", className)}>
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground min-w-0 flex-1">
|
||||||
<Building2 className="h-3 w-3" />
|
<Building2 className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="font-medium">{organizationName}</span>
|
<span className="font-medium truncate">{organizationName}</span>
|
||||||
<ArrowRight className="h-3 w-3" />
|
<ArrowRight className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
"font-medium",
|
"font-medium truncate",
|
||||||
hasOverride && "text-orange-600 dark:text-orange-400"
|
hasOverride && "text-orange-600 dark:text-orange-400"
|
||||||
)}>
|
)}>
|
||||||
{effectiveDestination}
|
{effectiveDestination}
|
||||||
</span>
|
</span>
|
||||||
{hasOverride && (
|
{hasOverride && (
|
||||||
<Badge variant="outline" className="h-4 px-1 text-[10px] border-orange-600 text-orange-600 dark:border-orange-400 dark:text-orange-400">
|
<Badge variant="outline" className="h-4 px-1 text-[10px] border-orange-600 text-orange-600 dark:border-orange-400 dark:text-orange-400 flex-shrink-0">
|
||||||
custom
|
custom
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -92,11 +92,11 @@ export function MirrorDestinationEditor({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 p-0 opacity-60 hover:opacity-100"
|
className="h-10 w-10 sm:h-6 sm:w-6 p-0 opacity-60 hover:opacity-100"
|
||||||
title="Edit mirror destination"
|
title="Edit mirror destination"
|
||||||
disabled={isUpdating || isLoading}
|
disabled={isUpdating || isLoading}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-3 w-3" />
|
<Edit3 className="h-5 w-5 sm:h-3 sm:w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80" align="end">
|
<PopoverContent className="w-80" align="end">
|
||||||
|
|||||||
@@ -127,9 +127,9 @@ export function OrganizationList({
|
|||||||
}, [organizations, filter]);
|
}, [organizations, filter]);
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<Skeleton key={i} className="h-[136px] w-full" />
|
<Skeleton key={i} className="h-[180px] w-full" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : filteredOrganizations.length === 0 ? (
|
) : filteredOrganizations.length === 0 ? (
|
||||||
@@ -161,7 +161,7 @@ export function OrganizationList({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 pb-20 sm:pb-0">
|
||||||
{filteredOrganizations.map((org, index) => {
|
{filteredOrganizations.map((org, index) => {
|
||||||
const isLoading = loadingOrgIds.has(org.id ?? "");
|
const isLoading = loadingOrgIds.has(org.id ?? "");
|
||||||
const statusBadge = getStatusBadge(org.status);
|
const statusBadge = getStatusBadge(org.status);
|
||||||
@@ -171,20 +171,33 @@ export function OrganizationList({
|
|||||||
<Card
|
<Card
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-hidden p-4 transition-all hover:shadow-md min-h-[160px]",
|
"overflow-hidden p-4 sm:p-6 transition-all hover:shadow-lg hover:border-foreground/10",
|
||||||
isLoading && "opacity-75"
|
isLoading && "opacity-75"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
{/* Mobile Layout */}
|
||||||
<div className="flex-1">
|
<div className="flex flex-col gap-3 sm:hidden">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
{/* Header with org name and badges */}
|
||||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
<div className="space-y-2">
|
||||||
<a
|
<div className="flex items-start justify-between gap-2">
|
||||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
className="font-medium hover:underline cursor-pointer"
|
<Building2 className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||||
>
|
<a
|
||||||
{org.name}
|
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||||
</a>
|
className="font-medium hover:underline cursor-pointer truncate"
|
||||||
|
>
|
||||||
|
{org.name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Badge variant={statusBadge.variant} className="flex-shrink-0">
|
||||||
|
{StatusIcon && <StatusIcon className={cn(
|
||||||
|
"h-3 w-3",
|
||||||
|
org.status === "mirroring" && "animate-pulse"
|
||||||
|
)} />}
|
||||||
|
{statusBadge.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-2 py-0.5 rounded-full capitalize ${
|
className={`text-xs px-2 py-0.5 rounded-full capitalize ${
|
||||||
org.membershipRole === "member"
|
org.membershipRole === "member"
|
||||||
@@ -195,128 +208,166 @@ export function OrganizationList({
|
|||||||
{org.membershipRole}
|
{org.membershipRole}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Destination override section */}
|
{/* Destination override section */}
|
||||||
<div className="mt-2">
|
<div>
|
||||||
<MirrorDestinationEditor
|
<MirrorDestinationEditor
|
||||||
organizationId={org.id!}
|
organizationId={org.id!}
|
||||||
organizationName={org.name!}
|
organizationName={org.name!}
|
||||||
currentDestination={org.destinationOrg}
|
currentDestination={org.destinationOrg}
|
||||||
onUpdate={(newDestination) => handleUpdateDestination(org.id!, newDestination)}
|
onUpdate={(newDestination) => handleUpdateDestination(org.id!, newDestination)}
|
||||||
isUpdating={isLoading}
|
isUpdating={isLoading}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Layout */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
{/* Header with org icon, name, role badge and status */}
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-1">
|
||||||
|
<a
|
||||||
|
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||||
|
className="text-xl font-semibold hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
{org.name}
|
||||||
|
</a>
|
||||||
|
<Badge
|
||||||
|
variant={org.membershipRole === "member" ? "secondary" : "default"}
|
||||||
|
className="capitalize"
|
||||||
|
>
|
||||||
|
{org.membershipRole}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Badge variant={statusBadge.variant} className="flex items-center gap-1">
|
||||||
|
{StatusIcon && <StatusIcon className={cn(
|
||||||
|
"h-3.5 w-3.5",
|
||||||
|
org.status === "mirroring" && "animate-pulse"
|
||||||
|
)} />}
|
||||||
|
{statusBadge.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Destination override section */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<MirrorDestinationEditor
|
||||||
|
organizationId={org.id!}
|
||||||
|
organizationName={org.name!}
|
||||||
|
currentDestination={org.destinationOrg}
|
||||||
|
onUpdate={(newDestination) => handleUpdateDestination(org.id!, newDestination)}
|
||||||
|
isUpdating={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repository statistics */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-lg">{org.repositoryCount}</span>
|
||||||
|
<span className="text-muted-foreground ml-1">
|
||||||
|
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repository breakdown */}
|
||||||
|
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton className="h-4 w-20" />
|
||||||
|
<Skeleton className="h-4 w-20" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{org.publicRepositoryCount !== undefined && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{org.publicRepositoryCount} public
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-orange-500" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{org.privateRepositoryCount} private
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={statusBadge.variant} className="ml-2">
|
|
||||||
{StatusIcon && <StatusIcon className={cn(
|
|
||||||
"h-3 w-3",
|
|
||||||
org.status === "mirroring" && "animate-pulse"
|
|
||||||
)} />}
|
|
||||||
{statusBadge.label}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground mb-4">
|
{/* Mobile Actions */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:hidden">
|
||||||
<span className="font-medium">
|
|
||||||
{org.repositoryCount}{" "}
|
|
||||||
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/* Always render this section to prevent layout shift */}
|
|
||||||
<div className="flex gap-4 mt-2 text-xs min-h-[20px]">
|
|
||||||
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
|
|
||||||
<>
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{org.publicRepositoryCount !== undefined ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
|
||||||
{org.publicRepositoryCount} public
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
|
||||||
{org.privateRepositoryCount} private
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
|
||||||
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{/* Show a placeholder if no counts are available to maintain height */}
|
|
||||||
{org.publicRepositoryCount === undefined &&
|
|
||||||
org.privateRepositoryCount === undefined &&
|
|
||||||
org.forkRepositoryCount === undefined && (
|
|
||||||
<span className="invisible">Loading counts...</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{org.status === "imported" && (
|
{org.status === "imported" && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="default"
|
||||||
onClick={() => org.id && onMirror({ orgId: org.id })}
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
Starting...
|
Starting...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Mirror"
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Mirror Organization
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{org.status === "mirroring" && (
|
{org.status === "mirroring" && (
|
||||||
<Button size="sm" disabled variant="outline">
|
<Button size="default" disabled variant="outline" className="w-full h-10">
|
||||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
Mirroring...
|
Mirroring...
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{org.status === "mirrored" && (
|
{org.status === "mirrored" && (
|
||||||
<Button size="sm" disabled variant="secondary">
|
<Button size="default" disabled variant="secondary" className="w-full h-10">
|
||||||
<Check className="h-3 w-3 mr-2" />
|
<Check className="h-4 w-4 mr-2" />
|
||||||
Mirrored
|
Mirrored
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{org.status === "failed" && (
|
{org.status === "failed" && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="default"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => org.id && onMirror({ orgId: org.id })}
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
Retrying...
|
Retrying...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<AlertCircle className="h-3 w-3 mr-2" />
|
<AlertCircle className="h-4 w-4 mr-2" />
|
||||||
Retry
|
Retry Mirror
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
{(() => {
|
{(() => {
|
||||||
const giteaUrl = getGiteaOrgUrl(org);
|
const giteaUrl = getGiteaOrgUrl(org);
|
||||||
|
|
||||||
@@ -337,34 +388,166 @@ export function OrganizationList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return giteaUrl ? (
|
return giteaUrl ? (
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<Button variant="outline" size="default" asChild className="flex-1 h-10 min-w-0">
|
||||||
<a
|
<a
|
||||||
href={giteaUrl}
|
href={giteaUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<SiGitea className="h-4 w-4" />
|
<SiGitea className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="text-xs">Gitea</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="ghost" size="icon" disabled title={tooltip}>
|
<Button variant="outline" size="default" disabled title={tooltip} className="flex-1 h-10">
|
||||||
<SiGitea className="h-4 w-4" />
|
<SiGitea className="h-4 w-4" />
|
||||||
|
<span className="text-xs ml-2">Gitea</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<Button variant="outline" size="default" asChild className="flex-1 h-10 min-w-0">
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/${org.name}`}
|
href={`https://github.com/${org.name}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title="View on GitHub"
|
title="View on GitHub"
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<SiGithub className="h-4 w-4" />
|
<SiGithub className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="text-xs">GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Actions */}
|
||||||
|
<div className="hidden sm:flex items-center justify-between mt-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{org.status === "imported" && (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
Starting mirror...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Mirror Organization
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{org.status === "mirroring" && (
|
||||||
|
<Button size="default" disabled variant="outline">
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
Mirroring in progress...
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{org.status === "mirrored" && (
|
||||||
|
<Button size="default" disabled variant="secondary">
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
Successfully mirrored
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{org.status === "failed" && (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
Retrying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="h-4 w-4 mr-2" />
|
||||||
|
Retry Mirror
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(() => {
|
||||||
|
const giteaUrl = getGiteaOrgUrl(org);
|
||||||
|
|
||||||
|
// Determine tooltip based on status and configuration
|
||||||
|
let tooltip: string;
|
||||||
|
if (!giteaConfig?.url) {
|
||||||
|
tooltip = "Gitea not configured";
|
||||||
|
} else if (org.status === 'imported') {
|
||||||
|
tooltip = "Organization not yet mirrored to Gitea";
|
||||||
|
} else if (org.status === 'failed') {
|
||||||
|
tooltip = "Organization mirroring failed";
|
||||||
|
} else if (org.status === 'mirroring') {
|
||||||
|
tooltip = "Organization is being mirrored to Gitea";
|
||||||
|
} else if (giteaUrl) {
|
||||||
|
tooltip = "View on Gitea";
|
||||||
|
} else {
|
||||||
|
tooltip = "Gitea organization not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center border rounded-md">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
asChild={!!giteaUrl}
|
||||||
|
disabled={!giteaUrl}
|
||||||
|
title={tooltip}
|
||||||
|
className="rounded-none rounded-l-md border-r"
|
||||||
|
>
|
||||||
|
{giteaUrl ? (
|
||||||
|
<a
|
||||||
|
href={giteaUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<SiGitea className="h-4 w-4 mr-2" />
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SiGitea className="h-4 w-4 mr-2" />
|
||||||
|
Gitea
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="rounded-none rounded-r-md"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${org.name}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="View on GitHub"
|
||||||
|
>
|
||||||
|
<SiGithub className="h-4 w-4 mr-2" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -752,18 +752,16 @@ export default function Repository() {
|
|||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{selectedRepoIds.size === 0 && (
|
<Button
|
||||||
<Button
|
variant="default"
|
||||||
variant="default"
|
size="icon"
|
||||||
size="icon"
|
onClick={handleMirrorAllRepos}
|
||||||
onClick={handleMirrorAllRepos}
|
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||||
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
title="Mirror all repositories"
|
||||||
title="Mirror all repositories"
|
className="h-10 w-10 shrink-0"
|
||||||
className="h-10 w-10 shrink-0"
|
>
|
||||||
>
|
<FlipHorizontal className="h-4 w-4" />
|
||||||
<FlipHorizontal className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop: Original layout */}
|
{/* Desktop: Original layout */}
|
||||||
@@ -844,24 +842,80 @@ export default function Repository() {
|
|||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk actions on desktop - integrated into the same line */}
|
||||||
|
<div className="flex items-center gap-2 border-l pl-4">
|
||||||
|
{selectedRepoIds.size === 0 ? (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleMirrorAllRepos}
|
||||||
|
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
|
Mirror All
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedRepoIds.size} selected
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => setSelectedRepoIds(new Set())}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableActions.includes('mirror') && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="default"
|
||||||
|
onClick={handleBulkMirror}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
|
Mirror ({selectedRepoIds.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('sync') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
onClick={handleBulkSync}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Sync ({selectedRepoIds.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('retry') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
onClick={handleBulkRetry}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons - shows when items are selected or Mirror All on desktop */}
|
{/* Action buttons for mobile - only show when items are selected */}
|
||||||
<div className={`flex items-center gap-2 flex-wrap ${selectedRepoIds.size === 0 ? 'hidden sm:flex' : ''}`}>
|
{selectedRepoIds.size > 0 && (
|
||||||
{selectedRepoIds.size === 0 ? (
|
<div className="flex items-center gap-2 flex-wrap sm:hidden">
|
||||||
<Button
|
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
|
||||||
variant="default"
|
|
||||||
onClick={handleMirrorAllRepos}
|
|
||||||
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
|
||||||
className="w-auto"
|
|
||||||
>
|
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
|
||||||
Mirror All
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
|
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{selectedRepoIds.size} selected
|
{selectedRepoIds.size} selected
|
||||||
</span>
|
</span>
|
||||||
@@ -877,44 +931,43 @@ export default function Repository() {
|
|||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{availableActions.includes('mirror') && (
|
{availableActions.includes('mirror') && (
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBulkMirror}
|
onClick={handleBulkMirror}
|
||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
<span className="hidden sm:inline">Mirror </span>({selectedRepoIds.size})
|
<span>Mirror </span>({selectedRepoIds.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{availableActions.includes('sync') && (
|
{availableActions.includes('sync') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBulkSync}
|
onClick={handleBulkSync}
|
||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span className="hidden sm:inline">Sync </span>({selectedRepoIds.size})
|
<span className="hidden sm:inline">Sync </span>({selectedRepoIds.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{availableActions.includes('retry') && (
|
{availableActions.includes('retry') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBulkRetry}
|
onClick={handleBulkRetry}
|
||||||
disabled={loadingRepoIds.size > 0}
|
disabled={loadingRepoIds.size > 0}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isGitHubConfigured ? (
|
{!isGitHubConfigured ? (
|
||||||
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-md">
|
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-md">
|
||||||
@@ -946,7 +999,9 @@ export default function Repository() {
|
|||||||
loadingRepoIds={loadingRepoIds}
|
loadingRepoIds={loadingRepoIds}
|
||||||
selectedRepoIds={selectedRepoIds}
|
selectedRepoIds={selectedRepoIds}
|
||||||
onSelectionChange={setSelectedRepoIds}
|
onSelectionChange={setSelectedRepoIds}
|
||||||
onRefresh={() => fetchRepositories(false)}
|
onRefresh={async () => {
|
||||||
|
await fetchRepositories(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -177,106 +177,159 @@ export default function RepositoryTable({
|
|||||||
return (
|
return (
|
||||||
<Card className="mb-3">
|
<Card className="mb-3">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Checkbox
|
{/* Header with checkbox and repo name */}
|
||||||
checked={isSelected}
|
<div className="flex items-start gap-3">
|
||||||
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, checked as boolean)}
|
<Checkbox
|
||||||
className="mt-1"
|
checked={isSelected}
|
||||||
/>
|
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, checked as boolean)}
|
||||||
<div className="flex-1 space-y-3">
|
className="mt-1 h-5 w-5"
|
||||||
{/* Repository Info */}
|
aria-label={`Select ${repo.name}`}
|
||||||
<div>
|
/>
|
||||||
<h3 className="font-medium text-sm break-all">{repo.name}</h3>
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-medium text-base truncate">{repo.name}</h3>
|
||||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
{repo.isPrivate && <Badge variant="secondary" className="text-xs"><Lock className="h-3 w-3 mr-1" />Private</Badge>}
|
{repo.isPrivate && <Badge variant="secondary" className="text-xs h-5"><Lock className="h-3 w-3 mr-1" />Private</Badge>}
|
||||||
{repo.isForked && <Badge variant="secondary" className="text-xs"><GitFork className="h-3 w-3 mr-1" />Fork</Badge>}
|
{repo.isForked && <Badge variant="secondary" className="text-xs h-5"><GitFork className="h-3 w-3 mr-1" />Fork</Badge>}
|
||||||
{repo.isStarred && <Badge variant="secondary" className="text-xs"><Star className="h-3 w-3 mr-1" />Starred</Badge>}
|
{repo.isStarred && <Badge variant="secondary" className="text-xs h-5"><Star className="h-3 w-3 mr-1" />Starred</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repository details */}
|
||||||
|
<div className="space-y-2">
|
||||||
{/* Owner & Organization */}
|
{/* Owner & Organization */}
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
<div>Owner: {repo.owner}</div>
|
<div className="flex items-center gap-2">
|
||||||
{repo.organization && <div>Org: {repo.organization}</div>}
|
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Owner:</span>
|
||||||
{repo.destinationOrg && <div>Destination: {repo.destinationOrg}</div>}
|
<span className="truncate">{repo.owner}</span>
|
||||||
|
</div>
|
||||||
|
{repo.organization && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Org:</span>
|
||||||
|
<span className="truncate">{repo.organization}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{repo.destinationOrg && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Dest:</span>
|
||||||
|
<span className="truncate">{repo.destinationOrg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status & Last Mirrored */}
|
{/* Status & Last Mirrored */}
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
<div className={`h-2.5 w-2.5 rounded-full ${getStatusColor(repo.status)}`} />
|
||||||
<span className="capitalize">{repo.status}</span>
|
<span className="text-sm font-medium capitalize">{repo.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never"}
|
{repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never mirrored"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex flex-col gap-2">
|
||||||
{(repo.status === "imported" || repo.status === "failed") && (
|
{/* Primary action button */}
|
||||||
<Button
|
{(repo.status === "imported" || repo.status === "failed") && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="default"
|
size="default"
|
||||||
onClick={() => repo.id && onMirror({ repoId: repo.id })}
|
variant="default"
|
||||||
disabled={isLoading}
|
onClick={() => repo.id && onMirror({ repoId: repo.id })}
|
||||||
>
|
disabled={isLoading}
|
||||||
<FlipHorizontal className="h-3 w-3 mr-1" />
|
className="w-full h-10"
|
||||||
Mirror
|
>
|
||||||
</Button>
|
{isLoading ? (
|
||||||
)}
|
<>
|
||||||
{(repo.status === "mirrored" || repo.status === "synced") && (
|
<FlipHorizontal className="h-4 w-4 mr-2 animate-spin" />
|
||||||
<Button
|
Mirroring...
|
||||||
size="sm"
|
</>
|
||||||
variant="outline"
|
) : (
|
||||||
onClick={() => repo.id && onSync({ repoId: repo.id })}
|
<>
|
||||||
disabled={isLoading}
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
>
|
Mirror Repository
|
||||||
<RefreshCw className="h-3 w-3 mr-1" />
|
</>
|
||||||
Sync
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{repo.status === "failed" && (
|
{(repo.status === "mirrored" || repo.status === "synced") && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="default"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => repo.id && onRetry({ repoId: repo.id })}
|
onClick={() => repo.id && onSync({ repoId: repo.id })}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
className="w-full h-10"
|
||||||
<RotateCcw className="h-3 w-3 mr-1" />
|
>
|
||||||
Retry
|
{isLoading ? (
|
||||||
</Button>
|
<>
|
||||||
)}
|
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Sync Repository
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{repo.status === "failed" && (
|
||||||
|
<Button
|
||||||
|
size="default"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => repo.id && onRetry({ repoId: repo.id })}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Retrying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
Retry Mirror
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Links */}
|
{/* External links */}
|
||||||
<div className="flex gap-1 ml-auto">
|
<div className="flex gap-2">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" asChild>
|
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
|
||||||
|
<a
|
||||||
|
href={repo.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="View on GitHub"
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<SiGithub className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="text-xs">GitHub</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
{giteaUrl ? (
|
||||||
|
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
|
||||||
<a
|
<a
|
||||||
href={repo.url}
|
href={giteaUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title="View on GitHub"
|
title="View on Gitea"
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<SiGithub className="h-4 w-4" />
|
<SiGitea className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="text-xs">Gitea</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
{giteaUrl ? (
|
) : (
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" asChild>
|
<Button variant="outline" size="default" disabled className="flex-1 h-10 min-w-0">
|
||||||
<a
|
<SiGitea className="h-4 w-4" />
|
||||||
href={giteaUrl}
|
<span className="text-xs ml-2">Gitea</span>
|
||||||
target="_blank"
|
</Button>
|
||||||
rel="noopener noreferrer"
|
)}
|
||||||
title="View on Gitea"
|
|
||||||
>
|
|
||||||
<SiGitea className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" disabled title="Not mirrored to Gitea">
|
|
||||||
<SiGitea className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,13 +457,14 @@ export default function RepositoryTable({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Mobile card view */}
|
{/* Mobile card view */}
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden pb-20">
|
||||||
{/* Select all checkbox */}
|
{/* Select all checkbox */}
|
||||||
<div className="flex items-center gap-2 mb-3 p-2 bg-muted/50 rounded-md">
|
<div className="flex items-center gap-3 mb-3 p-3 bg-muted/50 rounded-md">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isAllSelected}
|
checked={isAllSelected}
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
aria-label="Select all repositories"
|
aria-label="Select all repositories"
|
||||||
|
className="h-5 w-5"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Select All ({filteredRepositories.length})
|
Select All ({filteredRepositories.length})
|
||||||
@@ -424,7 +478,7 @@ export default function RepositoryTable({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop table view */}
|
{/* Desktop table view */}
|
||||||
<div className="hidden lg:block border rounded-md">
|
<div className="hidden lg:flex flex-col border rounded-md">
|
||||||
{/* Table header */}
|
{/* Table header */}
|
||||||
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
||||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
@@ -453,215 +507,281 @@ export default function RepositoryTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table body with virtualization */}
|
{/* Table body wrapper (for a parent in virtualization) */}
|
||||||
<div
|
<div
|
||||||
ref={tableParentRef}
|
ref={tableParentRef}
|
||||||
className="overflow-auto max-h-[calc(100dvh-25rem)]"
|
className="flex flex-col max-h-[calc(100dvh-276px)] overflow-y-auto"
|
||||||
style={{
|
|
||||||
contain: "strict",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
width: "100%",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow, index) => {
|
||||||
const repo = filteredRepositories[virtualRow.index];
|
const repo = filteredRepositories[virtualRow.index];
|
||||||
const isLoading = repo.id ? loadingRepoIds.has(repo.id) : false;
|
const isLoading = loadingRepoIds.has(repo.id ?? "");
|
||||||
const isSelected = repo.id ? selectedRepoIds.has(repo.id) : false;
|
|
||||||
const giteaUrl = getGiteaRepoUrl(repo);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={virtualRow.key}
|
key={index}
|
||||||
|
ref={rowVirtualizer.measureElement}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
|
||||||
height: `${virtualRow.size}px`,
|
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
className="flex items-center justify-between border-b bg-transparent hover:bg-muted/50 transition-colors"
|
data-index={virtualRow.index}
|
||||||
|
className="h-[65px] flex items-center justify-between bg-transparent border-b hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={repo.id ? selectedRepoIds.has(repo.id) : false}
|
||||||
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, checked as boolean)}
|
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, !!checked)}
|
||||||
|
aria-label={`Select ${repo.name}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full p-3 flex-[2.5] pr-2">
|
|
||||||
<div className="flex items-center gap-2">
|
{/* Repository */}
|
||||||
<span className="font-medium text-sm truncate">{repo.name}</span>
|
<div className="h-full p-3 flex items-center gap-2 flex-[2.5]">
|
||||||
{repo.isPrivate && (
|
<GitFork className="h-4 w-4 text-muted-foreground" />
|
||||||
<TooltipProvider>
|
<div className="flex-1">
|
||||||
<Tooltip>
|
<div className="font-medium flex items-center gap-1">
|
||||||
<TooltipTrigger>
|
{repo.name}
|
||||||
<Lock className="h-3 w-3 text-muted-foreground" />
|
{repo.isStarred && (
|
||||||
</TooltipTrigger>
|
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500" />
|
||||||
<TooltipContent>
|
)}
|
||||||
<p>Private repository</p>
|
</div>
|
||||||
</TooltipContent>
|
<div className="text-xs text-muted-foreground">
|
||||||
</Tooltip>
|
{repo.fullName}
|
||||||
</TooltipProvider>
|
</div>
|
||||||
)}
|
|
||||||
{repo.isForked && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<GitFork className="h-3 w-3 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Forked repository</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
{repo.isStarred && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Star className="h-3 w-3 text-yellow-500" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Starred repository</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
{repo.isPrivate && (
|
||||||
{repo.fullName}
|
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||||
|
Private
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{repo.isForked && (
|
||||||
|
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||||
|
Fork
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Owner */}
|
||||||
|
<div className="h-full p-3 flex items-center flex-[1]">
|
||||||
|
<p className="text-sm">{repo.owner}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organization */}
|
||||||
|
<div className="h-full p-3 flex items-center flex-[1]">
|
||||||
|
<InlineDestinationEditor
|
||||||
|
repository={repo}
|
||||||
|
giteaConfig={giteaConfig}
|
||||||
|
onUpdate={handleUpdateDestination}
|
||||||
|
isUpdating={loadingRepoIds.has(repo.id ?? "")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Last Mirrored */}
|
||||||
|
<div className="h-full p-3 flex items-center flex-[1]">
|
||||||
|
<p className="text-sm">
|
||||||
|
{repo.lastMirrored
|
||||||
|
? formatDate(new Date(repo.lastMirrored))
|
||||||
|
: "Never"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full p-3 flex-[1] text-sm">
|
|
||||||
{repo.owner}
|
|
||||||
</div>
|
|
||||||
<div className="h-full p-3 flex-[1] text-sm">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>{repo.organization || "-"}</span>
|
|
||||||
{repo.destinationOrg && repo.id && (
|
|
||||||
<InlineDestinationEditor
|
|
||||||
repositoryId={repo.id}
|
|
||||||
currentDestination={repo.destinationOrg}
|
|
||||||
onUpdate={handleUpdateDestination}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-full p-3 flex-[1] text-sm">
|
|
||||||
{repo.lastMirrored ? formatDate(repo.lastMirrored) : "Never"}
|
|
||||||
</div>
|
|
||||||
<div className="h-full p-3 flex-[1] flex items-center">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
|
||||||
repo.status
|
|
||||||
)}`}
|
|
||||||
/>
|
|
||||||
<span className="text-sm capitalize">{repo.status}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-full p-3 flex-[1] flex items-center gap-1">
|
|
||||||
{(repo.status === "imported" || repo.status === "failed") && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="default"
|
|
||||||
onClick={() => repo.id && onMirror({ repoId: repo.id })}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
|
||||||
Mirror
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{(repo.status === "mirrored" || repo.status === "synced") && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => repo.id && onSync({ repoId: repo.id })}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
|
||||||
Sync
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{repo.status === "failed" && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => repo.id && onRetry({ repoId: repo.id })}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-full p-3 flex-[0.8] flex items-center justify-center gap-1">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" asChild>
|
|
||||||
<a
|
|
||||||
href={repo.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<SiGithub className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>View on GitHub</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{giteaUrl ? (
|
{/* Status */}
|
||||||
|
<div className="h-full p-3 flex items-center gap-x-2 flex-[1]">
|
||||||
|
{repo.status === "failed" && repo.errorMessage ? (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<div className="flex items-center gap-x-2 cursor-help">
|
||||||
<a
|
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||||
href={giteaUrl}
|
<span className="text-sm capitalize underline decoration-dotted">{repo.status}</span>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<SiGitea className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent className="max-w-xs">
|
||||||
<p>View on Gitea</p>
|
<p className="text-sm">{repo.errorMessage}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : (
|
) : (
|
||||||
<TooltipProvider>
|
<>
|
||||||
<Tooltip>
|
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||||
<TooltipTrigger asChild>
|
<span className="text-sm capitalize">{repo.status}</span>
|
||||||
<Button variant="ghost" size="icon" disabled>
|
</>
|
||||||
<SiGitea className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Not mirrored to Gitea</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="h-full p-3 flex items-center justify-start flex-[1]">
|
||||||
|
<RepoActionButton
|
||||||
|
repo={{ id: repo.id ?? "", status: repo.status }}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onMirror={() => onMirror({ repoId: repo.id ?? "" })}
|
||||||
|
onSync={() => onSync({ repoId: repo.id ?? "" })}
|
||||||
|
onRetry={() => onRetry({ repoId: repo.id ?? "" })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Links */}
|
||||||
|
<div className="h-full p-3 flex items-center justify-center gap-x-2 flex-[0.8]">
|
||||||
|
{(() => {
|
||||||
|
const giteaUrl = getGiteaRepoUrl(repo);
|
||||||
|
|
||||||
|
// Determine tooltip based on status and configuration
|
||||||
|
let tooltip: string;
|
||||||
|
if (!giteaConfig?.url) {
|
||||||
|
tooltip = "Gitea not configured";
|
||||||
|
} else if (repo.status === 'imported') {
|
||||||
|
tooltip = "Repository not yet mirrored to Gitea";
|
||||||
|
} else if (repo.status === 'failed') {
|
||||||
|
tooltip = "Repository mirroring failed";
|
||||||
|
} else if (repo.status === 'mirroring') {
|
||||||
|
tooltip = "Repository is being mirrored to Gitea";
|
||||||
|
} else if (giteaUrl) {
|
||||||
|
tooltip = "View on Gitea";
|
||||||
|
} else {
|
||||||
|
tooltip = "Gitea repository not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
return giteaUrl ? (
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<a
|
||||||
|
href={giteaUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={tooltip}
|
||||||
|
>
|
||||||
|
<SiGitea className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="icon" disabled title={tooltip}>
|
||||||
|
<SiGitea className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<a
|
||||||
|
href={repo.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="View on GitHub"
|
||||||
|
>
|
||||||
|
<SiGithub className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status Bar */}
|
||||||
|
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3 relative">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
{hasAnyFilter
|
||||||
|
? `Showing ${filteredRepositories.length} of ${repositories.length} repositories`
|
||||||
|
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center - Live active indicator */}
|
||||||
|
{isLiveActive && (
|
||||||
|
<div className="flex items-center gap-1.5 absolute left-1/2 transform -translate-x-1/2">
|
||||||
|
<div
|
||||||
|
className="h-1 w-1 rounded-full bg-emerald-500"
|
||||||
|
style={{
|
||||||
|
animation: 'pulse 2s ease-in-out infinite'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
|
||||||
|
Live active
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="h-1 w-1 rounded-full bg-emerald-500"
|
||||||
|
style={{
|
||||||
|
animation: 'pulse 2s ease-in-out infinite',
|
||||||
|
animationDelay: '1s'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasAnyFilter && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Filters applied
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RepoActionButton({
|
||||||
|
repo,
|
||||||
|
isLoading,
|
||||||
|
onMirror,
|
||||||
|
onSync,
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
repo: { id: string; status: string };
|
||||||
|
isLoading: boolean;
|
||||||
|
onMirror: () => void;
|
||||||
|
onSync: () => void;
|
||||||
|
onRetry: () => void;
|
||||||
|
}) {
|
||||||
|
let label = "";
|
||||||
|
let icon = <></>;
|
||||||
|
let onClick = () => {};
|
||||||
|
let disabled = isLoading;
|
||||||
|
|
||||||
|
if (repo.status === "failed") {
|
||||||
|
label = "Retry";
|
||||||
|
icon = <RotateCcw className="h-4 w-4 mr-1" />;
|
||||||
|
onClick = onRetry;
|
||||||
|
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) {
|
||||||
|
label = "Sync";
|
||||||
|
icon = <RefreshCw className="h-4 w-4 mr-1" />;
|
||||||
|
onClick = onSync;
|
||||||
|
disabled ||= repo.status === "syncing";
|
||||||
|
} else if (["imported", "mirroring"].includes(repo.status)) {
|
||||||
|
label = "Mirror";
|
||||||
|
icon = <FlipHorizontal className="h-4 w-4 mr-1" />;
|
||||||
|
onClick = onMirror;
|
||||||
|
disabled ||= repo.status === "mirroring";
|
||||||
|
} else {
|
||||||
|
return null; // unsupported status
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
className="min-w-[80px] justify-start"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin mr-1" />
|
||||||
|
{label}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user