Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { Alert } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
Expand All @@ -20,6 +19,7 @@ import {
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@unkey/ui";
import { Button, Checkbox, InfoTooltip } from "@unkey/ui";
import { ArrowUpDown, Minus, MoreHorizontal, MoreVertical, Trash } from "lucide-react";
import ms from "ms";
Expand All @@ -42,7 +42,7 @@ type Props = {

export const RootKeyTable: React.FC<Props> = ({ data }) => {
const router = useRouter();
const deleteKey = trpc.rootKey.delete.useMutation({
const deleteKey = trpc.settings.rootKeys.delete.useMutation({
onSuccess: () => {
toast.success("Root Key was deleted");
router.refresh();
Expand Down
207 changes: 207 additions & 0 deletions apps/dashboard/components/dashboard/root-key-table/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"use client";

import {
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { Button, Input } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useState } from "react";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}

export function DataTable<TData, TValue>({ data, columns }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [rowSelection, setRowSelection] = useState({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const router = useRouter();

const deleteKey = trpc.settings.rootKeys.delete.useMutation({
onSuccess: (_data, variables) => {
setRowSelection({});
toast.success(
variables.keyIds.length > 1
? `All ${variables.keyIds.length} keys were deleted`
: "Key deleted",
);
router.refresh();
},
onError: (err, variables) => {
router.refresh();
console.error(err);
toast.error(`Could not delete key ${JSON.stringify(variables)}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Maintain consistency in toast notifications.

The error toast here uses toast.error(), while in index.tsx it uses toast(). Use consistent toast methods across components.

-      toast.error(`Could not delete key ${JSON.stringify(variables)}`);
+      toast(`Could not delete key ${JSON.stringify(variables)}`);

Or update both files to use the semantic toast.error() method for error cases.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toast.error(`Could not delete key ${JSON.stringify(variables)}`);
toast(`Could not delete key ${JSON.stringify(variables)}`);
🤖 Prompt for AI Agents
In apps/dashboard/components/dashboard/root-key-table/table.tsx at line 70, the
toast notification for errors uses toast.error(), which is inconsistent with the
toast() method used in index.tsx. To maintain consistency, update the toast call
in this file to use the same method as in index.tsx, or alternatively, update
both files to use toast.error() for error notifications to keep semantic
clarity. Choose one approach and apply it consistently across both files.

},
});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,

getSortedRowModel: getSortedRowModel(),
state: {
sorting,
columnFilters,
rowSelection,
columnVisibility,
},
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection,
});

return (
<div>
<div>
<div className="flex flex-wrap items-center justify-between space-x-0 space-y-4 md:space-x-4 sm:space-y-0 sm:flex-nowrap">
{Object.values(rowSelection).length > 0 ? (
<Dialog>
<DialogTrigger asChild>
<Button className="min-w-full md:min-w-min ">Revoke selected keys</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Revoke {Object.keys(rowSelection).length} keys</DialogTitle>
<DialogDescription className="text-alert">
This action can not be undone. Your root key(s) will no longer be able to create
resources.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button
disabled={deleteKey.isLoading}
onClick={() => {
const keyIds = table
.getSelectedRowModel()
// @ts-ignore
.rows.map((row) => row.original.id as string);
deleteKey.mutate({ keyIds });
Comment on lines +115 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove @ts-ignore and fix the type issue properly.

The @ts-ignore comment indicates a TypeScript issue that should be resolved. Based on the context, the row data should be properly typed.

-                      const keyIds = table
-                        .getSelectedRowModel()
-                        // @ts-ignore
-                        .rows.map((row) => row.original.id as string);
+                      const keyIds = table
+                        .getSelectedRowModel()
+                        .rows.map((row) => (row.original as { id: string }).id);

Alternatively, ensure the generic type TData extends a base type with an id property:

interface BaseRowData {
  id: string;
}

export function DataTable<TData extends BaseRowData, TValue>({ data, columns }: DataTableProps<TData, TValue>) {
🤖 Prompt for AI Agents
In apps/dashboard/components/dashboard/root-key-table/table.tsx around lines 115
to 119, remove the @ts-ignore comment and fix the type issue by properly typing
the row data. Ensure that the generic type TData extends a base interface that
includes an id property (e.g., interface BaseRowData { id: string }). Update the
DataTable component's generic constraint to TData extends BaseRowData so that
row.original.id is recognized as a string without type errors.

}}
loading={deleteKey.isLoading}
>
Delete permanently
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : null}
<Input
placeholder="Filter keys"
value={(table.getColumn("start")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("start")?.setFilterValue(event.target.value)}
className="max-w-sm md:max-w-2xl"
/>
Comment on lines +129 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add proper ARIA labels for better accessibility.

The filter input should have proper labeling for screen readers.

           <Input
             placeholder="Filter keys"
+            aria-label="Filter root keys"
             value={(table.getColumn("start")?.getFilterValue() as string) ?? ""}
             onChange={(event) => table.getColumn("start")?.setFilterValue(event.target.value)}
             className="max-w-sm md:max-w-2xl"
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Input
placeholder="Filter keys"
value={(table.getColumn("start")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("start")?.setFilterValue(event.target.value)}
className="max-w-sm md:max-w-2xl"
/>
<Input
placeholder="Filter keys"
aria-label="Filter root keys"
value={(table.getColumn("start")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("start")?.setFilterValue(event.target.value)}
className="max-w-sm md:max-w-2xl"
/>
🤖 Prompt for AI Agents
In apps/dashboard/components/dashboard/root-key-table/table.tsx around lines 129
to 134, the Input component used for filtering keys lacks proper ARIA labels,
which reduces accessibility for screen reader users. Add an appropriate
aria-label attribute to the Input element that clearly describes its purpose,
such as "Filter keys input," to improve accessibility compliance.

<DropdownMenu>
<DropdownMenuTrigger>
<Button className="min-w-full mt-4 ml-0 md:min-w-min md:mt-0 md:ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Table className="mt-4">
<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 className="flex items-center justify-end py-4 space-x-2">
<Button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Previous
</Button>
<Button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</Button>
</div>
</div>
);
}
Loading
Loading