mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat(compliance): new compliance selector (#7118)
This commit is contained in:
@@ -122,6 +122,22 @@
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:45:26.352Z",
|
||||
"updated_at": "2024-10-18T11:16:23.533Z",
|
||||
"provider": "kubernetes",
|
||||
"uid": "gke_lucky-coast-419309_us-central1_autopilot-cluster-2",
|
||||
"alias": "k8s_testing_2",
|
||||
"connected": true,
|
||||
"connection_last_checked_at": "2024-10-18T11:16:23.503Z",
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "11491b47-75ae-4f71-ad8d-3e630a72182e",
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 5,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-01T17:25:27.050Z",
|
||||
"started_at": "2024-09-01T17:25:27.050Z",
|
||||
@@ -33,9 +31,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 20,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T17:24:27.050Z",
|
||||
"started_at": "2024-09-02T17:24:27.050Z",
|
||||
@@ -55,9 +51,7 @@
|
||||
"unique_resource_count": 10,
|
||||
"duration": 10,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudsql_instance_automated_backups"
|
||||
]
|
||||
"checks_to_execute": ["cloudsql_instance_automated_backups"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:26:27.050Z",
|
||||
"started_at": "2024-09-02T19:26:27.050Z",
|
||||
@@ -77,9 +71,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 35,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:27:27.050Z",
|
||||
"started_at": "2024-09-02T19:27:27.050Z",
|
||||
@@ -97,9 +89,7 @@
|
||||
"name": "test scheduled aws scan",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudformation_stack_outputs_find_secrets"
|
||||
]
|
||||
"checks_to_execute": ["cloudformation_stack_outputs_find_secrets"]
|
||||
},
|
||||
"scheduled_at": "2030-09-02T19:20:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:24:27.050Z",
|
||||
@@ -178,9 +168,7 @@
|
||||
"unique_resource_count": 19,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"duration": 7,
|
||||
"scheduled_at": null,
|
||||
@@ -190,6 +178,56 @@
|
||||
"completed_at": "2024-10-18T10:46:05.127Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "6dd8925f-a52d-48de-a546-d2d90db30ab1",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan azure",
|
||||
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 20,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"account_security_contact_information_is_registered"
|
||||
]
|
||||
},
|
||||
"duration": 4,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T11:16:21.358Z",
|
||||
"updated_at": "2024-10-18T11:16:26.060Z",
|
||||
"started_at": "2024-10-18T11:16:21.593Z",
|
||||
"completed_at": "2024-10-18T11:16:26.060Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "4ca7ce89-3236-41a8-a369-8937bc152af5",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan k8s",
|
||||
"provider": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 20,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"account_security_contact_information_is_registered"
|
||||
]
|
||||
},
|
||||
"duration": 4,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T11:16:21.358Z",
|
||||
"updated_at": "2024-10-18T11:16:26.060Z",
|
||||
"started_at": "2024-10-18T11:16:21.593Z",
|
||||
"completed_at": "2024-10-18T11:16:26.060Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01929f57-c0ee-7553-be0b-cbde006fb6f7",
|
||||
|
||||
@@ -7,9 +7,11 @@ import { parseStringify } from "@/lib";
|
||||
export const getCompliancesOverview = async ({
|
||||
scanId,
|
||||
region,
|
||||
query,
|
||||
}: {
|
||||
scanId: string;
|
||||
region?: string | string[];
|
||||
query?: string;
|
||||
}) => {
|
||||
const session = await auth();
|
||||
|
||||
@@ -17,6 +19,7 @@ export const getCompliancesOverview = async ({
|
||||
const url = new URL(`${keyServer}/compliance-overviews`);
|
||||
|
||||
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
|
||||
if (region) {
|
||||
const regionValue = Array.isArray(region) ? region.join(",") : region;
|
||||
|
||||
@@ -82,17 +82,15 @@ export const getMetadataInfo = async ({
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch services regions: ${response.statusText}`,
|
||||
);
|
||||
throw new Error(`Failed to fetch metadata info: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const parsedData = parseStringify(data);
|
||||
const parsedData = parseStringify(await response.json());
|
||||
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching services regions:", error);
|
||||
console.error("Error fetching metadata info:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,88 +4,107 @@ import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getCompliancesOverview } from "@/actions/compliances";
|
||||
import { getMetadataInfo } from "@/actions/findings";
|
||||
import { getProvider } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import {
|
||||
ComplianceCard,
|
||||
ComplianceSkeletonGrid,
|
||||
NoScansAvailable,
|
||||
} from "@/components/compliance";
|
||||
import { DataCompliance } from "@/components/compliance/data-compliance";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ComplianceOverviewData, SearchParamsProps } from "@/types";
|
||||
import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom";
|
||||
import { ComplianceOverviewData, ScanProps, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Compliance({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
let scansData;
|
||||
let scanList: {
|
||||
id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
progress: number;
|
||||
}[] = [];
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
try {
|
||||
scansData = await getScans({});
|
||||
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,
|
||||
})) || [];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching scans data:", error);
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
|
||||
);
|
||||
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
},
|
||||
});
|
||||
|
||||
if (!scansData?.data) {
|
||||
return <NoScansAvailable />;
|
||||
}
|
||||
|
||||
const selectedScanId = searchParams.scanId || scanList[0]?.id || null;
|
||||
// Expand scans with provider information
|
||||
const expandedScansData = await Promise.all(
|
||||
scansData.data.map(async (scan: ScanProps) => {
|
||||
const providerId = scan.relationships?.provider?.data?.id;
|
||||
|
||||
// If there are no scans available, return a message
|
||||
if (!selectedScanId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">No scans available to select.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!providerId) {
|
||||
return { ...scan, providerInfo: null };
|
||||
}
|
||||
|
||||
// Fetch compliance data for regions
|
||||
let compliancesData;
|
||||
let regions: string[] = [];
|
||||
try {
|
||||
compliancesData = await getCompliancesOverview({
|
||||
scanId: selectedScanId as string,
|
||||
});
|
||||
regions = compliancesData?.data
|
||||
? Array.from(
|
||||
new Set(
|
||||
compliancesData.data.map(
|
||||
(compliance: ComplianceOverviewData) =>
|
||||
compliance.attributes.region as string,
|
||||
),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching compliance data:", error);
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
|
||||
const providerData = await getProvider(formData);
|
||||
|
||||
return {
|
||||
...scan,
|
||||
providerInfo: providerData?.data
|
||||
? {
|
||||
provider: providerData.data.attributes.provider,
|
||||
uid: providerData.data.attributes.uid,
|
||||
alias: providerData.data.attributes.alias,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedScanId =
|
||||
searchParams.scanId || expandedScansData[0]?.id || null;
|
||||
const query = (filters["filter[search]"] as string) || "";
|
||||
|
||||
const metadataInfoData = await getMetadataInfo({
|
||||
query,
|
||||
filters: {
|
||||
"filter[scan]": selectedScanId,
|
||||
},
|
||||
});
|
||||
|
||||
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
|
||||
|
||||
return (
|
||||
<ContentLayout title="Compliance" icon="fluent-mdl2:compliance-audit">
|
||||
<DataCompliance scans={scanList} regions={regions} />
|
||||
<Spacer y={12} />
|
||||
<Suspense fallback={<ComplianceSkeletonGrid />}>
|
||||
<SSRComplianceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
{selectedScanId ? (
|
||||
<>
|
||||
<FilterControls search />
|
||||
<Spacer y={8} />
|
||||
<DataCompliance scans={expandedScansData} />
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom
|
||||
filters={[
|
||||
{
|
||||
key: "region__in",
|
||||
labelCheckboxGroup: "Regions",
|
||||
values: uniqueRegions,
|
||||
},
|
||||
]}
|
||||
defaultOpen={true}
|
||||
/>
|
||||
<Spacer y={12} />
|
||||
<Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}>
|
||||
<SSRComplianceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
) : (
|
||||
<NoScansAvailable />
|
||||
)}
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -98,30 +117,25 @@ const SSRComplianceGrid = async ({
|
||||
const scanId = searchParams.scanId?.toString() || "";
|
||||
const regionFilter = searchParams["filter[region__in]"]?.toString() || "";
|
||||
|
||||
// Fetch compliance data
|
||||
let compliancesData;
|
||||
try {
|
||||
compliancesData = await getCompliancesOverview({
|
||||
scanId,
|
||||
region: regionFilter,
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching compliances overview:", error);
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">
|
||||
Failed to load compliance data. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Extract all filter parameters
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
|
||||
);
|
||||
|
||||
// Extract query from filters
|
||||
const query = (filters["filter[search]"] as string) || "";
|
||||
|
||||
const compliancesData = await getCompliancesOverview({
|
||||
scanId,
|
||||
region: regionFilter,
|
||||
query,
|
||||
});
|
||||
|
||||
// Check if the response contains no data
|
||||
if (!compliancesData || compliancesData?.data?.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">
|
||||
<div className="flex h-full items-center">
|
||||
<div className="text-sm text-default-500">
|
||||
No compliance data available for the selected scan.
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,8 +145,8 @@ const SSRComplianceGrid = async ({
|
||||
// Handle errors returned by the API
|
||||
if (compliancesData?.errors?.length > 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">Provide a valid scan ID.</div>
|
||||
<div className="flex h-full items-center">
|
||||
<div className="text-sm text-default-500">Provide a valid scan ID.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -42,6 +42,18 @@ export interface CollapseMenuButtonProps {
|
||||
isOpen: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface SelectScanComplianceDataProps {
|
||||
scans: (ScanProps & {
|
||||
providerInfo: {
|
||||
provider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
})[];
|
||||
selectedScanId: string;
|
||||
onSelectionChange: (selectedKey: string) => void;
|
||||
}
|
||||
|
||||
export type NextUIVariants =
|
||||
| "solid"
|
||||
| "faded"
|
||||
|
||||
Reference in New Issue
Block a user