feat: upgrade to React 19, Next.js 15, React Compiler, HeroUI and Tailwind 4 (#8748)

Co-authored-by: Alan Buscaglia <alanbuscaglia@MacBook-Pro.local>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: César Arroba <cesar@prowler.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
This commit is contained in:
Alan Buscaglia
2025-09-30 09:59:51 +02:00
committed by GitHub
parent 2a4b62527a
commit 4d5676f00e
261 changed files with 6339 additions and 6126 deletions

2
.env
View File

@@ -99,7 +99,7 @@ SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local SENTRY_RELEASE=local
#### Prowler release version #### #### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.10.0 NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.12.2
# Social login credentials # Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google" SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"

View File

@@ -45,3 +45,7 @@ pypi-upload: ## Upload package
help: ## Show this help. help: ## Show this help.
@echo "Prowler Makefile" @echo "Prowler Makefile"
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development Environment
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat --build

View File

@@ -12,6 +12,7 @@ node_modules
coverage coverage
.next .next
build build
next-env.d.ts
!.commitlintrc.cjs !.commitlintrc.cjs
!.lintstagedrc.cjs !.lintstagedrc.cjs
!jest.config.js !jest.config.js

View File

@@ -9,11 +9,18 @@ All notable changes to the **Prowler UI** are documented in this file.
- Support for Markdown and AdditionalURLs in findings detail page [(#8704)](https://github.com/prowler-cloud/prowler/pull/8704) - Support for Markdown and AdditionalURLs in findings detail page [(#8704)](https://github.com/prowler-cloud/prowler/pull/8704)
- `Prowler Hub` menu item with tooltip [(#8692)] (https://github.com/prowler-cloud/prowler/pull/8692) - `Prowler Hub` menu item with tooltip [(#8692)] (https://github.com/prowler-cloud/prowler/pull/8692)
- Copy link button to finding detail page [(#8685)] (https://github.com/prowler-cloud/prowler/pull/8685) - Copy link button to finding detail page [(#8685)] (https://github.com/prowler-cloud/prowler/pull/8685)
- React Compiler support for automatic optimization [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Turbopack support for faster development builds [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Add compliance name in compliance detail view [(#8775)](https://github.com/prowler-cloud/prowler/pull/8775) - Add compliance name in compliance detail view [(#8775)](https://github.com/prowler-cloud/prowler/pull/8775)
### 🔄 Changed ### 🔄 Changed
### 🐞 Fixed - Upgraded React to version 19.1.1 with async components support [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Upgraded Next.js to version 15.5.3 with enhanced App Router [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Updated from NextUI to HeroUI [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Updated LangChain to latest versions with API improvements [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Migrated all page components to async `params`/`searchParams` API [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
- Migrated from `useFormState` to `useActionState` for React 19 compatibility [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
--- ---

View File

@@ -1,6 +1,7 @@
"use server"; "use server";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getCompliancesOverview = async ({ export const getCompliancesOverview = async ({
scanId, scanId,
@@ -28,7 +29,7 @@ export const getCompliancesOverview = async ({
headers, headers,
}); });
return handleApiResponse(response, "/compliance"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching providers:", error); console.error("Error fetching providers:", error);
return undefined; return undefined;

View File

@@ -2,7 +2,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getFindings = async ({ export const getFindings = async ({
page = 1, page = 1,
@@ -32,7 +33,7 @@ export const getFindings = async ({
const findings = await fetch(url.toString(), { const findings = await fetch(url.toString(), {
headers, headers,
}); });
return handleApiResponse(findings, "/findings"); return handleApiResponse(findings);
} catch (error) { } catch (error) {
console.error("Error fetching findings:", error); console.error("Error fetching findings:", error);
return undefined; return undefined;
@@ -69,7 +70,7 @@ export const getLatestFindings = async ({
const findings = await fetch(url.toString(), { const findings = await fetch(url.toString(), {
headers, headers,
}); });
return handleApiResponse(findings, "/findings"); return handleApiResponse(findings);
} catch (error) { } catch (error) {
console.error("Error fetching findings:", error); console.error("Error fetching findings:", error);
return undefined; return undefined;

View File

@@ -3,13 +3,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { pollTaskUntilSettled } from "@/actions/task/poll"; import { pollTaskUntilSettled } from "@/actions/task/poll";
import { import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
parseStringify,
} from "@/lib";
import { IntegrationType } from "@/types/integrations"; import { IntegrationType } from "@/types/integrations";
import type { TaskState } from "@/types/tasks"; import type { TaskState } from "@/types/tasks";

View File

@@ -1,7 +1,8 @@
"use server"; "use server";
import { pollTaskUntilSettled } from "@/actions/task/poll"; import { pollTaskUntilSettled } from "@/actions/task/poll";
import { apiBaseUrl, getAuthHeaders, handleApiError } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiError } from "@/lib/server-actions-helper";
import type { import type {
IntegrationProps, IntegrationProps,
JiraDispatchRequest, JiraDispatchRequest,

View File

@@ -2,7 +2,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib/helper"; import { apiBaseUrl, getAuthHeaders } from "@/lib/helper";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { samlConfigFormSchema } from "@/types/formSchemas"; import { samlConfigFormSchema } from "@/types/formSchemas";
export const createSamlConfig = async (_prevState: any, formData: FormData) => { export const createSamlConfig = async (_prevState: any, formData: FormData) => {

View File

@@ -3,12 +3,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getInvitations = async ({ export const getInvitations = async ({
page = 1, page = 1,
@@ -40,7 +36,7 @@ export const getInvitations = async ({
headers, headers,
}); });
return handleApiResponse(response, "/invitations"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching invitations:", error); console.error("Error fetching invitations:", error);
return undefined; return undefined;

View File

@@ -3,13 +3,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders, getErrorMessage } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
getErrorMessage,
handleApiError,
handleApiResponse,
} from "@/lib";
import { ManageGroupPayload, ProviderGroupsResponse } from "@/types/components"; import { ManageGroupPayload, ProviderGroupsResponse } from "@/types/components";
export const getProviderGroups = async ({ export const getProviderGroups = async ({
@@ -48,7 +43,7 @@ export const getProviderGroups = async ({
headers, headers,
}); });
return handleApiResponse(response, "/manage-groups"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching provider groups:", error); console.error("Error fetching provider groups:", error);
return undefined; return undefined;
@@ -161,7 +156,7 @@ export const updateProviderGroup = async (
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
return handleApiResponse(response, "/manage-groups"); return handleApiResponse(response);
} catch (error) { } catch (error) {
handleApiError(error); handleApiError(error);
} }

View File

@@ -1,7 +1,8 @@
"use server"; "use server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getProvidersOverview = async ({ export const getProvidersOverview = async ({
page = 1, page = 1,
@@ -31,7 +32,7 @@ export const getProvidersOverview = async ({
headers, headers,
}); });
return handleApiResponse(response, "/"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching providers overview:", error); console.error("Error fetching providers overview:", error);
return undefined; return undefined;
@@ -72,7 +73,7 @@ export const getFindingsByStatus = async ({
headers, headers,
}); });
return handleApiResponse(response, "/"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching findings severity overview:", error); console.error("Error fetching findings severity overview:", error);
return undefined; return undefined;
@@ -108,7 +109,7 @@ export const getFindingsBySeverity = async ({
headers, headers,
}); });
return handleApiResponse(response, "/"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching findings severity overview:", error); console.error("Error fetching findings severity overview:", error);
return undefined; return undefined;

View File

@@ -3,16 +3,10 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib";
apiBaseUrl,
getAuthHeaders,
getFormValue,
handleApiError,
handleApiResponse,
wait,
} from "@/lib";
import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials"; import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import { ProvidersApiResponse, ProviderType } from "@/types/providers"; import { ProvidersApiResponse, ProviderType } from "@/types/providers";
export const getProviders = async ({ export const getProviders = async ({
@@ -45,7 +39,7 @@ export const getProviders = async ({
headers, headers,
}); });
return handleApiResponse(response, "/providers"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching providers:", error); console.error("Error fetching providers:", error);
return undefined; return undefined;
@@ -63,7 +57,7 @@ export const getProvider = async (formData: FormData) => {
headers, headers,
}); });
return handleApiResponse(response, "/providers"); return handleApiResponse(response);
} catch (error) { } catch (error) {
return handleApiError(error); return handleApiError(error);
} }

View File

@@ -2,7 +2,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getResources = async ({ export const getResources = async ({
page = 1, page = 1,
@@ -46,7 +47,7 @@ export const getResources = async ({
headers, headers,
}); });
return handleApiResponse(response, "/resources"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching resources:", error); console.error("Error fetching resources:", error);
return undefined; return undefined;
@@ -95,7 +96,7 @@ export const getLatestResources = async ({
headers, headers,
}); });
return handleApiResponse(response, "/resources"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching latest resources:", error); console.error("Error fetching latest resources:", error);
return undefined; return undefined;

View File

@@ -3,12 +3,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getRoles = async ({ export const getRoles = async ({
page = 1, page = 1,
@@ -40,7 +36,7 @@ export const getRoles = async ({
headers, headers,
}); });
return handleApiResponse(response, "/roles"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching roles:", error); console.error("Error fetching roles:", error);
return undefined; return undefined;

View File

@@ -2,13 +2,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders, getErrorMessage } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
getErrorMessage,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getScans = async ({ export const getScans = async ({
page = 1, page = 1,
@@ -44,7 +39,7 @@ export const getScans = async ({
try { try {
const response = await fetch(url.toString(), { headers }); const response = await fetch(url.toString(), { headers });
return handleApiResponse(response, "/scans"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching scans:", error); console.error("Error fetching scans:", error);
return undefined; return undefined;

View File

@@ -1,11 +1,7 @@
"use server"; "use server";
import { import { apiBaseUrl, getAuthHeaders } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getTask = async (taskId: string) => { export const getTask = async (taskId: string) => {
const headers = await getAuthHeaders({ contentType: false }); const headers = await getAuthHeaders({ contentType: false });

View File

@@ -2,12 +2,8 @@
import { z } from "zod"; import { z } from "zod";
import { import { apiBaseUrl, getAuthHeaders } from "@/lib/helper";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib/helper";
export const getAllTenants = async () => { export const getAllTenants = async () => {
const headers = await getAuthHeaders({ contentType: false }); const headers = await getAuthHeaders({ contentType: false });
@@ -23,7 +19,7 @@ export const getAllTenants = async () => {
throw new Error(`Failed to fetch tenants data: ${response.statusText}`); throw new Error(`Failed to fetch tenants data: ${response.statusText}`);
} }
return handleApiResponse(response, "/profile"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching tenants:", error); console.error("Error fetching tenants:", error);
return undefined; return undefined;
@@ -41,7 +37,7 @@ const editTenantFormSchema = z
path: ["name"], path: ["name"],
}); });
export async function updateTenantName(prevState: any, formData: FormData) { export async function updateTenantName(_prevState: any, formData: FormData) {
const headers = await getAuthHeaders({ contentType: true }); const headers = await getAuthHeaders({ contentType: true });
const formDataObject = Object.fromEntries(formData); const formDataObject = Object.fromEntries(formData);
const validatedData = editTenantFormSchema.safeParse(formDataObject); const validatedData = editTenantFormSchema.safeParse(formDataObject);

View File

@@ -3,12 +3,8 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import { apiBaseUrl, getAuthHeaders } from "@/lib";
apiBaseUrl, import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getUsers = async ({ export const getUsers = async ({
page = 1, page = 1,
@@ -40,7 +36,7 @@ export const getUsers = async ({
headers, headers,
}); });
return handleApiResponse(users, "/users"); return handleApiResponse(users);
} catch (error) { } catch (error) {
console.error("Error fetching users:", error); console.error("Error fetching users:", error);
return undefined; return undefined;
@@ -178,7 +174,7 @@ export const getUserInfo = async () => {
throw new Error(`Failed to fetch user data: ${response.statusText}`); throw new Error(`Failed to fetch user data: ${response.statusText}`);
} }
return handleApiResponse(response, "/profile"); return handleApiResponse(response);
} catch (error) { } catch (error) {
console.error("Error fetching profile:", error); console.error("Error fetching profile:", error);
return undefined; return undefined;

View File

@@ -47,7 +47,7 @@ export default async function RootLayout({
<body <body
suppressHydrationWarning suppressHydrationWarning
className={cn( className={cn(
"min-h-screen bg-background font-sans antialiased", "bg-background min-h-screen font-sans antialiased",
fontSans.variable, fontSans.variable,
)} )}
> >

View File

@@ -3,10 +3,15 @@ import { getAuthUrl, isGithubOAuthEnabled } from "@/lib/helper";
import { isGoogleOAuthEnabled } from "@/lib/helper"; import { isGoogleOAuthEnabled } from "@/lib/helper";
import { SearchParamsProps } from "@/types"; import { SearchParamsProps } from "@/types";
const SignUp = ({ searchParams }: { searchParams: SearchParamsProps }) => { const SignUp = async ({
searchParams,
}: {
searchParams: Promise<SearchParamsProps>;
}) => {
const resolvedSearchParams = await searchParams;
const invitationToken = const invitationToken =
typeof searchParams?.invitation_token === "string" typeof resolvedSearchParams?.invitation_token === "string"
? searchParams.invitation_token ? resolvedSearchParams.invitation_token
: null; : null;
const GOOGLE_AUTH_URL = getAuthUrl("google"); const GOOGLE_AUTH_URL = getAuthUrl("google");

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import Image from "next/image"; import Image from "next/image";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
@@ -48,12 +48,12 @@ const ComplianceIconSmall = ({
title: string; title: string;
}) => { }) => {
return ( return (
<div className="relative h-6 w-6 flex-shrink-0"> <div className="relative h-6 w-6 shrink-0">
<Image <Image
src={logoPath} src={logoPath}
alt={`${title} logo`} alt={`${title} logo`}
fill fill
className="h-8 w-8 min-w-8 rounded-md border-1 border-gray-300 bg-white object-contain p-[2px]" className="h-8 w-8 min-w-8 rounded-md border border-gray-300 bg-white object-contain p-[2px]"
/> />
</div> </div>
); );
@@ -76,18 +76,19 @@ export default async function ComplianceDetail({
params, params,
searchParams, searchParams,
}: { }: {
params: { compliancetitle: string }; params: Promise<{ compliancetitle: string }>;
searchParams: ComplianceDetailSearchParams; searchParams: Promise<ComplianceDetailSearchParams>;
}) { }) {
const { compliancetitle } = params; const { compliancetitle } = await params;
const { complianceId, version, scanId, scanData } = searchParams; const resolvedSearchParams = await searchParams;
const regionFilter = searchParams["filter[region__in]"]; const { complianceId, version, scanId, scanData } = resolvedSearchParams;
const cisProfileFilter = searchParams["filter[cis_profile_level]"]; const regionFilter = resolvedSearchParams["filter[region__in]"];
const cisProfileFilter = resolvedSearchParams["filter[cis_profile_level]"];
const logoPath = getComplianceIcon(compliancetitle); const logoPath = getComplianceIcon(compliancetitle);
// Create a key that excludes pagination parameters to preserve accordion state avoiding reloads with pagination // Create a key that excludes pagination parameters to preserve accordion state avoiding reloads with pagination
const paramsForKey = Object.fromEntries( const paramsForKey = Object.fromEntries(
Object.entries(searchParams).filter( Object.entries(resolvedSearchParams).filter(
([key]) => key !== "page" && key !== "pageSize", ([key]) => key !== "page" && key !== "pageSize",
), ),
); );
@@ -153,7 +154,7 @@ export default async function ComplianceDetail({
<Suspense <Suspense
key={searchParamsKey} key={searchParamsKey}
fallback={ fallback={
<div className="space-y-8"> <div className="flex flex-col gap-8">
<ChartsWrapper logoPath={logoPath}> <ChartsWrapper logoPath={logoPath}>
<PieChartSkeleton /> <PieChartSkeleton />
<BarChartSkeleton /> <BarChartSkeleton />
@@ -200,7 +201,7 @@ const SSRComplianceContent = async ({
if (!scanId || type === "tasks") { if (!scanId || type === "tasks") {
return ( return (
<div className="space-y-8"> <div className="flex flex-col gap-8">
<ChartsWrapper logoPath={logoPath}> <ChartsWrapper logoPath={logoPath}>
<PieChart pass={0} fail={0} manual={0} /> <PieChart pass={0} fail={0} manual={0} />
<BarChart sections={[]} /> <BarChart sections={[]} />
@@ -231,7 +232,7 @@ const SSRComplianceContent = async ({
const topFailedSections = mapper.getTopFailedSections(data); const topFailedSections = mapper.getTopFailedSections(data);
return ( return (
<div className="space-y-8"> <div className="flex flex-col gap-8">
<ChartsWrapper logoPath={logoPath}> <ChartsWrapper logoPath={logoPath}>
<PieChart <PieChart
pass={totalRequirements.pass} pass={totalRequirements.pass}

View File

@@ -22,12 +22,15 @@ import { ComplianceOverviewData } from "@/types/compliance";
export default async function Compliance({ export default async function Compliance({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
const filters = Object.fromEntries( const filters = Object.fromEntries(
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), Object.entries(resolvedSearchParams).filter(([key]) =>
key.startsWith("filter["),
),
); );
const scansData = await getScans({ const scansData = await getScans({
@@ -72,7 +75,7 @@ export default async function Compliance({
.filter(Boolean) as ExpandedScanData[]; .filter(Boolean) as ExpandedScanData[];
const selectedScanId = const selectedScanId =
searchParams.scanId || expandedScansData[0]?.id || null; resolvedSearchParams.scanId || expandedScansData[0]?.id || null;
const query = (filters["filter[search]"] as string) || ""; const query = (filters["filter[search]"] as string) || "";
// Find the selected scan // Find the selected scan
@@ -103,7 +106,7 @@ export default async function Compliance({
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
return ( return (
<ContentLayout title="Compliance" icon="fluent-mdl2:compliance-audit"> <ContentLayout title="Compliance" icon="lucide:shield-check">
{selectedScanId ? ( {selectedScanId ? (
<> <>
<ComplianceHeader <ComplianceHeader
@@ -112,7 +115,7 @@ export default async function Compliance({
/> />
<Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}> <Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}>
<SSRComplianceGrid <SSRComplianceGrid
searchParams={searchParams} searchParams={resolvedSearchParams}
selectedScan={selectedScanData} selectedScan={selectedScanData}
/> />
</Suspense> </Suspense>
@@ -163,7 +166,7 @@ const SSRComplianceGrid = async ({
) { ) {
return ( return (
<div className="flex h-full items-center"> <div className="flex h-full items-center">
<div className="text-sm text-default-500"> <div className="text-default-500 text-sm">
No compliance data available for the selected scan. No compliance data available for the selected scan.
</div> </div>
</div> </div>
@@ -174,7 +177,7 @@ const SSRComplianceGrid = async ({
if (compliancesData?.errors?.length > 0) { if (compliancesData?.errors?.length > 0) {
return ( return (
<div className="flex h-full items-center"> <div className="flex h-full items-center">
<div className="text-sm text-default-500">Provide a valid scan ID.</div> <div className="text-default-500 text-sm">Provide a valid scan ID.</div>
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import { import {
@@ -33,13 +33,15 @@ import { FindingProps, SearchParamsProps } from "@/types/components";
export default async function Findings({ export default async function Findings({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const { searchParamsKey, encodedSort } = extractSortAndKey(searchParams); const resolvedSearchParams = await searchParams;
const { filters, query } = extractFiltersAndQuery(searchParams); const { searchParamsKey, encodedSort } =
extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
// Check if the searchParams contain any date or scan filter // Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(searchParams); const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams);
const [metadataInfoData, providersData, scansData] = await Promise.all([ const [metadataInfoData, providersData, scansData] = await Promise.all([
(hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({ (hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({
@@ -81,7 +83,7 @@ export default async function Findings({
) as { [uid: string]: ScanEntity }[]; ) as { [uid: string]: ScanEntity }[];
return ( return (
<ContentLayout title="Findings" icon="carbon:data-view-alt"> <ContentLayout title="Findings" icon="lucide:tag">
<FindingsFilters <FindingsFilters
providerUIDs={providerUIDs} providerUIDs={providerUIDs}
providerDetails={providerDetails} providerDetails={providerDetails}
@@ -94,7 +96,7 @@ export default async function Findings({
/> />
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}> <Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -157,12 +159,13 @@ const SSRDataTable = async ({
return ( return (
<> <>
{findingsData?.errors && ( {findingsData?.errors && (
<div className="mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-small text-red-700"> <div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
<p className="mr-2 font-semibold">Error:</p> <p className="mr-2 font-semibold">Error:</p>
<p>{findingsData.errors[0].detail}</p> <p>{findingsData.errors[0].detail}</p>
</div> </div>
)} )}
<DataTable <DataTable
key={Date.now()}
columns={ColumnFindings} columns={ColumnFindings}
data={expandedResponse?.data || []} data={expandedResponse?.data || []}
metadata={findingsData?.meta} metadata={findingsData?.meta}

View File

@@ -6,19 +6,25 @@ import { S3IntegrationsManager } from "@/components/integrations/s3/s3-integrati
import { ContentLayout } from "@/components/ui"; import { ContentLayout } from "@/components/ui";
interface S3IntegrationsProps { interface S3IntegrationsProps {
searchParams: { [key: string]: string | string[] | undefined }; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
} }
export default async function S3Integrations({ export default async function S3Integrations({
searchParams, searchParams,
}: S3IntegrationsProps) { }: S3IntegrationsProps) {
const page = parseInt(searchParams.page?.toString() || "1", 10); const resolvedSearchParams = await searchParams;
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); const page = parseInt(resolvedSearchParams.page?.toString() || "1", 10);
const sort = searchParams.sort?.toString(); const pageSize = parseInt(
resolvedSearchParams.pageSize?.toString() || "10",
10,
);
const sort = resolvedSearchParams.sort?.toString();
// Extract all filter parameters // Extract all filter parameters
const filters = Object.fromEntries( const filters = Object.fromEntries(
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), Object.entries(resolvedSearchParams).filter(([key]) =>
key.startsWith("filter["),
),
); );
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
@@ -49,8 +55,8 @@ export default async function S3Integrations({
return ( return (
<ContentLayout title="Amazon S3"> <ContentLayout title="Amazon S3">
<div className="space-y-6"> <div className="flex flex-col gap-6">
<div className="space-y-4"> <div className="flex flex-col gap-4">
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
Configure Amazon S3 integration to automatically export your scan Configure Amazon S3 integration to automatically export your scan
results to S3 buckets. results to S3 buckets.
@@ -60,7 +66,7 @@ export default async function S3Integrations({
<h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100">
Features: Features:
</h3> </h3>
<ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 dark:text-gray-300 md:grid-cols-2"> <ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 md:grid-cols-2 dark:text-gray-300">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-green-500" /> <span className="h-1.5 w-1.5 rounded-full bg-green-500" />
Automated scan result exports Automated scan result exports

View File

@@ -6,18 +6,24 @@ import { SecurityHubIntegrationsManager } from "@/components/integrations/securi
import { ContentLayout } from "@/components/ui"; import { ContentLayout } from "@/components/ui";
interface SecurityHubIntegrationsProps { interface SecurityHubIntegrationsProps {
searchParams: { [key: string]: string | string[] | undefined }; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
} }
export default async function SecurityHubIntegrations({ export default async function SecurityHubIntegrations({
searchParams, searchParams,
}: SecurityHubIntegrationsProps) { }: SecurityHubIntegrationsProps) {
const page = parseInt(searchParams.page?.toString() || "1", 10); const resolvedSearchParams = await searchParams;
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); const page = parseInt(resolvedSearchParams.page?.toString() || "1", 10);
const sort = searchParams.sort?.toString(); const pageSize = parseInt(
resolvedSearchParams.pageSize?.toString() || "10",
10,
);
const sort = resolvedSearchParams.sort?.toString();
const filters = Object.fromEntries( const filters = Object.fromEntries(
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), Object.entries(resolvedSearchParams).filter(([key]) =>
key.startsWith("filter["),
),
); );
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
@@ -47,8 +53,8 @@ export default async function SecurityHubIntegrations({
return ( return (
<ContentLayout title="AWS Security Hub"> <ContentLayout title="AWS Security Hub">
<div className="space-y-6"> <div className="flex flex-col gap-6">
<div className="space-y-4"> <div className="flex flex-col gap-4">
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
Configure AWS Security Hub integration to automatically send your Configure AWS Security Hub integration to automatically send your
security findings for centralized monitoring and compliance. security findings for centralized monitoring and compliance.
@@ -58,7 +64,7 @@ export default async function SecurityHubIntegrations({
<h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100">
Features: Features:
</h3> </h3>
<ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 dark:text-gray-300 md:grid-cols-2"> <ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 md:grid-cols-2 dark:text-gray-300">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-green-500" /> <span className="h-1.5 w-1.5 rounded-full bg-green-500" />
Automated findings export Automated findings export

View File

@@ -3,19 +3,25 @@ import { JiraIntegrationsManager } from "@/components/integrations/jira/jira-int
import { ContentLayout } from "@/components/ui"; import { ContentLayout } from "@/components/ui";
interface JiraIntegrationsProps { interface JiraIntegrationsProps {
searchParams: { [key: string]: string | string[] | undefined }; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
} }
export default async function JiraIntegrations({ export default async function JiraIntegrations({
searchParams, searchParams,
}: JiraIntegrationsProps) { }: JiraIntegrationsProps) {
const page = parseInt(searchParams.page?.toString() || "1", 10); const resolvedSearchParams = await searchParams;
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); const page = parseInt(resolvedSearchParams.page?.toString() || "1", 10);
const sort = searchParams.sort?.toString(); const pageSize = parseInt(
resolvedSearchParams.pageSize?.toString() || "10",
10,
);
const sort = resolvedSearchParams.sort?.toString();
// Extract all filter parameters // Extract all filter parameters
const filters = Object.fromEntries( const filters = Object.fromEntries(
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), Object.entries(resolvedSearchParams).filter(([key]) =>
key.startsWith("filter["),
),
); );
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
@@ -42,8 +48,8 @@ export default async function JiraIntegrations({
return ( return (
<ContentLayout title="Jira"> <ContentLayout title="Jira">
<div className="space-y-6"> <div className="flex flex-col gap-6">
<div className="space-y-4"> <div className="flex flex-col gap-4">
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
Configure Jira integration to automatically create issues for Configure Jira integration to automatically create issues for
security findings in your Jira projects. security findings in your Jira projects.
@@ -53,7 +59,7 @@ export default async function JiraIntegrations({
<h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100">
Features: Features:
</h3> </h3>
<ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 dark:text-gray-300 md:grid-cols-2"> <ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 md:grid-cols-2 dark:text-gray-300">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-green-500" /> <span className="h-1.5 w-1.5 rounded-full bg-green-500" />
Automated issue creation Automated issue creation

View File

@@ -10,8 +10,8 @@ import { ContentLayout } from "@/components/ui";
export default async function Integrations() { export default async function Integrations() {
return ( return (
<ContentLayout title="Integrations" icon="lucide:puzzle"> <ContentLayout title="Integrations" icon="lucide:puzzle">
<div className="space-y-6"> <div className="flex flex-col gap-6">
<div className="space-y-4"> <div className="flex flex-col gap-4">
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
Connect external services to enhance your security workflow and Connect external services to enhance your security workflow and
automatically export your scan results. automatically export your scan results.

View File

@@ -8,13 +8,14 @@ import { SearchParamsProps } from "@/types";
export default async function CheckDetailsPage({ export default async function CheckDetailsPage({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<Suspense key={searchParamsKey} fallback={<SkeletonInvitationInfo />}> <Suspense key={searchParamsKey} fallback={<SkeletonInvitationInfo />}>
<SSRDataInvitation searchParams={searchParams} /> <SSRDataInvitation searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
); );
} }

View File

@@ -1,6 +1,6 @@
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import React from "react"; import React from "react";
import { WorkflowSendInvite } from "@/components/invitations/workflow"; import { WorkflowSendInvite } from "@/components/invitations/workflow";
@@ -19,8 +19,8 @@ export default function InvitationLayout({ children }: InvitationLayoutProps) {
href="/invitations" href="/invitations"
/> />
<Spacer y={16} /> <Spacer y={16} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12"> <div className="grid grid-cols-1 gap-8 px-4 lg:grid-cols-12 lg:px-0">
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block"> <div className="order-1 my-auto h-full lg:col-span-4 lg:col-start-2">
<WorkflowSendInvite /> <WorkflowSendInvite />
</div> </div>
<div className="order-2 my-auto lg:col-span-5 lg:col-start-6"> <div className="order-2 my-auto lg:col-span-5 lg:col-start-6">

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import { getInvitations } from "@/actions/invitations/invitation"; import { getInvitations } from "@/actions/invitations/invitation";
@@ -17,12 +17,13 @@ import { InvitationProps, Role, SearchParamsProps } from "@/types";
export default async function Invitations({ export default async function Invitations({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<ContentLayout title="Invitations" icon="ci:users"> <ContentLayout title="Invitations" icon="lucide:mail">
<FilterControls search /> <FilterControls search />
<Spacer y={8} /> <Spacer y={8} />
<SendInvitationButton /> <SendInvitationButton />
@@ -31,7 +32,7 @@ export default async function Invitations({
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableInvitation />}> <Suspense key={searchParamsKey} fallback={<SkeletonTableInvitation />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -110,6 +111,7 @@ const SSRDataTable = async ({
return ( return (
<DataTable <DataTable
key={Date.now()}
columns={ColumnsInvitation} columns={ColumnsInvitation}
data={expandedResponse?.data || []} data={expandedResponse?.data || []}
metadata={invitationsData?.meta} metadata={invitationsData?.meta}

View File

@@ -45,7 +45,7 @@ export default async function RootLayout({
<body <body
suppressHydrationWarning suppressHydrationWarning
className={cn( className={cn(
"min-h-screen bg-background font-sans antialiased", "bg-background min-h-screen font-sans antialiased",
fontSans.variable, fontSans.variable,
)} )}
> >

View File

@@ -10,10 +10,7 @@ interface ProviderLayoutProps {
export default function ProviderLayout({ children }: ProviderLayoutProps) { export default function ProviderLayout({ children }: ProviderLayoutProps) {
return ( return (
<ContentLayout <ContentLayout title="Manage Groups" icon="lucide:group">
title="Manage Groups"
icon="solar:users-group-two-rounded-outline"
>
{children} {children}
</ContentLayout> </ContentLayout>
); );

View File

@@ -1,4 +1,5 @@
import { Divider, Spacer } from "@nextui-org/react"; import { Divider } from "@heroui/divider";
import { Spacer } from "@heroui/spacer";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
@@ -15,26 +16,27 @@ import { ColumnGroups } from "@/components/manage-groups/table";
import { DataTable } from "@/components/ui/table"; import { DataTable } from "@/components/ui/table";
import { ProviderProps, Role, SearchParamsProps } from "@/types"; import { ProviderProps, Role, SearchParamsProps } from "@/types";
export default function ManageGroupsPage({ export default async function ManageGroupsPage({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams); const resolvedSearchParams = await searchParams;
const providerGroupId = searchParams.groupId; const searchParamsKey = JSON.stringify(resolvedSearchParams);
const providerGroupId = resolvedSearchParams.groupId;
return ( return (
<div className="grid min-h-[70vh] grid-cols-1 items-center justify-center gap-4 md:grid-cols-12"> <div className="grid min-h-[70vh] grid-cols-1 items-center justify-center gap-4 md:grid-cols-12">
<div className="col-span-1 flex justify-end md:col-span-4"> <div className="col-span-1 flex justify-end md:col-span-4">
<Suspense key={searchParamsKey} fallback={<SkeletonManageGroups />}> <Suspense key={searchParamsKey} fallback={<SkeletonManageGroups />}>
{providerGroupId ? ( {providerGroupId ? (
<SSRDataEditGroup searchParams={searchParams} /> <SSRDataEditGroup searchParams={resolvedSearchParams} />
) : ( ) : (
<div className="flex flex-col"> <div className="flex flex-col">
<h1 className="mb-2 text-xl font-medium" id="getting-started"> <h1 className="mb-2 text-xl font-medium" id="getting-started">
Create a new provider group Create a new provider group
</h1> </h1>
<p className="mb-5 text-small text-default-500"> <p className="text-small text-default-500 mb-5">
Create a new provider group to manage the providers and roles. Create a new provider group to manage the providers and roles.
</p> </p>
<SSRAddGroupForm /> <SSRAddGroupForm />
@@ -50,7 +52,7 @@ export default function ManageGroupsPage({
<Spacer y={8} /> <Spacer y={8} />
<h3 className="mb-4 text-sm font-bold uppercase">Provider Groups</h3> <h3 className="mb-4 text-sm font-bold uppercase">Provider Groups</h3>
<Suspense key={searchParamsKey} fallback={<SkeletonManageGroups />}> <Suspense key={searchParamsKey} fallback={<SkeletonManageGroups />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</div> </div>
</div> </div>
@@ -143,7 +145,7 @@ const SSRDataEditGroup = async ({
<h1 className="mb-2 text-xl font-medium" id="getting-started"> <h1 className="mb-2 text-xl font-medium" id="getting-started">
Edit provider group Edit provider group
</h1> </h1>
<p className="mb-5 text-small text-default-500"> <p className="text-small text-default-500 mb-5">
Edit the provider group to manage the providers and roles. Edit the provider group to manage the providers and roles.
</p> </p>
<EditGroupForm <EditGroupForm
@@ -181,11 +183,15 @@ const SSRDataTable = async ({
filters, filters,
pageSize, pageSize,
}); });
return ( return (
<DataTable <>
columns={ColumnGroups} <DataTable
data={providerGroupsData?.data || []} key={`groups-${Date.now()}`}
metadata={providerGroupsData?.meta} columns={ColumnGroups}
/> data={providerGroupsData?.data || []}
metadata={providerGroupsData?.meta}
/>
</>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { getLatestFindings } from "@/actions/findings/findings"; import { getLatestFindings } from "@/actions/findings/findings";
@@ -37,14 +37,15 @@ function pickFilterParams(
); );
} }
export default function Home({ export default async function Home({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<ContentLayout title="Overview" icon="solar:pie-chart-2-outline"> <ContentLayout title="Overview" icon="lucide:square-chart-gantt">
<FilterControls providers mutedFindings showClearButton={false} /> <FilterControls providers mutedFindings showClearButton={false} />
<div className="grid grid-cols-12 gap-12 lg:gap-6"> <div className="grid grid-cols-12 gap-12 lg:gap-6">
@@ -56,13 +57,13 @@ export default function Home({
<div className="col-span-12 lg:col-span-4"> <div className="col-span-12 lg:col-span-4">
<Suspense fallback={<SkeletonFindingsBySeverityChart />}> <Suspense fallback={<SkeletonFindingsBySeverityChart />}>
<SSRFindingsBySeverity searchParams={searchParams} /> <SSRFindingsBySeverity searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</div> </div>
<div className="col-span-12 lg:col-span-4"> <div className="col-span-12 lg:col-span-4">
<Suspense fallback={<SkeletonFindingsByStatusChart />}> <Suspense fallback={<SkeletonFindingsByStatusChart />}>
<SSRFindingsByStatus searchParams={searchParams} /> <SSRFindingsByStatus searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</div> </div>
@@ -72,7 +73,7 @@ export default function Home({
key={searchParamsKey} key={searchParamsKey}
fallback={<SkeletonTableNewFindings />} fallback={<SkeletonTableNewFindings />}
> >
<SSRDataNewFindingsTable searchParams={searchParams} /> <SSRDataNewFindingsTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</div> </div>
</div> </div>
@@ -205,6 +206,7 @@ const SSRDataNewFindingsTable = async ({
<LighthouseBanner /> <LighthouseBanner />
<DataTable <DataTable
key={`dashboard-${Date.now()}`}
columns={ColumnNewFindingsToDate} columns={ColumnNewFindingsToDate}
data={expandedResponse?.data || []} data={expandedResponse?.data || []}
// metadata={findingsData?.meta} // metadata={findingsData?.meta}

View File

@@ -17,7 +17,7 @@ import {
export default async function Profile() { export default async function Profile() {
return ( return (
<ContentLayout title="User Profile" icon="ci:users"> <ContentLayout title="User Profile" icon="lucide:users">
<Suspense fallback={<SkeletonUserInfo />}> <Suspense fallback={<SkeletonUserInfo />}>
<SSRDataUser /> <SSRDataUser />
</Suspense> </Suspense>
@@ -96,10 +96,10 @@ const SSRDataUser = async () => {
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<UserBasicInfoCard user={userData} tenantId={userTenantId || ""} /> <UserBasicInfoCard user={userData} tenantId={userTenantId || ""} />
<div className="flex flex-col gap-6 xl:flex-row"> <div className="flex flex-col gap-6 xl:flex-row">
<div className="w-full lg:w-2/3 xl:w-1/2"> <div className="w-full">
<RolesCard roles={roleDetails} roleDetails={roleDetailsMap} /> <RolesCard roles={roleDetails} roleDetails={roleDetailsMap} />
</div> </div>
<div className="w-full lg:w-2/3 xl:w-1/2"> <div className="w-full">
<MembershipsCard <MembershipsCard
memberships={membershipsIncluded} memberships={membershipsIncluded}
tenantsMap={tenantsMap} tenantsMap={tenantsMap}
@@ -108,7 +108,7 @@ const SSRDataUser = async () => {
</div> </div>
</div> </div>
{hasManageIntegrations && ( {hasManageIntegrations && (
<div className="w-full pr-0 lg:w-2/3 xl:w-1/2 xl:pr-3"> <div className="w-full pr-0 xl:w-1/2 xl:pr-3">
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} /> <SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
</div> </div>
)} )}

View File

@@ -14,11 +14,12 @@ import { getProviderFormType } from "@/lib/provider-helpers";
import { ProviderType } from "@/types/providers"; import { ProviderType } from "@/types/providers";
interface Props { interface Props {
searchParams: { type: ProviderType; id: string; via?: string }; searchParams: Promise<{ type: ProviderType; id: string; via?: string }>;
} }
export default function AddCredentialsPage({ searchParams }: Props) { export default async function AddCredentialsPage({ searchParams }: Props) {
const { type: providerType, via } = searchParams; const resolvedSearchParams = await searchParams;
const { type: providerType, via } = resolvedSearchParams;
const formType = getProviderFormType(providerType, via); const formType = getProviderFormType(providerType, via);
switch (formType) { switch (formType) {
@@ -30,13 +31,13 @@ export default function AddCredentialsPage({ searchParams }: Props) {
return null; return null;
case "credentials": case "credentials":
return <AddViaCredentialsForm searchParams={searchParams} />; return <AddViaCredentialsForm searchParams={resolvedSearchParams} />;
case "role": case "role":
return <AddViaRoleForm searchParams={searchParams} />; return <AddViaRoleForm searchParams={resolvedSearchParams} />;
case "service-account": case "service-account":
return <AddViaServiceAccountForm searchParams={searchParams} />; return <AddViaServiceAccountForm searchParams={resolvedSearchParams} />;
default: default:
return null; return null;

View File

@@ -1,6 +1,6 @@
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import React from "react"; import React from "react";
import { WorkflowAddProvider } from "@/components/providers/workflow"; import { WorkflowAddProvider } from "@/components/providers/workflow";

View File

@@ -6,11 +6,12 @@ import { SkeletonProviderWorkflow } from "@/components/providers/workflow";
import { TestConnectionForm } from "@/components/providers/workflow/forms"; import { TestConnectionForm } from "@/components/providers/workflow/forms";
interface Props { interface Props {
searchParams: { type: string; id: string; updated: string }; searchParams: Promise<{ type: string; id: string; updated: string }>;
} }
export default async function TestConnectionPage({ searchParams }: Props) { export default async function TestConnectionPage({ searchParams }: Props) {
const providerId = searchParams.id; const resolvedSearchParams = await searchParams;
const providerId = resolvedSearchParams.id;
if (!providerId) { if (!providerId) {
redirect("/providers/connect-account"); redirect("/providers/connect-account");
@@ -18,7 +19,7 @@ export default async function TestConnectionPage({ searchParams }: Props) {
return ( return (
<Suspense fallback={<SkeletonProviderWorkflow />}> <Suspense fallback={<SkeletonProviderWorkflow />}>
<SSRTestConnection searchParams={searchParams} /> <SSRTestConnection searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
); );
} }

View File

@@ -10,16 +10,17 @@ import { getProviderFormType } from "@/lib/provider-helpers";
import { ProviderType } from "@/types/providers"; import { ProviderType } from "@/types/providers";
interface Props { interface Props {
searchParams: { searchParams: Promise<{
type: ProviderType; type: ProviderType;
id: string; id: string;
via?: string; via?: string;
secretId?: string; secretId?: string;
}; }>;
} }
export default function UpdateCredentialsPage({ searchParams }: Props) { export default async function UpdateCredentialsPage({ searchParams }: Props) {
const { type: providerType, via } = searchParams; const resolvedSearchParams = await searchParams;
const { type: providerType, via } = resolvedSearchParams;
const formType = getProviderFormType(providerType, via); const formType = getProviderFormType(providerType, via);
switch (formType) { switch (formType) {
@@ -29,13 +30,15 @@ export default function UpdateCredentialsPage({ searchParams }: Props) {
); );
case "credentials": case "credentials":
return <UpdateViaCredentialsForm searchParams={searchParams} />; return <UpdateViaCredentialsForm searchParams={resolvedSearchParams} />;
case "role": case "role":
return <UpdateViaRoleForm searchParams={searchParams} />; return <UpdateViaRoleForm searchParams={resolvedSearchParams} />;
case "service-account": case "service-account":
return <UpdateViaServiceAccountForm searchParams={searchParams} />; return (
<UpdateViaServiceAccountForm searchParams={resolvedSearchParams} />
);
default: default:
return null; return null;

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { getProviders } from "@/actions/providers"; import { getProviders } from "@/actions/providers";
@@ -19,12 +19,13 @@ import { ProviderProps, SearchParamsProps } from "@/types";
export default async function Providers({ export default async function Providers({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<ContentLayout title="Cloud Providers" icon="fluent:cloud-sync-24-regular"> <ContentLayout title="Cloud Providers" icon="lucide:cloud-cog">
<FilterControls search customFilters={filterProviders || []} /> <FilterControls search customFilters={filterProviders || []} />
<Spacer y={8} /> <Spacer y={8} />
<Suspense <Suspense
@@ -45,7 +46,7 @@ export default async function Providers({
</> </>
} }
> >
<ProvidersContent searchParams={searchParams} /> <ProvidersContent searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -106,6 +107,7 @@ const ProvidersContent = async ({
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div className="col-span-12"> <div className="col-span-12">
<DataTable <DataTable
key={`providers-${Date.now()}`}
columns={ColumnProviders} columns={ColumnProviders}
data={enrichedProviders || []} data={enrichedProviders || []}
metadata={providersData?.meta} metadata={providersData?.meta}

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { import {
@@ -24,14 +24,16 @@ import { ResourceProps, SearchParamsProps } from "@/types";
export default async function Resources({ export default async function Resources({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const { searchParamsKey, encodedSort } = extractSortAndKey(searchParams); const resolvedSearchParams = await searchParams;
const { filters, query } = extractFiltersAndQuery(searchParams); const { searchParamsKey, encodedSort } =
extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
const outputFilters = replaceFieldKey(filters, "inserted_at", "updated_at"); const outputFilters = replaceFieldKey(filters, "inserted_at", "updated_at");
// Check if the searchParams contain any date or scan filter // Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(searchParams); const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams);
const metadataInfoData = await ( const metadataInfoData = await (
hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo
@@ -47,7 +49,7 @@ export default async function Resources({
const uniqueResourceTypes = metadataInfoData?.data?.attributes?.types || []; const uniqueResourceTypes = metadataInfoData?.data?.attributes?.types || [];
return ( return (
<ContentLayout title="Resources" icon="carbon:data-view"> <ContentLayout title="Resources" icon="lucide:warehouse">
<FilterControls search date /> <FilterControls search date />
<DataTableFilterCustom <DataTableFilterCustom
filters={[ filters={[
@@ -71,7 +73,7 @@ export default async function Resources({
/> />
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableResources />}> <Suspense key={searchParamsKey} fallback={<SkeletonTableResources />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -140,12 +142,13 @@ const SSRDataTable = async ({
return ( return (
<> <>
{resourcesData?.errors && ( {resourcesData?.errors && (
<div className="mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-small text-red-700"> <div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
<p className="mr-2 font-semibold">Error:</p> <p className="mr-2 font-semibold">Error:</p>
<p>{resourcesData.errors[0].detail}</p> <p>{resourcesData.errors[0].detail}</p>
</div> </div>
)} )}
<DataTable <DataTable
key={`resources-${Date.now()}`}
columns={ColumnResources} columns={ColumnResources}
data={expandedResources || []} data={expandedResources || []}
metadata={resourcesData?.meta} metadata={resourcesData?.meta}

View File

@@ -10,13 +10,14 @@ import { SearchParamsProps } from "@/types";
export default async function EditRolePage({ export default async function EditRolePage({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<Suspense key={searchParamsKey} fallback={<SkeletonRoleForm />}> <Suspense key={searchParamsKey} fallback={<SkeletonRoleForm />}>
<SSRDataRole searchParams={searchParams} /> <SSRDataRole searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
); );
} }

View File

@@ -1,6 +1,6 @@
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import React from "react"; import React from "react";
import { WorkflowAddEditRole } from "@/components/roles/workflow"; import { WorkflowAddEditRole } from "@/components/roles/workflow";
@@ -19,7 +19,7 @@ export default function RoleLayout({ children }: RoleLayoutProps) {
href="/roles" href="/roles"
/> />
<Spacer y={16} /> <Spacer y={16} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12"> <div className="grid grid-cols-1 gap-8 px-4 sm:px-6 lg:grid-cols-12 lg:px-0">
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block"> <div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
<WorkflowAddEditRole /> <WorkflowAddEditRole />
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { getRoles } from "@/actions/roles"; import { getRoles } from "@/actions/roles";
@@ -14,12 +14,13 @@ import { SearchParamsProps } from "@/types";
export default async function Roles({ export default async function Roles({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<ContentLayout title="Roles" icon="mdi:account-key-outline"> <ContentLayout title="Roles" icon="lucide:user-cog">
<FilterControls search /> <FilterControls search />
<Spacer y={8} /> <Spacer y={8} />
<AddRoleButton /> <AddRoleButton />
@@ -28,7 +29,7 @@ export default async function Roles({
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableRoles />}> <Suspense key={searchParamsKey} fallback={<SkeletonTableRoles />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -55,6 +56,7 @@ const SSRDataTable = async ({
return ( return (
<DataTable <DataTable
key={`roles-${Date.now()}`}
columns={ColumnsRoles} columns={ColumnsRoles}
data={rolesData?.data || []} data={rolesData?.data || []}
metadata={rolesData?.meta} metadata={rolesData?.meta}

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { getProviders } from "@/actions/providers"; import { getProviders } from "@/actions/providers";
@@ -26,12 +26,12 @@ import { ProviderProps, ScanProps, SearchParamsProps } from "@/types";
export default async function Scans({ export default async function Scans({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const session = await auth(); const session = await auth();
const filteredParams = { ...searchParams }; const resolvedSearchParams = await searchParams;
const filteredParams = { ...resolvedSearchParams };
delete filteredParams.scanId; delete filteredParams.scanId;
const searchParamsKey = JSON.stringify(filteredParams);
const providersData = await getProviders({ const providersData = await getProviders({
pageSize: 50, pageSize: 50,
@@ -77,14 +77,14 @@ export default async function Scans({
if (thereIsNoProviders) { if (thereIsNoProviders) {
return ( return (
<ContentLayout title="Scans" icon="lucide:scan-search"> <ContentLayout title="Scans" icon="lucide:timer">
<NoProvidersAdded /> <NoProvidersAdded />
</ContentLayout> </ContentLayout>
); );
} }
return ( return (
<ContentLayout title="Scans" icon="lucide:scan-search"> <ContentLayout title="Scans" icon="lucide:timer">
<AutoRefresh hasExecutingScan={hasExecutingScan} /> <AutoRefresh hasExecutingScan={hasExecutingScan} />
<> <>
{!hasManageScansPermission ? ( {!hasManageScansPermission ? (
@@ -111,8 +111,8 @@ export default async function Scans({
<MutedFindingsConfigButton /> <MutedFindingsConfigButton />
</div> </div>
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}> <Suspense fallback={<SkeletonTableScans />}>
<SSRDataTableScans searchParams={searchParams} /> <SSRDataTableScans searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</> </>
</ContentLayout> </ContentLayout>
@@ -178,6 +178,7 @@ const SSRDataTableScans = async ({
return ( return (
<DataTable <DataTable
key={`scans-${Date.now()}`}
columns={ColumnGetScans} columns={ColumnGetScans}
data={expandedScansData || []} data={expandedScansData || []}
metadata={scansData?.meta} metadata={scansData?.meta}

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { FilterControls } from "@/components/filters"; import { FilterControls } from "@/components/filters";
import { ContentLayout } from "@/components/ui"; import { ContentLayout } from "@/components/ui";

View File

@@ -1,4 +1,4 @@
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { Suspense } from "react"; import { Suspense } from "react";
import { getUsers } from "@/actions/users/users"; import { getUsers } from "@/actions/users/users";
@@ -13,12 +13,13 @@ import { Role, SearchParamsProps, UserProps } from "@/types";
export default async function Users({ export default async function Users({
searchParams, searchParams,
}: { }: {
searchParams: SearchParamsProps; searchParams: Promise<SearchParamsProps>;
}) { }) {
const searchParamsKey = JSON.stringify(searchParams || {}); const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
return ( return (
<ContentLayout title="Users" icon="ci:users"> <ContentLayout title="Users" icon="lucide:user">
<FilterControls search /> <FilterControls search />
<Spacer y={8} /> <Spacer y={8} />
<AddUserButton /> <AddUserButton />
@@ -27,7 +28,7 @@ export default async function Users({
<Spacer y={8} /> <Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}> <Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}>
<SSRDataTable searchParams={searchParams} /> <SSRDataTable searchParams={resolvedSearchParams} />
</Suspense> </Suspense>
</ContentLayout> </ContentLayout>
); );
@@ -91,6 +92,7 @@ const SSRDataTable = async ({
return ( return (
<DataTable <DataTable
key={`scans-${Date.now()}`}
columns={ColumnsUser} columns={ColumnsUser}
data={expandedUsers || []} data={expandedUsers || []}
metadata={usersData?.meta} metadata={usersData?.meta}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { NextUIProvider } from "@nextui-org/system"; import { HeroUIProvider } from "@heroui/system";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from "next-themes";
@@ -17,9 +17,9 @@ export function Providers({ children, themeProps }: ProvidersProps) {
return ( return (
<SessionProvider> <SessionProvider>
<NextUIProvider navigate={router.push}> <HeroUIProvider navigate={router.push}>
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider> <NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
</NextUIProvider> </HeroUIProvider>
</SessionProvider> </SessionProvider>
); );
} }

View File

@@ -32,12 +32,13 @@ const refreshAccessToken = async (token: JwtPayload) => {
}, },
body: JSON.stringify(bodyData), body: JSON.stringify(bodyData),
}); });
const newTokens = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const newTokens = await response.json();
return { return {
...token, ...token,
accessToken: newTokens.data.attributes.access, accessToken: newTokens.data.attributes.access,

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { SwitchProps, useSwitch } from "@nextui-org/react"; import type { SwitchProps } from "@heroui/switch";
import { useSwitch } from "@heroui/switch";
import { useIsSSR } from "@react-aria/ssr"; import { useIsSSR } from "@react-aria/ssr";
import { VisuallyHidden } from "@react-aria/visually-hidden"; import { VisuallyHidden } from "@react-aria/visually-hidden";
import clsx from "clsx"; import clsx from "clsx";

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import { Button } from "@heroui/button";
import { Checkbox } from "@heroui/checkbox";
import { Divider } from "@heroui/divider";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { Button, Checkbox, Divider } from "@nextui-org/react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -190,9 +192,9 @@ export const AuthForm = ({
{/* Auth Form */} {/* Auth Form */}
<div className="relative flex w-full items-center justify-center lg:w-full"> <div className="relative flex w-full items-center justify-center lg:w-full">
{/* Background Pattern */} {/* Background Pattern */}
<div className="absolute h-full w-full bg-[radial-gradient(#6af400_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)]"></div> <div className="absolute h-full w-full bg-[radial-gradient(#6af400_1px,transparent_1px)] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)] bg-size-[16px_16px]"></div>
<div className="relative z-10 flex w-full max-w-sm flex-col gap-4 rounded-large border-1 border-divider bg-white/90 px-8 py-10 shadow-small dark:bg-background/85 md:max-w-md"> <div className="rounded-large border-divider shadow-small dark:bg-background/85 relative z-10 flex w-full max-w-sm flex-col gap-4 border bg-white/90 px-8 py-10 md:max-w-md">
{/* Prowler Logo */} {/* Prowler Logo */}
<div className="absolute -top-[100px] left-1/2 z-10 flex h-fit w-fit -translate-x-1/2"> <div className="absolute -top-[100px] left-1/2 z-10 flex h-fit w-fit -translate-x-1/2">
<ProwlerExtended width={300} /> <ProwlerExtended width={300} />
@@ -350,7 +352,7 @@ export const AuthForm = ({
<> <>
<div className="flex items-center gap-4 py-2"> <div className="flex items-center gap-4 py-2">
<Divider className="flex-1" /> <Divider className="flex-1" />
<p className="shrink-0 text-tiny text-default-500">OR</p> <p className="text-tiny text-default-500 shrink-0">OR</p>
<Divider className="flex-1" /> <Divider className="flex-1" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -387,7 +389,7 @@ export const AuthForm = ({
<> <>
<div className="flex items-center gap-4 py-2"> <div className="flex items-center gap-4 py-2">
<Divider className="flex-1" /> <Divider className="flex-1" />
<p className="shrink-0 text-tiny text-default-500">OR</p> <p className="text-tiny text-default-500 shrink-0">OR</p>
<Divider className="flex-1" /> <Divider className="flex-1" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -401,14 +403,14 @@ export const AuthForm = ({
</> </>
)} )}
{type === "sign-in" ? ( {type === "sign-in" ? (
<p className="text-center text-small"> <p className="text-small text-center">
Need to create an account?&nbsp; Need to create an account?&nbsp;
<CustomLink size="base" href="/sign-up" target="_self"> <CustomLink size="base" href="/sign-up" target="_self">
Sign up Sign up
</CustomLink> </CustomLink>
</p> </p>
) : ( ) : (
<p className="text-center text-small"> <p className="text-small text-center">
Already have an account?&nbsp; Already have an account?&nbsp;
<CustomLink size="base" href="/sign-in" target="_self"> <CustomLink size="base" href="/sign-in" target="_self">
Log in Log in

View File

@@ -69,25 +69,28 @@ export const PasswordRequirementsMessage = ({
{allRequirementsMet ? ( {allRequirementsMet ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle <CheckCircle
className="h-4 w-4 flex-shrink-0 text-system-success" className="text-system-success h-4 w-4 shrink-0"
aria-hidden="true" aria-hidden="true"
/> />
<p className="text-xs font-medium leading-tight text-system-success"> <p className="text-system-success text-xs leading-tight font-medium">
Password meets all requirements Password meets all requirements
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle <AlertCircle
className="h-4 w-4 flex-shrink-0 text-red-600" className="h-4 w-4 shrink-0 text-red-600"
aria-hidden="true" aria-hidden="true"
/> />
<p className="text-xs font-medium leading-tight text-red-700"> <p className="text-xs leading-tight font-medium text-red-700">
Password must include: Password must include:
</p> </p>
</div> </div>
<ul className="ml-6 space-y-0.5" aria-label="Password requirements"> <ul
className="ml-6 flex flex-col gap-0.5"
aria-label="Password requirements"
>
{results.map((req) => ( {results.map((req) => (
<li <li
key={req.key} key={req.key}
@@ -95,7 +98,7 @@ export const PasswordRequirementsMessage = ({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-2 w-2 flex-shrink-0 rounded-full ${ className={`h-2 w-2 shrink-0 rounded-full ${
req.isMet ? "bg-system-success" : "bg-red-400" req.isMet ? "bg-system-success" : "bg-red-400"
}`} }`}
aria-hidden="true" aria-hidden="true"

View File

@@ -1,5 +1,6 @@
import { Button } from "@heroui/button";
import { Tooltip } from "@heroui/tooltip";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { Button, Tooltip } from "@nextui-org/react";
import { CustomLink } from "@/components/ui/custom/custom-link"; import { CustomLink } from "@/components/ui/custom/custom-link";
@@ -24,7 +25,7 @@ export const SocialButtons = ({
</CustomLink> </CustomLink>
</div> </div>
} }
placement="right-start" placement="top"
shadow="sm" shadow="sm"
isDisabled={isGoogleOAuthEnabled} isDisabled={isGoogleOAuthEnabled}
className="w-96" className="w-96"
@@ -51,7 +52,7 @@ export const SocialButtons = ({
</CustomLink> </CustomLink>
</div> </div>
} }
placement="right-start" placement="top"
shadow="sm" shadow="sm"
isDisabled={isGithubOAuthEnabled} isDisabled={isGithubOAuthEnabled}
className="w-96" className="w-96"

View File

@@ -116,7 +116,7 @@ export const ClientAccordionContent = ({
return ( return (
<div className="w-full"> <div className="w-full">
{renderDetails()} {renderDetails()}
<p className="mb-1 mt-3 text-sm font-medium text-gray-800 dark:text-gray-200"> <p className="mt-3 mb-1 text-sm font-medium text-gray-800 dark:text-gray-200">
This requirement has no checks; therefore, there are no findings. This requirement has no checks; therefore, there are no findings.
</p> </p>
</div> </div>
@@ -127,7 +127,7 @@ export const ClientAccordionContent = ({
const checksList = ( const checksList = (
<div className="flex items-center px-2 text-sm"> <div className="flex items-center px-2 text-sm">
<div className="w-full flex-col"> <div className="w-full flex-col">
<div className="mb-1 mt-[-8px] h-1 w-full border-b border-gray-200 dark:border-gray-800" /> <div className="mt-[-8px] mb-1 h-1 w-full border-b border-gray-200 dark:border-gray-800" />
<span className="text-gray-600 dark:text-gray-200" aria-label="Checks"> <span className="text-gray-600 dark:text-gray-200" aria-label="Checks">
{checks.join(", ")} {checks.join(", ")}
</span> </span>
@@ -170,7 +170,7 @@ export const ClientAccordionContent = ({
} }
return ( return (
<div className="mb-1 mt-3 text-sm font-medium text-gray-800 dark:text-gray-200"> <div className="mt-3 mb-1 text-sm font-medium text-gray-800 dark:text-gray-200">
There are no findings for these regions There are no findings for these regions
</div> </div>
); );
@@ -186,7 +186,7 @@ export const ClientAccordionContent = ({
items={accordionChecksItems} items={accordionChecksItems}
variant="light" variant="light"
defaultExpandedKeys={[""]} defaultExpandedKeys={[""]}
className="rounded-lg bg-gray-50 dark:bg-prowler-blue-400" className="dark:bg-prowler-blue-400 rounded-lg bg-gray-50"
/> />
</div> </div>
)} )}

View File

@@ -1,4 +1,4 @@
import { Tooltip } from "@nextui-org/react"; import { Tooltip } from "@heroui/tooltip";
interface ComplianceAccordionTitleProps { interface ComplianceAccordionTitleProps {
label: string; label: string;
@@ -24,7 +24,7 @@ export const ComplianceAccordionTitle = ({
<div className="flex flex-col items-start justify-between gap-1 md:flex-row md:items-center md:gap-2"> <div className="flex flex-col items-start justify-between gap-1 md:flex-row md:items-center md:gap-2">
<div className="overflow-hidden md:min-w-0 md:flex-1"> <div className="overflow-hidden md:min-w-0 md:flex-1">
<span <span
className="block max-w-[600px] overflow-hidden truncate text-ellipsis text-sm" className="block max-w-[600px] truncate overflow-hidden text-sm text-ellipsis"
title={label} title={label}
> >
{label.charAt(0).toUpperCase() + label.slice(1)} {label.charAt(0).toUpperCase() + label.slice(1)}
@@ -33,7 +33,7 @@ export const ComplianceAccordionTitle = ({
<div className="mr-4 flex items-center gap-2"> <div className="mr-4 flex items-center gap-2">
<div className="hidden lg:block"> <div className="hidden lg:block">
{total > 0 && isParentLevel && ( {total > 0 && isParentLevel && (
<span className="whitespace-nowrap text-xs font-medium text-gray-600"> <span className="text-xs font-medium whitespace-nowrap text-gray-600">
Requirements: Requirements:
</span> </span>
)} )}
@@ -127,7 +127,7 @@ export const ComplianceAccordionTitle = ({
size="sm" size="sm"
placement="top" placement="top"
> >
<div className="min-w-[32px] text-center text-xs font-medium text-default-600"> <div className="text-default-600 min-w-[32px] text-center text-xs font-medium">
{total > 0 ? total : "—"} {total > 0 ? total : "—"}
</div> </div>
</Tooltip> </Tooltip>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { Card, CardBody, Progress } from "@nextui-org/react"; import { Card, CardBody } from "@heroui/card";
import { Progress } from "@heroui/progress";
import Image from "next/image"; import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -94,6 +95,11 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
); );
} }
const regionFilter = searchParams.get("filter[region__in]");
if (regionFilter) {
params.set("filter[region__in]", regionFilter);
}
router.push(`${path}?${params.toString()}`); router.push(`${path}?${params.toString()}`);
}; };
const handleDownload = async () => { const handleDownload = async () => {
@@ -106,22 +112,19 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
}; };
return ( return (
<Card <Card fullWidth isHoverable shadow="sm">
fullWidth <CardBody
isHoverable className="dark:bg-prowler-blue-800 flex cursor-pointer flex-row items-center justify-between gap-4"
shadow="sm" onClick={navigateToDetail}
isPressable >
onPress={navigateToDetail} <div className="flex w-full items-center gap-4">
>
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
<div className="flex w-full items-center space-x-4">
<Image <Image
src={getComplianceIcon(title)} src={getComplianceIcon(title)}
alt={`${title} logo`} alt={`${title} logo`}
className="h-10 w-10 min-w-10 rounded-md border-1 border-gray-300 bg-white object-contain p-1" className="h-10 w-10 min-w-10 rounded-md border border-gray-300 bg-white object-contain p-1"
/> />
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<h4 className="mb-1 text-small font-bold leading-5"> <h4 className="text-small mb-1 leading-5 font-bold">
{formatTitle(title)} {formatTitle(title)}
{version ? ` - ${version}` : ""} {version ? ` - ${version}` : ""}
</h4> </h4>
@@ -146,13 +149,24 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
Passing Requirements Passing Requirements
</small> </small>
<DownloadIconButton <div
paramId={complianceId} onClick={(e) => e.stopPropagation()}
onDownload={handleDownload} onKeyDown={(e) => {
textTooltip="Download compliance CSV report" if (e.key === "Enter" || e.key === " ") {
isDisabled={hasRegionFilter} e.stopPropagation();
isDownloading={isDownloading} }
/> }}
role="button"
tabIndex={0}
>
<DownloadIconButton
paramId={complianceId}
onDownload={handleDownload}
textTooltip="Download compliance CSV report"
isDisabled={hasRegionFilter}
isDownloading={isDownloading}
/>
</div>
{/* <small>{getScanChange()}</small> */} {/* <small>{getScanChange()}</small> */}
</div> </div>
</div> </div>

View File

@@ -43,7 +43,7 @@ interface FailedSectionsListProps {
} }
const title = ( const title = (
<h3 className="mb-2 whitespace-nowrap text-xs font-semibold uppercase tracking-wide"> <h3 className="mb-2 text-xs font-semibold tracking-wide whitespace-nowrap uppercase">
Top Failed Sections Top Failed Sections
</h3> </h3>
); );

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { cn } from "@nextui-org/react"; import { cn } from "@heroui/theme";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useState } from "react"; import { useState } from "react";
@@ -30,7 +30,7 @@ const capitalizeFirstLetter = (text: string): string => {
}; };
const title = ( const title = (
<h3 className="mb-2 whitespace-nowrap text-xs font-semibold uppercase tracking-wide"> <h3 className="mb-2 text-xs font-semibold tracking-wide whitespace-nowrap uppercase">
Sections Failure Rate Sections Failure Rate
</h3> </h3>
); );
@@ -135,13 +135,30 @@ export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => {
color: theme === "dark" ? "white" : "black", color: theme === "dark" ? "white" : "black",
}} }}
> >
<div className="mb-1 font-semibold"> <div
className="mb-1 font-semibold"
style={{ color: theme === "dark" ? "white" : "black" }}
>
{capitalizeFirstLetter(hoveredItem.name)} {capitalizeFirstLetter(hoveredItem.name)}
</div> </div>
<div>Failure Rate: {hoveredItem.failurePercentage}%</div>
<div> <div>
Failed: {hoveredItem.failedRequirements}/ <span
{hoveredItem.totalRequirements} style={{
color: getHeatmapColor(hoveredItem.failurePercentage),
}}
>
Failure Rate: {hoveredItem.failurePercentage}%
</span>
</div>
<div>
<span
style={{
color: getHeatmapColor(hoveredItem.failurePercentage),
}}
>
Failed: {hoveredItem.failedRequirements}/
{hoveredItem.totalRequirements}
</span>
</div> </div>
</div> </div>
)} )}

View File

@@ -113,7 +113,7 @@ export const PieChart = ({ pass, fail, manual }: PieChartProps) => {
return ( return (
<div className="flex h-[320px] flex-col items-center justify-between"> <div className="flex h-[320px] flex-col items-center justify-between">
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide"> <h3 className="text-xs font-semibold tracking-wide whitespace-nowrap uppercase">
Requirements Status Requirements Status
</h3> </h3>
@@ -176,15 +176,15 @@ export const PieChart = ({ pass, fail, manual }: PieChartProps) => {
<div className="mt-2 grid grid-cols-3 gap-4"> <div className="mt-2 grid grid-cols-3 gap-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-muted-foreground text-sm">Pass</div> <div className="text-muted-foreground text-sm">Pass</div>
<div className="font-semibold text-system-success-medium">{pass}</div> <div className="text-system-success-medium font-semibold">{pass}</div>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-muted-foreground text-sm">Fail</div> <div className="text-muted-foreground text-sm">Fail</div>
<div className="font-semibold text-system-error-medium">{fail}</div> <div className="text-system-error-medium font-semibold">{fail}</div>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-muted-foreground text-sm">Manual</div> <div className="text-muted-foreground text-sm">Manual</div>
<div className="font-semibold text-prowler-grey-light">{manual}</div> <div className="text-prowler-grey-light font-semibold">{manual}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -117,7 +117,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
{requirement.references && ( {requirement.references && (
<ComplianceDetailSection title="References"> <ComplianceDetailSection title="References">
<div className="space-y-1"> <div className="flex flex-col gap-1">
{processReferences(requirement.references).map( {processReferences(requirement.references).map(
(url: string, index: number) => ( (url: string, index: number) => (
<div key={index}> <div key={index}>

View File

@@ -71,9 +71,12 @@ export const MITRECustomDetails = ({
{cloudServices && cloudServices.length > 0 && ( {cloudServices && cloudServices.length > 0 && (
<ComplianceDetailSection title="Cloud Security Mappings"> <ComplianceDetailSection title="Cloud Security Mappings">
<div className="space-y-4"> <div className="flex flex-col gap-4">
{cloudServices.map((service, index) => ( {cloudServices.map((service, index) => (
<div key={index} className="space-y-3 rounded-lg border p-4"> <div
key={index}
className="flex flex-col gap-3 rounded-lg border p-4"
>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<ComplianceBadge <ComplianceBadge
label="Service" label="Service"

View File

@@ -5,7 +5,7 @@ export const ComplianceDetailContainer = ({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
return <div className="space-y-4">{children}</div>; return <div className="flex flex-col gap-4">{children}</div>;
}; };
export const ComplianceDetailSection = ({ export const ComplianceDetailSection = ({
@@ -107,7 +107,7 @@ export const ComplianceBulletList = ({
return ( return (
<ComplianceDetailSection title={title}> <ComplianceDetailSection title={title}>
<div className="space-y-2"> <div className="flex flex-col gap-2">
{items.map((item: string, index: number) => ( {items.map((item: string, index: number) => (
<div key={index} className="flex items-start gap-2"> <div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span> <span className="text-muted-foreground mt-1 text-xs"></span>
@@ -134,7 +134,7 @@ export const ComplianceChipContainer = ({
{items.map((item: string, index: number) => ( {items.map((item: string, index: number) => (
<span <span
key={index} key={index}
className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20" className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20"
> >
{item} {item}
</span> </span>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { FilterControls } from "@/components/filters"; import { FilterControls } from "@/components/filters";
import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom"; import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom";

View File

@@ -1,4 +1,5 @@
import { Divider, Tooltip } from "@nextui-org/react"; import { Divider } from "@heroui/divider";
import { Tooltip } from "@heroui/tooltip";
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
import { ProviderType } from "@/types"; import { ProviderType } from "@/types";
@@ -34,7 +35,7 @@ export const ComplianceScanInfo = ({ scan }: ComplianceScanInfoProps) => {
placement="top" placement="top"
size="sm" size="sm"
> >
<p className="text-xs text-default-500"> <p className="text-default-500 text-xs">
{scan.attributes.name || "- -"} {scan.attributes.name || "- -"}
</p> </p>
</Tooltip> </Tooltip>

View File

@@ -1,4 +1,4 @@
import { Select, SelectItem } from "@nextui-org/react"; import { Select, SelectItem } from "@heroui/select";
import { ProviderType, ScanProps } from "@/types"; import { ProviderType, ScanProps } from "@/types";

View File

@@ -9,7 +9,7 @@ export const NoScansAvailable = () => {
return ( return (
<div className="flex h-full min-h-[calc(100vh-56px)] items-center justify-center"> <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="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="dark:bg-prowler-blue-400 flex items-center justify-start rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700">
<div className="flex w-full items-center justify-between gap-6"> <div className="flex w-full items-center justify-between gap-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<InfoIcon className="mt-1 h-5 w-5 text-gray-400 dark:text-gray-300" /> <InfoIcon className="mt-1 h-5 w-5 text-gray-400 dark:text-gray-300" />
@@ -25,7 +25,7 @@ export const NoScansAvailable = () => {
</div> </div>
<CustomButton <CustomButton
asLink="/scans" asLink="/scans"
className="flex-shrink-0" className="shrink-0"
ariaLabel="Go to Scans page" ariaLabel="Go to Scans page"
variant="solid" variant="solid"
color="action" color="action"

View File

@@ -1,20 +1,20 @@
"use client"; "use client";
import { Skeleton } from "@nextui-org/react"; import { Skeleton } from "@heroui/skeleton";
export const BarChartSkeleton = () => { export const BarChartSkeleton = () => {
return ( return (
<div className="flex w-[400px] flex-col items-center justify-between"> <div className="flex w-[400px] flex-col items-center justify-between">
{/* Title skeleton */} {/* Title skeleton */}
<Skeleton className="h-4 w-40 rounded-lg"> <Skeleton className="h-4 w-40 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
{/* Chart area skeleton */} {/* Chart area skeleton */}
<div className="ml-24 flex h-full flex-col justify-center space-y-2 p-4"> <div className="ml-24 flex h-full flex-col justify-center gap-2 p-4">
{/* Bar chart skeleton - 5 horizontal bars */} {/* Bar chart skeleton - 5 horizontal bars */}
{Array.from({ length: 5 }).map((_, index) => ( {Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="flex items-center space-x-4"> <div key={index} className="flex items-center gap-4">
{/* Bar skeleton with varying widths */} {/* Bar skeleton with varying widths */}
<Skeleton <Skeleton
className={`h-10 rounded-lg ${ className={`h-10 rounded-lg ${
@@ -29,20 +29,20 @@ export const BarChartSkeleton = () => {
: "w-16" : "w-16"
}`} }`}
> >
<div className="h-6 bg-default-200" /> <div className="bg-default-200 h-6" />
</Skeleton> </Skeleton>
</div> </div>
))} ))}
{/* Legend skeleton */} {/* Legend skeleton */}
<div className="flex justify-center space-x-4 pt-2"> <div className="flex justify-center gap-4 pt-2">
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="flex items-center space-x-1"> <div key={index} className="flex items-center gap-1">
<Skeleton className="h-3 w-3 rounded-full"> <Skeleton className="h-3 w-3 rounded-full">
<div className="h-3 w-3 bg-default-200" /> <div className="bg-default-200 h-3 w-3" />
</Skeleton> </Skeleton>
<Skeleton className="h-3 w-16 rounded-lg"> <Skeleton className="h-3 w-16 rounded-lg">
<div className="h-3 bg-default-200" /> <div className="bg-default-200 h-3" />
</Skeleton> </Skeleton>
</div> </div>
))} ))}

View File

@@ -1,4 +1,4 @@
import { Skeleton } from "@nextui-org/react"; import { Skeleton } from "@heroui/skeleton";
import React from "react"; import React from "react";
interface SkeletonAccordionProps { interface SkeletonAccordionProps {
@@ -16,7 +16,7 @@ export const SkeletonAccordion = ({
return ( return (
<div <div
className={`w-full space-y-2 ${className} rounded-xl border border-gray-300 p-2 dark:border-gray-700`} className={`flex w-full flex-col gap-2 ${className} rounded-xl border border-gray-300 p-2 dark:border-gray-700`}
> >
{[...Array(itemCount)].map((_, index) => ( {[...Array(itemCount)].map((_, index) => (
<Skeleton key={index} className="rounded-lg"> <Skeleton key={index} className="rounded-lg">

View File

@@ -1,14 +1,15 @@
import { Card, Skeleton } from "@nextui-org/react"; import { Card } from "@heroui/card";
import { Skeleton } from "@heroui/skeleton";
import React from "react"; import React from "react";
export const ComplianceSkeletonGrid = () => { export const ComplianceSkeletonGrid = () => {
return ( return (
<Card className="h-fit w-full p-4"> <Card className="h-fit w-full p-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 3xl:grid-cols-4"> <div className="3xl:grid-cols-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
{[...Array(28)].map((_, index) => ( {[...Array(28)].map((_, index) => (
<div key={index} className="flex flex-col space-y-4"> <div key={index} className="flex flex-col gap-4">
<Skeleton className="h-28 rounded-lg"> <Skeleton className="h-28 rounded-lg">
<div className="h-full bg-default-300"></div> <div className="bg-default-300 h-full"></div>
</Skeleton> </Skeleton>
</div> </div>
))} ))}

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { Skeleton } from "@nextui-org/react"; import { Skeleton } from "@heroui/skeleton";
export const HeatmapChartSkeleton = () => { export const HeatmapChartSkeleton = () => {
return ( return (
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]"> <div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
{/* Title skeleton */} {/* Title skeleton */}
<Skeleton className="h-4 w-36 rounded-lg"> <Skeleton className="h-4 w-36 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
{/* Heatmap area skeleton - 3x3 grid like the real component */} {/* Heatmap area skeleton - 3x3 grid like the real component */}
@@ -18,7 +18,7 @@ export const HeatmapChartSkeleton = () => {
key={index} key={index}
className="flex items-center justify-center rounded border" className="flex items-center justify-center rounded border"
> >
<div className="h-full w-full bg-default-200" /> <div className="bg-default-200 h-full w-full" />
</Skeleton> </Skeleton>
))} ))}
</div> </div>

View File

@@ -1,32 +1,32 @@
"use client"; "use client";
import { Skeleton } from "@nextui-org/react"; import { Skeleton } from "@heroui/skeleton";
export const PieChartSkeleton = () => { export const PieChartSkeleton = () => {
return ( return (
<div className="flex h-[320px] flex-col items-center justify-between"> <div className="flex h-[320px] flex-col items-center justify-between">
{/* Title skeleton */} {/* Title skeleton */}
<Skeleton className="h-4 w-32 rounded-lg"> <Skeleton className="h-4 w-32 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
{/* Pie chart skeleton */} {/* Pie chart skeleton */}
<div className="relative flex aspect-square w-[200px] min-w-[200px] items-center justify-center"> <div className="relative flex aspect-square w-[200px] min-w-[200px] items-center justify-center">
{/* Outer circle */} {/* Outer circle */}
<Skeleton className="absolute h-[200px] w-[200px] rounded-full"> <Skeleton className="absolute h-[200px] w-[200px] rounded-full">
<div className="h-[200px] w-[200px] bg-default-200" /> <div className="bg-default-200 h-[200px] w-[200px]" />
</Skeleton> </Skeleton>
{/* Inner circle (donut hole) */} {/* Inner circle (donut hole) */}
<div className="absolute h-[140px] w-[140px] rounded-full bg-background"></div> <div className="bg-background absolute h-[140px] w-[140px] rounded-full"></div>
{/* Center text skeleton */} {/* Center text skeleton */}
<div className="absolute flex flex-col items-center"> <div className="absolute flex flex-col items-center">
<Skeleton className="h-6 w-8 rounded-lg"> <Skeleton className="h-6 w-8 rounded-lg">
<div className="h-6 bg-default-300" /> <div className="bg-default-300 h-6" />
</Skeleton> </Skeleton>
<Skeleton className="mt-1 h-3 w-6 rounded-lg"> <Skeleton className="mt-1 h-3 w-6 rounded-lg">
<div className="h-3 bg-default-300" /> <div className="bg-default-300 h-3" />
</Skeleton> </Skeleton>
</div> </div>
</div> </div>
@@ -35,26 +35,26 @@ export const PieChartSkeleton = () => {
<div className="mt-2 grid grid-cols-3 gap-4"> <div className="mt-2 grid grid-cols-3 gap-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Skeleton className="h-4 w-8 rounded-lg"> <Skeleton className="h-4 w-8 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
<Skeleton className="mt-1 h-5 w-6 rounded-lg"> <Skeleton className="mt-1 h-5 w-6 rounded-lg">
<div className="h-5 bg-default-200" /> <div className="bg-default-200 h-5" />
</Skeleton> </Skeleton>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Skeleton className="h-4 w-6 rounded-lg"> <Skeleton className="h-4 w-6 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
<Skeleton className="mt-1 h-5 w-6 rounded-lg"> <Skeleton className="mt-1 h-5 w-6 rounded-lg">
<div className="h-5 bg-default-200" /> <div className="bg-default-200 h-5" />
</Skeleton> </Skeleton>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Skeleton className="h-4 w-12 rounded-lg"> <Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200" /> <div className="bg-default-200 h-4" />
</Skeleton> </Skeleton>
<Skeleton className="mt-1 h-5 w-6 rounded-lg"> <Skeleton className="mt-1 h-5 w-6 rounded-lg">
<div className="h-5 bg-default-200" /> <div className="bg-default-200 h-5" />
</Skeleton> </Skeleton>
</div> </div>
</div> </div>

View File

@@ -53,7 +53,7 @@ export const FeedsDetail = () => {
<Icon size={18} /> <Icon size={18} />
{/* TODO: Update this condition once the RSS data response structure is finalized */} {/* TODO: Update this condition once the RSS data response structure is finalized */}
{feed.length > 0 && ( {feed.length > 0 && (
<span className="absolute right-0 top-0 h-2 w-2 rounded-full bg-red-500 dark:bg-gray-400"></span> <span className="absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500 dark:bg-gray-400"></span>
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -72,7 +72,7 @@ export const FeedsDetail = () => {
target="_blank" target="_blank"
className="flex flex-col" className="flex flex-col"
> >
<h3 className="text-small font-medium leading-none"> <h3 className="text-small leading-none font-medium">
{item.title} {item.title}
</h3> </h3>
<p className="text-sm text-gray-500">{item.description}</p> <p className="text-sm text-gray-500">{item.description}</p>

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { Select, SelectItem } from "@nextui-org/react"; import { Select, SelectItem } from "@heroui/select";
const accounts = [ const accounts = [
{ key: "audit-test-1", label: "740350143844" }, { key: "audit-test-1", label: "740350143844" },

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Checkbox } from "@nextui-org/react"; import { Checkbox } from "@heroui/checkbox";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useState } from "react"; import { useState } from "react";

View File

@@ -1,12 +1,13 @@
"use client"; "use client";
import { Button, ButtonGroup } from "@heroui/button";
import { DatePicker } from "@heroui/date-picker";
import { import {
getLocalTimeZone, getLocalTimeZone,
startOfMonth, startOfMonth,
startOfWeek, startOfWeek,
today, today,
} from "@internationalized/date"; } from "@internationalized/date";
import { Button, ButtonGroup, DatePicker } from "@nextui-org/react";
import { useLocale } from "@react-aria/i18n"; import { useLocale } from "@react-aria/i18n";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
@@ -66,10 +67,10 @@ export const CustomDatePicker = () => {
CalendarTopContent={ CalendarTopContent={
<ButtonGroup <ButtonGroup
fullWidth fullWidth
className="bg-content1 px-3 pb-2 pt-3 dark:bg-prowler-blue-400 [&>button]:border-default-200/60 [&>button]:text-default-500" className="bg-content1 dark:bg-prowler-blue-400 [&>button]:border-default-200/60 [&>button]:text-default-500 px-3 pt-3 pb-2"
radius="full" radius="full"
size="sm" size="sm"
variant="bordered" variant="flat"
> >
<Button onPress={() => handleDateChange(now)}>Today</Button> <Button onPress={() => handleDateChange(now)}>Today</Button>
<Button onPress={() => handleDateChange(nextWeek)}> <Button onPress={() => handleDateChange(nextWeek)}>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Select, SelectItem } from "@nextui-org/react"; import { Select, SelectItem } from "@heroui/select";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";

View File

@@ -1,8 +1,7 @@
import { Input } from "@nextui-org/react"; import { Input } from "@heroui/input";
import debounce from "lodash.debounce";
import { SearchIcon, XCircle } from "lucide-react"; import { SearchIcon, XCircle } from "lucide-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useUrlFilters } from "@/hooks/use-url-filters"; import { useUrlFilters } from "@/hooks/use-url-filters";
@@ -10,6 +9,7 @@ export const CustomSearchInput: React.FC = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { updateFilter } = useUrlFilters(); const { updateFilter } = useUrlFilters();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const applySearch = useCallback( const applySearch = useCallback(
(query: string) => { (query: string) => {
@@ -24,7 +24,12 @@ export const CustomSearchInput: React.FC = () => {
const debouncedChangeHandler = useCallback( const debouncedChangeHandler = useCallback(
(value: string) => { (value: string) => {
debounce((val) => applySearch(val), 300)(value); if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
debounceTimeoutRef.current = setTimeout(() => {
applySearch(value);
}, 300);
}, },
[applySearch], [applySearch],
); );
@@ -39,11 +44,19 @@ export const CustomSearchInput: React.FC = () => {
setSearchQuery(searchFromUrl); setSearchQuery(searchFromUrl);
}, [searchParams]); }, [searchParams]);
useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
};
}, []);
return ( return (
<Input <Input
variant="flat" variant="flat"
classNames={{ classNames={{
label: "tracking-tight font-light !text-default-600 text-sm !z-0 pb-1", label: "tracking-tight font-light !text-default-600 text-sm z-0! pb-1",
}} }}
aria-label="Search" aria-label="Search"
label="Search" label="Search"
@@ -59,7 +72,7 @@ export const CustomSearchInput: React.FC = () => {
endContent={ endContent={
searchQuery && ( searchQuery && (
<button onClick={clearIconSearch} className="focus:outline-none"> <button onClick={clearIconSearch} className="focus:outline-none">
<XCircle className="h-4 w-4 text-default-400" /> <XCircle className="text-default-400 h-4 w-4" />
</button> </button>
) )
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Select, SelectItem } from "@nextui-org/react"; import { Select, SelectItem } from "@heroui/select";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
@@ -85,7 +85,7 @@ export const CustomSelectProvider: React.FC = () => {
placeholder="Select a provider" placeholder="Select a provider"
classNames={{ classNames={{
selectorIcon: "right-2", selectorIcon: "right-2",
label: "!z-0 mb-2", label: "z-0! mb-2",
}} }}
label="Provider" label="Provider"
labelPlacement="inside" labelPlacement="inside"

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Spacer } from "@nextui-org/react"; import { Spacer } from "@heroui/spacer";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";

View File

@@ -1,4 +1,4 @@
import { Tooltip } from "@nextui-org/react"; import { Tooltip } from "@heroui/tooltip";
import { MutedIcon } from "../icons"; import { MutedIcon } from "../icons";
@@ -15,8 +15,8 @@ export const Muted = ({
return ( return (
<Tooltip content={mutedReason} className="text-xs"> <Tooltip content={mutedReason} className="text-xs">
<div className="w-fit rounded-full border border-system-severity-critical/40 p-1"> <div className="border-system-severity-critical/40 w-fit rounded-full border p-1">
<MutedIcon className="h-4 w-4 text-system-severity-critical" /> <MutedIcon className="text-system-severity-critical h-4 w-4" />
</div> </div>
</Tooltip> </Tooltip>
); );

View File

@@ -1,7 +1,9 @@
"use client"; "use client";
import { Input } from "@heroui/input";
import { Select, SelectItem } from "@heroui/select";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Input, Select, type Selection, SelectItem } from "@nextui-org/react"; import type { Selection } from "@react-types/shared";
import { Search, Send } from "lucide-react"; import { Search, Send } from "lucide-react";
import { import {
type Dispatch, type Dispatch,
@@ -222,7 +224,10 @@ export const SendToJiraModal = ({
} }
> >
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4"> <form
onSubmit={form.handleSubmit(handleSubmit)}
className="flex flex-col gap-4"
>
{/* Integration Selection */} {/* Integration Selection */}
{integrations.length > 1 && ( {integrations.length > 1 && (
<FormField <FormField
@@ -256,7 +261,7 @@ export const SendToJiraModal = ({
trigger: "min-h-12", trigger: "min-h-12",
popoverContent: "dark:bg-gray-800", popoverContent: "dark:bg-gray-800",
label: label:
"tracking-tight font-light !text-default-500 text-xs !z-0", "tracking-tight font-light !text-default-500 text-xs z-0!",
value: "text-default-500 text-small dark:text-gray-300", value: "text-default-500 text-small dark:text-gray-300",
}} }}
> >
@@ -277,7 +282,7 @@ export const SendToJiraModal = ({
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormMessage className="text-xs text-system-error" /> <FormMessage className="text-system-error text-xs" />
</> </>
)} )}
/> />
@@ -312,13 +317,13 @@ export const SendToJiraModal = ({
popoverContent: "dark:bg-gray-800", popoverContent: "dark:bg-gray-800",
listboxWrapper: "max-h-[300px] dark:bg-gray-800", listboxWrapper: "max-h-[300px] dark:bg-gray-800",
label: label:
"tracking-tight font-light !text-default-500 text-xs !z-0", "tracking-tight font-light !text-default-500 text-xs z-0!",
value: "text-default-500 text-small dark:text-gray-300", value: "text-default-500 text-small dark:text-gray-300",
}} }}
listboxProps={{ listboxProps={{
topContent: topContent:
filteredProjects.length > 5 ? ( filteredProjects.length > 5 ? (
<div className="sticky top-0 z-10 bg-content1 py-2 dark:bg-gray-800"> <div className="bg-content1 sticky top-0 z-10 py-2 dark:bg-gray-800">
<Input <Input
isClearable isClearable
placeholder="Search projects..." placeholder="Search projects..."
@@ -350,7 +355,7 @@ export const SendToJiraModal = ({
<span className="text-tiny text-default-500"> <span className="text-tiny text-default-500">
- -
</span> </span>
<span className="truncate text-small"> <span className="text-small truncate">
{name} {name}
</span> </span>
</div> </div>
@@ -361,7 +366,7 @@ export const SendToJiraModal = ({
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormMessage className="text-xs text-system-error" /> <FormMessage className="text-system-error text-xs" />
</> </>
)} )}
/> />
@@ -393,7 +398,7 @@ export const SendToJiraModal = ({
popoverContent: "dark:bg-gray-800", popoverContent: "dark:bg-gray-800",
listboxWrapper: "max-h-[300px] dark:bg-gray-800", listboxWrapper: "max-h-[300px] dark:bg-gray-800",
label: label:
"tracking-tight font-light !text-default-500 text-xs !z-0", "tracking-tight font-light !text-default-500 text-xs z-0!",
value: "text-default-500 text-small dark:text-gray-300", value: "text-default-500 text-small dark:text-gray-300",
}} }}
listboxProps={{ listboxProps={{

View File

@@ -109,18 +109,18 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
const { delta } = row.original.attributes; const { delta } = row.original.attributes;
return ( return (
<div className="relative flex max-w-[410px] flex-row items-center gap-2 3xl:max-w-[660px]"> <div className="3xl:max-w-[660px] relative flex max-w-[410px] flex-row items-center gap-2">
<div className="flex flex-row items-center gap-4"> <div className="flex flex-row items-center gap-4">
{delta === "new" || delta === "changed" ? ( {delta === "new" || delta === "changed" ? (
<DeltaIndicator delta={delta} /> <DeltaIndicator delta={delta} />
) : ( ) : (
<div className="w-2" /> <div className="w-2" />
)} )}
<p className="mr-7 whitespace-normal break-words text-sm"> <p className="mr-7 text-sm break-words whitespace-normal">
{checktitle} {checktitle}
</p> </p>
</div> </div>
<span className="absolute -right-2 top-1/2 -translate-y-1/2"> <span className="absolute top-1/2 -right-2 -translate-y-1/2">
<Muted isMuted={muted} mutedReason={muted_reason || ""} /> <Muted isMuted={muted} mutedReason={muted_reason || ""} />
</span> </span>
</div> </div>

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { Button } from "@heroui/button";
import { import {
Button,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownSection, DropdownSection,
DropdownTrigger, DropdownTrigger,
} from "@nextui-org/react"; } from "@heroui/dropdown";
import { Row } from "@tanstack/react-table"; import { Row } from "@tanstack/react-table";
import { useState } from "react"; import { useState } from "react";
@@ -38,7 +38,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
<div className="relative flex items-center justify-end gap-2"> <div className="relative flex items-center justify-end gap-2">
<Dropdown <Dropdown
className="shadow-xl dark:bg-prowler-blue-800" className="dark:bg-prowler-blue-800 shadow-xl"
placement="bottom" placement="bottom"
> >
<DropdownTrigger> <DropdownTrigger>
@@ -60,7 +60,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
startContent={ startContent={
<JiraIcon <JiraIcon
size={20} size={20}
className="pointer-events-none flex-shrink-0 text-default-500" className="text-default-500 pointer-events-none shrink-0"
/> />
} }
onPress={() => setIsJiraModalOpen(true)} onPress={() => setIsJiraModalOpen(true)}

View File

@@ -1,4 +1,4 @@
import { Tooltip } from "@nextui-org/react"; import { Tooltip } from "@heroui/tooltip";
import { CustomButton } from "@/components/ui/custom/custom-button"; import { CustomButton } from "@/components/ui/custom/custom-button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -22,7 +22,7 @@ export const DeltaIndicator = ({ delta }: DeltaIndicatorProps) => {
ariaLabel="Learn more about findings" ariaLabel="Learn more about findings"
color="transparent" color="transparent"
size="sm" size="sm"
className="h-auto min-w-0 p-0 text-primary" className="text-primary h-auto min-w-0 p-0"
asLink="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-8-analyze-the-findings" asLink="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-8-analyze-the-findings"
target="_blank" target="_blank"
> >

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Snippet } from "@nextui-org/react"; import { Snippet } from "@heroui/snippet";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
@@ -20,7 +20,7 @@ import { DeltaIndicator } from "./delta-indicator";
const MarkdownContainer = ({ children }: { children: string }) => { const MarkdownContainer = ({ children }: { children: string }) => {
return ( return (
<div className="prose prose-sm max-w-none whitespace-normal break-words dark:prose-invert"> <div className="prose prose-sm dark:prose-invert max-w-none break-words whitespace-normal">
<ReactMarkdown>{children}</ReactMarkdown> <ReactMarkdown>{children}</ReactMarkdown>
</div> </div>
); );
@@ -65,7 +65,7 @@ export const FindingDetail = ({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="line-clamp-2 text-lg font-medium leading-tight text-gray-800 dark:text-prowler-theme-pale/90"> <h2 className="dark:text-prowler-theme-pale/90 line-clamp-2 text-lg leading-tight font-medium text-gray-800">
{renderValue(attributes.check_metadata.checktitle)} {renderValue(attributes.check_metadata.checktitle)}
<CopyLinkButton url={url} /> <CopyLinkButton url={url} />
</h2> </h2>
@@ -82,7 +82,7 @@ export const FindingDetail = ({
? "bg-green-100 text-green-600" ? "bg-green-100 text-green-600"
: attributes.status === "MANUAL" : attributes.status === "MANUAL"
? "bg-gray-100 text-gray-600" ? "bg-gray-100 text-gray-600"
: "bg-red-100 text-system-severity-critical" : "text-system-severity-critical bg-red-100"
}`} }`}
> >
{renderValue(attributes.status)} {renderValue(attributes.status)}
@@ -159,7 +159,7 @@ export const FindingDetail = ({
{attributes.check_metadata.remediation && ( {attributes.check_metadata.remediation && (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h4 className="text-sm font-bold text-gray-700 dark:text-prowler-theme-pale/90"> <h4 className="dark:text-prowler-theme-pale/90 text-sm font-bold text-gray-700">
Remediation Details Remediation Details
</h4> </h4>
@@ -189,7 +189,7 @@ export const FindingDetail = ({
{attributes.check_metadata.remediation.code.cli && ( {attributes.check_metadata.remediation.code.cli && (
<InfoField label="CLI Command" variant="simple"> <InfoField label="CLI Command" variant="simple">
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800"> <Snippet className="bg-gray-50 py-1 dark:bg-slate-800">
<span className="whitespace-pre-line text-xs"> <span className="text-xs whitespace-pre-line">
{attributes.check_metadata.remediation.code.cli} {attributes.check_metadata.remediation.code.cli}
</span> </span>
</Snippet> </Snippet>
@@ -216,7 +216,7 @@ export const FindingDetail = ({
key={idx} key={idx}
href={link} href={link}
size="sm" size="sm"
className="!whitespace-normal break-all" className="break-all whitespace-normal!"
> >
{link} {link}
</CustomLink> </CustomLink>
@@ -237,7 +237,7 @@ export const FindingDetail = ({
<CustomSection title="Resource Details"> <CustomSection title="Resource Details">
<InfoField label="Resource ID" variant="simple"> <InfoField label="Resource ID" variant="simple">
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol> <Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol>
<span className="whitespace-pre-line text-xs"> <span className="text-xs whitespace-pre-line">
{renderValue(resource.uid)} {renderValue(resource.uid)}
</span> </span>
</Snippet> </Snippet>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { SettingsIcon } from "lucide-react"; import { SettingsIcon } from "lucide-react";
import { JiraIcon } from "@/components/icons/services/IconServices"; import { JiraIcon } from "@/components/icons/services/IconServices";
@@ -19,7 +19,7 @@ export const JiraIntegrationCard = () => {
Jira Jira
</h4> </h4>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="text-nowrap text-xs text-gray-500 dark:text-gray-300"> <p className="text-xs text-nowrap text-gray-500 dark:text-gray-300">
Create and manage security issues in Jira. Create and manage security issues in Jira.
</p> </p>
<CustomLink <CustomLink

View File

@@ -222,11 +222,11 @@ export const JiraIntegrationForm = ({
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col space-y-6" className="flex flex-col gap-6"
> >
<div className="flex flex-col space-y-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="flex items-center gap-2 text-sm text-default-500"> <p className="text-default-500 flex items-center gap-2 text-sm">
Need help configuring your Jira integration? Need help configuring your Jira integration?
</p> </p>
<CustomLink <CustomLink

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { format } from "date-fns"; import { format } from "date-fns";
import { PlusIcon, Trash2Icon } from "lucide-react"; import { PlusIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -214,7 +214,7 @@ export const JiraIntegrationsManager = ({
title="Delete Jira Integration" title="Delete Jira Integration"
description="This action cannot be undone. This will permanently delete your Jira integration." description="This action cannot be undone. This will permanently delete your Jira integration."
> >
<div className="flex w-full justify-center space-x-6"> <div className="flex w-full justify-center gap-6">
<CustomButton <CustomButton
type="button" type="button"
ariaLabel="Cancel" ariaLabel="Cancel"
@@ -265,7 +265,7 @@ export const JiraIntegrationsManager = ({
/> />
</CustomAlertModal> </CustomAlertModal>
<div className="space-y-6"> <div className="flex flex-col gap-6">
{/* Header with Add Button */} {/* Header with Add Button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { SettingsIcon } from "lucide-react"; import { SettingsIcon } from "lucide-react";
import { AmazonS3Icon } from "@/components/icons/services/IconServices"; import { AmazonS3Icon } from "@/components/icons/services/IconServices";
@@ -19,7 +19,7 @@ export const S3IntegrationCard = () => {
Amazon S3 Amazon S3
</h4> </h4>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="text-nowrap text-xs text-gray-500 dark:text-gray-300"> <p className="text-xs text-nowrap text-gray-500 dark:text-gray-300">
Export security findings to Amazon S3 buckets. Export security findings to Amazon S3 buckets.
</p> </p>
<CustomLink <CustomLink

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Divider } from "@heroui/divider";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Divider } from "@nextui-org/react";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useState } from "react"; import { useState } from "react";
@@ -275,7 +275,7 @@ export const S3IntegrationForm = ({
return ( return (
<> <>
{/* Provider Selection */} {/* Provider Selection */}
<div className="space-y-4"> <div className="flex flex-col gap-4">
<EnhancedProviderSelector <EnhancedProviderSelector
control={form.control} control={form.control}
name="providers" name="providers"
@@ -291,7 +291,7 @@ export const S3IntegrationForm = ({
<Divider /> <Divider />
{/* S3 Configuration */} {/* S3 Configuration */}
<div className="space-y-4"> <div className="flex flex-col gap-4">
<CustomInput <CustomInput
control={form.control} control={form.control}
name="bucket_name" name="bucket_name"
@@ -386,11 +386,11 @@ export const S3IntegrationForm = ({
? handleNext ? handleNext
: form.handleSubmit(onSubmit) : form.handleSubmit(onSubmit)
} }
className="flex flex-col space-y-6" className="flex flex-col gap-6"
> >
<div className="flex flex-col space-y-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="flex items-center gap-2 text-sm text-default-500"> <p className="text-default-500 flex items-center gap-2 text-sm">
Need help configuring your Amazon S3 integration? Need help configuring your Amazon S3 integration?
</p> </p>
<CustomLink <CustomLink

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { format } from "date-fns"; import { format } from "date-fns";
import { PlusIcon, Trash2Icon } from "lucide-react"; import { PlusIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -214,7 +214,7 @@ export const S3IntegrationsManager = ({
title="Delete S3 Integration" title="Delete S3 Integration"
description="This action cannot be undone. This will permanently delete your S3 integration." description="This action cannot be undone. This will permanently delete your S3 integration."
> >
<div className="flex w-full justify-center space-x-6"> <div className="flex w-full justify-center gap-6">
<CustomButton <CustomButton
type="button" type="button"
ariaLabel="Cancel" ariaLabel="Cancel"
@@ -271,7 +271,7 @@ export const S3IntegrationsManager = ({
/> />
</CustomAlertModal> </CustomAlertModal>
<div className="space-y-6"> <div className="flex flex-col gap-6">
{/* Header with Add Button */} {/* Header with Add Button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>

View File

@@ -1,7 +1,13 @@
"use client"; "use client";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import {
import { useFormState } from "react-dom"; Dispatch,
SetStateAction,
useActionState,
useEffect,
useRef,
useState,
} from "react";
import { z } from "zod"; import { z } from "zod";
import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
@@ -110,7 +116,7 @@ export const SamlConfigForm = ({
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
samlConfig?: any; samlConfig?: any;
}) => { }) => {
const [state, formAction, isPending] = useFormState( const [state, formAction, isPending] = useActionState(
samlConfig?.id ? updateSamlConfig : createSamlConfig, samlConfig?.id ? updateSamlConfig : createSamlConfig,
null, null,
); );
@@ -251,7 +257,7 @@ export const SamlConfigForm = ({
: `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`; : `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`;
return ( return (
<form ref={formRef} action={formAction} className="flex flex-col space-y-2"> <form ref={formRef} action={formAction} className="flex flex-col gap-2">
<div className="py-1 text-xs"> <div className="py-1 text-xs">
Need help configuring SAML SSO?{" "} Need help configuring SAML SSO?{" "}
<CustomLink <CustomLink
@@ -287,14 +293,14 @@ export const SamlConfigForm = ({
}} }}
/> />
<div className="space-y-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800"> <div className="flex flex-col gap-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">
Identity Provider Configuration Identity Provider Configuration
</h3> </h3>
<div className="space-y-4"> <div className="flex flex-col gap-4">
<div> <div>
<span className="mb-2 block text-sm font-medium text-default-500"> <span className="text-default-500 mb-2 block text-sm font-medium">
ACS URL: ACS URL:
</span> </span>
<SnippetChip <SnippetChip
@@ -305,7 +311,7 @@ export const SamlConfigForm = ({
</div> </div>
<div> <div>
<span className="mb-2 block text-sm font-medium text-default-500"> <span className="text-default-500 mb-2 block text-sm font-medium">
Audience: Audience:
</span> </span>
<SnippetChip <SnippetChip
@@ -316,19 +322,19 @@ export const SamlConfigForm = ({
</div> </div>
<div> <div>
<span className="mb-2 block text-sm font-medium text-default-500"> <span className="text-default-500 mb-2 block text-sm font-medium">
Name ID Format: Name ID Format:
</span> </span>
<span className="w-full text-sm text-default-600"> <span className="text-default-600 w-full text-sm">
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
</span> </span>
</div> </div>
<div> <div>
<span className="mb-2 block text-sm font-medium text-default-500"> <span className="text-default-500 mb-2 block text-sm font-medium">
Supported Assertion Attributes: Supported Assertion Attributes:
</span> </span>
<ul className="ml-4 space-y-1 text-sm text-default-600"> <ul className="text-default-600 ml-4 flex flex-col gap-1 text-sm">
<li> firstName</li> <li> firstName</li>
<li> lastName</li> <li> lastName</li>
<li> userType</li> <li> userType</li>
@@ -347,8 +353,8 @@ export const SamlConfigForm = ({
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-col items-start space-y-2"> <div className="flex flex-col items-start gap-2">
<span className="text-xs text-default-500"> <span className="text-default-500 text-xs">
Metadata XML File <span className="text-red-500">*</span> Metadata XML File <span className="text-red-500">*</span>
</span> </span>
<CustomButton <CustomButton
@@ -364,7 +370,7 @@ export const SamlConfigForm = ({
} }
}} }}
startContent={<AddIcon size={20} />} startContent={<AddIcon size={20} />}
className={`h-10 justify-start rounded-medium border-2 text-default-500 ${ className={`rounded-medium text-default-500 h-10 justify-start border-2 ${
( (
clientErrors.metadata_xml === null clientErrors.metadata_xml === null
? undefined ? undefined
@@ -378,7 +384,7 @@ export const SamlConfigForm = ({
> >
<span className="text-small"> <span className="text-small">
{uploadedFile ? ( {uploadedFile ? (
<span className="flex items-center space-x-2"> <span className="flex items-center gap-2">
<span className="max-w-36 truncate">{uploadedFile.name}</span> <span className="max-w-36 truncate">{uploadedFile.name}</span>
</span> </span>
) : ( ) : (

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { CheckIcon, Trash2Icon } from "lucide-react"; import { CheckIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Card, CardBody, CardHeader } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { SettingsIcon } from "lucide-react"; import { SettingsIcon } from "lucide-react";
import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices"; import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices";
@@ -19,7 +19,7 @@ export const SecurityHubIntegrationCard = () => {
AWS Security Hub AWS Security Hub
</h4> </h4>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="text-nowrap text-xs text-gray-500 dark:text-gray-300"> <p className="text-xs text-nowrap text-gray-500 dark:text-gray-300">
Send security findings to AWS Security Hub. Send security findings to AWS Security Hub.
</p> </p>
<CustomLink <CustomLink

View File

@@ -1,7 +1,9 @@
"use client"; "use client";
import { Checkbox } from "@heroui/checkbox";
import { Divider } from "@heroui/divider";
import { Radio, RadioGroup } from "@heroui/radio";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Checkbox, Divider, Radio, RadioGroup } from "@nextui-org/react";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -250,7 +252,7 @@ export const SecurityHubIntegrationForm = ({
if (isEditingCredentials) { if (isEditingCredentials) {
// When editing credentials, show the credential type selector first // When editing credentials, show the credential type selector first
return ( return (
<div className="space-y-4"> <div className="flex flex-col gap-4">
<RadioGroup <RadioGroup
size="sm" size="sm"
aria-label="Credential type" aria-label="Credential type"
@@ -319,7 +321,7 @@ export const SecurityHubIntegrationForm = ({
<> <>
{!isEditingConfig && ( {!isEditingConfig && (
<> <>
<div className="space-y-4"> <div className="flex flex-col gap-4">
<EnhancedProviderSelector <EnhancedProviderSelector
control={form.control} control={form.control}
name="provider_id" name="provider_id"
@@ -473,11 +475,11 @@ export const SecurityHubIntegrationForm = ({
? handleNext ? handleNext
: form.handleSubmit(onSubmit) : form.handleSubmit(onSubmit)
} }
className="flex flex-col space-y-6" className="flex flex-col gap-6"
> >
<div className="flex flex-col space-y-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="flex items-center gap-2 text-sm text-default-500"> <p className="text-default-500 flex items-center gap-2 text-sm">
Need help configuring your AWS Security Hub integration? Need help configuring your AWS Security Hub integration?
</p> </p>
<CustomLink <CustomLink

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { Card, CardBody, CardHeader, Chip } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { Chip } from "@heroui/chip";
import { format } from "date-fns"; import { format } from "date-fns";
import { PlusIcon, Trash2Icon } from "lucide-react"; import { PlusIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -263,7 +264,7 @@ export const SecurityHubIntegrationsManager = ({
title="Delete Security Hub Integration" title="Delete Security Hub Integration"
description="This action cannot be undone. This will permanently delete your Security Hub integration." description="This action cannot be undone. This will permanently delete your Security Hub integration."
> >
<div className="flex w-full justify-center space-x-6"> <div className="flex w-full justify-center gap-6">
<CustomButton <CustomButton
type="button" type="button"
ariaLabel="Cancel" ariaLabel="Cancel"
@@ -321,7 +322,7 @@ export const SecurityHubIntegrationsManager = ({
/> />
</CustomAlertModal> </CustomAlertModal>
<div className="space-y-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Chip } from "@nextui-org/react"; import { Chip } from "@heroui/chip";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { ReactNode } from "react"; import { ReactNode } from "react";

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { Skeleton } from "@heroui/skeleton";
import { ReactNode } from "react"; import { ReactNode } from "react";
interface IntegrationSkeletonProps { interface IntegrationSkeletonProps {
@@ -75,7 +76,7 @@ export const IntegrationSkeleton = ({
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
<Skeleton className="h-6 w-16 rounded-full" /> <Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" /> <Skeleton className="h-6 w-20 rounded-full" />
<Skeleton className="w-18 h-6 rounded-full" /> <Skeleton className="h-6 w-18 rounded-full" />
</div> </div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<Skeleton className="h-3 w-32 rounded" /> <Skeleton className="h-3 w-32 rounded" />

View File

@@ -62,7 +62,7 @@ export const DeleteForm = ({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmitClient)}> <form onSubmit={form.handleSubmit(onSubmitClient)}>
<input type="hidden" name="id" value={invitationId} /> <input type="hidden" name="id" value={invitationId} />
<div className="flex w-full justify-center sm:space-x-6"> <div className="flex w-full justify-center sm:gap-6">
<CustomButton <CustomButton
type="button" type="button"
ariaLabel="Cancel" ariaLabel="Cancel"

Some files were not shown because too many files have changed in this diff Show More