Merge pull request #96 from prowler-cloud/PRWLR-5142-Prowler-V-release-final-tweaks-for-Findings-page

Tweaks for findings details and filters
This commit is contained in:
Pablo Lara
2024-11-20 16:06:19 +01:00
committed by GitHub
41 changed files with 639 additions and 295 deletions
+13 -7
View File
@@ -17,12 +17,18 @@ export default async function Compliance({
searchParams: SearchParamsProps;
}) {
const scansData = await getScans({});
const scanList = scansData?.data.map((scan: any) => ({
id: scan.id,
name: scan.attributes.name || "Unnamed Scan",
state: scan.attributes.state,
progress: scan.attributes.progress,
}));
const scanList = scansData?.data
.filter(
(scan: any) =>
scan.attributes.state === "completed" &&
scan.attributes.progress === 100,
)
.map((scan: any) => ({
id: scan.id,
name: scan.attributes.name || "Unnamed Scan",
state: scan.attributes.state,
progress: scan.attributes.progress,
}));
const selectedScanId = searchParams.scanId || scanList[0]?.id;
@@ -92,7 +98,7 @@ const SSRComplianceGrid = async ({
}
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{compliancesData.data.map((compliance: ComplianceOverviewData) => {
const { attributes } = compliance;
const {
+90 -7
View File
@@ -2,6 +2,8 @@ import { Spacer } from "@nextui-org/react";
import React, { Suspense } from "react";
import { getFindings } from "@/actions/findings";
import { getProviders } from "@/actions/providers";
import { getScans } from "@/actions/scans";
import { filterFindings } from "@/components/filters/data-filters";
import { FilterControls } from "@/components/filters/filter-controls";
import {
@@ -9,7 +11,7 @@ import {
SkeletonTableFindings,
} from "@/components/findings/table";
import { Header } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import { createDict } from "@/lib";
import { FindingProps, SearchParamsProps } from "@/types/components";
@@ -20,13 +22,95 @@ export default async function Findings({
}) {
const searchParamsKey = JSON.stringify(searchParams || {});
// Get findings data
const findingsData = await getFindings({});
const providersData = await getProviders({});
const scansData = await getScans({});
// Extract provider UIDs
const providerUIDs = providersData?.data
?.map((provider: any) => provider.attributes.uid)
.filter(Boolean);
// Extract scan UUIDs with "completed" state and more than one resource
const completedScans = scansData?.data
?.filter(
(scan: any) =>
scan.attributes.state === "completed" &&
scan.attributes.unique_resource_count > 1 &&
scan.attributes.name, // Ensure it has a name
)
.map((scan: any) => ({
id: scan.id,
name: scan.attributes.name,
}));
const completedScanIds = completedScans?.map((scan: any) => scan.id) || [];
// Create resource dictionary
const resourceDict = createDict("resources", findingsData);
// Get unique regions and services
const allRegionsAndServices = findingsData?.data
?.flatMap((finding: FindingProps) => {
const resource =
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
return {
region: resource?.attributes?.region,
service: resource?.attributes?.service,
};
})
.filter(Boolean);
const uniqueRegions = Array.from(
new Set<string>(
allRegionsAndServices
.map((item: { region: string }) => item.region)
.filter(Boolean),
),
);
const uniqueServices = Array.from(
new Set<string>(
allRegionsAndServices
.map((item: { service: string }) => item.service)
.filter(Boolean),
),
);
return (
<>
<Header title="Findings" icon="ph:list-checks-duotone" />
<Spacer />
<Spacer y={4} />
<FilterControls search providers date />
<Spacer y={4} />
<FilterControls search date />
<Spacer y={8} />
<DataTableFilterCustom
filters={[
...filterFindings,
{
key: "region__in",
labelCheckboxGroup: "Regions",
values: uniqueRegions,
},
{
key: "service__in",
labelCheckboxGroup: "Services",
values: uniqueServices,
},
{
key: "provider_uid__in",
labelCheckboxGroup: "Account",
values: providerUIDs,
},
{
key: "scan__in",
labelCheckboxGroup: "Scans",
values: completedScanIds, // Use UUIDs in the filter
},
]}
defaultOpen={true}
/>
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
@@ -53,9 +137,9 @@ const SSRDataTable = async ({
const findingsData = await getFindings({ query, page, sort, filters });
// Create dictionaries for resources, scans, and providers
const resourceDict = createDict("Resource", findingsData);
const scanDict = createDict("Scan", findingsData);
const providerDict = createDict("Provider", findingsData);
const resourceDict = createDict("resources", findingsData);
const scanDict = createDict("scans", findingsData);
const providerDict = createDict("providers", findingsData);
// Expand each finding with its corresponding resource, scan, and provider
const expandedFindings = findingsData?.data
@@ -84,7 +168,6 @@ const SSRDataTable = async ({
columns={ColumnFindings}
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
customFilters={filterFindings}
/>
);
};
+4 -3
View File
@@ -10,7 +10,7 @@ import {
SkeletonTableInvitation,
} from "@/components/invitations/table";
import { Header } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import { SearchParamsProps } from "@/types";
export default async function Invitations({
@@ -25,9 +25,11 @@ export default async function Invitations({
<Header title="Invitations" icon="ci:users" />
<Spacer y={4} />
<FilterControls search />
<Spacer y={4} />
<Spacer y={8} />
<SendInvitationButton />
<Spacer y={4} />
<DataTableFilterCustom filters={filterInvitations || []} />
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableInvitation />}>
<SSRDataTable searchParams={searchParams} />
@@ -59,7 +61,6 @@ const SSRDataTable = async ({
columns={ColumnsInvitation}
data={invitationsData?.data || []}
metadata={invitationsData?.meta}
customFilters={filterInvitations}
/>
);
};
+1 -1
View File
@@ -46,7 +46,7 @@ export default function RootLayout({
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
<div className="flex h-dvh items-center justify-center overflow-hidden">
<SidebarWrap />
<main className="no-scrollbar container mb-auto h-full flex-1 flex-col overflow-y-auto p-4">
<main className="no-scrollbar mb-auto h-full flex-1 flex-col overflow-y-auto px-6 py-4 xl:px-10">
{children}
<Toaster />
</main>
+4 -3
View File
@@ -9,7 +9,7 @@ import {
SkeletonTableProviders,
} from "@/components/providers/table";
import { Header } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import { SearchParamsProps } from "@/types";
export default async function Providers({
@@ -25,9 +25,11 @@ export default async function Providers({
<Spacer y={4} />
<FilterControls search providers />
<Spacer y={4} />
<Spacer y={8} />
<AddProvider />
<Spacer y={4} />
<DataTableFilterCustom filters={filterProviders || []} />
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableProviders />}>
<SSRDataTable searchParams={searchParams} />
@@ -58,7 +60,6 @@ const SSRDataTable = async ({
columns={ColumnProviders}
data={providersData?.data || []}
metadata={providersData?.meta}
customFilters={filterProviders}
/>
);
};
+3 -2
View File
@@ -8,7 +8,7 @@ import { LaunchScanWorkflow } from "@/components/scans/launch-workflow";
import { SkeletonTableScans } from "@/components/scans/table";
import { ColumnGetScans } from "@/components/scans/table/scans";
import { Header } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import { ProviderProps, SearchParamsProps } from "@/types";
export default async function Scans({
@@ -39,6 +39,8 @@ export default async function Scans({
<Spacer y={4} />
<LaunchScanWorkflow providers={providerInfo} />
<Spacer y={8} />
<DataTableFilterCustom filters={filterScans || []} />
<Spacer y={8} />
<div className="grid grid-cols-12 items-start gap-4">
<div className="col-span-12">
@@ -76,7 +78,6 @@ const SSRDataTableScans = async ({
columns={ColumnGetScans}
data={scansData?.data || []}
metadata={scansData?.meta}
customFilters={filterScans}
/>
);
};
+4 -3
View File
@@ -5,7 +5,7 @@ import { getUsers } from "@/actions/users/users";
import { FilterControls } from "@/components/filters";
import { filterUsers } from "@/components/filters/data-filters";
import { Header } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import { AddUserButton } from "@/components/users";
import { ColumnsUser, SkeletonTableUser } from "@/components/users/table";
import { SearchParamsProps } from "@/types";
@@ -22,9 +22,11 @@ export default async function Users({
<Header title="Users" icon="ci:users" />
<Spacer y={4} />
<FilterControls search />
<Spacer y={4} />
<Spacer y={8} />
<AddUserButton />
<Spacer y={4} />
<DataTableFilterCustom filters={filterUsers || []} />
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}>
<SSRDataTable searchParams={searchParams} />
@@ -56,7 +58,6 @@ const SSRDataTable = async ({
columns={ColumnsUser}
data={usersData?.data || []}
metadata={usersData?.meta}
customFilters={filterUsers}
/>
);
};
+15 -17
View File
@@ -16,27 +16,25 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
title,
passingRequirements,
totalRequirements,
prevPassingRequirements,
prevTotalRequirements,
}) => {
const ratingPercentage = Math.floor(
(passingRequirements / totalRequirements) * 100,
);
const prevRatingPercentage = Math.floor(
(prevPassingRequirements / prevTotalRequirements) * 100,
);
// const prevRatingPercentage = Math.floor(
// (prevPassingRequirements / prevTotalRequirements) * 100,
// );
const getScanChange = () => {
const scanDifference = ratingPercentage - prevRatingPercentage;
if (scanDifference < 0 && scanDifference <= -1) {
return `${scanDifference}% from last scan`;
}
if (scanDifference > 0 && scanDifference >= 1) {
return `+${scanDifference}% from last scan`;
}
return "No change from last scan";
};
// const getScanChange = () => {
// const scanDifference = ratingPercentage - prevRatingPercentage;
// if (scanDifference < 0 && scanDifference <= -1) {
// return `${scanDifference}% from last scan`;
// }
// if (scanDifference > 0 && scanDifference >= 1) {
// return `+${scanDifference}% from last scan`;
// }
// return "No changes from last scan";
// };
const getRatingColor = (ratingPercentage: number) => {
if (ratingPercentage <= 10) {
@@ -50,7 +48,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
return (
<Card fullWidth isPressable isHoverable shadow="sm">
<CardBody className="flex flex-row items-center justify-between space-x-4">
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
<div className="flex w-full items-center space-x-4">
<Image
src={getComplianceIcon(title)}
@@ -75,7 +73,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
</span>
Passing Requirements
</small>
<small>{getScanChange()}</small>
{/* <small>{getScanChange()}</small> */}
</div>
</div>
</div>
+1 -1
View File
@@ -63,7 +63,7 @@ export const CustomDatePicker = () => {
CalendarTopContent={
<ButtonGroup
fullWidth
className="bg-content1 px-3 pb-2 pt-3 [&>button]:border-default-200/60 [&>button]:text-default-500"
className="bg-content1 px-3 pb-2 pt-3 dark:bg-prowler-blue-400 [&>button]:border-default-200/60 [&>button]:text-default-500"
radius="full"
size="sm"
variant="bordered"
@@ -42,16 +42,16 @@ export const CustomSelectProvider: React.FC = () => {
(value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set("filter[provider__in]", value);
params.set("filter[provider_type]", value);
} else {
params.delete("filter[provider__in]");
params.delete("filter[provider_type]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
[router, searchParams],
);
const currentProvider = searchParams.get("filter[provider__in]") || "";
const currentProvider = searchParams.get("filter[provider_type]") || "";
const selectedKeys = useMemo(() => {
return dataInputsProvider.some(
+8 -3
View File
@@ -35,20 +35,25 @@ export const filterScans = [
export const filterFindings = [
{
key: "severity",
key: "severity__in",
labelCheckboxGroup: "Severity",
values: ["critical", "high", "medium", "low", "informational"],
},
{
key: "status",
key: "status__in",
labelCheckboxGroup: "Status",
values: ["PASS", "FAIL", "MANUAL", "MUTED"],
},
{
key: "delta",
key: "delta__in",
labelCheckboxGroup: "Delta",
values: ["new", "changed"],
},
{
key: "provider_type__in",
labelCheckboxGroup: "Provider",
values: ["aws", "azure", "gcp", "kubernetes"],
},
// Add more filter categories as needed
];
+1 -1
View File
@@ -8,8 +8,8 @@ import { FilterControlsProps } from "@/types";
import { CrossIcon } from "../icons";
import { CustomButton } from "../ui/custom";
import { DataTableFilterCustom } from "../ui/table";
import { CustomCheckboxMutedFindings } from "./custo-checkbox-muted-findings";
import { CustomAccountSelection } from "./custom-account-selection";
import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings";
import { CustomDatePicker } from "./custom-date-picker";
import { CustomRegionSelection } from "./custom-region-selection";
import { CustomSearchInput } from "./custom-search-input";
+1 -1
View File
@@ -1,5 +1,5 @@
export * from "./custo-checkbox-muted-findings";
export * from "./custom-account-selection";
export * from "./custom-checkbox-muted-findings";
export * from "./custom-date-picker";
export * from "./custom-provider-inputs";
export * from "./custom-region-selection";
+56 -39
View File
@@ -1,22 +1,20 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { useSearchParams } from "next/navigation";
import { DataTableRowDetails } from "@/components/findings/table";
import { PlusIcon } from "@/components/icons";
import { InfoIcon } from "@/components/icons";
import { TriggerSheet } from "@/components/ui/sheet";
import { SeverityBadge, Status, StatusBadge } from "@/components/ui/table";
import {
DataTableColumnHeader,
SeverityBadge,
StatusFindingBadge,
} from "@/components/ui/table";
import { FindingProps } from "@/types";
import { DataTableRowActions } from "./data-table-row-actions";
const statusMap: Record<"PASS" | "FAIL" | "MANUAL" | "MUTED", Status> = {
PASS: "completed",
FAIL: "failed",
MANUAL: "completed",
MUTED: "cancelled",
};
const getFindingsData = (row: { original: FindingProps }) => {
return row.original;
};
@@ -58,30 +56,23 @@ const getScanData = (
export const ColumnFindings: ColumnDef<FindingProps>[] = [
{
accessorKey: "check",
header: "Check",
header: ({ column }) => (
<DataTableColumnHeader column={column} title={"Check"} param="check_id" />
),
cell: ({ row }) => {
const { checktitle } = getFindingsMetadata(row);
return <p className="max-w-96 truncate text-medium">{checktitle}</p>;
},
},
{
accessorKey: "scanName",
header: "Scan Name",
cell: ({ row }) => {
const name = getScanData(row, "name");
return (
<p className="max-w-96 truncate text-medium">
{typeof name === "string" || typeof name === "number"
? name
: "Invalid data"}
</p>
);
return <p className="max-w-96 truncate text-small">{checktitle}</p>;
},
},
{
accessorKey: "severity",
header: "Severity",
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={"Severity"}
param="severity"
/>
),
cell: ({ row }) => {
const {
attributes: { severity },
@@ -91,15 +82,30 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
},
{
accessorKey: "status",
header: "Status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title={"Status"} param="status" />
),
cell: ({ row }) => {
const {
attributes: { status },
} = getFindingsData(row);
const mappedStatus = statusMap[status];
return <StatusFindingBadge size="sm" status={status} />;
},
},
{
accessorKey: "scanName",
header: "Scan Name",
cell: ({ row }) => {
const name = getScanData(row, "name");
return <StatusBadge status={mappedStatus} />;
return (
<p className="text-small">
{typeof name === "string" || typeof name === "number"
? name
: "Invalid data"}
</p>
);
},
},
{
@@ -120,7 +126,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
header: "Service",
cell: ({ row }) => {
const { servicename } = getFindingsMetadata(row);
return <p className="max-w-96 truncate text-medium">{servicename}</p>;
return <p className="max-w-96 truncate text-small">{servicename}</p>;
},
},
{
@@ -131,7 +137,9 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
return (
<>
<div>{typeof account === "string" ? account : "Invalid account"}</div>
<p className="max-w-96 truncate text-small">
{typeof account === "string" ? account : "Invalid account"}
</p>
</>
);
},
@@ -140,14 +148,23 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
id: "moreInfo",
header: "Details",
cell: ({ row }) => {
const searchParams = useSearchParams();
const findingId = searchParams.get("id");
const isOpen = findingId === row.original.id;
return (
<TriggerSheet
triggerComponent={<PlusIcon />}
title="Finding Details"
description="View the finding details"
>
<DataTableRowDetails finding={getFindingsData(row)} />
</TriggerSheet>
<div className="flex justify-center">
<TriggerSheet
triggerComponent={<InfoIcon className="text-primary" size={16} />}
title="Finding Details"
description="View the finding details"
defaultOpen={isOpen}
>
<DataTableRowDetails
entityId={row.original.id}
findingDetails={row.original}
/>
</TriggerSheet>
</div>
);
},
},
@@ -31,7 +31,6 @@ export function DataTableRowActions<FindingProps>({
row,
}: DataTableRowActionsProps<FindingProps>) {
const findingId = (row.original as { id: string }).id;
console.log(findingId);
return (
<>
{/* <CustomAlertModal
@@ -56,7 +55,10 @@ export function DataTableRowActions<FindingProps>({
</CustomAlertModal> */}
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<Dropdown
className="shadow-xl dark:bg-prowler-blue-800"
placement="bottom"
>
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
@@ -76,6 +78,7 @@ export function DataTableRowActions<FindingProps>({
startContent={<EditDocumentBulkIcon className={iconClasses} />}
// onClick={() => setIsEditOpen(true)}
>
<span className="hidden text-sm">{findingId}</span>
Send to Jira
</DropdownItem>
<DropdownItem
@@ -1,8 +1,40 @@
"use client";
import { FindingDetail } from "@/components/findings/table";
import { FindingProps } from "@/types";
// import { usePathname, useRouter, useSearchParams } from "next/navigation";
// import { useEffect } from "react";
export const DataTableRowDetails = ({ finding }: { finding: FindingProps }) => {
return <FindingDetail findingDetails={finding} />;
import { FindingProps } from "@/types/components";
import { FindingDetail } from "./finding-detail";
export const DataTableRowDetails = ({
// entityId,
findingDetails,
}: {
entityId: string;
findingDetails: FindingProps;
}) => {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// useEffect(() => {
// if (entityId) {
// const params = new URLSearchParams(searchParams.toString());
// params.set("id", entityId);
// router.push(`${pathname}?${params.toString()}`, { scroll: false });
// }
// return () => {
// if (entityId) {
// const cleanupParams = new URLSearchParams(searchParams.toString());
// cleanupParams.delete("id");
// router.push(`${pathname}?${cleanupParams.toString()}`, {
// scroll: false,
// });
// }
// };
// }, [entityId, pathname, router, searchParams]);
return <FindingDetail findingDetails={findingDetails} />;
};
+202 -108
View File
@@ -1,14 +1,11 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@nextui-org/react";
import { Snippet } from "@nextui-org/react";
import Link from "next/link";
import { SnippetId } from "@/components/ui/entities";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import { SeverityBadge } from "@/components/ui/table/severity-badge";
import { FindingProps } from "@/types";
export const FindingDetail = ({
@@ -17,108 +14,205 @@ export const FindingDetail = ({
findingDetails: FindingProps;
}) => {
const finding = findingDetails;
const attributes = finding.attributes;
const resource = finding.relationships.resource.attributes;
const remediation = attributes.check_metadata.remediation;
return (
<div className="space-y-4">
<div className="flex flex-col gap-6 rounded-lg">
{/* Header */}
<div className="flex items-center justify-between">
<Table aria-label="Example static collection table">
<TableHeader>
<TableColumn>Name </TableColumn>
<TableColumn>Value</TableColumn>
</TableHeader>
<TableBody>
<TableRow key="1">
<TableCell>Resource ID</TableCell>
<TableCell>{finding.relationships.resource.id}</TableCell>
</TableRow>
<TableRow key="2">
<TableCell>Resource ARN</TableCell>
<TableCell>
{finding.relationships.resource.attributes.uid}
</TableCell>
</TableRow>
<TableRow key="3">
<TableCell>Check ID</TableCell>
<TableCell>{finding.attributes.check_id}</TableCell>
</TableRow>
<TableRow key="4">
<TableCell>Types</TableCell>
<TableCell>
{finding.attributes.check_metadata.checktype}
</TableCell>
</TableRow>
<TableRow key="5">
<TableCell>Scan time</TableCell>
<TableCell>{finding.attributes.inserted_at}</TableCell>
</TableRow>
<TableRow key="6">
<TableCell>Prowler Finding ID</TableCell>
<TableCell>
{finding.relationships.resource.attributes.uid}
</TableCell>
</TableRow>
<TableRow key="7">
<TableCell>Severity</TableCell>
<TableCell>{finding.id}</TableCell>
</TableRow>
<TableRow key="8">
<TableCell>Status</TableCell>
<TableCell>{finding.attributes.status}</TableCell>
</TableRow>
<TableRow key="9">
<TableCell>Region</TableCell>
<TableCell>
{finding.relationships.resource.attributes.region}
</TableCell>
</TableRow>
<TableRow key="10">
<TableCell>Service</TableCell>
<TableCell>
{finding.relationships.resource.attributes.service}
</TableCell>
</TableRow>
<TableRow key="11">
<TableCell>Account</TableCell>
<TableCell>
{finding.relationships.provider.attributes.uid}
</TableCell>
</TableRow>
<TableRow key="12">
<TableCell>Details</TableCell>
<TableCell>{finding.attributes.status_extended}</TableCell>
</TableRow>
<TableRow key="13">
<TableCell>Risk</TableCell>
<TableCell>{finding.attributes.check_metadata.risk}</TableCell>
</TableRow>
<TableRow key="14">
<TableCell>Recommendation</TableCell>
<TableCell>
{
finding.attributes.check_metadata.remediation.recommendation
.text
}
</TableCell>
</TableRow>
<TableRow key="15">
<TableCell>CLI</TableCell>
<TableCell>
{finding.attributes.check_metadata.remediation.code.cli}
</TableCell>
</TableRow>
<TableRow key="16">
<TableCell>Other</TableCell>
<TableCell>
{finding.attributes.check_metadata.remediation.code.other}
</TableCell>
</TableRow>
<TableRow key="17">
<TableCell>Terraform</TableCell>
<TableCell>
{finding.attributes.check_metadata.remediation.code.terraform}
</TableCell>
</TableRow>
</TableBody>
</Table>
<div>
<h2 className="line-clamp-2 text-xl font-bold leading-tight text-gray-800 dark:text-prowler-theme-pale/90">
{attributes.check_metadata.checktitle}
</h2>
<p className="text-sm text-gray-500 dark:text-prowler-theme-pale/70">
{resource.service}
</p>
</div>
<div
className={`rounded-lg px-3 py-1 text-sm font-semibold ${
attributes.status === "PASS"
? "bg-green-100 text-green-600"
: attributes.status === "MANUAL"
? "bg-gray-100 text-gray-600"
: "bg-red-100 text-red-600"
}`}
>
{attributes.status}
</div>
</div>
{/* Check Metadata */}
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Check Metadata
</h3>
<SeverityBadge severity={attributes.severity} />
</div>
{attributes.status === "FAIL" && (
<Snippet
className="max-w-full py-4"
color="danger"
hideCopyButton
hideSymbol
>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Risk
</p>
<p className="whitespace-pre-line text-gray-800 dark:text-prowler-theme-pale/90">
{attributes.check_metadata.risk}
</p>
</Snippet>
)}
<div className="flex flex-col gap-2">
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Description
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
{attributes.check_metadata.description}
</p>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold dark:text-prowler-theme-pale">
Remediation
</h3>
<div className="text-gray-800 dark:text-prowler-theme-pale/90">
{remediation.recommendation && (
<>
<p className="text-sm font-semibold">Recommendation:</p>
<p>{remediation.recommendation.text}</p>
<Link
target="_blank"
href={remediation.recommendation.url}
className="mt-2 inline-block text-sm text-blue-500 underline"
>
Learn more
</Link>
</>
)}
{remediation.code &&
Object.values(remediation.code).some(Boolean) && (
<div className="flex flex-col gap-2">
<p className="mt-4 text-sm font-semibold">
Check these links:
</p>
<div className="flex flex-col gap-2">
{remediation.code.cli && (
<div>
<p className="text-sm font-semibold">CLI Command:</p>
<Snippet hideSymbol size="sm" className="max-w-full">
<p className="whitespace-pre-line">
{remediation.code.cli}
</p>
</Snippet>
</div>
)}
<div className="flex flex-row gap-4">
{Object.entries(remediation.code)
.filter(([key]) => key !== "cli")
.map(([key, value]) =>
value ? (
<Link
key={key}
href={value}
target="_blank"
className="text-sm font-medium text-blue-500"
>
{key === "other"
? "External doc"
: key.charAt(0).toUpperCase() +
key.slice(1).toLowerCase()}
</Link>
) : null,
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Resources Section */}
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Resource Details
</h3>
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Resource ID
</p>
<Snippet size="sm" hideSymbol className="max-w-full">
<p className="whitespace-pre-line">{resource.uid}</p>
</Snippet>
</div>
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Resource Name
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
{resource.name}
</p>
</div>
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Region
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
{resource.region}
</p>
</div>
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Resource Type
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
{resource.type}
</p>
</div>
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Severity
</p>
<SeverityBadge severity={attributes.severity} />
</div>
{resource.tags &&
Object.entries(resource.tags).map(([key, value]) => (
<div key={key}>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Tag: {key}
</p>
<SnippetId
entityId={value}
hideSymbol
size="sm"
className="max-w-full"
>
<p className="whitespace-pre-line">{value}</p>
</SnippetId>
</div>
))}
<div className="col-span-2 grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Inserted At
</p>
<DateWithTime inline dateTime={resource.inserted_at} />
</div>
<div>
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
Updated At
</p>
<DateWithTime inline dateTime={resource.updated_at} />
</div>
</div>
</div>
</div>
</div>
);
@@ -32,7 +32,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
<div className="flex flex-col gap-x-4 gap-y-8">
<Card
isBlurred
className="border-none bg-background/60 dark:bg-default-100/50"
className="border-none bg-background/60 dark:bg-prowler-blue-800"
shadow="sm"
>
<CardBody>
@@ -59,7 +59,10 @@ export function DataTableRowActions<InvitationProps>({
</CustomAlertModal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<Dropdown
className="shadow-xl dark:bg-prowler-blue-800"
placement="bottom"
>
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
@@ -55,7 +55,7 @@ export const WorkflowSendInvite = () => {
<VerticalSteps
hideProgressBars
currentStep={currentStep}
stepClassName="border border-default-200 dark:border-default-50 aria-[current]:bg-default-100 dark:aria-[current]:bg-default-50 cursor-default"
stepClassName="border border-default-200 dark:border-default-50 aria-[current]:bg-default-100 dark:aria-[current]:bg-prowler-blue-800 cursor-default"
steps={steps}
/>
<Spacer y={4} />
@@ -61,7 +61,10 @@ export function DataTableRowActions<ProviderProps>({
</CustomAlertModal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<Dropdown
className="shadow-xl dark:bg-prowler-blue-800"
placement="bottom"
>
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
@@ -67,7 +67,7 @@ export const WorkflowAddProvider = () => {
<VerticalSteps
hideProgressBars
currentStep={currentStep}
stepClassName="border border-default-200 dark:border-default-50 aria-[current]:bg-default-100 dark:aria-[current]:bg-default-50 cursor-default"
stepClassName="border border-default-200 dark:border-default-50 aria-[current]:bg-default-100 dark:aria-[current]:bg-prowler-blue-800 cursor-default"
steps={steps}
/>
<Spacer y={4} />
+52 -37
View File
@@ -17,22 +17,25 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
const taskDetails = scanDetails.taskDetails;
return (
<div className="space-y-4">
<div className="flex flex-col gap-6 rounded-lg">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex flex-col items-baseline md:flex-row md:gap-x-4">
<h2 className="text-2xl font-bold">Scan Details</h2>
</div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Scan Details
</h2>
<StatusBadge
size="lg"
status={scanOnDemand.state}
loadingProgress={scanOnDemand.progress}
/>
</div>
<Divider />
<div className="relative z-0 flex w-full flex-col justify-between gap-4 overflow-auto rounded-large bg-content1 p-4 shadow-small">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-4">
<Divider className="border-gray-300 dark:border-gray-600" />
{/* Details Section */}
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-4">
<DetailItem label="Scan Name" value={scanOnDemand.name} />
<DetailItem
label="ID"
@@ -49,7 +52,7 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
value={`${scanOnDemand.duration} seconds`}
/>
</div>
<div className="space-y-4">
<div className="flex flex-col gap-4">
<DateItem
label="Started At"
value={
@@ -68,7 +71,7 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
dateTime={scanOnDemand.completed_at.toString()}
/>
) : (
"Not Started"
"Not Completed"
)
}
/>
@@ -109,17 +112,21 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
</div>
</div>
</div>
<Card className="relative w-full border-small border-default-100 p-3 shadow-lg">
<CardHeader className="py-2">
<h2 className="text-2xl font-bold">Scan arguments</h2>
{/* Scan Arguments Section */}
<Card className="rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<CardHeader className="pb-4">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Scan Arguments
</h3>
</CardHeader>
<Divider />
<CardBody className="p-4">
<Divider className="border-gray-300 dark:border-gray-600" />
<CardBody className="pt-4">
<div className="flex flex-col gap-2">
<span className="font-semibold text-default-500">Checks</span>
<span className="text-default-700">
<span className="text-sm font-semibold text-gray-600 dark:text-gray-300">
Checks
</span>
<span className="text-gray-800 dark:text-prowler-theme-pale/90">
{(scanOnDemand.scanner_args as any)?.checks_to_execute?.join(
", ",
) || "N/A"}
@@ -127,25 +134,28 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
</div>
</CardBody>
</Card>
{/* Task Details Section */}
{taskDetails && (
<Card className="relative w-full border-small border-default-100 p-3 shadow-lg">
<CardHeader className="py-2">
<h2 className="text-2xl font-bold">State details</h2>
<Card className="rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<CardHeader className="pb-4">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
State Details
</h3>
</CardHeader>
<Divider />
<CardBody className="p-4">
<div className="flex flex-col gap-2">
<Divider className="border-gray-300 dark:border-gray-600" />
<CardBody className="pt-4">
<div className="flex flex-col gap-4">
<DetailItem label="State" value={taskDetails.attributes.state} />
<DetailItem
label="Completed At"
value={taskDetails.attributes.completed_at}
value={taskDetails.attributes.completed_at || "N/A"}
/>
{taskDetails.attributes.result && (
<>
<DetailItem
label="Error Type"
value={taskDetails.attributes.result.exc_type}
value={taskDetails.attributes.result.exc_type || "N/A"}
/>
{taskDetails.attributes.result.exc_message && (
<DetailItem
@@ -157,12 +167,13 @@ export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
)}
</>
)}
<DetailItem
label="Checks to Execute"
value={taskDetails.attributes.task_args.checks_to_execute?.join(
", ",
)}
value={
taskDetails.attributes.task_args.checks_to_execute?.join(
", ",
) || "N/A"
}
/>
</div>
</CardBody>
@@ -180,8 +191,10 @@ const DateItem = ({
value: React.ReactNode;
}) => (
<div className="flex items-center justify-between">
<span className="font-semibold text-default-500">{label}:</span>
<span className="text-default-700">{value}</span>
<p className="text-sm font-semibold text-gray-600 dark:text-gray-300">
{label}:
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">{value}</p>
</div>
);
@@ -193,7 +206,9 @@ const DetailItem = ({
value: React.ReactNode;
}) => (
<div className="flex items-center justify-between">
<span className="font-semibold text-default-500">{label}:</span>
<span className="text-default-700">{value}</span>
<p className="text-sm font-semibold text-gray-600 dark:text-gray-300">
{label}:
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">{value}</p>
</div>
);
@@ -49,7 +49,10 @@ export function DataTableRowActions<ScanProps>({
</CustomAlertModal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<Dropdown
className="shadow-xl dark:bg-prowler-blue-800"
placement="bottom"
>
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
@@ -21,6 +21,7 @@ export const CustomAlertModal: React.FC<CustomAlertModalProps> = ({
isOpen={isOpen}
onOpenChange={onOpenChange}
size="xl"
className="dark:bg-prowler-blue-800"
backdrop="blur"
>
<ModalContent className="py-4">
+2 -1
View File
@@ -8,7 +8,8 @@ import { NextUIColors, NextUIVariants } from "@/types";
export const buttonClasses = {
base: "px-4 inline-flex items-center justify-center relative z-0 text-center whitespace-nowrap",
primary: "bg-default-100 hover:bg-default-200 text-default-800",
primary:
"bg-default-100 hover:bg-default-200 text-default-800 dark:bg-prowler-blue-800",
secondary: "bg-prowler-grey-light dark:bg-prowler-grey-medium text-white",
action: "bg-prowler-theme-green font-bold text-prowler-theme-midnight",
dashed:
+44 -21
View File
@@ -10,20 +10,21 @@ import {
PopoverTrigger,
ScrollShadow,
} from "@nextui-org/react";
import _ from "lodash";
import { useSearchParams } from "next/navigation";
import { XCircle } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { PlusCircleIcon } from "@/components/icons";
import { CustomDropdownFilterProps } from "@/types";
const filterSelectedClass =
"inline-flex items-center border py-0.5 text-xs transition-colors border-transparent bg-default-500 text-secondary-foreground hover:bg-default-500/80 rounded-md px-2 font-normal";
"inline-flex items-center border py-1 text-xs transition-colors border-transparent bg-default-500 text-secondary-foreground hover:bg-default-500/80 rounded-md px-2 font-normal";
export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
filter,
onFilterChange,
}) => {
const router = useRouter();
const searchParams = useSearchParams();
const [groupSelected, setGroupSelected] = useState(new Set<string>());
@@ -92,14 +93,24 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
setGroupSelected(new Set(["all", ...allFilterKeys]));
}
}, [groupSelected, allFilterKeys]);
const onClearFilter = useCallback(
(filterKey: string) => {
const params = new URLSearchParams(searchParams.toString());
params.delete(`filter[${filterKey}]`);
router.push(`?${params.toString()}`, { scroll: false });
},
[router, searchParams],
);
return (
<div className="flex w-full flex-col gap-2">
<Popover backdrop="transparent" placement="bottom-start">
<PopoverTrigger>
<Button
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center justify-center whitespace-nowrap rounded-md border border-dashed bg-background px-3 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50"
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md border border-dashed bg-background px-3 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50 dark:bg-prowler-blue-800"
startContent={<PlusCircleIcon size={16} />}
size="sm"
size="md"
>
<h3 className="text-small">{filter?.labelCheckboxGroup}</h3>
@@ -107,26 +118,38 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
<>
<Divider orientation="vertical" className="mx-2 h-4" />
<div className="no-scrollbar hidden max-w-24 space-x-1 overflow-x-auto lg:flex">
{groupSelected.size > 3 ? (
<span
className={filterSelectedClass}
>{`+${groupSelected.size - 2} selected`}</span>
) : (
Array.from(groupSelected)
.filter((value) => value !== "all")
.map((value) => (
<div key={value} className={filterSelectedClass}>
{_.capitalize(value)}
</div>
))
)}
<div className="flex items-center gap-2">
<div className="no-scrollbar hidden max-w-24 space-x-1 overflow-x-auto lg:flex">
{groupSelected.size > 3 ? (
<span className={filterSelectedClass}>
{`+${groupSelected.size - 2} selected`}
</span>
) : (
Array.from(groupSelected)
.filter((value) => value !== "all")
.map((value) => (
<div key={value} className={filterSelectedClass}>
{value}
</div>
))
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onClearFilter(filter.key);
}}
className="absolute right-0 top-1/2 z-40 h-10 w-10 -translate-y-1/2 focus:outline-none"
>
<XCircle className="h-4 w-4 text-default-400" />
</button>
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<PopoverContent className="w-80 dark:bg-prowler-blue-800">
<div className="flex w-full flex-col gap-6 p-2">
<CheckboxGroup
color="default"
@@ -149,7 +172,7 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
>
{allFilterKeys.map((value) => (
<Checkbox className="font-normal" key={value} value={value}>
{_.capitalize(value)}
{value}
</Checkbox>
))}
</ScrollShadow>
+5 -1
View File
@@ -4,11 +4,13 @@ import React from "react";
interface DateWithTimeProps {
dateTime: string | null; // e.g., "2024-07-17T09:55:14.191475Z"
showTime?: boolean;
inline?: boolean;
}
export const DateWithTime: React.FC<DateWithTimeProps> = ({
dateTime,
showTime = true,
inline = false,
}) => {
if (!dateTime) return <span>--</span>;
const date = parseISO(dateTime);
@@ -17,7 +19,9 @@ export const DateWithTime: React.FC<DateWithTimeProps> = ({
return (
<div className="max-w-fit">
<div className="flex flex-col items-start">
<div
className={`flex ${inline ? "flex-row items-center gap-2" : "flex-col"}`}
>
<span className="text-md font-semibold">{formattedDate}</span>
{showTime && (
<span className="text-sm text-gray-500">{formattedTime}</span>
+2 -2
View File
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950",
"fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950 dark:border-prowler-blue-800",
{
variants: {
side: {
@@ -40,7 +40,7 @@ const sheetVariants = cva(
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
"inset-y-0 right-0 h-full w-3/4 border-t-1 border-b-1 border-l-2 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
},
},
defaultVariants: {
+1 -1
View File
@@ -27,7 +27,7 @@ export function TriggerSheet({
<SheetTrigger className="flex items-center gap-2">
{triggerComponent}
</SheetTrigger>
<SheetContent className="my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] rounded-l-xl pt-10 md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
<SheetContent className="my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] overflow-y-auto rounded-l-xl pt-10 dark:bg-prowler-theme-midnight md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
<SheetHeader>
<SheetTitle className="sr-only">{title}</SheetTitle>
<SheetDescription className="sr-only">{description}</SheetDescription>
+1 -1
View File
@@ -280,7 +280,7 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
itemClasses={{
...itemClasses,
base: clsx(
"px-3 rounded-large data-[selected=true]:bg-default-100",
"px-3 rounded-large data-[selected=true]:bg-default-100 dark:data-[selected=true]:bg-prowler-blue-800",
itemClasses?.base,
),
title: clsx(
@@ -10,14 +10,16 @@ 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(false);
const [showFilters, setShowFilters] = useState(defaultOpen);
const pushDropdownFilter = useCallback(
(key: string, values: string[]) => {
@@ -36,14 +38,19 @@ export const DataTableFilterCustom = ({
);
return (
<div className="flex flex-col gap-4 md:flex-row md:items-start">
<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="sm"
size="md"
startContent={<CustomFilterIcon size={16} />}
onPress={() => setShowFilters(!showFilters)}
className="w-fit"
>
<h3 className="text-small">
{showFilters ? "Hide Filters" : "Show Filters"}
@@ -53,11 +60,17 @@ export const DataTableFilterCustom = ({
<div
className={`transition-all duration-700 ease-in-out ${
showFilters
? "max-h-96 w-full translate-x-0 overflow-visible opacity-100 md:max-w-80"
? "max-h-96 w-full translate-x-0 overflow-visible opacity-100"
: "max-h-0 -translate-x-full overflow-hidden opacity-0"
}`}
>
<div className="flex flex-col gap-4 md:flex-row">
<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}
@@ -49,7 +49,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
<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"
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"
>
@@ -57,7 +57,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
</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"
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"
>
@@ -65,14 +65,14 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
</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"
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"
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" />
+1 -8
View File
@@ -14,7 +14,6 @@ import {
import { useState } from "react";
import {
DataTableFilterCustom,
Table,
TableBody,
TableCell,
@@ -36,7 +35,6 @@ export function DataTable<TData, TValue>({
columns,
data,
metadata,
customFilters,
}: DataTableProviderProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@@ -58,12 +56,7 @@ export function DataTable<TData, TValue>({
return (
<>
{customFilters && (
<div className="mb-6">
<DataTableFilterCustom filters={customFilters || []} />
</div>
)}
<div className="relative z-0 flex w-full flex-col justify-between gap-4 overflow-auto rounded-large bg-content1 p-4 shadow-small">
<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) => (
+1
View File
@@ -4,4 +4,5 @@ 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";
+2 -1
View File
@@ -37,7 +37,8 @@ export const SeverityBadge = ({ severity }: { severity: Severity }) => {
return (
<Chip
className={clsx("gap-1 border-none capitalize text-default-600", {
"bg-rose-700/20": severity === "critical",
"bg-system-severity-critical text-white dark:text-white":
severity === "critical",
})}
size="sm"
variant="flat"
@@ -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>
);
};
+1 -1
View File
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
<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: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",
"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}
@@ -62,7 +62,10 @@ export function DataTableRowActions<ProviderProps>({
</CustomAlertModal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<Dropdown
className="shadow-xl dark:bg-prowler-blue-800"
placement="bottom"
>
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
+1
View File
@@ -31,6 +31,7 @@ module.exports = {
},
blue: {
800: "#1e293bff",
400: "#1A202C",
},
grey: {
medium: "#353a4d",