-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: vet JSON Report Visualization (#10)
* 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
Showing
10 changed files
with
1,776 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.