mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
},
|
||||
blue: {
|
||||
800: "#1e293bff",
|
||||
400: "#1A202C",
|
||||
},
|
||||
grey: {
|
||||
medium: "#353a4d",
|
||||
|
||||
Reference in New Issue
Block a user