mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
chore(ui): Merge UI repository
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Metadata, Viewport } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
import { Toaster } from "@/components/ui";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib";
|
||||
|
||||
import { Providers } from "../providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
||||
],
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const session = await auth();
|
||||
|
||||
if (session?.user) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { AuthForm } from "@/components/auth/oss";
|
||||
|
||||
const SignIn = () => {
|
||||
return <AuthForm type="sign-in" />;
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AuthForm } from "@/components/auth/oss";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
const SignUp = ({ searchParams }: { searchParams: SearchParamsProps }) => {
|
||||
const invitationToken =
|
||||
typeof searchParams?.invitation_token === "string"
|
||||
? searchParams.invitation_token
|
||||
: null;
|
||||
|
||||
return <AuthForm type="sign-up" invitationToken={invitationToken} />;
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Categories() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Categories" icon="material-symbols:folder-open-outline" />
|
||||
<Spacer y={4} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getCompliancesOverview } from "@/actions/compliances";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import {
|
||||
ComplianceCard,
|
||||
ComplianceSkeletonGrid,
|
||||
} from "@/components/compliance";
|
||||
import { DataCompliance } from "@/components/compliance/data-compliance";
|
||||
import { Header } from "@/components/ui";
|
||||
import { ComplianceOverviewData, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Compliance({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const scansData = await getScans({});
|
||||
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;
|
||||
|
||||
// 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} />
|
||||
<DataCompliance scans={scanList} regions={regions as string[]} />
|
||||
<Spacer y={12} />
|
||||
<Suspense fallback={<ComplianceSkeletonGrid />}>
|
||||
<SSRComplianceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">
|
||||
No compliance data available for the selected scan.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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 {
|
||||
framework,
|
||||
requirements_status: { passed, total },
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<ComplianceCard
|
||||
key={compliance.id}
|
||||
title={framework}
|
||||
passingRequirements={passed}
|
||||
totalRequirements={total}
|
||||
prevPassingRequirements={passed}
|
||||
prevTotalRequirements={total}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { RocketIcon } from "@/components/icons";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
// reset,
|
||||
}: {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
/* eslint-disable no-console */
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<Alert className="mx-auto mt-[35%] w-fit">
|
||||
<RocketIcon className="h-5 w-5" />
|
||||
<AlertTitle className="text-lg">An unexpected error occurred</AlertTitle>
|
||||
<AlertDescription className="mb-5">
|
||||
We're sorry for the inconvenience. Please try again or contact support
|
||||
if the problem persists.
|
||||
</AlertDescription>
|
||||
<Link href={"/"} className="font-bold">
|
||||
{" "}
|
||||
Go to the homepage
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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 {
|
||||
ColumnFindings,
|
||||
SkeletonTableFindings,
|
||||
} from "@/components/findings/table";
|
||||
import { Header } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import { FindingProps, SearchParamsProps } from "@/types/components";
|
||||
|
||||
export default async function Findings({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
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 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// 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 findingsData = await getFindings({ query, page, sort, filters });
|
||||
|
||||
// Create dictionaries for resources, scans, and providers
|
||||
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
|
||||
? findingsData.data.map((finding: FindingProps) => {
|
||||
const scan = scanDict[finding.relationships?.scan?.data?.id];
|
||||
const resource =
|
||||
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
|
||||
const provider =
|
||||
providerDict[resource?.relationships?.provider?.data?.id];
|
||||
|
||||
return {
|
||||
...finding,
|
||||
relationships: { scan, resource, provider },
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
// Create the new object while maintaining the original structure
|
||||
const expandedResponse = {
|
||||
...findingsData,
|
||||
data: expandedFindings,
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnFindings}
|
||||
data={expandedResponse?.data || []}
|
||||
metadata={findingsData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default function Integrations() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Integrations" icon="tabler:puzzle" />
|
||||
|
||||
<p>Hi hi from Integration page</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getInvitationInfoById } from "@/actions/invitations/invitation";
|
||||
import { InvitationDetails } from "@/components/invitations";
|
||||
import { SkeletonInvitationInfo } from "@/components/invitations/workflow";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function CheckDetailsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
return (
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonInvitationInfo />}>
|
||||
<SSRDataInvitation searchParams={searchParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataInvitation = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const invitationId = searchParams.id;
|
||||
|
||||
if (!invitationId) {
|
||||
return <div>Invalid invitation ID</div>;
|
||||
}
|
||||
|
||||
const invitationData = (await getInvitationInfoById(invitationId as string))
|
||||
.data;
|
||||
|
||||
if (!invitationData) {
|
||||
return <div>Invitation not found</div>;
|
||||
}
|
||||
|
||||
const { attributes, links } = invitationData;
|
||||
|
||||
return <InvitationDetails attributes={attributes} selfLink={links.self} />;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
import { WorkflowSendInvite } from "@/components/invitations/workflow";
|
||||
import { NavigationHeader } from "@/components/ui";
|
||||
|
||||
interface InvitationLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function InvitationLayout({ children }: InvitationLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NavigationHeader
|
||||
title="Send Invitation"
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/invitations"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowSendInvite />
|
||||
</div>
|
||||
<div className="order-2 my-auto lg:col-span-5 lg:col-start-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { SendInvitationForm } from "@/components/invitations/workflow/forms/send-invitation-form";
|
||||
|
||||
export default function SendInvitationPage() {
|
||||
return <SendInvitationForm />;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getInvitations } from "@/actions/invitations/invitation";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { filterInvitations } from "@/components/filters/data-filters";
|
||||
import { SendInvitationButton } from "@/components/invitations";
|
||||
import {
|
||||
ColumnsInvitation,
|
||||
SkeletonTableInvitation,
|
||||
} from "@/components/invitations/table";
|
||||
import { Header } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Invitations({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Invitations" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search />
|
||||
<Spacer y={8} />
|
||||
<SendInvitationButton />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterInvitations || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableInvitation />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// 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 invitationsData = await getInvitations({ query, page, sort, filters });
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnsInvitation}
|
||||
data={invitationsData?.data || []}
|
||||
metadata={invitationsData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Metadata, Viewport } from "next";
|
||||
import React from "react";
|
||||
|
||||
import { SidebarWrap, Toaster } from "@/components/ui";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Providers } from "../providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
<div className="flex h-dvh items-center justify-center overflow-hidden">
|
||||
<SidebarWrap />
|
||||
<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>
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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={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} />;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
// import { getUserByMe } from "@/actions/auth/auth";
|
||||
import { auth } from "@/auth.config";
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Profile() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
// redirect("/sign-in?returnTo=/profile");
|
||||
redirect("/sign-in");
|
||||
}
|
||||
|
||||
// const user = await getUserByMe();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="User Profile" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<Spacer y={6} />
|
||||
<pre>{JSON.stringify(session.user, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.userId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.tenantId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
ViaCredentialsForm,
|
||||
ViaRoleForm,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
|
||||
interface Props {
|
||||
searchParams: { type: string; id: string; via?: string };
|
||||
}
|
||||
|
||||
export default function AddCredentialsPage({ searchParams }: Props) {
|
||||
if (
|
||||
!searchParams.type ||
|
||||
!searchParams.id ||
|
||||
(searchParams.type === "aws" && !searchParams.via)
|
||||
) {
|
||||
redirect("/providers/connect-account");
|
||||
}
|
||||
|
||||
const useCredentialsForm =
|
||||
(searchParams.type === "aws" && searchParams.via === "credentials") ||
|
||||
(searchParams.type !== "aws" && !searchParams.via);
|
||||
|
||||
const useRoleForm =
|
||||
searchParams.type === "aws" && searchParams.via === "role";
|
||||
|
||||
return (
|
||||
<>
|
||||
{useCredentialsForm && <ViaCredentialsForm searchParams={searchParams} />}
|
||||
{useRoleForm && <ViaRoleForm searchParams={searchParams} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { ConnectAccountForm } from "@/components/providers/workflow/forms";
|
||||
|
||||
export default function ConnectAccountPage() {
|
||||
return <ConnectAccountForm />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { getProvider } from "@/actions/providers";
|
||||
import { LaunchScanForm } from "@/components/providers/workflow/forms";
|
||||
|
||||
interface Props {
|
||||
searchParams: { type: string; id: string };
|
||||
}
|
||||
|
||||
export default async function LaunchScanPage({ searchParams }: Props) {
|
||||
const providerId = searchParams.id;
|
||||
|
||||
if (!providerId) {
|
||||
redirect("/providers/connect-account");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
|
||||
const providerData = await getProvider(formData);
|
||||
|
||||
const isConnected = providerData?.data?.attributes?.connection?.connected;
|
||||
|
||||
if (!isConnected) {
|
||||
redirect("/providers/connect-account");
|
||||
}
|
||||
|
||||
return (
|
||||
<LaunchScanForm searchParams={searchParams} providerData={providerData} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
import { WorkflowAddProvider } from "@/components/providers/workflow";
|
||||
import { NavigationHeader } from "@/components/ui";
|
||||
|
||||
interface ProviderLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ProviderLayout({ children }: ProviderLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NavigationHeader
|
||||
title="Connect your cloud account"
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/providers"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowAddProvider />
|
||||
</div>
|
||||
<div className="order-2 my-auto lg:col-span-5 lg:col-start-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
import { getProvider } from "@/actions/providers";
|
||||
import { TestConnectionForm } from "@/components/providers/workflow/forms";
|
||||
|
||||
interface Props {
|
||||
searchParams: { type: string; id: string };
|
||||
}
|
||||
|
||||
export default async function TestConnectionPage({ searchParams }: Props) {
|
||||
const providerId = searchParams.id;
|
||||
|
||||
if (!providerId) {
|
||||
redirect("/providers/connect-account");
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<SSRTestConnection searchParams={searchParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function SSRTestConnection({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: string; id: string };
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", searchParams.id);
|
||||
|
||||
const providerData = await getProvider(formData);
|
||||
if (providerData.errors) {
|
||||
redirect("/providers/connect-account");
|
||||
}
|
||||
|
||||
return (
|
||||
<TestConnectionForm
|
||||
searchParams={searchParams}
|
||||
providerData={providerData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { FilterControls, filterProviders } from "@/components/filters";
|
||||
import { AddProvider } from "@/components/providers";
|
||||
import {
|
||||
ColumnProviders,
|
||||
SkeletonTableProviders,
|
||||
} from "@/components/providers/table";
|
||||
import { Header } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Providers({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers />
|
||||
<Spacer y={8} />
|
||||
<AddProvider />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterProviders || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableProviders />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// 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 providersData = await getProviders({ query, page, sort, filters });
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnProviders}
|
||||
data={providersData?.data || []}
|
||||
metadata={providersData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import { filterScans } from "@/components/filters";
|
||||
import { ButtonRefreshData } from "@/components/scans";
|
||||
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, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { ProviderProps, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Scans({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const filteredParams = { ...searchParams };
|
||||
delete filteredParams.scanId;
|
||||
const searchParamsKey = JSON.stringify(filteredParams);
|
||||
|
||||
const providersData = await getProviders({});
|
||||
|
||||
const providerInfo = providersData?.data?.length
|
||||
? providersData.data.map((provider: ProviderProps) => ({
|
||||
providerId: provider.id,
|
||||
alias: provider.attributes.alias,
|
||||
providerType: provider.attributes.provider,
|
||||
uid: provider.attributes.uid,
|
||||
connected: provider.attributes.connection.connected,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// const executingScans = await getExecutingScans();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Scans" icon="lucide:scan-search" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<LaunchScanWorkflow providers={providerInfo} />
|
||||
<Spacer y={8} />
|
||||
<div className="flex flex-row justify-between">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<ButtonRefreshData
|
||||
onPress={async () => {
|
||||
"use server";
|
||||
await getScans({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 items-start gap-4">
|
||||
<div className="col-span-12">
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
|
||||
<SSRDataTableScans searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataTableScans = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// Extract all filter parameters, excluding scanId
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(searchParams).filter(
|
||||
([key]) => key.startsWith("filter[") && key !== "scanId",
|
||||
),
|
||||
);
|
||||
|
||||
// Extract query from filters
|
||||
const query = (filters["filter[search]"] as string) || "";
|
||||
|
||||
const scansData = await getScans({ query, page, sort, filters });
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnGetScans}
|
||||
data={scansData?.data || []}
|
||||
metadata={scansData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const getExecutingScans = async () => {
|
||||
// const scansData = await getScans({});
|
||||
|
||||
// return scansData?.data?.some(
|
||||
// (scan: ScanProps) =>
|
||||
// scan.attributes.state === "executing" && scan.attributes.progress < 100,
|
||||
// );
|
||||
// };
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getServices } from "@/actions/services";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { ServiceCard, ServiceSkeletonGrid } from "@/components/services";
|
||||
import { Header } from "@/components/ui";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Services({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Services"
|
||||
icon="material-symbols:linked-services-outline"
|
||||
/>
|
||||
<Spacer y={4} />
|
||||
<FilterControls />
|
||||
<Spacer y={4} />
|
||||
<Suspense key={searchParamsKey} fallback={<ServiceSkeletonGrid />}>
|
||||
<SSRServiceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRServiceGrid = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const servicesData = await getServices(searchParams);
|
||||
const [services] = await Promise.all([servicesData]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{services?.map((service: any) => (
|
||||
<ServiceCard
|
||||
key={service.service_id}
|
||||
fidingsFailed={service.fail_findings}
|
||||
serviceAlias={service.service_alias}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Settings() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Settings" icon="solar:settings-outline" />
|
||||
<Spacer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
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, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { AddUserButton } from "@/components/users";
|
||||
import { ColumnsUser, SkeletonTableUser } from "@/components/users/table";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Users({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Users" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search />
|
||||
<Spacer y={8} />
|
||||
<AddUserButton />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterUsers || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// 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 usersData = await getUsers({ query, page, sort, filters });
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnsUser}
|
||||
data={usersData?.data || []}
|
||||
metadata={usersData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Workloads() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Workloads" icon="lucide:tags" />
|
||||
<Spacer y={4} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/auth.config";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import data from "../../../dataServices.json";
|
||||
|
||||
export async function GET() {
|
||||
// Simulate fetching data with a delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
return NextResponse.json({ services: data });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { NextUIProvider } from "@nextui-org/system";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from "react";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
export function Providers({ children, themeProps }: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user