mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Add page to list indexed repositories
This commit is contained in:
parent
c1227000da
commit
1283f6487f
14 changed files with 781 additions and 27 deletions
|
|
@ -18,11 +18,13 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@tanstack/react-query": "^5.53.3",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
BIN
public/sb_logo_dark_small.png
Normal file
BIN
public/sb_logo_dark_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
public/sb_logo_light_small.png
Normal file
BIN
public/sb_logo_light_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
8
src/app/api/(server)/repos/route.ts
Normal file
8
src/app/api/(server)/repos/route.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
'use server';
|
||||
|
||||
import { listRepositories } from "@/lib/server/searchService";
|
||||
|
||||
export const GET = async () => {
|
||||
const response = await listRepositories();
|
||||
return Response.json(response);
|
||||
}
|
||||
79
src/app/navigationMenu.tsx
Normal file
79
src/app/navigationMenu.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NavigationMenu as NavigationMenuBase, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||
import Link from "next/link";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { SettingsDropdown } from "./settingsDropdown";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Image from "next/image";
|
||||
import logoDark from "../../public/sb_logo_dark_small.png";
|
||||
import logoLight from "../../public/sb_logo_light_small.png";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const SOURCEBOT_GITHUB_URL = "https://github.com/TaqlaAI/sourcebot";
|
||||
|
||||
export const NavigationMenu = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-screen h-fit">
|
||||
<div className="flex flex-row justify-between items-center py-1.5 px-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div
|
||||
className="mr-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/");
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-11 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-11 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavigationMenuBase>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/" legacyBehavior passHref>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
Search
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/repos" legacyBehavior passHref>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
Repositories
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenuBase>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
window.open(SOURCEBOT_GITHUB_URL, "_blank");
|
||||
}}
|
||||
>
|
||||
<GitHubLogoIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<SettingsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
|
@ -3,32 +3,15 @@
|
|||
import Image from "next/image";
|
||||
import logoDark from "../../public/sb_logo_dark_large.png";
|
||||
import logoLight from "../../public/sb_logo_light_large.png";
|
||||
import { NavigationMenu } from "./navigationMenu";
|
||||
import { SearchBar } from "./searchBar";
|
||||
import { SettingsDropdown } from "./settingsDropdown";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const SOURCEBOT_GITHUB_URL = "https://github.com/TaqlaAI/sourcebot";
|
||||
|
||||
export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center">
|
||||
{/* TopBar */}
|
||||
<div className="absolute top-0 left-0 right-0">
|
||||
<div className="flex flex-row justify-end items-center py-1.5 px-3 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
window.open(SOURCEBOT_GITHUB_URL, "_blank");
|
||||
}}
|
||||
>
|
||||
<GitHubLogoIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<SettingsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<NavigationMenu />
|
||||
|
||||
<div className="flex flex-col justify-center items-center p-4 mt-48">
|
||||
<div className="max-h-44 w-auto">
|
||||
<Image
|
||||
|
|
|
|||
129
src/app/repos/columns.tsx
Normal file
129
src/app/repos/columns.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||
import { ArrowUpDown } from "lucide-react"
|
||||
|
||||
|
||||
|
||||
export type RepositoryColumnInfo = {
|
||||
name: string;
|
||||
branches: {
|
||||
name: string,
|
||||
version: string,
|
||||
}[];
|
||||
repoSizeBytes: number;
|
||||
indexedFiles: number;
|
||||
indexSizeBytes: number;
|
||||
shardCount: number;
|
||||
lastIndexed: string;
|
||||
latestCommit: string;
|
||||
commitUrlTemplate: string;
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "branches",
|
||||
header: "Branches",
|
||||
cell: ({ row }) => {
|
||||
const branches = row.original.branches;
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{branches.map(({ name, version }, index) => {
|
||||
const shortVersion = version.substring(0, 8);
|
||||
return (
|
||||
<span key={index}>
|
||||
{name}
|
||||
@
|
||||
<span
|
||||
className="cursor-pointer text-blue-500 hover:underline"
|
||||
onClick={() => {
|
||||
const url = row.original.commitUrlTemplate.replace("{{.Version}}", version);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
{shortVersion}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "shardCount",
|
||||
header: ({ column }) => createSortHeader("Shard Count", column),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right">{row.original.shardCount}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "indexedFiles",
|
||||
header: ({ column }) => createSortHeader("Indexed Files", column),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-right">{row.original.indexedFiles}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "indexSizeBytes",
|
||||
header: ({ column }) => createSortHeader("Index Size", column),
|
||||
cell: ({ row }) => {
|
||||
const size = getFormattedSizeString(row.original.indexSizeBytes);
|
||||
return <div className="text-right">{size}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "repoSizeBytes",
|
||||
header: ({ column }) => createSortHeader("Repository Size", column),
|
||||
cell: ({ row }) => {
|
||||
const size = getFormattedSizeString(row.original.repoSizeBytes);
|
||||
return <div className="text-right">{size}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "lastIndexed",
|
||||
header: ({ column }) => createSortHeader("Last Indexed", column),
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.original.lastIndexed);
|
||||
return date.toISOString();
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "latestCommit",
|
||||
header: ({ column }) => createSortHeader("Latest Commit", column),
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.original.latestCommit);
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getFormattedSizeString = (bytes: number): string => {
|
||||
let size = bytes;
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
const createSortHeader = (name: string, column: Column<RepositoryColumnInfo, unknown>) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{name}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
43
src/app/repos/page.tsx
Normal file
43
src/app/repos/page.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { NavigationMenu } from "../navigationMenu";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { columns, RepositoryColumnInfo } from "./columns";
|
||||
import { listRepositories } from "@/lib/server/searchService";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
|
||||
export default async function ReposPage() {
|
||||
const _repos = await listRepositories();
|
||||
|
||||
if (isServiceError(_repos)) {
|
||||
return <div>Error fetching repositories</div>;
|
||||
}
|
||||
const repos = _repos.List.Repos.map((repo): RepositoryColumnInfo => {
|
||||
return {
|
||||
name: repo.Repository.Name,
|
||||
branches: repo.Repository.Branches.map((branch) => {
|
||||
return {
|
||||
name: branch.Name,
|
||||
version: branch.Version,
|
||||
}
|
||||
}),
|
||||
repoSizeBytes: repo.Stats.ContentBytes,
|
||||
indexSizeBytes: repo.Stats.IndexBytes,
|
||||
shardCount: repo.Stats.Shards,
|
||||
lastIndexed: repo.IndexMetadata.IndexTime,
|
||||
latestCommit: repo.Repository.LatestCommitDate,
|
||||
indexedFiles: repo.Stats.Documents,
|
||||
commitUrlTemplate: repo.Repository.CommitURLTemplate,
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center">
|
||||
<NavigationMenu />
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={repos}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Search repositories..."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
136
src/components/ui/data-table.tsx
Normal file
136
src/components/ui/data-table.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
searchKey: string
|
||||
searchPlaceholder?: string
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
searchKey,
|
||||
searchPlaceholder,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn(searchKey)?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<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}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
128
src/components/ui/navigation-menu.tsx
Normal file
128
src/components/ui/navigation-menu.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||
)
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
117
src/components/ui/table.tsx
Normal file
117
src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
|
|
@ -17,11 +17,11 @@ export type SearchResultLocation = z.infer<typeof locationSchema>;
|
|||
|
||||
// @see : https://github.com/TaqlaAI/zoekt/blob/main/api.go#L212
|
||||
const locationSchema = z.object({
|
||||
// 0-based byte offset from the beginning of the file
|
||||
// 0-based byte offset from the beginning of the file
|
||||
ByteOffset: z.number(),
|
||||
// 1-based line number from the beginning of the file
|
||||
// 1-based line number from the beginning of the file
|
||||
LineNumber: z.number(),
|
||||
// 1-based column number (in runes) from the beginning of line
|
||||
// 1-based column number (in runes) from the beginning of line
|
||||
Column: z.number(),
|
||||
});
|
||||
|
||||
|
|
@ -67,3 +67,62 @@ export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
|
|||
export const fileSourceResponseSchema = z.object({
|
||||
source: z.string(),
|
||||
});
|
||||
|
||||
|
||||
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
||||
|
||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
||||
export const statsSchema = z.object({
|
||||
Repos: z.number(),
|
||||
Shards: z.number(),
|
||||
Documents: z.number(),
|
||||
IndexBytes: z.number(),
|
||||
ContentBytes: z.number(),
|
||||
NewLinesCount: z.number(),
|
||||
DefaultBranchNewLinesCount: z.number(),
|
||||
OtherBranchesNewLinesCount: z.number(),
|
||||
});
|
||||
|
||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L716
|
||||
export const indexMetadataSchema = z.object({
|
||||
IndexFormatVersion: z.number(),
|
||||
IndexFeatureVersion: z.number(),
|
||||
IndexMinReaderVersion: z.number(),
|
||||
IndexTime: z.string(),
|
||||
PlainASCII: z.boolean(),
|
||||
LanguageMap: z.record(z.string(), z.number()),
|
||||
ZoektVersion: z.string(),
|
||||
ID: z.string(),
|
||||
});
|
||||
|
||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555
|
||||
export const repositorySchema = z.object({
|
||||
Name: z.string(),
|
||||
URL: z.string(),
|
||||
Source: z.string(),
|
||||
Branches: z.array(z.object({
|
||||
Name: z.string(),
|
||||
Version: z.string(),
|
||||
})),
|
||||
CommitURLTemplate: z.string(),
|
||||
FileURLTemplate: z.string(),
|
||||
LineFragmentTemplate: z.string(),
|
||||
RawConfig: z.record(z.string(), z.string()),
|
||||
Rank: z.number(),
|
||||
IndexOptions: z.string(),
|
||||
HasSymbols: z.boolean(),
|
||||
Tombstone: z.boolean(),
|
||||
LatestCommitDate: z.string(),
|
||||
FileTombstones: z.string().optional(),
|
||||
});
|
||||
|
||||
export const listRepositoriesResponseSchema = z.object({
|
||||
List: z.object({
|
||||
Repos: z.array(z.object({
|
||||
Repository: repositorySchema,
|
||||
IndexMetadata: indexMetadataSchema,
|
||||
Stats: statsSchema,
|
||||
})),
|
||||
Stats: statsSchema,
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import escapeStringRegexp from "escape-string-regexp";
|
||||
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
|
||||
import { FileSourceRequest, FileSourceResponse, SearchRequest, SearchResponse, searchResponseSchema } from "../schemas";
|
||||
import { fileNotFound, invalidZoektResponse, schemaValidationError, ServiceError, unexpectedError } from "../serviceError";
|
||||
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, listRepositoriesResponseSchema, SearchRequest, SearchResponse, searchResponseSchema } from "../schemas";
|
||||
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
|
||||
import { isServiceError } from "../utils";
|
||||
import { zoektFetch } from "./zoektClient";
|
||||
import escapeStringRegexp from "escape-string-regexp";
|
||||
|
||||
export const search = async ({ query, numResults, whole }: SearchRequest): Promise<SearchResponse | ServiceError> => {
|
||||
const body = JSON.stringify({
|
||||
|
|
@ -64,3 +64,29 @@ export const getFileSource = async ({ fileName, repository }: FileSourceRequest)
|
|||
source
|
||||
}
|
||||
}
|
||||
|
||||
export const listRepositories = async (): Promise<ListRepositoriesResponse | ServiceError> => {
|
||||
const body = JSON.stringify({
|
||||
opts: {
|
||||
Field: 0,
|
||||
}
|
||||
});
|
||||
const listResponse = await zoektFetch({
|
||||
path: "/api/list",
|
||||
body,
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
if (!listResponse.ok) {
|
||||
return invalidZoektResponse(listResponse);
|
||||
}
|
||||
|
||||
const listBody = await listResponse.json();
|
||||
const parsedListResponse = listRepositoriesResponseSchema.safeParse(listBody);
|
||||
if (!parsedListResponse.success) {
|
||||
console.error(`Failed to parse zoekt response. Error: ${parsedListResponse.error}`);
|
||||
return unexpectedError(`Something went wrong while parsing the response from zoekt`);
|
||||
}
|
||||
|
||||
return parsedListResponse.data;
|
||||
}
|
||||
44
yarn.lock
44
yarn.lock
|
|
@ -582,6 +582,26 @@
|
|||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.7"
|
||||
|
||||
"@radix-ui/react-navigation-menu@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz#884c9b9fd141cc5db257bd3f6bf3b84e349c6617"
|
||||
integrity sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-collection" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-visually-hidden" "1.1.0"
|
||||
|
||||
"@radix-ui/react-popper@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"
|
||||
|
|
@ -689,6 +709,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
||||
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
|
||||
|
||||
"@radix-ui/react-use-previous@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
|
||||
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
|
||||
|
||||
"@radix-ui/react-use-rect@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
|
||||
|
|
@ -703,6 +728,13 @@
|
|||
dependencies:
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-visually-hidden@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2"
|
||||
integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/rect@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
|
||||
|
|
@ -743,6 +775,18 @@
|
|||
dependencies:
|
||||
"@tanstack/query-core" "5.53.3"
|
||||
|
||||
"@tanstack/react-table@^8.20.5":
|
||||
version "8.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8"
|
||||
integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.20.5"
|
||||
|
||||
"@tanstack/table-core@8.20.5":
|
||||
version "8.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
|
||||
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
|
|
|
|||
Loading…
Reference in a new issue