chore(ui): Merge UI repository

This commit is contained in:
Pedro De Castro committed 2024-11-25 13:15:14 +01:00
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>
);
}
+116
View File
@@ -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>
)}
</>
);
}
+8
View File
@@ -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";
+51
View File
@@ -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>
);
};
+64
View File
@@ -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>
);
};
+122
View File
@@ -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,
};