Merge pull request #98 from prowler-cloud/PRWLR-5379-Overview-Page-Provider-Overview

feat(overview): overview page provider overview
This commit is contained in:
Pablo Lara
2024-11-25 12:15:31 +01:00
committed by GitHub
6 changed files with 317 additions and 21 deletions
+49
View File
@@ -0,0 +1,49 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { auth } from "@/auth.config";
import { parseStringify } from "@/lib";
export const getProvidersOverview = async ({
page = 1,
query = "",
sort = "",
filters = {},
}) => {
const session = await auth();
if (isNaN(Number(page)) || page < 1) redirect("/providers-overview");
const keyServer = process.env.API_BASE_URL;
const url = new URL(`${keyServer}/overviews/providers`);
if (page) url.searchParams.append("page[number]", page.toString());
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
try {
const response = await fetch(url.toString(), {
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${session?.accessToken}`,
},
});
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/providers-overview");
return parsedData;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching providers overview:", error);
return undefined;
}
};
+31 -21
View File
@@ -1,32 +1,42 @@
import { Spacer } from "@nextui-org/react";
import { Suspense } from "react";
import { getProvidersOverview } from "@/actions/overview/overview";
import {
ProvidersOverview,
SkeletonProvidersOverview,
} from "@/components/overview";
import { Header } from "@/components/ui";
export default function Home() {
return (
<>
<Header title="Scan Overview" icon="solar:pie-chart-2-outline" />
<Spacer y={14} />
{/* <div className="grid grid-cols-12 gap-4">
<CustomBox
preTitle={"Findings by Status"}
className="col-span-12 md:col-span-8 xl:col-span-5"
>
<StatusChart />
</CustomBox>
<CustomBox
preTitle={"Findings by Severity"}
className="col-span-12 md:col-span-4 xl:col-span-3"
>
<SeverityChart />
</CustomBox>
<CustomBox
preTitle={"Attack Surface"}
className="col-span-12 sm:col-span-12 xl:col-span-4"
>
<AttackSurface />
</CustomBox>
</div> */}
<Spacer y={4} />
<div className="min-h-screen">
<div className="container mx-auto space-y-8 px-0 py-6">
{/* Providers Overview */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<Suspense fallback={<SkeletonProvidersOverview />}>
<SSRProvidersOverview />
</Suspense>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"></div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"></div>
</div>
</div>
</>
);
}
const SSRProvidersOverview = async () => {
const providersOverview = await getProvidersOverview({});
if (!providersOverview) {
return <p>There is no providers overview info available</p>;
}
return <ProvidersOverview providersOverview={providersOverview} />;
};
+2
View File
@@ -1 +1,3 @@
export * from "./AttackSurface";
export * from "./provider-overview/provider-overview";
export * from "./provider-overview/skeleton-provider-overview";
@@ -0,0 +1,150 @@
"use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react";
import { AddIcon } from "@/components/icons/Icons";
import {
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
KS8ProviderBadge,
} from "@/components/icons/providers-badge";
import { CustomButton } from "@/components/ui/custom/custom-button";
import { ProviderOverviewProps } from "@/types";
export const ProvidersOverview = ({
providersOverview,
}: {
providersOverview: ProviderOverviewProps;
}) => {
console.log(providersOverview);
if (!providersOverview || !Array.isArray(providersOverview.data)) {
return <p>No provider data available</p>;
}
const calculatePassingPercentage = (pass: number, total: number) =>
total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00";
const renderProviderBadge = (providerId: string) => {
switch (providerId) {
case "aws":
return <AWSProviderBadge width={30} height={30} />;
case "azure":
return <AzureProviderBadge width={30} height={30} />;
case "gcp":
return <GCPProviderBadge width={30} height={30} />;
case "kubernetes":
return <KS8ProviderBadge width={30} height={30} />;
default:
return null;
}
};
const providers = [
{ id: "aws", name: "AWS" },
{ id: "azure", name: "Azure" },
{ id: "gcp", name: "GCP" },
{ id: "kubernetes", name: "Kubernetes" },
];
return (
<Card className="dark:bg-prowler-blue-400">
<CardHeader>
<h3 className="text-sm font-bold">Providers Overview</h3>
</CardHeader>
<CardBody>
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-4 border-b pb-2 text-xs font-semibold">
<span className="text-center">Provider</span>
<span className="flex flex-col items-center text-center">
<span>Percent</span>
<span>Passing</span>
</span>
<span className="flex flex-col items-center text-center">
<span>Failing</span>
<span>Checks</span>
</span>
<span className="flex flex-col items-center text-center">
<span>Total</span>
<span>Resources</span>
</span>
</div>
{providers.map((providerTemplate) => {
const providerData = providersOverview.data.find(
(p) => p.id === providerTemplate.id,
);
return (
<div
key={providerTemplate.id}
className="grid grid-cols-4 items-center border-b py-2 text-sm"
>
<span className="flex items-center justify-center px-4">
{renderProviderBadge(providerTemplate.id)}
</span>
<span className="text-center">
{providerData
? calculatePassingPercentage(
providerData.attributes.findings.pass,
providerData.attributes.findings.total,
)
: "0.00"}
%
</span>
<span className="text-center">
{providerData ? providerData.attributes.findings.fail : "-"}
</span>
<span className="text-center">
{providerData ? providerData.attributes.resources.total : "-"}
</span>
</div>
);
})}
{/* Totals row */}
<div className="grid grid-cols-4 items-center border-b py-2 text-sm font-semibold">
<span className="flex items-center justify-center px-4">Total</span>
<span className="text-center">
{calculatePassingPercentage(
providersOverview.data.reduce(
(sum, provider) => sum + provider.attributes.findings.pass,
0,
),
providersOverview.data.reduce(
(sum, provider) => sum + provider.attributes.findings.total,
0,
),
)}
%
</span>
<span className="text-center">
{providersOverview.data.reduce(
(sum, provider) => sum + provider.attributes.findings.fail,
0,
)}
</span>
<span className="text-center">
{providersOverview.data.reduce(
(sum, provider) => sum + provider.attributes.resources.total,
0,
)}
</span>
</div>
</div>
<div className="mt-4 flex w-full items-center justify-end">
<CustomButton
asLink="/providers/connect-account"
ariaLabel="Go to Providers page"
variant="solid"
color="action"
size="sm"
endContent={<AddIcon size={20} />}
>
Add Provider
</CustomButton>
</div>
</CardBody>
</Card>
);
};
@@ -0,0 +1,64 @@
import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react";
export const SkeletonProvidersOverview = () => {
const rows = 4;
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-1/3 rounded-lg">
<div className="h-6 bg-default-200"></div>
</Skeleton>
</CardHeader>
<CardBody>
<div className="grid grid-cols-1 gap-4">
{/* Header Skeleton */}
<div className="grid grid-cols-4 border-b pb-2 text-sm font-semibold">
<Skeleton className="h-5 w-full rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-full rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-full rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-full rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
</div>
{/* Row Skeletons */}
{Array.from({ length: rows }).map((_, index) => (
<div
key={index}
className="grid grid-cols-4 items-center border-b py-2 text-sm"
>
{/* Provider Name */}
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-5 rounded-lg">
<div className="h-5 w-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-1/3 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
</div>
{/* Percent Passing */}
<Skeleton className="h-5 w-1/4 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
{/* Failing Checks */}
<Skeleton className="h-5 w-1/4 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
{/* Total Resources */}
<Skeleton className="h-5 w-1/4 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
</div>
))}
</div>
</CardBody>
</Card>
);
};
+21
View File
@@ -26,6 +26,27 @@ export type NextUIColors =
| "danger"
| "default";
export interface ProviderOverviewProps {
data: {
type: "provider-overviews";
id: "aws" | "gcp" | "azure" | "kubernetes";
attributes: {
findings: {
pass: number;
fail: number;
manual: number;
total: number;
};
resources: {
total: number;
};
};
}[];
meta: {
version: string;
};
}
export interface TaskDetails {
attributes: {
state: string;