feat(compliance): new compliance selector (#7118)

This commit is contained in:
Pablo Lara
2025-03-05 16:34:07 +01:00
committed by Pepe Fagoaga
parent 0abcf80d19
commit d45eda2b2b
13 changed files with 300 additions and 190 deletions
@@ -0,0 +1,40 @@
import { Divider } from "@nextui-org/react";
import React from "react";
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
interface ComplianceScanInfoProps {
scan: {
providerInfo: {
provider: "aws" | "azure" | "gcp" | "kubernetes";
alias?: string;
uid?: string;
};
attributes: {
name?: string;
completed_at: string;
};
};
}
export const ComplianceScanInfo: React.FC<ComplianceScanInfoProps> = ({
scan,
}) => {
return (
<div className="flex w-fit items-center">
<EntityInfoShort
cloudProvider={scan.providerInfo.provider}
entityAlias={scan.providerInfo.alias}
entityId={scan.providerInfo.uid}
hideCopyButton
/>
<Divider orientation="vertical" className="mx-2 h-6" />
<div className="flex flex-col items-start">
<p className="text-xs text-default-500">
{scan.attributes.name || "- -"}
</p>
<DateWithTime inline dateTime={scan.attributes.completed_at} />
</div>
</div>
);
};
@@ -1,22 +1,20 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useEffect } from "react";
import { SelectScanComplianceData } from "@/components/compliance/data-compliance";
import { CrossIcon } from "@/components/icons";
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
import { SelectScanComplianceDataProps } from "@/types";
interface DataComplianceProps {
scans: { id: string; name: string; state: string; progress: number }[];
regions: string[];
scans: SelectScanComplianceDataProps["scans"];
}
export const DataCompliance = ({ scans, regions }: DataComplianceProps) => {
export const DataCompliance = ({ scans }: DataComplianceProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [showClearButton, setShowClearButton] = useState(false);
const scanIdParam = searchParams.get("scanId");
const selectedScanId = scanIdParam || (scans.length > 0 ? scans[0].id : "");
useEffect(() => {
@@ -27,71 +25,20 @@ export const DataCompliance = ({ scans, regions }: DataComplianceProps) => {
}
}, [scans, scanIdParam, searchParams, router]);
useEffect(() => {
const hasFilters = Array.from(searchParams.keys()).some(
(key) => key.startsWith("filter[") || key === "sort",
);
setShowClearButton(hasFilters);
}, [searchParams]);
const handleScanChange = (selectedKey: string) => {
const params = new URLSearchParams(searchParams);
params.set("scanId", selectedKey);
router.push(`?${params.toString()}`);
};
const pushDropdownFilter = useCallback(
(key: string, values: string[]) => {
const params = new URLSearchParams(searchParams);
const filterKey = `filter[${key}]`;
if (values.length === 0) {
params.delete(filterKey);
} else {
params.set(filterKey, values.join(","));
}
router.push(`?${params.toString()}`);
},
[router, searchParams],
);
const clearAllFilters = useCallback(() => {
const params = new URLSearchParams(searchParams.toString());
Array.from(params.keys()).forEach((key) => {
if (key.startsWith("filter[") || key === "sort") {
params.delete(key);
}
});
router.push(`?${params.toString()}`, { scroll: false });
}, [router, searchParams]);
return (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 lg:grid-cols-3">
<SelectScanComplianceData
scans={scans}
selectedScanId={selectedScanId}
onSelectionChange={handleScanChange}
/>
<CustomDropdownFilter
filter={{
key: "region__in",
values: regions,
labelCheckboxGroup: "Regions",
}}
onFilterChange={pushDropdownFilter}
/>
{showClearButton && (
<CustomButton
ariaLabel="Reset"
className="w-full md:w-fit"
onPress={clearAllFilters}
variant="dashed"
size="sm"
endContent={<CrossIcon size={24} />}
radius="sm"
>
Reset
</CustomButton>
)}
</div>
</div>
);
@@ -1,10 +1,8 @@
import { Select, SelectItem } from "@nextui-org/react";
interface SelectScanComplianceDataProps {
scans: { id: string; name: string; state: string; progress: number }[];
selectedScanId: string;
onSelectionChange: (selectedKey: string) => void;
}
import { SelectScanComplianceDataProps } from "@/types";
import { ComplianceScanInfo } from "../compliance-scan-info";
export const SelectScanComplianceData = ({
scans,
@@ -18,8 +16,8 @@ export const SelectScanComplianceData = ({
classNames={{
selectorIcon: "right-2",
}}
size="lg"
labelPlacement="outside"
size="md"
selectedKeys={new Set([selectedScanId])}
onSelectionChange={(keys) =>
onSelectionChange(Array.from(keys)[0] as string)
@@ -27,25 +25,15 @@ export const SelectScanComplianceData = ({
renderValue={() => {
const selectedItem = scans.find((item) => item.id === selectedScanId);
return selectedItem ? (
<div className="flex flex-col">
<span className="font-bold">{selectedItem.name}</span>
<span className="text-sm text-gray-500">
State: {selectedItem.state}, Progress: {selectedItem.progress}%
</span>
</div>
<ComplianceScanInfo scan={selectedItem} />
) : (
"Select a scan"
);
}}
>
{scans.map((scan) => (
<SelectItem key={scan.id} textValue={scan.name}>
<div className="flex flex-col">
<span className="font-bold">{scan.name}</span>
<span className="text-sm text-gray-500">
State: {scan.state}, Progress: {scan.progress}%
</span>
</div>
<SelectItem key={scan.id} textValue={scan.attributes.name || "- -"}>
<ComplianceScanInfo scan={scan} />
</SelectItem>
))}
</Select>
+2
View File
@@ -1,2 +1,4 @@
export * from "./compliance-card";
export * from "./compliance-scan-info";
export * from "./compliance-skeleton-grid";
export * from "./no-scans-available";
@@ -0,0 +1,41 @@
"use client";
import React from "react";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoScansAvailable = () => {
return (
<div className="flex h-full min-h-[calc(100vh-56px)] items-center justify-center">
<div className="mx-auto w-full max-w-2xl">
<div className="flex items-center justify-start rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-prowler-blue-400">
<div className="flex w-full items-center justify-between gap-6">
<div className="flex items-start gap-4">
<InfoIcon className="mt-1 h-5 w-5 text-gray-400 dark:text-gray-300" />
<div>
<h2 className="mb-1 text-base font-medium text-gray-900 dark:text-white">
No Scans available
</h2>
<p className="text-sm text-gray-500 dark:text-gray-300">
A scan must be completed before generating a compliance
report.
</p>
</div>
</div>
<CustomButton
asLink="/scans"
className="flex-shrink-0"
ariaLabel="Go to Scans page"
variant="solid"
color="action"
size="sm"
>
Go to Scans
</CustomButton>
</div>
</div>
</div>
</div>
);
};
@@ -7,22 +7,27 @@ interface EntityInfoProps {
cloudProvider: "aws" | "azure" | "gcp" | "kubernetes";
entityAlias?: string;
entityId?: string;
hideCopyButton?: boolean;
}
export const EntityInfoShort: React.FC<EntityInfoProps> = ({
cloudProvider,
entityAlias,
entityId,
hideCopyButton = false,
}) => {
return (
<div className="flex w-full items-center justify-between space-x-2">
<div className="flex items-center gap-x-2">
<div className="flex-shrink-0">{getProviderLogo(cloudProvider)}</div>
<div className="flex flex-col space-y-1">
<div className="flex flex-col">
{entityAlias && (
<span className="text-tiny text-default-500">{entityAlias}</span>
<span className="text-xs text-default-500">{entityAlias}</span>
)}
<SnippetId entityId={entityId ?? ""} />
<SnippetId
entityId={entityId ?? ""}
hideCopyButton={hideCopyButton}
/>
</div>
</div>
</div>
+7 -1
View File
@@ -5,9 +5,14 @@ import { CopyIcon, DoneIcon, IdIcon } from "@/components/icons";
interface SnippetIdProps {
entityId: string;
hideCopyButton?: boolean;
[key: string]: any;
}
export const SnippetId: React.FC<SnippetIdProps> = ({ entityId, ...props }) => {
export const SnippetId: React.FC<SnippetIdProps> = ({
entityId,
hideCopyButton = false,
...props
}) => {
return (
<Snippet
className="flex h-6 items-center py-0"
@@ -18,6 +23,7 @@ export const SnippetId: React.FC<SnippetIdProps> = ({ entityId, ...props }) => {
hideSymbol
copyIcon={<CopyIcon size={16} />}
checkIcon={<DoneIcon size={16} />}
hideCopyButton={hideCopyButton}
{...props}
>
<p className="flex items-center space-x-2">