diff --git a/apps/app/public/onboarding/cloud-management.webp b/apps/app/public/onboarding/cloud-management.webp new file mode 100644 index 0000000000..76cccee542 Binary files /dev/null and b/apps/app/public/onboarding/cloud-management.webp differ diff --git a/apps/app/public/onboarding/cloud-tests.png b/apps/app/public/onboarding/cloud-tests.png deleted file mode 100644 index 9bf69f8789..0000000000 Binary files a/apps/app/public/onboarding/cloud-tests.png and /dev/null differ diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/layout.tsx deleted file mode 100644 index 0562083cc4..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { auth } from "@comp/auth"; -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - - const organization = await auth.api.getFullOrganization({ - headers: await headers(), - }); - - if (!organization) { - redirect("/"); - } - - const organizationId = organization.id; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/loading.tsx new file mode 100644 index 0000000000..9e020d05a9 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/loading.tsx @@ -0,0 +1,63 @@ +import { Button } from "@comp/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@comp/ui/card"; +import { Input } from "@comp/ui/input"; +import { getI18n } from "@/locales/server"; + +export default async function Loading() { + const t = await getI18n(); + + return ( +
+ + + {t("settings.general.org_name")} + + {t("settings.general.org_name_description")} + + + + + + +
{t("settings.general.org_name_tip")}
+ +
+
+ + + + {t("settings.general.org_website")} + + {t("settings.general.org_website_description")} + + + + + + +
{t("settings.general.org_website_tip")}
+ +
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/page.tsx index 20a1b02893..6b569d8ea7 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/(overview)/page.tsx @@ -1,123 +1,49 @@ -import { cache } from "react"; -import { auth } from "@comp/auth"; -import { TestsSeverity } from "@/components/tests/charts/tests-severity"; -import { TestsByAssignee } from "@/components/tests/charts/tests-by-assignee"; +import { AppOnboarding } from "@/components/app-onboarding"; import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; -import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; -import { redirect } from "next/navigation"; -import { headers } from "next/headers"; -export default async function TestsOverview({ - params, +export default async function CloudTests({ + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }) { - const { locale } = await params; - setStaticParamsLocale(locale); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session?.session?.activeOrganizationId) { - redirect("/onboarding"); - } - - const overview = await getTestsOverview(session.session.activeOrganizationId); - - if (overview?.totalTests === 0) { - redirect(`/${session.session.activeOrganizationId}/tests/all`); - } - - return ( -
-
- - -
-
- ); + const { locale } = await params; + const t = await getI18n(); + setStaticParamsLocale(locale); + + return ( +
+
+ +
+
+ ); } -const getTestsOverview = cache(async (organizationId: string) => { - return await db.$transaction(async (tx) => { - const [ - totalTests, - infoSeverityTests, - lowSeverityTests, - mediumSeverityTests, - highSeverityTests, - criticalSeverityTests, - ] = await Promise.all([ - tx.integrationResult.count({ - where: { - organizationId, - }, - }), - tx.integrationResult.count({ - where: { - organizationId, - severity: "INFO", - }, - }), - tx.integrationResult.count({ - where: { - organizationId, - severity: "LOW", - }, - }), - tx.integrationResult.count({ - where: { - organizationId, - severity: "MEDIUM", - }, - }), - tx.integrationResult.count({ - where: { - organizationId, - severity: "HIGH", - }, - }), - tx.integrationResult.count({ - where: { - organizationId, - severity: "CRITICAL", - }, - }), - ]); - return { - totalTests, - infoSeverityTests, - lowSeverityTests, - mediumSeverityTests, - highSeverityTests, - criticalSeverityTests, - }; - }); -}); - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("sidebar.tests"), - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/assignTest.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/assignTest.ts deleted file mode 100644 index b3c26eac18..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/assignTest.ts +++ /dev/null @@ -1,90 +0,0 @@ -"use server"; - -import { authActionClient } from "@/actions/safe-action"; -import { db } from "@comp/db"; -import { Role } from "@prisma/client"; -import { z } from "zod"; - -export const assignTest = authActionClient - .schema( - z.object({ - id: z.string(), - assigneeId: z.string().nullable(), - }), - ) - .metadata({ - name: "assignTest", - track: { - event: "assign-test", - channel: "server", - }, - }) - .action(async ({ ctx, parsedInput }) => { - const { session } = ctx; - const { id, assigneeId } = parsedInput; - - if (!session.activeOrganizationId) { - return { - success: false, - error: "Not authorized - no organization found", - }; - } - - try { - // Verify the evidence exists and belongs to the organization - const test = await db.integrationResult.findFirst({ - where: { - id, - organizationId: session.activeOrganizationId, - }, - }); - - if (!test) { - return { - success: false, - error: "test not found", - }; - } - - // If assigneeId is provided, verify the user exists and has admin privileges - if (assigneeId) { - const adminMember = await db.member.findFirst({ - where: { - userId: assigneeId, - organizationId: session.activeOrganizationId, - role: { - in: [Role.admin], - }, - }, - }); - - if (!adminMember) { - return { - success: false, - error: "User not found or does not have admin privileges", - }; - } - } - - // Update the evidence with the new assignee - const updatedTest = await db.integrationResult.update({ - where: { - id, - }, - data: { - assignedUserId: assigneeId, - }, - }); - - return { - success: true, - data: updatedTest, - }; - } catch (error) { - console.error("Error assigning test:", error); - return { - success: false, - error: "Failed to assign Cloud Test", - }; - } - }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/getTest.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/getTest.ts deleted file mode 100644 index 3d43b1a569..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/getTest.ts +++ /dev/null @@ -1,72 +0,0 @@ -"use server"; - -import { auth } from "@comp/auth"; -import { db } from "@comp/db"; -import { appErrors, type ActionResponse } from "./types"; - -import type { Test } from "../../types"; -import { headers } from "next/headers"; - -export async function getTest(input: { - testId: string; -}): Promise> { - const { testId } = input; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - throw new Error("Organization ID not found"); - } - - try { - const results = await db.integrationResult.findUnique({ - where: { - id: testId, - organizationId: organizationId, - }, - include: { - integration: true, - }, - }); - - if (!results) { - return { - success: false, - error: appErrors.NOT_FOUND, - }; - } - - const integrationResult = results; - - // Format the result to match the expected Test structure - const result: Test = { - id: integrationResult.id, - title: integrationResult.title || "", - description: integrationResult.description || "", - remediation: integrationResult.remediation || "", - provider: integrationResult.integration.name, - status: integrationResult.status || "", - resultDetails: integrationResult.resultDetails, - severity: integrationResult.severity || "", - assignedUserId: integrationResult.assignedUserId || "", - organizationId: organizationId, - completedAt: integrationResult.completedAt || new Date(), - organizationIntegrationId: integrationResult.integrationId || "", - }; - - return { - success: true, - data: result, - }; - } catch (error) { - console.error("Error fetching integration result details:", error); - return { - success: false, - error: appErrors.UNEXPECTED_ERROR, - }; - } -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/types.ts deleted file mode 100644 index 19e19008dc..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { z } from "zod"; - -export const createTestCommentSchema = z.object({ - testId: z.string().min(1, { - message: "Test ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), -}); - -// Define the app errors -export const appErrors = { - NOT_FOUND: { message: "Cloud test not found" }, - UNEXPECTED_ERROR: { message: "An unexpected error occurred" } -}; - -export type AppError = typeof appErrors[keyof typeof appErrors]; - -// Define the input schema -export const cloudTestDetailsInputSchema = z.object({ - testId: z.string() -}); - -// Type-safe action response -export type ActionResponse = Promise< - { success: true; data: T } | { success: false; error: AppError } ->; \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/AssigneeSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/AssigneeSection.tsx deleted file mode 100644 index ea8468651d..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/AssigneeSection.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@comp/ui/select"; -import { Avatar, AvatarFallback, AvatarImage } from "@comp/ui/avatar"; -import { useAction } from "next-safe-action/hooks"; -import { toast } from "sonner"; -import { assignTest } from "../actions/assignTest"; -import { - type Admin, - useOrganizationAdmins, -} from "@/app/[locale]/(app)/(dashboard)/[orgId]/hooks/useOrganizationAdmins"; - -interface AssigneeSectionProps { - testId: string; - currentAssigneeId: string | null | undefined; - onSuccess: () => Promise; -} - -export function AssigneeSection({ - testId, - currentAssigneeId, - onSuccess, -}: AssigneeSectionProps) { - const [assigneeId, setAssigneeId] = useState( - currentAssigneeId || null, - ); - const { data: admins, isLoading, error } = useOrganizationAdmins(); - const [selectedAdmin, setSelectedAdmin] = useState(null); - - const { execute: updateAssignee, isExecuting } = useAction(assignTest, { - onSuccess: async () => { - toast.success("Assignee updated successfully"); - await onSuccess(); - }, - onError: (error) => { - console.log("error", error); - toast.error("Failed to update Assignee"); - }, - }); - - useEffect(() => { - setAssigneeId(currentAssigneeId || null); - }, [currentAssigneeId]); - - useEffect(() => { - if (admins && assigneeId) { - const admin = admins.find((a) => a.id === assigneeId); - setSelectedAdmin(admin || null); - } else { - setSelectedAdmin(null); - } - }, [admins, assigneeId]); - - const handleAssigneeChange = (value: string) => { - const newAssigneeId = value === "none" ? null : value; - setAssigneeId(newAssigneeId); - - if (newAssigneeId && admins) { - const admin = admins.find((a) => a.id === newAssigneeId); - setSelectedAdmin(admin || null); - } else { - setSelectedAdmin(null); - } - - updateAssignee({ id: testId, assigneeId: newAssigneeId }); - }; - - if (isLoading) { - return
; - } - - if (error || !admins) { - return

Failed to load

; - } - - return ( -
- -
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/TestDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/TestDetails.tsx deleted file mode 100644 index 5c5e054ba9..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/components/TestDetails.tsx +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; - -import { useI18n } from "@/locales/client"; -import type { User } from "@comp/db/types"; -import { Alert, AlertDescription, AlertTitle } from "@comp/ui/alert"; -import { Badge } from "@comp/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; -import { Label } from "@comp/ui/label"; -import { Skeleton } from "@comp/ui/skeleton"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@comp/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@comp/ui/tabs"; -import { AlertCircle, User as UserIcon } from "lucide-react"; -import { useTest } from "../../hooks/useTest"; -import { AssigneeSection } from "./AssigneeSection"; - -interface CloudTestDetailsProps { - testId: string; - users: User[]; -} - -export function TestDetails({ testId, users }: CloudTestDetailsProps) { - const t = useI18n(); - const { cloudTest, isLoading, error, mutate } = useTest(testId); - - if (error) { - return ( -
- - - Error - - {error.message || "An unexpected error occurred"} - - -
- ); - } - - if (isLoading) { - return ( -
-
- - -
-
- - - - - -
- - -
-
-
-
-
- ); - } - - if (!cloudTest) return null; - // Format the test provider for display - const providerLabel = - cloudTest.provider === "aws" - ? "Amazon Web Services" - : cloudTest.provider === "azure" - ? "Microsoft Azure" - : "Google Cloud Platform"; - - // Format the test status for display with appropriate badge color - const getStatusBadge = (status: string) => { - switch (status.toUpperCase()) { - case "PASSED": - return {status}; - case "IN_PROGRESS": - return {status}; - case "FAILED": - return {status}; - default: - return {status}; - } - }; - - // Check if resources exist and have items - const hasResources = - cloudTest.resultDetails?.Resources && - cloudTest.resultDetails.Resources.length > 0; - // Set default tab based on resources availability - const defaultTab = hasResources ? "resources" : "raw-log"; - - // // Helper function to get the appropriate icon for test run status - // const getRunStatusIcon = (status: string, result: string | null) => { - // if (status === "COMPLETED") { - // if (result === "PASS") { - // return ; - // } else if (result === "FAIL") { - // return ; - // } else { - // return ; - // } - // } else if (status === "IN_PROGRESS") { - // return ; - // } else if (status === "PENDING") { - // return ; - // } else { - // return ; - // } - // }; - - return ( -
-
-

- {cloudTest.title} {getStatusBadge(cloudTest.status)} -

-
- - {providerLabel} - -
-
- - {cloudTest.description && ( -

{cloudTest.description}

- )} - - - - Concerns - - -

{cloudTest.description}

-
- -

- ASSIGNEE -

-
- mutate() as Promise} - /> -
-
- - - - Remediation - - -

{cloudTest.remediation}

-
-
- - - - {hasResources && ( - Resources - )} - Raw Log - - - {hasResources && ( - - - - Resources - - - - - - Id - Type - Region - Partition - - - - {cloudTest.resultDetails?.Resources.map((resource: any) => ( - - -
- {resource.Id} -
-
- -
- {resource.Type} -
-
- -
- {resource.Region} -
-
- -
- {resource.Partition} -
-
-
- ))} -
-
-
-
-
- )} - - - - - Test Results - - -
-
- -
-										{JSON.stringify(cloudTest.resultDetails, null, 2)}
-									
-
-
-
-
-
-
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx deleted file mode 100644 index e91993e7c1..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; -import type { Metadata } from "next"; -import { setStaticParamsLocale } from "next-international/server"; -import { redirect } from "next/navigation"; -import { TestDetails } from "./components/TestDetails"; -import { headers } from "next/headers"; - -export default async function TestDetailsPage({ - params, -}: { - params: Promise<{ locale: string; testId: string }>; -}) { - const { locale, testId } = await params; - setStaticParamsLocale(locale); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - redirect("/"); - } - - const users = await getUsers(organizationId); - - return ; -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string; testId: string }>; -}): Promise { - const { locale } = await params; - - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("tests.test_details"), - }; -} - -const getUsers = async (organizationId: string) => { - const users = await db.user.findMany({ - where: { members: { some: { organizationId } } }, - }); - - return users; -}; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/getTests.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/getTests.ts deleted file mode 100644 index f945c3935d..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/getTests.ts +++ /dev/null @@ -1,149 +0,0 @@ -"use server"; - -import { db } from "@comp/db"; -import { authActionClient } from "@/actions/safe-action"; -import { testsInputSchema } from "../types"; - -export const getTests = authActionClient - .schema(testsInputSchema) - .metadata({ - name: "get-tests", - track: { - event: "get-tests", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { search, severity, status, page = 1, pageSize = 10 } = parsedInput; - const { session } = ctx; - - console.log("--------------------------------"); - console.log("search", search); - console.log("severity", severity); - console.log("status", status); - console.log("page", page); - console.log("pageSize", pageSize); - console.log("--------------------------------"); - - if (!session.activeOrganizationId) { - return { - success: false, - error: "You are not authorized to view tests", - }; - } - - try { - const skip = (page - 1) * pageSize; - - // Use the prisma client with correct model - const [integrationResults, total] = await Promise.all([ - db.integrationResult.findMany({ - where: { - organizationId: session.activeOrganizationId, - ...(search - ? { - OR: [ - { - integration: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - resultDetails: { - path: ["Title"], - string_contains: search, - }, - }, - ], - } - : {}), - ...(status - ? { status: { equals: status, mode: "insensitive" } } - : {}), - ...(severity - ? { severity: { equals: severity, mode: "insensitive" } } - : {}), - }, - include: { - integration: { - select: { - id: true, - name: true, - integrationId: true, - }, - }, - assignedUser: { - select: { - id: true, - name: true, - email: true, - image: true, - }, - }, - }, - skip, - take: pageSize, - orderBy: { completedAt: "desc" }, - }), - db.integrationResult.count({ - where: { - organizationId: session.activeOrganizationId, - ...(search - ? { - OR: [ - { - integration: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - resultDetails: { - path: ["Title"], - string_contains: search, - }, - }, - ], - } - : {}), - ...(status - ? { status: { equals: status, mode: "insensitive" } } - : {}), - ...(severity - ? { severity: { equals: severity, mode: "insensitive" } } - : {}), - }, - }), - ]); - - // Transform the data to include integration info - const transformedTests = integrationResults.map((result: any) => { - return { - id: result.id, - severity: result.severity, - result: result.status, - title: result.title || result.integration.name, - provider: result.integration.integrationId, - createdAt: result.completedAt || new Date(), - // The executedBy information is no longer available in the new schema - assignedUser: result.assignedUser, - }; - }); - - return { - success: true, - data: { tests: transformedTests, total }, - }; - } catch (error) { - console.error("Error fetching integration results:", error); - return { - success: false, - error: "An unexpected error occurred", - }; - } - }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/refreshTests.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/refreshTests.ts deleted file mode 100644 index 6d27e56645..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/refreshTests.ts +++ /dev/null @@ -1,121 +0,0 @@ -"use server"; - -import { authActionClient } from "@/actions/safe-action"; -import { decrypt } from "@comp/app/src/lib/encryption"; -import { db } from "@comp/db"; -import type { DecryptFunction } from "@comp/integrations"; -import { getIntegrationHandler } from "@comp/integrations"; - -export const refreshTestsAction = authActionClient - .metadata({ - name: "refresh-tests", - track: { - event: "refresh-tests", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { session } = ctx; - - if (!session?.activeOrganizationId || !session.activeOrganizationId) { - throw new Error("Invalid user input"); - } - - const integrationsTable = await db.integration.findMany({ - where: { - organizationId: session.activeOrganizationId, - }, - }); - - for (const integration of integrationsTable) { - // Get the integration handler with proper typing - const integrationHandler = getIntegrationHandler( - integration.integrationId, - ); - - // Skip if no handler is found for this integration type - if (!integrationHandler) { - console.log( - `No handler found for integration type: ${integration.integrationId}`, - ); - continue; - } - - try { - // Process credentials using the integration handler - const userSettings = integration.userSettings as unknown as Record< - string, - unknown - >; - - // Pass the decrypt function to the processCredentials method - const typedCredentials = await integrationHandler.processCredentials( - userSettings, - // Cast decrypt to match the expected DecryptFunction type - decrypt as unknown as DecryptFunction, - ); - - // Fetch results using properly typed credentials - const results = await integrationHandler.fetch(typedCredentials); - - // Store the integration results using model name that matches the database - for (const result of results) { - // First verify the integration exists - const existingIntegration = await db.integration.findUnique({ - where: { id: integration.id }, - }); - - if (!existingIntegration) { - console.log(`Integration with ID ${integration.id} not found`); - continue; - } - - // Check if a result with the same title already exists - const existingResult = await db.integrationResult.findFirst({ - where: { - title: result.title, - integrationId: existingIntegration.id, - }, - }); - - if (existingResult) { - // Update the existing result instead of creating a new one - await db.integrationResult.update({ - where: { id: existingResult.id }, - data: { - title: result.title, - description: result.description, - remediation: result.remediation, - status: result.status, - severity: result.severity, - resultDetails: result.resultDetails, - }, - }); - continue; - } - - await db.integrationResult.create({ - data: { - title: result.title, - description: result.description, - remediation: result.remediation, - status: result.status, - severity: result.severity, - resultDetails: result.resultDetails, - integrationId: existingIntegration.id, - organizationId: integration.organizationId, - }, - }); - } - } catch (error) { - console.error( - `Error processing ${integration.integrationId} integration:`, - error, - ); - } - } - - console.log("Refreshing tests"); - - return { success: true }; - }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/TestsList.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/TestsList.tsx deleted file mode 100644 index bf2135cf09..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/TestsList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { NoTests } from "./table/empty-states"; -import { Loading } from "./table/loading"; -import { useTests } from "../hooks/useTests"; -import { TestsTable } from "./table/TestsTable"; -import { TestsTableProvider } from "../hooks/useTestsTableContext"; - -export function TestsList() { - const { tests, isLoading, error } = useTests(''); - - if (isLoading) { - return ; - } - - if (error) { - return ( -
-

Tests

-
- Error loading tests: {error.message} -
-
- ); - } - - if (tests.length === 0) { - return ( -
- - -
- ); - } - - return ( - -
- -
-
- ); -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/TestsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/TestsTable.tsx deleted file mode 100644 index d9e00bf6ff..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/TestsTable.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { DataTable } from "@/components/ui/data-table"; -import { useParams, useRouter } from "next/navigation"; -import { getFilterCategories } from "./filterCategories"; -import { getColumns } from "./columns"; -import { useTestsTable } from "../../hooks/useTestsTableContext"; -import { RefreshCcw } from "lucide-react"; -import { refreshTestsAction } from "../../actions/refreshTests"; -import { useAction } from "next-safe-action/hooks"; -import { toast } from "sonner"; -import { useI18n } from "@/locales/client"; - -export function TestsTable() { - const router = useRouter(); - const { orgId } = useParams<{ orgId: string }>(); - const t = useI18n(); - - const { - page, - setPage, - pageSize, - setPageSize, - tests, - total, - search, - setSearch, - status, - setStatus, - severity, - setSeverity, - hasActiveFilters, - clearFilters, - isLoading, - isSearching, - } = useTestsTable(); - - const refreshTests = useAction(refreshTestsAction, { - onSuccess: () => { - toast.success(t("tests.actions.refresh_success")); - window.location.reload(); - }, - onError: () => { - toast.error(t("tests.actions.refresh_error")); - }, - }); - - const handleRowClick = (testId: string) => { - router.replace(`/${orgId}/tests/all/${testId}`); - }; - - const activeFilterCount = [status, severity].filter(Boolean).length; - - const filterCategories = getFilterCategories({ - status, - setStatus, - severity: severity, - setSeverity: setSeverity, - setPage, - }); - - // Calculate pagination values only when total is defined - const pagination = - total !== undefined - ? { - page: Number(page), - pageSize: Number(pageSize), - totalCount: total, - totalPages: Math.ceil(total / Number(pageSize)), - hasNextPage: Number(page) * Number(pageSize) < total, - hasPreviousPage: Number(page) > 1, - } - : undefined; - - return ( - handleRowClick(row.id)} - emptyMessage="No tests found." - isLoading={isLoading || isSearching} - pagination={pagination} - onPageChange={(page) => setPage(page.toString())} - onPageSizeChange={(pageSize) => setPageSize(pageSize.toString())} - search={{ - value: search || "", - onChange: setSearch, - placeholder: "Search tests...", - }} - filters={{ - categories: filterCategories, - hasActiveFilters, - onClearFilters: clearFilters, - activeFilterCount, - }} - ctaButton={{ - label: "Refresh Tests", - onClick: () => refreshTests.execute(), - icon: , - }} - /> - ); -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/columns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/columns.tsx deleted file mode 100644 index 9704290fea..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/columns.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client"; - -import type { ColumnDef } from "@tanstack/react-table"; -import type { TestRow, TestResult, TestSeverity } from "../../types"; -import { Badge } from "@comp/ui/badge"; -import Image from "next/image"; -import { integrations } from "@comp/integrations"; -import { AssignedUser } from "@/components/assigned-user"; - -const getSeverityBadge = (severity: TestSeverity) => { - if (!severity) return Unknown; - - switch (severity) { - case "INFO": - return {severity}; - case "LOW": - return {severity}; - case "MEDIUM": - return {severity}; - case "HIGH": - return {severity}; - case "CRITICAL": - return {severity}; - default: - return {severity}; - } -}; - -const getResultsBadge = (result: TestResult) => { - switch (result) { - case "PASSED": - return {result}; - case "IN_PROGRESS": - return {result}; - case "FAILED": - return {result}; - default: - return {result}; - } -}; - -const getProviderLogo = (provider: string): string => { - const integration = integrations.find((i) => i.id === provider); - return typeof integration?.logo === "string" ? integration.logo : ""; -}; - -export function getColumns( - handleRowClick: (testId: string) => void, -): ColumnDef[] { - return [ - { - id: "severity", - header: "Severity", - accessorKey: "severity", - cell: ({ row }) => { - return getSeverityBadge(row.original.severity); - }, - }, - { - id: "result", - header: "Result", - accessorKey: "result", - cell: ({ row }) => { - return getResultsBadge(row.original.result); - }, - }, - { - id: "title", - header: "Title", - accessorKey: "title", - cell: ({ row }) => { - const title = row.original.title; - return ( -
- -
- ); - }, - }, - { - id: "provider", - header: "Provider", - accessorKey: "provider", - cell: ({ row }) => { - const provider = row.original.provider; - const logo = getProviderLogo(provider); - - return ( -
- {logo && ( - {provider} - )} - {provider} -
- ); - }, - }, - { - id: "createdAt", - header: "Created", - accessorKey: "createdAt", - cell: ({ row }) => { - const date = new Date(row.original.createdAt); - return ( -
- {date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - })} -
- ); - }, - }, - { - id: "assignedUser", - header: "Assigned To", - accessorKey: "assignedUser", - cell: ({ row }) => { - const assignedUser = row.original.assignedUser; - if (!assignedUser) { - return ( - Not assigned - ); - } - return ( - - ); - }, - }, - ]; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/data-table-header.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/data-table-header.tsx deleted file mode 100644 index c95ff06dfa..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/data-table-header.tsx +++ /dev/null @@ -1,165 +0,0 @@ -"use client"; - -import { useI18n } from "@/locales/client"; -import { Button } from "@comp/ui/button"; -import { TableHead, TableHeader, TableRow } from "@comp/ui/table"; -import { ArrowDown, ArrowUp } from "lucide-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback } from "react"; - -type Props = { - table?: { - getIsAllPageRowsSelected: () => boolean; - getIsSomePageRowsSelected: () => boolean; - getAllLeafColumns: () => { - id: string; - getIsVisible: () => boolean; - }[]; - toggleAllPageRowsSelected: (value: boolean) => void; - }; - loading?: boolean; - isEmpty?: boolean; -}; - -export function DataTableHeader({ table, loading }: Props) { - const searchParams = useSearchParams(); - const pathname = usePathname(); - const router = useRouter(); - const t = useI18n(); - - const sortParam = searchParams.get("sort"); - const [column, value] = sortParam ? sortParam.split(":") : []; - - const createSortQuery = useCallback( - (name: string) => { - const params = new URLSearchParams(searchParams); - const prevSort = params.get("sort"); - - if (`${name}:asc` === prevSort) { - params.set("sort", `${name}:desc`); - } else if (`${name}:desc` === prevSort) { - params.delete("sort"); - } else { - params.set("sort", `${name}:asc`); - } - - router.replace(`${pathname}?${params.toString()}`); - }, - [searchParams, router, pathname], - ); - - const isVisible = (id: string) => - loading || - table - ?.getAllLeafColumns() - .find((col) => col.id === id) - ?.getIsVisible(); - - return ( - - - {isVisible("severity") && ( - - - - )} - - {isVisible("result") && ( - - - - )} - - {isVisible("title") && ( - - - - )} - - {isVisible("provider") && ( - - - - )} - - {isVisible("createdAt") && ( - - - - )} - - {isVisible("assignedUser") && ( - - - - )} - - - ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/empty-states.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/empty-states.tsx deleted file mode 100644 index eda40ef3aa..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/empty-states.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import { useI18n } from "@/locales/client"; -import { Button } from "@comp/ui/button"; -import { CloudOff } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; -import { refreshTestsAction } from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/refreshTests"; -import { useAction } from "next-safe-action/hooks"; -import { toast } from "sonner"; - -export function NoTests() { - const t = useI18n(); - - const refreshTests = useAction(refreshTestsAction, { - onSuccess: () => { - toast.success(t("tests.actions.refresh_success")); - window.location.reload(); - }, - onError: () => { - toast.error(t("tests.actions.refresh_error")); - }, - }); - - const refreshTestsClick = () => { - refreshTests.execute(); - }; - - return ( -
-
- -

- {t("tests.empty.no_tests.title")} -

-

- {t("tests.empty.no_tests.description")} -

- -
-
- ); -} - -export function NoResults({ hasFilters }: { hasFilters: boolean }) { - const router = useRouter(); - const t = useI18n(); - const { orgId } = useParams<{ orgId: string }>(); - - return ( -
-
- -
-

- {t("tests.empty.no_results.title")} -

-

- {hasFilters - ? t("tests.empty.no_results.description_with_filters") - : t("tests.empty.no_results.description")} -

-
- - {hasFilters && ( - - )} -
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filter-toolbar.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filter-toolbar.tsx deleted file mode 100644 index b0fb13b082..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filter-toolbar.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useI18n } from "@/locales/client"; -import { Button } from "@comp/ui/button"; -import { Input } from "@comp/ui/input"; -import { Search, X } from "lucide-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useState, useTransition } from "react"; -import { useDebounce } from "use-debounce"; -import { refreshTestsAction } from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/actions/refreshTests"; -import { useAction } from "next-safe-action/hooks"; -import { toast } from "sonner"; - -interface FilterToolbarProps { - isEmpty?: boolean; -} - -export function FilterToolbar({ isEmpty = false }: FilterToolbarProps) { - const router = useRouter(); - const pathname = usePathname(); - const searchParams = useSearchParams(); - const t = useI18n(); - const [isPending, startTransition] = useTransition(); - const [inputValue, setInputValue] = useState( - searchParams?.get("search") ?? "", - ); - - const refreshTests = useAction(refreshTestsAction, { - onSuccess: () => { - toast.success(t("tests.actions.refresh_success")); - window.location.reload(); - }, - onError: () => { - toast.error(t("tests.actions.refresh_error")); - }, - }); - - const refreshTestsClick = () => { - refreshTests.execute(); - }; - - const createQueryString = useCallback( - (params: Record) => { - const newSearchParams = new URLSearchParams(searchParams?.toString()); - - for (const [key, value] of Object.entries(params)) { - if (value === null) { - newSearchParams.delete(key); - } else { - newSearchParams.set(key, value); - } - } - - return newSearchParams.toString(); - }, - [searchParams], - ); - - const [debouncedValue] = useDebounce(inputValue, 300); - - useEffect(() => { - startTransition(() => { - router.push( - `${pathname}?${createQueryString({ - search: debouncedValue || null, - page: null, - })}`, - ); - }); - }, [debouncedValue, createQueryString, pathname, router]); - - return ( -
-
- {!isEmpty && ( -
- - setInputValue(e.target.value)} - /> -
- )} -
- -
- {inputValue && ( - - )} - -
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterCategories.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterCategories.tsx deleted file mode 100644 index 8644b36808..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterCategories.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { STATUS_FILTERS, SEVERITY_FILTERS } from "./filterConfigs"; - -interface FilterCategoriesProps { - status: string | null; - setStatus: (status: string | null) => void; - severity: string | null; - setSeverity: (severity: string | null) => void; - setPage: (page: string) => void; -} - -export function getFilterCategories({ - status, - setStatus, - severity, - setSeverity, - setPage, -}: FilterCategoriesProps) { - return [ - { - label: "Filter by Status", - items: STATUS_FILTERS.map((filter) => ({ - ...filter, - checked: status === filter.value, - onChange: (checked: boolean) => { - setStatus(checked ? filter.value : null); - setPage("1"); - }, - })), - }, - { - label: "Filter by Severity", - items: SEVERITY_FILTERS.map((filter) => ({ - ...filter, - checked: severity === filter.value, - onChange: (checked: boolean) => { - setSeverity(checked ? filter.value : null); - setPage("1"); - }, - })), - }, - ]; -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterConfigs.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterConfigs.tsx deleted file mode 100644 index 89dac81f09..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/filterConfigs.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { CheckCircle2, XCircle, Clock } from "lucide-react"; - -export const STATUS_FILTERS = [ - { - label: "Passed", - value: "PASSED", - icon: , - }, - { - label: "Failed", - value: "FAILED", - icon: , - }, - { - label: "In Progress", - value: "IN_PROGRESS", - icon: , - }, -] as const; - -export const SEVERITY_FILTERS = [ - { - label: "Critical", - value: "CRITICAL", - icon:
, - }, - { - label: "High", - value: "HIGH", - icon:
, - }, - { - label: "Medium", - value: "MEDIUM", - icon:
, - }, - { - label: "Low", - value: "LOW", - icon:
, - }, - { - label: "Info", - value: "INFO", - icon:
, - }, -] as const; \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/loading.tsx deleted file mode 100644 index e5eb104310..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/components/table/loading.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { cn } from "@comp/ui/cn"; -import { Skeleton } from "@comp/ui/skeleton"; -import { Table, TableBody, TableCell, TableRow } from "@comp/ui/table"; -import { Suspense } from "react"; -import { DataTableHeader } from "./data-table-header"; - -const data = [...Array(10)].map((_, i) => ({ id: i.toString() })); - -export function Loading({ isEmpty }: { isEmpty: boolean }) { - return ( - }> -
- - - - - {data?.map((row) => ( - - - - - - - - - - - - - - - - - - - - - ))} - -
-
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTest.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTest.ts deleted file mode 100644 index bb0b6a0ff6..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTest.ts +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import useSWR from "swr"; -import { getTest } from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/actions/getTest"; -import type { - AppError, - Test, -} from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types"; - -async function fetchTest(testId: string): Promise { - try { - const response = await getTest({ testId }); - - if (response.success) { - return response.data; - } - - throw response.error; - } catch (error) { - if (error && typeof error === "object" && "message" in error) { - throw error as AppError; - } - throw { message: "An unexpected error occurred" }; - } -} - -export function useTest(testId: string) { - const { data, error, isLoading, mutate } = useSWR( - testId ? ["cloud-test-details", testId] : null, - () => fetchTest(testId), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - - return { - cloudTest: data, - isLoading, - error, - mutate, - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTests.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTests.ts deleted file mode 100644 index 829acd7f07..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTests.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; -import useSWR from "swr"; -import { useSearchParams } from "next/navigation"; - -import { getTests } from "../actions/getTests"; -import type { TestsResponse, TestsInput, AppError } from "../types"; - -/** Fetcher function for tests */ -async function fetchTests(input: TestsInput): Promise { - const result = await getTests(input); - - if (!result) { - const error: AppError = { - code: "UNEXPECTED_ERROR", - message: "An unexpected error occurred", - }; - throw error; - } - - if (result.serverError) { - const error: AppError = { - code: "UNEXPECTED_ERROR", - message: result.serverError || "An unexpected error occurred", - }; - throw error; - } - - if (!result.data) { - const error: AppError = { - code: "UNEXPECTED_ERROR", - message: "No data returned from server", - }; - throw error; - } - - return result.data.data as TestsResponse; -} - -export function useTests(search: string | undefined) { - const searchParams = useSearchParams(); - const severity = searchParams.get("severity") || undefined; - const status = searchParams.get("status") || undefined; - const page = Number(searchParams.get("page")) || 1; - const pageSize = Number(searchParams.get("pageSize")) || 10; - - /** SWR for fetching tests */ - const { - data, - error, - isLoading, - mutate: revalidateTests, - } = useSWR( - ["tests", { search, severity, status, page, pageSize }], - () => fetchTests({ search, severity, status, page, pageSize }), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - - // Format the tests to match the TestRow type - const formattedTests = data?.tests.map(test => ({ - ...test, - // Convert Date to string for use in components if needed - createdAt: test.createdAt - })) || []; - - return { - tests: formattedTests, - total: data?.total ?? 0, - isLoading, - error, - revalidateTests - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTestsTableContext.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTestsTableContext.tsx deleted file mode 100644 index 181651a1bd..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTestsTableContext.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client"; - -import { - createContext, - useContext, - useMemo, - useState, - useRef, - useEffect, - type ReactNode, -} from "react"; -import { useQueryState } from "nuqs"; -import { useTests } from "./useTests"; - -interface TestsTableContextType { - // State - search: string; - severity: string | null; - status: string | null; - page: string; - pageSize: string; - - // Setters - setSearch: (value: string) => void; - setSeverity: (value: string | null) => void; - setStatus: (value: string | null) => void; - setPage: (value: string) => void; - setPageSize: (value: string) => void; - - // Data - tests: any[] | undefined; - total: number | undefined; - isLoading: boolean; - isSearching: boolean; - - // Derived data - hasActiveFilters: boolean; - - // Actions - clearFilters: () => void; -} - -const TestsTableContext = createContext< - TestsTableContextType | undefined ->(undefined); - -export function TestsTableProvider({ children }: { children: ReactNode }) { - // Local state for search - const [search, setSearch] = useState(""); - - // Query state for other filters - const [severity, setSeverity] = useQueryState("severity"); - const [status, setStatus] = useQueryState("status"); - const [page, setPage] = useQueryState("page", { defaultValue: "1" }); - const [pageSize, setPageSize] = useQueryState("pageSize", { - defaultValue: "10", - }); - - // Track if this is initial load or a search/filter update - const initialLoadCompleted = useRef(false); - const [isSearching, setIsSearching] = useState(false); - - const currentPage = Number.parseInt(page, 10); - const currentPageSize = Number.parseInt(pageSize, 10); - - // Fetch data - const { tests, total, isLoading } = useTests(search); - - // Track when search params change - useEffect(() => { - if (initialLoadCompleted.current) { - setIsSearching(true); - } - }, [search, severity, status, page, pageSize]); - - // Track when loading changes - useEffect(() => { - if (isLoading === false) { - // Small delay to ensure UI transitions properly - setTimeout(() => { - initialLoadCompleted.current = true; - setIsSearching(false); - }, 50); - } - }, [isLoading]); - - // Additional safety reset for isSearching when data changes - useEffect(() => { - if (tests && isSearching) { - // If we have data, ensure isSearching is eventually set to false - const timer = setTimeout(() => { - setIsSearching(false); - }, 100); - return () => clearTimeout(timer); - } - }, [tests, isSearching]); - - // Check if any filters are active - const hasActiveFilters = useMemo(() => { - return severity !== null || status !== null; - }, [severity, status]); - - // Clear all filters - const clearFilters = () => { - setSeverity(null); - setStatus(null); - setPage("1"); // Reset to first page when clearing filters - setSearch(""); // Clear search - }; - - const contextValue: TestsTableContextType = { - // State - search, - severity, - status, - page, - pageSize, - - // Setters - setSearch, - setSeverity, - setStatus, - setPage, - setPageSize, - - // Data - tests, - total, - isLoading, - isSearching, - - // Derived data - hasActiveFilters, - - // Actions - clearFilters, - }; - - return ( - - {children} - - ); -} - -export function useTestsTable() { - const context = useContext(TestsTableContext); - - if (context === undefined) { - throw new Error( - "useTestsTable must be used within a TestsTableProvider" - ); - } - - return context; -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/layout.tsx deleted file mode 100644 index 4261e1dd2a..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/layout.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; -import { AppOnboarding } from "@/components/app-onboarding"; -import { db } from "@comp/db"; -import { Suspense, cache } from "react"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session?.activeOrganizationId; - - const tests = await getTestsOverview(); - console.log(tests); - - if (!tests.length) { - return ( -
- Loading...
}> -
- -
- -
- ); - } - - return ( -
- -
{children}
-
- ); -} - -const getTestsOverview = cache(async () => { - const session = await auth.api.getSession({ - headers: await headers(), - }); - const orgId = session?.session?.activeOrganizationId; - - if (!orgId) return []; - - const tests = await db.integrationResult.findMany({ - where: { - organizationId: orgId, - }, - }); - - return tests; -}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/page.tsx deleted file mode 100644 index c8e01dcc54..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import type { Metadata } from "next"; -import { setStaticParamsLocale } from "next-international/server"; -import { redirect } from "next/navigation"; -import { TestsList } from "./components/TestsList"; -import { headers } from "next/headers"; - -export default async function TestsPage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { - const { locale } = await params; - setStaticParamsLocale(locale); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const organizationId = session?.session.activeOrganizationId; - - if (!organizationId) { - return redirect("/"); - } - - return ; -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("sidebar.tests"), - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/index.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/index.ts deleted file mode 100644 index 3170a36438..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { z } from "zod"; -import type { User } from "next-auth"; -import type { ReactNode } from "react"; - -export const testSchema = z.object({ - id: z.string(), - title: z.string(), - description: z.string().nullable(), - remediation: z.string().nullable(), - provider: z.string(), - status: z.string(), - resultDetails: z.any(), - severity: z.string().nullable(), - completedAt: z.date(), - organizationId: z.string(), - assignedUserId: z.string(), - organizationIntegrationId: z.string(), -}); - -export const testsInputSchema = z.object({ - search: z.string().optional(), - severity: z.string().optional(), - status: z.string().optional(), - page: z.number().optional(), - pageSize: z.number().optional(), -}); - -export type Test = z.infer; -export type TestsInput = z.infer; - -export interface TestsResponse { - tests: { - id: string; - severity: string | null; - result: string; - title: string; - provider: string; - createdAt: Date; - assignedUser: null; - }[]; - total: number; -} - -export interface AppError { - code: string; - message: string; -} - -export const appErrors = { - UNAUTHORIZED: { - code: "UNAUTHORIZED" as const, - message: "You are not authorized to view employees", - }, - UNEXPECTED_ERROR: { - code: "UNEXPECTED_ERROR" as const, - message: "An unexpected error occurred", - }, -} as const; - -export type TestResult = "PASSED" | "FAILED" | "IN_PROGRESS"; -export type TestSeverity = - | "INFO" - | "LOW" - | "MEDIUM" - | "HIGH" - | "CRITICAL" - | null; - -export interface TestRow { - id: string; - severity: TestSeverity; - result: TestResult; - title: string; - provider: string; - createdAt: string | Date; // Allow both string and Date to handle different data formats - assignedUser: { - id: string; - name: string | null; - image: string | null; - } | null; -} - -export interface TestsTableProps { - users: User[]; - ctaButton?: { - label: string; - onClick: () => void; - icon?: ReactNode; - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/search-params.ts deleted file mode 100644 index 2654a2e0ee..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/types/search-params.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - createSearchParamsCache, - parseAsInteger, - parseAsString, -} from "nuqs/server"; - -export const searchParamsCache = createSearchParamsCache({ - q: parseAsString, - page: parseAsInteger.withDefault(0), - start: parseAsString, - end: parseAsString, -}); diff --git a/apps/app/src/components/app-onboarding.tsx b/apps/app/src/components/app-onboarding.tsx index 6edf9fff8f..46493db92a 100644 --- a/apps/app/src/components/app-onboarding.tsx +++ b/apps/app/src/components/app-onboarding.tsx @@ -1,18 +1,18 @@ "use client"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@comp/ui/card"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@comp/ui/accordion"; import Image from "next/image"; import { Button } from "@comp/ui/button"; @@ -21,94 +21,96 @@ import { useI18n } from "@/locales/client"; import { useQueryState } from "nuqs"; interface FAQ { - questionKey: string; - answerKey: string; + questionKey: string; + answerKey: string; } type Props = { - title: string; - description: string; - cta: string; - imageSrc: string; - imageAlt: string; - faqs?: FAQ[]; - sheetName: string; + title: string; + description: string; + cta?: string; + imageSrc: string; + imageAlt: string; + faqs?: FAQ[]; + sheetName: string; }; export function AppOnboarding({ - title, - description, - cta, - imageSrc, - imageAlt, - faqs, - sheetName, + title, + description, + cta, + imageSrc, + imageAlt, + faqs, + sheetName, }: Props) { - const t = useI18n(); - const [open, setOpen] = useQueryState(sheetName); - const isOpen = Boolean(open); + const t = useI18n(); + const [open, setOpen] = useQueryState(sheetName); + const isOpen = Boolean(open); - return ( - -
-
-
- - {title} - - {description} - - - -
-
-
-

- {t("app_onboarding.risk_management.learn_more")} -

- {faqs && faqs.length > 0 && ( - - {faqs.map((faq) => ( - - - {faq.questionKey} - - {faq.answerKey} - - ))} - - )} -
-
- {imageAlt} -
-
-
-
-
-
- - {cta && ( - - )} - -
-
- ); + return ( + +
+
+
+ + {title} + + {description} + + + +
+
+
+

+ {t("app_onboarding.risk_management.learn_more")} +

+ {faqs && faqs.length > 0 && ( + + {faqs.map((faq) => ( + + + {faq.questionKey} + + {faq.answerKey} + + ))} + + )} +
+
+ {imageAlt} +
+
+
+
+
+
+ {cta && ( + + {cta && ( + + )} + + )} +
+
+ ); } diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx index 7cc8a36808..3f858ef3b1 100644 --- a/apps/app/src/components/main-menu.tsx +++ b/apps/app/src/components/main-menu.tsx @@ -4,263 +4,263 @@ import { useI18n } from "@/locales/client"; import { cn } from "@comp/ui/cn"; import { Icons } from "@comp/ui/icons"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@comp/ui/tooltip"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { - Blocks, - FileText, - FlaskConical, - Gauge, - ListCheck, - NotebookText, - ShieldEllipsis, - ShieldPlus, - Store, - Users, + Blocks, + FileText, + FlaskConical, + Gauge, + ListCheck, + NotebookText, + ShieldEllipsis, + ShieldPlus, + Store, + Users, } from "lucide-react"; // Define menu item types with icon component type MenuItem = { - id: string; - path: string; - name: string; - disabled: boolean; - icon: React.FC<{ size?: number }>; - protected: boolean; + id: string; + path: string; + name: string; + disabled: boolean; + icon: React.FC<{ size?: number }>; + protected: boolean; }; interface ItemProps { - item: MenuItem; - isActive: boolean; - disabled: boolean; - organizationId: string; - isCollapsed?: boolean; + item: MenuItem; + isActive: boolean; + disabled: boolean; + organizationId: string; + isCollapsed?: boolean; } const Item = ({ - item, - isActive, - disabled, - organizationId, - isCollapsed = false, + item, + isActive, + disabled, + organizationId, + isCollapsed = false, }: ItemProps) => { - const Icon = item.icon; - const linkDisabled = disabled || item.disabled; + const Icon = item.icon; + const linkDisabled = disabled || item.disabled; - // Replace the organizationId placeholder in the path - const itemPath = item.path.replace(":organizationId", organizationId); + // Replace the organizationId placeholder in the path + const itemPath = item.path.replace(":organizationId", organizationId); - return ( - - {linkDisabled ? ( -
- Coming -
- ) : ( - - - -
-
- {Icon && } - {!isCollapsed && ( - - {item.name} - - )} -
-
-
- - {item.name} - -
- - )} -
- ); + return ( + + {linkDisabled ? ( +
+ Coming +
+ ) : ( + + + +
+
+ {Icon && } + {!isCollapsed && ( + + {item.name} + + )} +
+
+
+ + {item.name} + +
+ + )} +
+ ); }; type Props = { - organizationId: string; - //userIsAdmin: boolean; - isCollapsed?: boolean; + organizationId: string; + //userIsAdmin: boolean; + isCollapsed?: boolean; }; export function MainMenu({ - organizationId, - //userIsAdmin, - isCollapsed = false, + organizationId, + //userIsAdmin, + isCollapsed = false, }: Props) { - const t = useI18n(); - const pathname = usePathname(); + const t = useI18n(); + const pathname = usePathname(); - const items: MenuItem[] = [ - { - id: "frameworks", - path: "/:organizationId/frameworks", - name: t("sidebar.frameworks"), - disabled: false, - icon: Gauge, - protected: false, - }, - { - id: "controls", - path: "/:organizationId/controls", - name: t("sidebar.controls"), - disabled: false, - icon: ShieldEllipsis, - protected: false, - }, - { - id: "policies", - path: "/:organizationId/policies", - name: t("sidebar.policies"), - disabled: false, - icon: NotebookText, - protected: false, - }, - { - id: "evidence", - path: "/:organizationId/evidence", - name: t("sidebar.evidence"), - disabled: false, - icon: ListCheck, - protected: false, - }, - { - id: "employees", - path: "/:organizationId/employees", - name: t("sidebar.employees"), - disabled: false, - icon: Users, - protected: false, - }, - { - id: "risk", - path: "/:organizationId/risk", - name: t("sidebar.risk"), - disabled: false, - icon: Icons.Risk, - protected: false, - }, - { - id: "vendors", - path: "/:organizationId/vendors", - name: t("sidebar.vendors"), - disabled: false, - icon: Store, - protected: false, - }, - { - id: "integrations", - path: "/:organizationId/integrations", - name: t("sidebar.integrations"), - disabled: false, - icon: Blocks, - protected: true, - }, - { - id: "tests", - path: "/:organizationId/tests", - name: t("sidebar.tests"), - disabled: false, - icon: FlaskConical, - protected: false, - }, - { - id: "settings", - path: "/:organizationId/settings", - name: t("sidebar.settings"), - disabled: false, - icon: Icons.Settings, - protected: true, - }, - ]; + const items: MenuItem[] = [ + { + id: "frameworks", + path: "/:organizationId/frameworks", + name: t("sidebar.frameworks"), + disabled: false, + icon: Gauge, + protected: false, + }, + { + id: "controls", + path: "/:organizationId/controls", + name: t("sidebar.controls"), + disabled: false, + icon: ShieldEllipsis, + protected: false, + }, + { + id: "policies", + path: "/:organizationId/policies", + name: t("sidebar.policies"), + disabled: false, + icon: NotebookText, + protected: false, + }, + { + id: "evidence", + path: "/:organizationId/evidence", + name: t("sidebar.evidence"), + disabled: false, + icon: ListCheck, + protected: false, + }, + { + id: "employees", + path: "/:organizationId/employees", + name: t("sidebar.employees"), + disabled: false, + icon: Users, + protected: false, + }, + { + id: "risk", + path: "/:organizationId/risk", + name: t("sidebar.risk"), + disabled: false, + icon: Icons.Risk, + protected: false, + }, + { + id: "vendors", + path: "/:organizationId/vendors", + name: t("sidebar.vendors"), + disabled: false, + icon: Store, + protected: false, + }, + { + id: "tests", + path: "/:organizationId/tests", + name: t("sidebar.tests"), + disabled: false, + icon: FlaskConical, + protected: false, + }, + { + id: "integrations", + path: "/:organizationId/integrations", + name: t("sidebar.integrations"), + disabled: false, + icon: Blocks, + protected: true, + }, + { + id: "settings", + path: "/:organizationId/settings", + name: t("sidebar.settings"), + disabled: false, + icon: Icons.Settings, + protected: true, + }, + ]; - // Helper function to check if a path is active - const isPathActive = (itemPath: string) => { - const normalizedItemPath = itemPath.replace( - ":organizationId", - organizationId, - ); + // Helper function to check if a path is active + const isPathActive = (itemPath: string) => { + const normalizedItemPath = itemPath.replace( + ":organizationId", + organizationId, + ); - // Extract the base path from the menu item (first two segments after normalization) - const itemPathParts = normalizedItemPath.split("/").filter(Boolean); - const itemBaseSegment = itemPathParts.length > 1 ? itemPathParts[1] : ""; + // Extract the base path from the menu item (first two segments after normalization) + const itemPathParts = normalizedItemPath.split("/").filter(Boolean); + const itemBaseSegment = itemPathParts.length > 1 ? itemPathParts[1] : ""; - // Extract the current path parts - const currentPathParts = pathname.split("/").filter(Boolean); - const currentBaseSegment = - currentPathParts.length > 1 ? currentPathParts[1] : ""; + // Extract the current path parts + const currentPathParts = pathname.split("/").filter(Boolean); + const currentBaseSegment = + currentPathParts.length > 1 ? currentPathParts[1] : ""; - // Special case for root organization path - if ( - normalizedItemPath === `/${organizationId}` || - normalizedItemPath === `/${organizationId}/frameworks` - ) { - return ( - pathname === `/${organizationId}` || - pathname?.startsWith(`/${organizationId}/frameworks`) - ); - } + // Special case for root organization path + if ( + normalizedItemPath === `/${organizationId}` || + normalizedItemPath === `/${organizationId}/frameworks` + ) { + return ( + pathname === `/${organizationId}` || + pathname?.startsWith(`/${organizationId}/frameworks`) + ); + } - // Compare the base segments (usually the feature section like "evidence", "settings", etc.) - return itemBaseSegment === currentBaseSegment; - }; + // Compare the base segments (usually the feature section like "evidence", "settings", etc.) + return itemBaseSegment === currentBaseSegment; + }; - return ( -
- +
+ ); } diff --git a/apps/app/src/locales/onboarding/app-onboarding.ts b/apps/app/src/locales/onboarding/app-onboarding.ts index 723f5ddac3..9f6096bf71 100644 --- a/apps/app/src/locales/onboarding/app-onboarding.ts +++ b/apps/app/src/locales/onboarding/app-onboarding.ts @@ -55,17 +55,21 @@ export const app_onboarding = { }, }, cloud_tests: { - title: "Cloud Security Testing", - description: "Test and validate your cloud infrastructure security.", + title: "Cloud Compliance - Coming Soon", + description: + "Test and validate your cloud infrastructure security with automated tests and reports.", cta: "Create your first cloud test", learn_more: "Learn more", faqs: { question_1: "What are cloud security tests?", - answer_1: "Cloud security tests are automated assessments that evaluate your cloud infrastructure for security vulnerabilities, misconfigurations, and compliance with security best practices.", + answer_1: + "Cloud security tests are automated assessments that evaluate your cloud infrastructure for security vulnerabilities, misconfigurations, and compliance with security best practices.", question_2: "Why are cloud security tests important?", - answer_2: "Cloud security tests help identify potential security risks in your cloud environment, ensure compliance with security standards, and provide evidence of your security controls for auditors. They're essential for maintaining a strong security posture in cloud environments.", + answer_2: + "Cloud security tests help identify potential security risks in your cloud environment, ensure compliance with security standards, and provide evidence of your security controls for auditors. They're essential for maintaining a strong security posture in cloud environments.", question_3: "What types of cloud tests are available?", - answer_3: "Cloud security tests can include infrastructure scanning, configuration analysis, vulnerability assessments, and compliance checks. These tests help ensure your cloud resources are properly secured and configured according to best practices.", + answer_3: + "Cloud security tests can include infrastructure scanning, configuration analysis, vulnerability assessments, and compliance checks. These tests help ensure your cloud resources are properly secured and configured according to best practices.", }, }, } as const;