Skip to content

Commit cdc176e

Browse files
snomiaoclaude
andauthored
feat: add CNRepos dashboard page for repository management (#45)
* feat: add CNRepos dashboard page for repository management Add comprehensive React page to display Custom Node repositories with: - Status indicators for Registry/ComfyUI-Manager integration - Interactive table with repository details and pull request links - Expandable YAML details for debugging - Visual legend for repository states 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: enhance CNRepos dashboard UI with improved styling and layout - Upgrade CNRepos dashboard with shadcn/ui components (Card, Badge, Table) - Replace basic HTML table with shadcn Table component for better UX - Improve status legend presentation with Card layout - Add responsive grid layout for better mobile experience - Enhance visual hierarchy with proper spacing and typography - Add hover states and transitions for better interactivity - Improve accessibility with semantic HTML and proper contrast 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: resolve TypeScript errors in CNRepos page - Import TaskDataOrNull and TaskErrorOrNull helper functions - Replace direct property access with safe Task helpers - Fix type errors for candidate, crPulls, info, and pulls data access - Ensure proper error handling for Task states 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: add pagination to CNRepos dashboard - Add pagination component with page navigation controls - Implement server-side pagination with 20 items per page - Add page parameter support via URL search params - Display current page and total pages information - Include ellipsis for large page ranges 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: update CNRepos page searchParams to Promise for Next.js 15 compatibility Fixes TypeScript build error by updating searchParams type to Promise<{page?: string}> and properly awaiting it in the component, as required by Next.js 15. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: improve cnrepos table density and add modal for details - Increased page size from 20 to 60 items - Made table more dense with reduced padding and smaller text - Replaced HTML details tag with modal dialog for repository info - Added interactive Info button that opens detailed modal - Created client component for table to handle modal state - Enhanced modal content with additional repository metadata 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: resolve TypeScript build errors in CNReposTableClient - Fixed _id property type issue by extending CNRepo type - Updated info field to use only available properties - Changed pull request state property from 'state' to 'prState' - Removed non-existent properties from repo info display 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 29cf49e commit cdc176e

File tree

10 files changed

+1028
-277
lines changed

10 files changed

+1028
-277
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"use client";
2+
3+
import { Badge } from "@/components/ui/badge";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogTrigger,
12+
} from "@/components/ui/dialog";
13+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
14+
import { TaskDataOrNull, TaskErrorOrNull } from "@/packages/mongodb-pipeline-ts/Task";
15+
import type { CNRepo } from "@/src/CNRepos";
16+
import { Info } from "lucide-react";
17+
import { useState } from "react";
18+
import yaml from "yaml";
19+
20+
interface CNReposTableClientProps {
21+
repos: Array<CNRepo & { _id?: any }>;
22+
}
23+
24+
export function CNReposTableClient({ repos }: CNReposTableClientProps) {
25+
return (
26+
<Table className="text-sm">
27+
<TableHeader>
28+
<TableRow className="h-8">
29+
<TableHead className="w-12 px-2 py-1">Status</TableHead>
30+
<TableHead className="px-2 py-1">Repository</TableHead>
31+
<TableHead className="w-20 text-center px-2 py-1">Registry</TableHead>
32+
<TableHead className="w-28 text-center px-2 py-1">ComfyUI-Manager</TableHead>
33+
<TableHead className="w-20 text-center px-2 py-1">Candidate</TableHead>
34+
<TableHead className="px-2 py-1">Pull Requests</TableHead>
35+
<TableHead className="w-20 px-2 py-1">Info</TableHead>
36+
</TableRow>
37+
</TableHeader>
38+
<TableBody>
39+
{repos.map((repo) => (
40+
<CNRepoRow key={repo._id?.toString()} repo={repo} />
41+
))}
42+
</TableBody>
43+
</Table>
44+
);
45+
}
46+
47+
function CNRepoRow({ repo }: { repo: CNRepo & { _id?: any } }) {
48+
const [isOpen, setIsOpen] = useState(false);
49+
50+
const getStatusIcon = () => {
51+
if (!repo.cr && !repo.cm) return "🫗";
52+
if (!!repo.cr && !!repo.cm && repo.crPulls?.state === "ok") return "✅";
53+
if (!!repo.cr && !!repo.cm) return "☑️";
54+
if (!!repo.cr && !repo.cm) return "✔️";
55+
if (!repo.crPulls) return "🧪";
56+
if (repo.crPulls.state === "ok") return "👀";
57+
if (TaskErrorOrNull(repo.crPulls)) return "❗";
58+
return "❓";
59+
};
60+
61+
const getStatusDescription = () => {
62+
const icon = getStatusIcon();
63+
const descriptions = {
64+
"🫗": "Repository not listed in ComfyUI-Manager or Registry",
65+
"✅": "Listed in both Registry and ComfyUI-Manager (PR successful)",
66+
"☑️": "Listed in both Registry and ComfyUI-Manager",
67+
"✔️": "Listed in Registry only",
68+
"🧪": "Ready to create PR",
69+
"👀": "Pull request pending review",
70+
"❗": "Error occurred during processing",
71+
"❓": "Unknown status",
72+
};
73+
return descriptions[icon as keyof typeof descriptions] || "Unknown status";
74+
};
75+
76+
const getRepoName = (url: string) => {
77+
return url.replace(/^https:\/\/github\.com\//, "");
78+
};
79+
80+
return (
81+
<TableRow className="h-10">
82+
<TableCell className="text-center text-base px-2 py-1" title={getStatusDescription()}>
83+
{getStatusIcon()}
84+
</TableCell>
85+
<TableCell className="px-2 py-1">
86+
<a
87+
href={repo.repository}
88+
target="_blank"
89+
rel="noreferrer"
90+
className="text-primary hover:underline font-mono text-xs transition-colors"
91+
>
92+
{getRepoName(repo.repository)}
93+
</a>
94+
</TableCell>
95+
<TableCell className="text-center px-2 py-1">
96+
{repo.cr_ids?.length || repo.cr ? (
97+
<Badge variant="secondary" className="text-xs px-1 py-0">
98+
99+
</Badge>
100+
) : (
101+
<span className="text-muted-foreground">-</span>
102+
)}
103+
</TableCell>
104+
<TableCell className="text-center px-2 py-1">
105+
{repo.cm_ids?.length || repo.cm ? (
106+
<Badge variant="secondary" className="text-xs px-1 py-0">
107+
108+
</Badge>
109+
) : (
110+
<span className="text-muted-foreground">-</span>
111+
)}
112+
</TableCell>
113+
<TableCell className="text-center px-2 py-1">
114+
{TaskDataOrNull(repo.candidate) ? (
115+
<Badge variant="outline" className="text-xs px-1 py-0">
116+
117+
</Badge>
118+
) : (
119+
<span className="text-muted-foreground">-</span>
120+
)}
121+
</TableCell>
122+
<TableCell className="px-2 py-1">
123+
<div className="space-y-0.5">
124+
{TaskDataOrNull(repo.crPulls)?.map((pull, idx) => (
125+
<div key={idx} className="text-xs">
126+
{pull.pull?.html_url ? (
127+
<a
128+
href={pull.pull.html_url}
129+
target="_blank"
130+
rel="noreferrer"
131+
className="text-primary hover:underline transition-colors"
132+
>
133+
#{pull.pull.html_url.split("/").pop()} ({pull.type})
134+
</a>
135+
) : (
136+
<span className="text-muted-foreground">Pending</span>
137+
)}
138+
</div>
139+
))}
140+
{TaskErrorOrNull(repo.crPulls) && (
141+
<Badge variant="destructive" className="text-xs" title={TaskErrorOrNull(repo.crPulls) || ""}>
142+
Error
143+
</Badge>
144+
)}
145+
</div>
146+
</TableCell>
147+
<TableCell className="px-2 py-1">
148+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
149+
<DialogTrigger asChild>
150+
<Button variant="ghost" size="sm" className="h-7 px-2">
151+
<Info className="h-4 w-4" />
152+
</Button>
153+
</DialogTrigger>
154+
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
155+
<DialogHeader>
156+
<DialogTitle className="font-mono text-sm">{getRepoName(repo.repository)}</DialogTitle>
157+
<DialogDescription>Repository details and metadata</DialogDescription>
158+
</DialogHeader>
159+
<div className="mt-4">
160+
<pre className="p-4 bg-muted rounded-lg text-xs overflow-auto font-mono">
161+
{yaml.stringify({
162+
repository: repo.repository,
163+
status: getStatusDescription(),
164+
cr_ids: repo.cr_ids?.length || 0,
165+
cm_ids: repo.cm_ids?.length || 0,
166+
candidate: TaskDataOrNull(repo.candidate),
167+
info: TaskDataOrNull(repo.info)
168+
? {
169+
archived: TaskDataOrNull(repo.info)?.archived,
170+
private: TaskDataOrNull(repo.info)?.private,
171+
default_branch: TaskDataOrNull(repo.info)?.default_branch,
172+
html_url: TaskDataOrNull(repo.info)?.html_url,
173+
}
174+
: null,
175+
pulls_count: TaskDataOrNull(repo.pulls)?.length || 0,
176+
crPulls: TaskDataOrNull(repo.crPulls)?.map((pull) => ({
177+
type: pull.type,
178+
url: pull.pull?.html_url,
179+
state: pull.pull?.prState,
180+
})),
181+
crPulls_state: repo.crPulls?.state,
182+
error: TaskErrorOrNull(repo.crPulls),
183+
})}
184+
</pre>
185+
</div>
186+
</DialogContent>
187+
</Dialog>
188+
</TableCell>
189+
</TableRow>
190+
);
191+
}

app/(dashboard)/cnrepos/page.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2+
import {
3+
Pagination,
4+
PaginationContent,
5+
PaginationItem,
6+
PaginationLink,
7+
PaginationNext,
8+
PaginationPrevious,
9+
} from "@/components/ui/pagination";
10+
import { CNRepos } from "@/src/CNRepos";
11+
import { Suspense } from "react";
12+
import { CNReposTableClient } from "./CNReposTableClient";
13+
14+
interface CNReposPageProps {
15+
searchParams?: Promise<{
16+
page?: string;
17+
}>;
18+
}
19+
20+
export default async function CNReposPage({ searchParams }: CNReposPageProps) {
21+
const resolvedSearchParams = await searchParams;
22+
const page = Number(resolvedSearchParams?.page) || 1;
23+
24+
return (
25+
<div className="container mx-auto p-6 space-y-6">
26+
<div className="space-y-2">
27+
<h1 className="text-3xl font-bold tracking-tight">Custom Node Repositories</h1>
28+
<p className="text-muted-foreground">
29+
Manage and monitor ComfyUI custom node repositories and their registry integration status.
30+
</p>
31+
</div>
32+
33+
<Card>
34+
<CardHeader>
35+
<CardTitle className="text-lg">Status Legend</CardTitle>
36+
</CardHeader>
37+
<CardContent>
38+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
39+
<div className="flex items-center gap-2">
40+
<span className="text-lg"></span>
41+
<span className="text-sm">Registry + ComfyUI-Manager</span>
42+
</div>
43+
<div className="flex items-center gap-2">
44+
<span className="text-lg">✔️</span>
45+
<span className="text-sm">Registry only</span>
46+
</div>
47+
<div className="flex items-center gap-2">
48+
<span className="text-lg">🧪</span>
49+
<span className="text-sm">Ready to Create PR</span>
50+
</div>
51+
<div className="flex items-center gap-2">
52+
<span className="text-lg">👀</span>
53+
<span className="text-sm">Pending Review</span>
54+
</div>
55+
<div className="flex items-center gap-2">
56+
<span className="text-lg">🫗</span>
57+
<span className="text-sm">Outside ComfyUI-Manager</span>
58+
</div>
59+
<div className="flex items-center gap-2">
60+
<span className="text-lg"></span>
61+
<span className="text-sm">Error occurred</span>
62+
</div>
63+
</div>
64+
</CardContent>
65+
</Card>
66+
67+
<Card>
68+
<CardHeader>
69+
<CardTitle>Repositories</CardTitle>
70+
<CardDescription>Recent repositories and their integration status</CardDescription>
71+
</CardHeader>
72+
<CardContent>
73+
<Suspense fallback={<div className="flex justify-center p-8">⏳ Loading repositories...</div>}>
74+
<CNReposTable page={page} />
75+
</Suspense>
76+
</CardContent>
77+
</Card>
78+
</div>
79+
);
80+
}
81+
82+
async function CNReposTable({ page }: { page: number }) {
83+
const pageSize = 60;
84+
const skip = (page - 1) * pageSize;
85+
86+
const [repos, totalCount] = await Promise.all([
87+
CNRepos.find({}).sort({ _id: -1 }).skip(skip).limit(pageSize).toArray(),
88+
CNRepos.countDocuments({}),
89+
]);
90+
91+
const totalPages = Math.ceil(totalCount / pageSize);
92+
93+
if (!repos.length) {
94+
return <div className="text-center p-8">No repositories found</div>;
95+
}
96+
97+
return (
98+
<div className="space-y-4">
99+
<CNReposTableClient repos={repos} />
100+
{totalPages > 1 && <CNReposPagination currentPage={page} totalPages={totalPages} />}
101+
</div>
102+
);
103+
}
104+
105+
function CNReposPagination({ currentPage, totalPages }: { currentPage: number; totalPages: number }) {
106+
const maxVisiblePages = 5;
107+
108+
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
109+
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
110+
111+
if (endPage - startPage + 1 < maxVisiblePages) {
112+
startPage = Math.max(1, endPage - maxVisiblePages + 1);
113+
}
114+
115+
const createPageUrl = (page: number) => {
116+
const params = new URLSearchParams();
117+
if (page > 1) params.set("page", page.toString());
118+
return `/cnrepos${params.toString() ? "?" + params.toString() : ""}`;
119+
};
120+
121+
return (
122+
<div className="flex items-center justify-between">
123+
<div className="text-sm text-muted-foreground">
124+
Page {currentPage} of {totalPages}
125+
</div>
126+
127+
<Pagination>
128+
<PaginationContent>
129+
{currentPage > 1 && (
130+
<PaginationItem>
131+
<PaginationPrevious href={createPageUrl(currentPage - 1)} />
132+
</PaginationItem>
133+
)}
134+
135+
{startPage > 1 && (
136+
<>
137+
<PaginationItem>
138+
<PaginationLink href={createPageUrl(1)}>1</PaginationLink>
139+
</PaginationItem>
140+
{startPage > 2 && <PaginationItem>...</PaginationItem>}
141+
</>
142+
)}
143+
144+
{Array.from({ length: endPage - startPage + 1 }, (_, i) => {
145+
const page = startPage + i;
146+
return (
147+
<PaginationItem key={page}>
148+
<PaginationLink href={createPageUrl(page)} isActive={page === currentPage}>
149+
{page}
150+
</PaginationLink>
151+
</PaginationItem>
152+
);
153+
})}
154+
155+
{endPage < totalPages && (
156+
<>
157+
{endPage < totalPages - 1 && <PaginationItem>...</PaginationItem>}
158+
<PaginationItem>
159+
<PaginationLink href={createPageUrl(totalPages)}>{totalPages}</PaginationLink>
160+
</PaginationItem>
161+
</>
162+
)}
163+
164+
{currentPage < totalPages && (
165+
<PaginationItem>
166+
<PaginationNext href={createPageUrl(currentPage + 1)} />
167+
</PaginationItem>
168+
)}
169+
</PaginationContent>
170+
</Pagination>
171+
</div>
172+
);
173+
}

0 commit comments

Comments
 (0)