mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-31 15:54:24 +00:00
chore(ui): Merge UI repository
This commit is contained in:
commit
725fcf80aa
274 files changed
+33204
No files matched your search
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { Column } from "@tanstack/react-table";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronsLeftRightIcon,
|
||||
} from "@/components/icons";
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>;
|
||||
title: string;
|
||||
param?: string;
|
||||
}
|
||||
|
||||
export const DataTableColumnHeader = <TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
param,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const getToggleSortingHandler = () => {
|
||||
const currentParams = new URLSearchParams(searchParams.toString());
|
||||
const currentSortParam = currentParams.get("sort");
|
||||
let newSortParam = "";
|
||||
|
||||
if (currentSortParam === `${param}`) {
|
||||
// If already sorting ascending, switch to descending
|
||||
newSortParam = `-${param}`;
|
||||
} else if (currentSortParam === `-${param}`) {
|
||||
// If already sorting descending, remove sorting
|
||||
newSortParam = "";
|
||||
} else {
|
||||
// Sort ascending for the first time or switch to a different column
|
||||
newSortParam = `${param}`;
|
||||
}
|
||||
|
||||
// Update or remove the sort parameter
|
||||
if (newSortParam) {
|
||||
currentParams.set("sort", newSortParam);
|
||||
} else {
|
||||
currentParams.delete("sort");
|
||||
}
|
||||
|
||||
// Construct the new URL with all parameters
|
||||
const newUrl = `${pathname}?${currentParams.toString()}`;
|
||||
|
||||
router.push(newUrl, {
|
||||
scroll: false,
|
||||
});
|
||||
};
|
||||
|
||||
const renderSortIcon = () => {
|
||||
const currentSortParam = searchParams.get("sort");
|
||||
if (
|
||||
!currentSortParam ||
|
||||
currentSortParam === "" ||
|
||||
(currentSortParam !== param && currentSortParam !== `-${param}`)
|
||||
) {
|
||||
return <ChevronsLeftRightIcon size={14} className="ml-2 rotate-90" />;
|
||||
}
|
||||
return currentSortParam === `-${param}` ? (
|
||||
<ArrowDownIcon size={12} className="ml-2" />
|
||||
) : (
|
||||
<ArrowUpIcon size={12} className="ml-2" />
|
||||
);
|
||||
};
|
||||
|
||||
if (!column.getCanSort()) {
|
||||
return <div>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="h-10 w-full justify-between whitespace-nowrap bg-transparent px-0 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none dark:text-slate-400"
|
||||
onClick={getToggleSortingHandler}
|
||||
>
|
||||
<span>{title}</span>
|
||||
{renderSortIcon()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { CustomFilterIcon } from "@/components/icons";
|
||||
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
|
||||
import { FilterOption } from "@/types";
|
||||
|
||||
export interface DataTableFilterCustomProps {
|
||||
filters: FilterOption[];
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export const DataTableFilterCustom = ({
|
||||
filters,
|
||||
defaultOpen = false,
|
||||
}: DataTableFilterCustomProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [showFilters, setShowFilters] = useState(defaultOpen);
|
||||
|
||||
const pushDropdownFilter = useCallback(
|
||||
(key: string, values: string[]) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
const filterKey = `filter[${key}]`;
|
||||
|
||||
if (values.length === 0) {
|
||||
params.delete(filterKey);
|
||||
} else {
|
||||
params.set(filterKey, values.join(","));
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${
|
||||
filters.length > 4 ? "flex-col" : "flex-col md:flex-row"
|
||||
} gap-4`}
|
||||
>
|
||||
<CustomButton
|
||||
ariaLabel={showFilters ? "Hide Filters" : "Show Filters"}
|
||||
variant="flat"
|
||||
color={showFilters ? "action" : "primary"}
|
||||
size="md"
|
||||
startContent={<CustomFilterIcon size={16} />}
|
||||
onPress={() => setShowFilters(!showFilters)}
|
||||
className="w-fit"
|
||||
>
|
||||
<h3 className="text-small">
|
||||
{showFilters ? "Hide Filters" : "Show Filters"}
|
||||
</h3>
|
||||
</CustomButton>
|
||||
|
||||
<div
|
||||
className={`transition-all duration-700 ease-in-out ${
|
||||
showFilters
|
||||
? "max-h-96 w-full translate-x-0 overflow-visible opacity-100"
|
||||
: "max-h-0 -translate-x-full overflow-hidden opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`grid gap-4 ${
|
||||
filters.length > 4
|
||||
? "grid-cols-1 md:grid-cols-4"
|
||||
: "grid-cols-1 md:grid-cols-3"
|
||||
}`}
|
||||
>
|
||||
{filters.map((filter) => (
|
||||
<CustomDropdownFilter
|
||||
key={filter.key}
|
||||
filter={{
|
||||
...filter,
|
||||
labelCheckboxGroup: filter.labelCheckboxGroup,
|
||||
}}
|
||||
onFilterChange={pushDropdownFilter}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
import { getPaginationInfo } from "@/lib";
|
||||
import { MetaDataProps } from "@/types";
|
||||
|
||||
interface DataTablePaginationProps {
|
||||
pageSizeOptions?: number[];
|
||||
metadata?: MetaDataProps;
|
||||
}
|
||||
|
||||
export function DataTablePagination({ metadata }: DataTablePaginationProps) {
|
||||
if (!metadata) return null;
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { currentPage, totalPages, totalEntries } = getPaginationInfo(metadata);
|
||||
|
||||
const createPageUrl = (pageNumber: number | string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
if (pageNumber === "...") return `${pathname}?${params.toString()}`;
|
||||
|
||||
if (+pageNumber > totalPages) {
|
||||
return `${pathname}?${params.toString()}`;
|
||||
}
|
||||
|
||||
params.set("page", pageNumber.toString());
|
||||
return `${pathname}?${params.toString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8">
|
||||
<div className="whitespace-nowrap text-sm font-medium">
|
||||
{totalEntries} entries in Total.
|
||||
</div>
|
||||
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
aria-label="Go to first page"
|
||||
className="page-link relative block rounded border-0 bg-transparent px-3 py-1.5 text-gray-800 outline-none transition-all duration-300 hover:bg-gray-200 hover:text-gray-800 focus:shadow-none dark:text-prowler-theme-green"
|
||||
href={createPageUrl(1)}
|
||||
aria-disabled="true"
|
||||
>
|
||||
<DoubleArrowLeftIcon className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Go to previous page"
|
||||
className="page-link relative block rounded border-0 bg-transparent px-3 py-1.5 text-gray-800 outline-none transition-all duration-300 hover:bg-gray-200 hover:text-gray-800 focus:shadow-none dark:text-prowler-theme-green"
|
||||
href={createPageUrl(currentPage - 1)}
|
||||
aria-disabled="true"
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Go to next page"
|
||||
className="page-link relative block rounded border-0 bg-transparent px-3 py-1.5 text-gray-800 outline-none transition-all duration-300 hover:bg-gray-200 hover:text-gray-800 focus:shadow-none dark:text-prowler-theme-green"
|
||||
href={createPageUrl(currentPage + 1)}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Go to last page"
|
||||
className="page-link relative block rounded border-0 bg-transparent px-3 py-1.5 text-gray-800 outline-none transition-all duration-300 hover:bg-gray-200 hover:text-gray-800 focus:shadow-none dark:text-prowler-theme-green"
|
||||
href={createPageUrl(totalPages)}
|
||||
>
|
||||
<DoubleArrowRightIcon className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { DataTablePagination } from "@/components/ui/table/data-table-pagination";
|
||||
import { FilterOption, MetaDataProps } from "@/types";
|
||||
|
||||
interface DataTableProviderProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
metadata?: MetaDataProps;
|
||||
customFilters?: FilterOption[];
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
metadata,
|
||||
}: DataTableProviderProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
enableSorting: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-0 flex w-full flex-col justify-between gap-4 overflow-auto rounded-large p-4 shadow-small dark:bg-prowler-blue-400">
|
||||
<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>
|
||||
{metadata && (
|
||||
<div className="flex w-full items-center space-x-2 py-4">
|
||||
<DataTablePagination metadata={metadata} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from "./data-table";
|
||||
export * from "./data-table-column-header";
|
||||
export * from "./data-table-filter-custom";
|
||||
export * from "./data-table-pagination";
|
||||
export * from "./severity-badge";
|
||||
export * from "./status-badge";
|
||||
export * from "./status-finding-badge";
|
||||
export * from "./table";
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Chip } from "@nextui-org/react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { AlertIcon } from "@/components/icons";
|
||||
|
||||
type Severity = "informational" | "low" | "medium" | "high" | "critical";
|
||||
|
||||
const severityIconMap = {
|
||||
critical: <AlertIcon size={14} className="mr-1" />,
|
||||
} as const;
|
||||
|
||||
const getSeverityColor = (
|
||||
severity: Severity,
|
||||
): "danger" | "warning" | "default" => {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
return "danger";
|
||||
case "high":
|
||||
return "danger";
|
||||
case "medium":
|
||||
return "warning";
|
||||
case "low":
|
||||
return "default";
|
||||
default:
|
||||
return "default"; // this is a fallback, though unnecessary due to typing
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityIcon = (severity: Severity): React.ReactNode | null => {
|
||||
return severity === "critical" ? severityIconMap.critical : null;
|
||||
};
|
||||
|
||||
export const SeverityBadge = ({ severity }: { severity: Severity }) => {
|
||||
const color = getSeverityColor(severity);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className={clsx("gap-1 border-none capitalize text-default-600", {
|
||||
"bg-system-severity-critical text-white dark:text-white":
|
||||
severity === "critical",
|
||||
})}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color={color}
|
||||
endContent={getSeverityIcon(severity)}
|
||||
>
|
||||
{severity}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Chip, CircularProgress } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export type Status =
|
||||
| "available"
|
||||
| "scheduled"
|
||||
| "executing"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled";
|
||||
|
||||
const statusColorMap: Record<
|
||||
Status,
|
||||
"danger" | "warning" | "success" | "default"
|
||||
> = {
|
||||
available: "default",
|
||||
scheduled: "warning",
|
||||
executing: "default",
|
||||
completed: "success",
|
||||
failed: "danger",
|
||||
cancelled: "danger",
|
||||
};
|
||||
|
||||
export const StatusBadge = ({
|
||||
status,
|
||||
size = "sm",
|
||||
loadingProgress,
|
||||
...props
|
||||
}: {
|
||||
status: Status;
|
||||
size?: "sm" | "md" | "lg";
|
||||
loadingProgress?: number;
|
||||
}) => {
|
||||
const color = statusColorMap[status as keyof typeof statusColorMap];
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className="gap-1 border-none px-2 py-4 capitalize text-default-600"
|
||||
size={size}
|
||||
variant="flat"
|
||||
color={color}
|
||||
{...props}
|
||||
>
|
||||
{status === "executing" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<CircularProgress
|
||||
size="md"
|
||||
classNames={{
|
||||
svg: "h-7 w-7 drop-shadow-md text-prowler-theme-green",
|
||||
indicator: "stroke-prowler-theme-green",
|
||||
track: "stroke-prowler-theme-green/10",
|
||||
}}
|
||||
aria-label="Loading..."
|
||||
value={loadingProgress}
|
||||
showValueLabel={true}
|
||||
/>
|
||||
executing
|
||||
</div>
|
||||
) : (
|
||||
status
|
||||
)}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Chip } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export type FindingStatus = "FAIL" | "PASS" | "MANUAL" | "MUTED";
|
||||
|
||||
const statusColorMap: Record<
|
||||
FindingStatus,
|
||||
"danger" | "warning" | "success" | "default"
|
||||
> = {
|
||||
FAIL: "danger",
|
||||
PASS: "success",
|
||||
MANUAL: "warning",
|
||||
MUTED: "default",
|
||||
};
|
||||
|
||||
export const StatusFindingBadge = ({
|
||||
status,
|
||||
size = "sm",
|
||||
...props
|
||||
}: {
|
||||
status: FindingStatus;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}) => {
|
||||
const color = statusColorMap[status];
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className="gap-1 border-none px-2 py-1 capitalize text-default-600"
|
||||
size={size}
|
||||
variant="flat"
|
||||
color={color}
|
||||
{...props}
|
||||
>
|
||||
{status}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { cn } from "@nextui-org/react";
|
||||
import * as React from "react";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto rounded-lg">
|
||||
<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]:first:rounded-lg [&>tr]:first:shadow-small",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn("", className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-medium dark:bg-slate-800/50 [&>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(
|
||||
"transition-colors hover:bg-slate-100/50 data-[state=selected]:bg-slate-100 dark:hover:bg-slate-800/50 dark:data-[state=selected]:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 whitespace-nowrap bg-default-100 px-4 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none first:rounded-l-lg last:rounded-r-lg data-[focus-visible=true]:z-10 data-[hover=true]:text-foreground-400 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-offset-2 data-[focus-visible=true]:outline-focus dark:bg-prowler-blue-800 dark:text-slate-400 rtl:text-right rtl:first:rounded-l-[unset] rtl:first:rounded-r-lg rtl:last:rounded-l-lg rtl:last:rounded-r-[unset] [&: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(
|
||||
"px-4 py-2.5 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-2 text-sm text-slate-500 dark:text-slate-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
};
|
||||
Reference in new issue
Block a user