chore(ui): Merge UI repository

This commit is contained in:
Pedro De Castro
2024-11-25 13:15:14 +01:00
274 changed files with 33204 additions and 0 deletions
+60
View File
@@ -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>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { AuthForm } from "@/components/auth/oss";
const SignIn = () => {
return <AuthForm type="sign-in" />;
};
export default SignIn;
+13
View File
@@ -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;
+12
View File
@@ -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} />
</>
);
}
+122
View File
@@ -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>
);
};
+36
View File
@@ -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>
);
}
+174
View File
@@ -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}
/>
);
};
+13
View File
@@ -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 />;
}
+66
View File
@@ -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}
/>
);
};
+58
View File
@@ -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>
);
}
+42
View File
@@ -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} />;
};
+30
View File
@@ -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}
/>
);
}
+65
View File
@@ -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}
/>
);
};
+104
View File
@@ -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,
// );
// };
+51
View File
@@ -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>
);
};
+12
View File
@@ -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 />
</>
);
}
+63
View File
@@ -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}
/>
);
};
+12
View File
@@ -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} />
</>
);
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/auth.config";
export const { GET, POST } = handlers;
+10
View File
@@ -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 });
}
+25
View File
@@ -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>
);
}