feat: first iteration of compliance dashboard is working

This commit is contained in:
Pablo Lara
2024-11-18 14:05:36 +01:00
parent 223073e3df
commit 985efc67cc
4 changed files with 112 additions and 27 deletions
+12 -3
View File
@@ -6,15 +6,23 @@ import { parseStringify } from "@/lib";
export const getCompliancesOverview = async ({
scanId,
region,
}: {
scanId: string;
region?: string | string[];
}) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const url = new URL(
`${keyServer}/compliance-overviews?filter[scan_id]=${scanId}`,
);
const url = new URL(`${keyServer}/compliance-overviews`);
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
if (region) {
const regionValue = Array.isArray(region) ? region.join(",") : region;
url.searchParams.append("filter[region__in]", regionValue);
}
try {
const compliances = await fetch(url.toString(), {
headers: {
@@ -24,6 +32,7 @@ export const getCompliancesOverview = async ({
});
const data = await compliances.json();
const parsedData = parseStringify(data);
revalidatePath("/compliance");
return parsedData;
} catch (error) {
+37 -11
View File
@@ -8,7 +8,6 @@ import {
ComplianceSkeletonGrid,
} from "@/components/compliance";
import { DataCompliance } from "@/components/compliance/data-compliance";
import { FilterControls } from "@/components/filters";
import { Header } from "@/components/ui";
import { ComplianceOverviewData, SearchParamsProps } from "@/types";
@@ -24,26 +23,53 @@ export default async function Compliance({
state: scan.attributes.state,
progress: scan.attributes.progress,
}));
const selectedScanId = searchParams.scanId || scanList[0]?.id;
// Fetch compliance data for regions
const compliancesData = await getCompliancesOverview({
scanId: selectedScanId,
});
// Extract unique regions
const regions = compliancesData?.data
? Array.from(
new Set(
compliancesData.data.map(
(compliance: ComplianceOverviewData) =>
compliance.attributes.region as string,
),
),
)
: [];
return (
<>
<Header title="Compliance" icon="fluent-mdl2:compliance-audit" />
<Spacer y={4} />
<div className="mb-6">
<DataCompliance scans={scanList} />
</div>
<FilterControls mutedFindings={false} />
<Spacer y={8} />
<DataCompliance scans={scanList} regions={regions as string[]} />
<Spacer y={12} />
<Suspense fallback={<ComplianceSkeletonGrid />}>
<SSRComplianceGrid scanId={selectedScanId} />
<SSRComplianceGrid searchParams={searchParams} />
</Suspense>
</>
);
}
const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => {
const compliancesData = await getCompliancesOverview({ scanId });
const SSRComplianceGrid = async ({
searchParams,
}: {
searchParams: SearchParamsProps;
}) => {
const scanId = searchParams.scanId?.toString() || "";
const regionFilter = searchParams["filter[region__in]"]?.toString() || "";
// Fetch compliance data
const compliancesData = await getCompliancesOverview({
scanId,
region: regionFilter,
});
// Check if the response contains no data
if (!compliancesData || compliancesData?.data?.length === 0) {
@@ -66,8 +92,8 @@ const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => {
}
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{compliancesData?.data?.map((compliance: ComplianceOverviewData) => {
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{compliancesData.data.map((compliance: ComplianceOverviewData) => {
const { attributes } = compliance;
const {
framework,
@@ -1,37 +1,89 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { SelectScanComplianceData } from "@/components/compliance/data-compliance";
import { CrossIcon } from "@/components/icons";
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
interface DataComplianceProps {
scans: { id: string; name: string; state: string; progress: number }[];
regions: string[];
}
export const DataCompliance = ({ scans }: DataComplianceProps) => {
export const DataCompliance = ({ scans, regions }: DataComplianceProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [showClearButton, setShowClearButton] = useState(false);
const scanIdParam = searchParams.get("scanId");
const selectedScanId =
scanIdParam === "undefined" || !scanIdParam ? scans[0]?.id : scanIdParam;
const selectedScanId = scanIdParam || scans[0]?.id;
if (scanIdParam === "undefined") {
router.replace("/compliance");
return null;
}
const handleSelectionChange = (selectedKey: string) => {
router.push(`?scanId=${selectedKey}`);
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">
<SelectScanComplianceData
scans={scans}
selectedScanId={selectedScanId}
onSelectionChange={handleSelectionChange}
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,5 +1,3 @@
"use client";
import { Select, SelectItem } from "@nextui-org/react";
interface SelectScanComplianceDataProps {