-
Notifications
You must be signed in to change notification settings - Fork 0
agent: @U0AJM7X8FBR Admin - please update the mono/admin repo with a table show #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import OrgsPage from "@/components/Orgs/OrgsPage"; | ||
|
|
||
| export const metadata = { | ||
| title: "Org Repos — Recoup Admin", | ||
| description: "View all GitHub org repositories and their commit statistics.", | ||
| }; | ||
|
|
||
| export default function Page() { | ||
| return <OrgsPage />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import OrgsTableContainer from "@/components/Orgs/OrgsTableContainer"; | ||
|
|
||
| export default function OrgsPage() { | ||
| return ( | ||
| <main className="mx-auto max-w-5xl px-4 py-10"> | ||
| <div className="mb-6"> | ||
| <h1 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-gray-100"> | ||
| Org Repos | ||
| </h1> | ||
| <p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> | ||
| All GitHub org repositories and their commit statistics. | ||
| </p> | ||
| </div> | ||
| <OrgsTableContainer /> | ||
| </main> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| flexRender, | ||
| getCoreRowModel, | ||
| getSortedRowModel, | ||
| useReactTable, | ||
| type SortingState, | ||
| } from "@tanstack/react-table"; | ||
| import { useState } from "react"; | ||
| import { | ||
| Table, | ||
| TableBody, | ||
| TableCell, | ||
| TableHead, | ||
| TableHeader, | ||
| TableRow, | ||
| } from "@/components/ui/table"; | ||
| import { orgsColumns } from "@/components/Orgs/orgsColumns"; | ||
| import type { OrgRepoRow } from "@/types/org"; | ||
|
|
||
| interface OrgsTableProps { | ||
| repos: OrgRepoRow[]; | ||
| } | ||
|
|
||
| /** | ||
| * Renders a shadcn Data Table of org repos and their commit statistics. | ||
| * Default sort: Total Commits descending (most commits first). | ||
| * | ||
| * @param repos - Array of org repo rows from the admin API | ||
| */ | ||
| export default function OrgsTable({ repos }: OrgsTableProps) { | ||
| const [sorting, setSorting] = useState<SortingState>([ | ||
| { id: "total_commits", desc: true }, | ||
| ]); | ||
|
|
||
| const table = useReactTable({ | ||
| data: repos, | ||
| columns: orgsColumns, | ||
| state: { sorting }, | ||
| onSortingChange: setSorting, | ||
| getCoreRowModel: getCoreRowModel(), | ||
| getSortedRowModel: getSortedRowModel(), | ||
| }); | ||
|
|
||
| return ( | ||
| <div className="rounded-lg border"> | ||
| <Table> | ||
| <TableHeader> | ||
| {table.getHeaderGroups().map(headerGroup => ( | ||
| <TableRow key={headerGroup.id}> | ||
| {headerGroup.headers.map(header => ( | ||
| <TableHead key={header.id}> | ||
| {header.isPlaceholder | ||
| ? null | ||
| : flexRender( | ||
| header.column.columnDef.header, | ||
| header.getContext(), | ||
| )} | ||
| </TableHead> | ||
| ))} | ||
| </TableRow> | ||
| ))} | ||
| </TableHeader> | ||
| <TableBody> | ||
| {table.getRowModel().rows.length ? ( | ||
| table.getRowModel().rows.map(row => ( | ||
| <TableRow key={row.id}> | ||
| {row.getVisibleCells().map(cell => ( | ||
| <TableCell key={cell.id}> | ||
| {flexRender(cell.column.columnDef.cell, cell.getContext())} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| )) | ||
| ) : ( | ||
| <TableRow> | ||
| <TableCell | ||
| colSpan={orgsColumns.length} | ||
| className="h-24 text-center" | ||
| > | ||
| No results. | ||
| </TableCell> | ||
| </TableRow> | ||
| )} | ||
| </TableBody> | ||
| </Table> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| "use client"; | ||
|
|
||
| import { useAdminSandboxOrgs } from "@/hooks/useAdminSandboxOrgs"; | ||
| import OrgsTable from "@/components/Orgs/OrgsTable"; | ||
|
|
||
| export default function OrgsTableContainer() { | ||
| const { data: repos, isLoading, error } = useAdminSandboxOrgs(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center py-12 text-sm text-gray-500"> | ||
| Loading… | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400"> | ||
| {error instanceof Error ? error.message : "Failed to load org repos"} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!repos || repos.length === 0) { | ||
| return ( | ||
| <div className="flex items-center justify-center py-12 text-sm text-gray-400"> | ||
| No org repo data found. | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return <OrgsTable repos={repos} />; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { type ColumnDef } from "@tanstack/react-table"; | ||
| import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import type { OrgRepoRow } from "@/types/org"; | ||
|
|
||
| export const orgsColumns: ColumnDef<OrgRepoRow>[] = [ | ||
| { | ||
| accessorKey: "repo_name", | ||
| header: "Repository", | ||
| cell: ({ row }) => ( | ||
| <a | ||
| href={row.original.repo_url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="font-medium text-blue-600 hover:underline dark:text-blue-400" | ||
| > | ||
| {row.getValue("repo_name")} | ||
| </a> | ||
| ), | ||
| }, | ||
| { | ||
| accessorKey: "total_commits", | ||
| header: ({ column }) => ( | ||
| <Button | ||
| variant="ghost" | ||
| onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} | ||
| className="-ml-3 h-8 px-3" | ||
| > | ||
| Total Commits | ||
| {column.getIsSorted() === "asc" ? ( | ||
| <ArrowUp className="ml-1 h-4 w-4" /> | ||
| ) : column.getIsSorted() === "desc" ? ( | ||
| <ArrowDown className="ml-1 h-4 w-4" /> | ||
| ) : ( | ||
| <ArrowUpDown className="ml-1 h-4 w-4 text-muted-foreground" /> | ||
| )} | ||
| </Button> | ||
| ), | ||
| }, | ||
| { | ||
| accessorKey: "latest_commit_messages", | ||
| header: "Latest Commits", | ||
| cell: ({ row }) => { | ||
| const messages = row.getValue<string[]>("latest_commit_messages"); | ||
| return ( | ||
| <ul className="space-y-0.5 text-xs text-gray-600 dark:text-gray-400"> | ||
| {messages.map((msg, i) => ( | ||
| <li key={i} className="truncate max-w-xs" title={msg}> | ||
| {msg} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| accessorKey: "earliest_committed_at", | ||
| header: ({ column }) => ( | ||
| <Button | ||
| variant="ghost" | ||
| onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} | ||
| className="-ml-3 h-8 px-3" | ||
| > | ||
| First Commit | ||
| {column.getIsSorted() === "asc" ? ( | ||
| <ArrowUp className="ml-1 h-4 w-4" /> | ||
| ) : column.getIsSorted() === "desc" ? ( | ||
| <ArrowDown className="ml-1 h-4 w-4" /> | ||
| ) : ( | ||
| <ArrowUpDown className="ml-1 h-4 w-4 text-muted-foreground" /> | ||
| )} | ||
| </Button> | ||
| ), | ||
| cell: ({ row }) => | ||
| new Date(row.getValue<string>("earliest_committed_at")).toLocaleString(), | ||
| sortingFn: "datetime", | ||
| }, | ||
| { | ||
| accessorKey: "latest_committed_at", | ||
| header: ({ column }) => ( | ||
| <Button | ||
| variant="ghost" | ||
| onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} | ||
| className="-ml-3 h-8 px-3" | ||
| > | ||
| Latest Commit | ||
| {column.getIsSorted() === "asc" ? ( | ||
| <ArrowUp className="ml-1 h-4 w-4" /> | ||
| ) : column.getIsSorted() === "desc" ? ( | ||
| <ArrowDown className="ml-1 h-4 w-4" /> | ||
| ) : ( | ||
| <ArrowUpDown className="ml-1 h-4 w-4 text-muted-foreground" /> | ||
| )} | ||
| </Button> | ||
| ), | ||
| cell: ({ row }) => | ||
| new Date(row.getValue<string>("latest_committed_at")).toLocaleString(), | ||
| sortingFn: "datetime", | ||
| }, | ||
| { | ||
| accessorKey: "account_repo_count", | ||
| header: ({ column }) => ( | ||
| <Button | ||
| variant="ghost" | ||
| onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} | ||
| className="-ml-3 h-8 px-3" | ||
| > | ||
| Account Repos | ||
| {column.getIsSorted() === "asc" ? ( | ||
| <ArrowUp className="ml-1 h-4 w-4" /> | ||
| ) : column.getIsSorted() === "desc" ? ( | ||
| <ArrowDown className="ml-1 h-4 w-4" /> | ||
| ) : ( | ||
| <ArrowUpDown className="ml-1 h-4 w-4 text-muted-foreground" /> | ||
| )} | ||
| </Button> | ||
| ), | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| "use client"; | ||
|
|
||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { usePrivy } from "@privy-io/react-auth"; | ||
| import { fetchAdminSandboxOrgs } from "@/lib/fetchAdminSandboxOrgs"; | ||
|
|
||
| /** | ||
| * Fetches org repo commit statistics from GET /api/admins/sandboxes/orgs | ||
| * using TanStack Query. Authenticates with the Privy access token. | ||
| */ | ||
| export function useAdminSandboxOrgs() { | ||
| const { ready, authenticated, getAccessToken } = usePrivy(); | ||
|
|
||
| return useQuery({ | ||
| queryKey: ["admin", "sandboxes", "orgs"], | ||
| queryFn: async () => { | ||
| const token = await getAccessToken(); | ||
| if (!token) throw new Error("Not authenticated"); | ||
| return fetchAdminSandboxOrgs(token); | ||
| }, | ||
| enabled: ready && authenticated, | ||
|
Comment on lines
+15
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether query cache is user-scoped or explicitly cleared on auth transitions.
rg -nP --type=ts --type=tsx 'queryKey:\s*\[\s*"admin"\s*,\s*"sandboxes"(?:\s*,\s*"orgs")?' -C2
rg -nP --type=ts --type=tsx 'removeQueries|resetQueries|clear\(|invalidateQueries|signOut|logout|logOut' -C2Repository: recoupable/admin Length of output: 115 🏁 Script executed: #!/bin/bash
# Find the target file and understand the codebase structure
find . -name "useAdminSandboxOrgs.ts" -o -name "useAdminSandboxOrgs.tsx" 2>/dev/null
# Also look for hooks directory
fd -type f 'useAdmin' . 2>/dev/null | head -20Repository: recoupable/admin Length of output: 90 🏁 Script executed: #!/bin/bash
# Search for cache clearing patterns without type restrictions
rg "queryKey.*admin.*sandbox" -B 2 -A 5 2>/dev/null
rg "invalidateQueries|removeQueries|resetQueries|clearCache|signOut|logout" -B 1 -A 2 2>/dev/null | head -50Repository: recoupable/admin Length of output: 1483 🏁 Script executed: #!/bin/bash
# Look for Privy auth setup and cache clearing
rg "usePrivy|signOut|logout|authentication" -B 1 -A 1 2>/dev/null | head -80Repository: recoupable/admin Length of output: 2343 🏁 Script executed: #!/bin/bash
# Check query client setup and cache clearing handlers
rg "QueryClient|new QueryClient" -B 2 -A 5 2>/dev/null
rg "logout|signOut" -B 3 -A 5 2>/dev/null | grep -A 5 -B 3 "logout\|signOut"Repository: recoupable/admin Length of output: 1844 🏁 Script executed: #!/bin/bash
# Check useIsAdmin.ts for same pattern
cat hooks/useIsAdmin.ts 2>/dev/nullRepository: recoupable/admin Length of output: 869 🏁 Script executed: #!/bin/bash
# Check where useAdminSandboxOrgs is used and if there's cache invalidation nearby
rg "useAdminSandboxOrgs" -B 2 -A 2 2>/dev/nullRepository: recoupable/admin Length of output: 1004 🏁 Script executed: #!/bin/bash
# Check PrivyProvider for logout handlers or cache clearing
cat providers/PrivyProvider.tsx 2>/dev/nullRepository: recoupable/admin Length of output: 973 🏁 Script executed: #!/bin/bash
# Check full useAdminSandboxOrgs.ts implementation
cat hooks/useAdminSandboxOrgs.ts 2>/dev/nullRepository: recoupable/admin Length of output: 785 Scope the query cache key to the authenticated identity. Using a static key for authenticated data risks serving stale cached data after logout/sign-in/user-switch in the same browser session. When This pattern affects multiple auth-bound queries ( 🔐 Proposed fix export function useAdminSandboxOrgs() {
- const { ready, authenticated, getAccessToken } = usePrivy();
+ const { ready, authenticated, user, getAccessToken } = usePrivy();
return useQuery({
- queryKey: ["admin", "sandboxes", "orgs"],
+ queryKey: ["admin", "sandboxes", "orgs", user?.id],
queryFn: async () => {
const token = await getAccessToken();
if (!token) throw new Error("Not authenticated");
return fetchAdminSandboxOrgs(token);
},
- enabled: ready && authenticated,
+ enabled: ready && authenticated && !!user?.id,
});
}🤖 Prompt for AI Agents |
||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { API_BASE_URL } from "@/lib/consts"; | ||
| import type { OrgRepoRow } from "@/types/org"; | ||
|
|
||
| /** | ||
| * Fetches org repo commit statistics from GET /api/admins/sandboxes/orgs. | ||
| * Authenticates using the caller's Privy access token. | ||
| * | ||
| * @param accessToken - Privy access token from getAccessToken() | ||
| * @returns Array of org repo rows | ||
| */ | ||
| export async function fetchAdminSandboxOrgs( | ||
| accessToken: string, | ||
| ): Promise<OrgRepoRow[]> { | ||
| const res = await fetch(`${API_BASE_URL}/api/admins/sandboxes/orgs`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
|
|
||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.message ?? `HTTP ${res.status}`); | ||
| } | ||
|
|
||
| const data = await res.json(); | ||
| return data.repos ?? []; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export interface OrgRepoRow { | ||
| repo_name: string; | ||
| repo_url: string; | ||
| total_commits: number; | ||
| latest_commit_messages: string[]; | ||
| earliest_committed_at: string; | ||
| latest_committed_at: string; | ||
| account_repo_count: number; | ||
| } | ||
|
|
||
| export interface AdminSandboxOrgsResponse { | ||
| status: "success" | "error"; | ||
| repos: OrgRepoRow[]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: recoupable/admin
Length of output: 143
🏁 Script executed:
Repository: recoupable/admin
Length of output: 2137
🏁 Script executed:
Repository: recoupable/admin
Length of output: 2830
🏁 Script executed:
Repository: recoupable/admin
Length of output: 1682
Handle disabled query state explicitly in OrgsTableContainer.
When the query is disabled (user not authenticated or Privy not ready),
isLoadinganderrorare both falsy, causing the component to display "No org repo data found." This is misleading UX that masks authentication state instead of indicating why data cannot be loaded.Add explicit
readyandauthenticatedchecks before showing the empty state. The hook already has access to Privy state; the container needs to mirror these checks or pass them through.Suggested approach
"use client"; import { useAdminSandboxOrgs } from "@/hooks/useAdminSandboxOrgs"; import OrgsTable from "@/components/Orgs/OrgsTable"; +import { usePrivy } from "@privy-io/react-auth"; export default function OrgsTableContainer() { + const { ready, authenticated } = usePrivy(); const { data: repos, isLoading, error } = useAdminSandboxOrgs(); + + if (!ready) { + return <div className="flex items-center justify-center py-12 text-sm text-gray-500">Loading…</div>; + } + + if (!authenticated) { + return <div className="flex items-center justify-center py-12 text-sm text-gray-400">Sign in to view org repos.</div>; + } if (isLoading) {📝 Committable suggestion
🤖 Prompt for AI Agents