Update the Ignore Repo

This commit is contained in:
Arunavo Ray
2025-08-28 12:58:58 +05:30
parent 7dfb6b5d18
commit d99f597988
4 changed files with 422 additions and 49 deletions

View File

@@ -234,4 +234,5 @@ Repositories can have the following statuses:
## Security Guidelines ## Security Guidelines
- **Confidentiality Guidelines**: - **Confidentiality Guidelines**:
- Dont ever say Claude Code or generated with AI anyhwere. - Dont ever say Claude Code or generated with AI anyhwere.
- Never commit without the explicict ask

View File

@@ -18,7 +18,7 @@ import {
SelectValue, SelectValue,
} from "../ui/select"; } from "../ui/select";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter } from "lucide-react"; import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter, Ban, Check } from "lucide-react";
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror"; import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
import { import {
Drawer, Drawer,
@@ -210,10 +210,13 @@ export default function Repository() {
return; return;
} }
// Filter out repositories that are already mirroring to avoid duplicate operations. also filter out mirrored (mirrored can be synced and not mirrored again) // Filter out repositories that are already mirroring, mirrored, or ignored
const eligibleRepos = repositories.filter( const eligibleRepos = repositories.filter(
(repo) => (repo) =>
repo.status !== "mirroring" && repo.status !== "mirrored" && repo.id //not ignoring failed ones because we want to retry them if not mirrored. if mirrored, gitea fucnion handlers will silently ignore them repo.status !== "mirroring" &&
repo.status !== "mirrored" &&
repo.status !== "ignored" && // Skip ignored repositories
repo.id
); );
if (eligibleRepos.length === 0) { if (eligibleRepos.length === 0) {
@@ -400,6 +403,80 @@ export default function Repository() {
} }
}; };
const handleBulkSkip = async (skip: boolean) => {
if (selectedRepoIds.size === 0) return;
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
const eligibleRepos = skip
? selectedRepos.filter(repo =>
repo.status !== "ignored" &&
repo.status !== "mirroring" &&
repo.status !== "syncing"
)
: selectedRepos.filter(repo => repo.status === "ignored");
if (eligibleRepos.length === 0) {
toast.info(`No eligible repositories to ${skip ? "ignore" : "include"} in selection`);
return;
}
const repoIds = eligibleRepos.map(repo => repo.id as string);
setLoadingRepoIds(prev => {
const newSet = new Set(prev);
repoIds.forEach(id => newSet.add(id));
return newSet;
});
try {
// Update each repository's status
const newStatus = skip ? "ignored" : "imported";
const promises = repoIds.map(repoId =>
apiRequest<{ success: boolean; repository?: Repository; error?: string }>(
`/repositories/${repoId}/status`,
{
method: "PATCH",
data: { status: newStatus, userId: user?.id },
}
)
);
const results = await Promise.allSettled(promises);
const successCount = results.filter(r => r.status === "fulfilled" && (r.value as any).success).length;
if (successCount > 0) {
toast.success(`${successCount} repositories ${skip ? "ignored" : "included"}`);
// Update local state for successful updates
const successfulRepoIds = new Set<string>();
results.forEach((result, index) => {
if (result.status === "fulfilled" && (result.value as any).success) {
successfulRepoIds.add(repoIds[index]);
}
});
setRepositories(prevRepos =>
prevRepos.map(repo => {
if (repo.id && successfulRepoIds.has(repo.id)) {
return { ...repo, status: newStatus as any };
}
return repo;
})
);
setSelectedRepoIds(new Set());
}
if (successCount < repoIds.length) {
toast.error(`Failed to ${skip ? "ignore" : "include"} ${repoIds.length - successCount} repositories`);
}
} catch (error) {
showErrorToast(error, toast);
} finally {
setLoadingRepoIds(new Set());
}
};
const handleSyncRepo = async ({ repoId }: { repoId: string }) => { const handleSyncRepo = async ({ repoId }: { repoId: string }) => {
try { try {
if (!user || !user.id) { if (!user || !user.id) {
@@ -440,6 +517,58 @@ export default function Repository() {
} }
}; };
const handleSkipRepo = async ({ repoId, skip }: { repoId: string; skip: boolean }) => {
try {
if (!user || !user.id) {
return;
}
// Check if repository is currently being processed
const repo = repositories.find(r => r.id === repoId);
if (skip && repo && (repo.status === "mirroring" || repo.status === "syncing")) {
toast.warning("Cannot skip repository while it's being processed");
return;
}
// Set loading state
setLoadingRepoIds(prev => {
const newSet = new Set(prev);
newSet.add(repoId);
return newSet;
});
const newStatus = skip ? "ignored" : "imported";
// Update repository status via API
const response = await apiRequest<{ success: boolean; repository?: Repository; error?: string }>(
`/repositories/${repoId}/status`,
{
method: "PATCH",
data: { status: newStatus, userId: user.id },
}
);
if (response.success && response.repository) {
toast.success(`Repository ${skip ? "ignored" : "included"}`);
setRepositories(prevRepos =>
prevRepos.map(repo =>
repo.id === repoId ? response.repository! : repo
)
);
} else {
showErrorToast(response.error || `Error ${skip ? "ignoring" : "including"} repository`, toast);
}
} catch (error) {
showErrorToast(error, toast);
} finally {
setLoadingRepoIds(prev => {
const newSet = new Set(prev);
newSet.delete(repoId);
return newSet;
});
}
};
const handleRetryRepoAction = async ({ repoId }: { repoId: string }) => { const handleRetryRepoAction = async ({ repoId }: { repoId: string }) => {
try { try {
if (!user || !user.id) { if (!user || !user.id) {
@@ -543,7 +672,6 @@ export default function Repository() {
if (selectedRepoIds.size === 0) return []; if (selectedRepoIds.size === 0) return [];
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id)); const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
const statuses = new Set(selectedRepos.map(repo => repo.status));
const actions = []; const actions = [];
@@ -562,10 +690,35 @@ export default function Repository() {
actions.push('retry'); actions.push('retry');
} }
// Check if any selected repos can be ignored
if (selectedRepos.some(repo => repo.status !== "ignored")) {
actions.push('ignore');
}
// Check if any selected repos can be included (unignored)
if (selectedRepos.some(repo => repo.status === "ignored")) {
actions.push('include');
}
return actions; return actions;
}; };
const availableActions = getAvailableActions(); const availableActions = getAvailableActions();
// Get counts for eligible repositories for each action
const getActionCounts = () => {
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
return {
mirror: selectedRepos.filter(repo => repo.status === "imported" || repo.status === "failed").length,
sync: selectedRepos.filter(repo => repo.status === "mirrored" || repo.status === "synced").length,
retry: selectedRepos.filter(repo => repo.status === "failed").length,
ignore: selectedRepos.filter(repo => repo.status !== "ignored").length,
include: selectedRepos.filter(repo => repo.status === "ignored").length,
};
};
const actionCounts = getActionCounts();
// Check if any filters are active // Check if any filters are active
const hasActiveFilters = !!(filter.owner || filter.organization || filter.status); const hasActiveFilters = !!(filter.owner || filter.organization || filter.status);
@@ -867,7 +1020,7 @@ export default function Repository() {
disabled={loadingRepoIds.size > 0} disabled={loadingRepoIds.size > 0}
> >
<FlipHorizontal className="h-4 w-4 mr-2" /> <FlipHorizontal className="h-4 w-4 mr-2" />
Mirror ({selectedRepoIds.size}) Mirror ({actionCounts.mirror})
</Button> </Button>
)} )}
@@ -879,7 +1032,7 @@ export default function Repository() {
disabled={loadingRepoIds.size > 0} disabled={loadingRepoIds.size > 0}
> >
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
Sync ({selectedRepoIds.size}) Sync ({actionCounts.sync})
</Button> </Button>
)} )}
@@ -894,6 +1047,30 @@ export default function Repository() {
Retry Retry
</Button> </Button>
)} )}
{availableActions.includes('ignore') && (
<Button
variant="ghost"
size="default"
onClick={() => handleBulkSkip(true)}
disabled={loadingRepoIds.size > 0}
>
<Ban className="h-4 w-4 mr-2" />
Ignore
</Button>
)}
{availableActions.includes('include') && (
<Button
variant="outline"
size="default"
onClick={() => handleBulkSkip(false)}
disabled={loadingRepoIds.size > 0}
>
<Check className="h-4 w-4 mr-2" />
Include
</Button>
)}
</> </>
)} )}
</div> </div>
@@ -926,7 +1103,7 @@ export default function Repository() {
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>Mirror </span>({selectedRepoIds.size}) <span>Mirror </span>({actionCounts.mirror})
</Button> </Button>
)} )}
@@ -938,7 +1115,7 @@ export default function Repository() {
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>({actionCounts.sync})
</Button> </Button>
)} )}
@@ -953,6 +1130,30 @@ export default function Repository() {
Retry Retry
</Button> </Button>
)} )}
{availableActions.includes('ignore') && (
<Button
variant="ghost"
size="sm"
onClick={() => handleBulkSkip(true)}
disabled={loadingRepoIds.size > 0}
>
<Ban className="h-4 w-4 mr-2" />
Ignore
</Button>
)}
{availableActions.includes('include') && (
<Button
variant="outline"
size="sm"
onClick={() => handleBulkSkip(false)}
disabled={loadingRepoIds.size > 0}
>
<Check className="h-4 w-4 mr-2" />
Include
</Button>
)}
</div> </div>
</div> </div>
)} )}
@@ -984,6 +1185,7 @@ export default function Repository() {
onMirror={handleMirrorRepo} onMirror={handleMirrorRepo}
onSync={handleSyncRepo} onSync={handleSyncRepo}
onRetry={handleRetryRepoAction} onRetry={handleRetryRepoAction}
onSkip={handleSkipRepo}
loadingRepoIds={loadingRepoIds} loadingRepoIds={loadingRepoIds}
selectedRepoIds={selectedRepoIds} selectedRepoIds={selectedRepoIds}
onSelectionChange={setSelectedRepoIds} onSelectionChange={setSelectedRepoIds}

View File

@@ -1,7 +1,7 @@
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock } from "lucide-react"; import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown } from "lucide-react";
import { SiGithub, SiGitea } from "react-icons/si"; import { SiGithub, SiGitea } from "react-icons/si";
import type { Repository } from "@/lib/db/schema"; import type { Repository } from "@/lib/db/schema";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -19,6 +19,12 @@ import {
import { InlineDestinationEditor } from "./InlineDestinationEditor"; import { InlineDestinationEditor } from "./InlineDestinationEditor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface RepositoryTableProps { interface RepositoryTableProps {
repositories: Repository[]; repositories: Repository[];
@@ -29,6 +35,7 @@ interface RepositoryTableProps {
onMirror: ({ repoId }: { repoId: string }) => Promise<void>; onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
onSync: ({ repoId }: { repoId: string }) => Promise<void>; onSync: ({ repoId }: { repoId: string }) => Promise<void>;
onRetry: ({ repoId }: { repoId: string }) => Promise<void>; onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
onSkip: ({ repoId, skip }: { repoId: string; skip: boolean }) => Promise<void>;
loadingRepoIds: Set<string>; loadingRepoIds: Set<string>;
selectedRepoIds: Set<string>; selectedRepoIds: Set<string>;
onSelectionChange: (selectedIds: Set<string>) => void; onSelectionChange: (selectedIds: Set<string>) => void;
@@ -44,6 +51,7 @@ export default function RepositoryTable({
onMirror, onMirror,
onSync, onSync,
onRetry, onRetry,
onSkip,
loadingRepoIds, loadingRepoIds,
selectedRepoIds, selectedRepoIds,
onSelectionChange, onSelectionChange,
@@ -306,6 +314,31 @@ export default function RepositoryTable({
</Button> </Button>
)} )}
{/* Ignore/Include button */}
{repo.status === "ignored" ? (
<Button
size="default"
variant="outline"
onClick={() => repo.id && onSkip({ repoId: repo.id, skip: false })}
disabled={isLoading}
className="w-full h-10"
>
<Check className="h-4 w-4 mr-2" />
Include Repository
</Button>
) : (
<Button
size="default"
variant="ghost"
onClick={() => repo.id && onSkip({ repoId: repo.id, skip: true })}
disabled={isLoading}
className="w-full h-10"
>
<Ban className="h-4 w-4 mr-2" />
Ignore Repository
</Button>
)}
{/* External links */} {/* External links */}
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild> <Button variant="outline" size="default" className="flex-1 h-10 min-w-0" asChild>
@@ -645,6 +678,7 @@ export default function RepositoryTable({
onMirror={() => onMirror({ repoId: repo.id ?? "" })} onMirror={() => onMirror({ repoId: repo.id ?? "" })}
onSync={() => onSync({ repoId: repo.id ?? "" })} onSync={() => onSync({ repoId: repo.id ?? "" })}
onRetry={() => onRetry({ repoId: repo.id ?? "" })} onRetry={() => onRetry({ repoId: repo.id ?? "" })}
onSkip={(skip) => onSkip({ repoId: repo.id ?? "", skip })}
/> />
</div> </div>
{/* Links */} {/* Links */}
@@ -754,54 +788,108 @@ function RepoActionButton({
onMirror, onMirror,
onSync, onSync,
onRetry, onRetry,
onSkip,
}: { }: {
repo: { id: string; status: string }; repo: { id: string; status: string };
isLoading: boolean; isLoading: boolean;
onMirror: () => void; onMirror: () => void;
onSync: () => void; onSync: () => void;
onRetry: () => void; onRetry: () => void;
onSkip: (skip: boolean) => void;
}) { }) {
let label = ""; // For ignored repos, show an "Include" action
let icon = <></>; if (repo.status === "ignored") {
let onClick = () => {}; return (
let disabled = isLoading; <Button
variant="outline"
if (repo.status === "failed") { disabled={isLoading}
label = "Retry"; onClick={() => onSkip(false)}
icon = <RotateCcw className="h-4 w-4 mr-1" />; className="min-w-[80px] justify-start"
onClick = onRetry; >
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) { <Check className="h-4 w-4 mr-1" />
label = "Sync"; Include
icon = <RefreshCw className="h-4 w-4 mr-1" />; </Button>
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
} }
// For actionable statuses, show action + dropdown for skip
let primaryLabel = "";
let primaryIcon = <></>;
let primaryOnClick = () => {};
let primaryDisabled = isLoading;
let showPrimaryAction = true;
if (repo.status === "failed") {
primaryLabel = "Retry";
primaryIcon = <RotateCcw className="h-4 w-4" />;
primaryOnClick = onRetry;
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) {
primaryLabel = "Sync";
primaryIcon = <RefreshCw className="h-4 w-4" />;
primaryOnClick = onSync;
primaryDisabled ||= repo.status === "syncing";
} else if (["imported", "mirroring"].includes(repo.status)) {
primaryLabel = "Mirror";
primaryIcon = <FlipHorizontal className="h-4 w-4" />;
primaryOnClick = onMirror;
primaryDisabled ||= repo.status === "mirroring";
} else {
showPrimaryAction = false;
}
// If there's no primary action, just show ignore button
if (!showPrimaryAction) {
return (
<Button
variant="ghost"
disabled={isLoading}
onClick={() => onSkip(true)}
className="min-w-[80px] justify-start"
>
<Ban className="h-4 w-4 mr-1" />
Ignore
</Button>
);
}
// Show primary action with dropdown for skip option
return ( return (
<Button <DropdownMenu>
variant="ghost" <div className="flex">
disabled={disabled} <Button
onClick={onClick} variant="ghost"
className="min-w-[80px] justify-start" disabled={primaryDisabled}
> onClick={primaryOnClick}
{isLoading ? ( className="min-w-[80px] justify-start rounded-r-none"
<> >
<RefreshCw className="h-4 w-4 animate-spin mr-1" /> {isLoading ? (
{label} <>
</> <RefreshCw className="h-4 w-4 animate-spin mr-1" />
) : ( {primaryLabel}
<> </>
{icon} ) : (
{label} <>
</> {primaryIcon}
)} <span className="ml-1">{primaryLabel}</span>
</Button> </>
)}
</Button>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
disabled={isLoading}
className="rounded-l-none px-2 border-l"
>
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onSkip(true)}>
<Ban className="h-4 w-4 mr-2" />
Ignore Repository
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
} }

View File

@@ -0,0 +1,82 @@
import type { APIContext } from "astro";
import { db, repositories } from "@/lib/db";
import { eq, and } from "drizzle-orm";
import { createSecureErrorResponse } from "@/lib/utils";
import { repoStatusEnum } from "@/types/Repository";
export async function PATCH({ params, request }: APIContext) {
try {
const { id } = params;
const body = await request.json();
const { status, userId } = body;
if (!id || !userId) {
return new Response(
JSON.stringify({
success: false,
error: "Repository ID and User ID are required",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
// Validate the status
const validStatuses = repoStatusEnum.options;
if (!validStatuses.includes(status)) {
return new Response(
JSON.stringify({
success: false,
error: `Invalid status. Must be one of: ${validStatuses.join(", ")}`,
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
// Update the repository status
const [updatedRepo] = await db
.update(repositories)
.set({
status,
updatedAt: new Date()
})
.where(
and(
eq(repositories.id, id),
eq(repositories.userId, userId)
)
)
.returning();
if (!updatedRepo) {
return new Response(
JSON.stringify({
success: false,
error: "Repository not found or you don't have permission to update it",
}),
{
status: 404,
headers: { "Content-Type": "application/json" },
}
);
}
return new Response(
JSON.stringify({
success: true,
repository: updatedRepo,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (error) {
return createSecureErrorResponse(error);
}
}