Skip to content

Commit

Permalink
feat: vet JSON Report Visualization (#10)
Browse files Browse the repository at this point in the history
* feat: Add support for vet JSON report visualization

* fix: Vulnerabilities tab

* fix: License tab

* fix: Popularity tab

* fix: Remove other tab

* fix: Add back button in JSON report
  • Loading branch information
abhisek authored Feb 26, 2025
1 parent 2ac28a7 commit b4207ec
Show file tree
Hide file tree
Showing 10 changed files with 1,776 additions and 0 deletions.
114 changes: 114 additions & 0 deletions src/app/vr/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"use client";

import { useState, useCallback } from "react";
import { Upload, FileJson } from "lucide-react";
import { VetData } from "@/app/vr/types";

interface FileUploadProps {
onDataUpdate: (data: VetData) => void;
}

export function FileUpload({ onDataUpdate }: FileUploadProps) {
const [isDragging, setIsDragging] = useState(false);
const [fileName, setFileName] = useState<string | null>(null);

const handleFile = useCallback(
(file: File) => {
if (file.type !== "application/json") {
console.error("Please upload a JSON file");
return;
}

setFileName(file.name);
const reader = new FileReader();
reader.onload = (e) => {
try {
const jsonData = JSON.parse(e.target?.result as string);
onDataUpdate(jsonData);
} catch (error) {
console.error("Error parsing JSON:", error);
}
};
reader.readAsText(file);
},
[onDataUpdate],
);

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);

const file = e.dataTransfer.files[0];
if (file) {
handleFile(file);
}
},
[handleFile],
);

const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);

const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);

const handleFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFile(file);
}
},
[handleFile],
);

return (
<div className="w-full">
<div
className={`
relative rounded-lg border-2 border-dashed p-6
${isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25"}
transition-colors duration-200
`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<input
type="file"
accept="application/json"
onChange={handleFileSelect}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
title="Choose a JSON file or drag it here"
/>
<div className="text-center space-y-4">
<div className="flex justify-center">
{fileName ? (
<div className="flex items-center gap-2 text-muted-foreground">
<FileJson className="w-8 h-8" />
<span className="text-sm">{fileName}</span>
</div>
) : (
<Upload className="w-8 h-8 text-muted-foreground" />
)}
</div>
<div className="space-y-1">
<p className="text-sm font-medium">
{fileName
? "Drop another file or click to replace"
: "Drop your vet JSON report here"}
</p>
<p className="text-xs text-muted-foreground">
or click to select a file from your computer
</p>
</div>
</div>
</div>
</div>
);
}
166 changes: 166 additions & 0 deletions src/app/vr/components/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"use client";

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
} from "@tanstack/react-table";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { TABLE_CONFIG } from "../config";

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}

export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
initialState: {
pagination: {
pageSize: TABLE_CONFIG.defaultPageSize,
},
},
});

return (
<div>
<div className="flex items-center py-4 gap-2">
{table
.getAllColumns()
.filter((column) => column.getCanFilter())
.map((column) => (
<Input
key={column.id}
placeholder={`Filter ${column.columnDef.header as string}...`}
value={(column.getFilterValue() ?? "") as string}
onChange={(e) => column.setFilterValue(e.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) => (
<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={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>

<div className="flex items-center justify-between space-x-2 py-4">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={TABLE_CONFIG.defaultPageSize} />
</SelectTrigger>
<SelectContent side="top">
{TABLE_CONFIG.pageSizeOptions.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="flex items-center space-x-2">
<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 className="flex items-center gap-1">
<div className="text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
</div>
</div>
</div>
</div>
);
}
54 changes: 54 additions & 0 deletions src/app/vr/components/tabs/LicensesTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Card, CardContent } from "@/components/ui/card";
import { ColumnDef } from "@tanstack/react-table";
import { PackageData } from "../../types";
import { DataTable } from "../DataTable";

interface License {
id: string;
packageName: string;
packageVersion: string;
}

const licenseColumns: ColumnDef<License>[] = [
{
accessorKey: "packageName",
header: "Package Name",
enableSorting: true,
enableColumnFilter: true,
},
{
accessorKey: "packageVersion",
header: "Version",
enableSorting: true,
enableColumnFilter: true,
},
{
accessorKey: "id",
header: "License Code",
enableSorting: true,
enableColumnFilter: true,
},
];

interface LicensesTabProps {
data: PackageData[];
}

export function LicensesTab({ data }: LicensesTabProps) {
const licenses = data.flatMap(
(pkg) =>
pkg.licenses?.map((l) => ({
packageName: pkg.package.name,
packageVersion: pkg.package.version,
id: l.id,
})) ?? [],
);

return (
<Card>
<CardContent className="pt-6">
<DataTable columns={licenseColumns} data={licenses} />
</CardContent>
</Card>
);
}
Loading

0 comments on commit b4207ec

Please sign in to comment.