From e338048c9cb584505519798fbe73682d7bc1abda Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Mon, 17 Feb 2025 17:40:15 -0800 Subject: [PATCH 01/10] show requirements on control --- .../framework/select-frameworks-action.ts | 24 +- .../getOrganizationControlRequirements.ts | 56 + .../[id]/Components/SingleControl.tsx | 41 +- ...nControl.tsx => useOrganizationControl.ts} | 0 .../useOrganizationControlRequirements.ts | 42 + .../Components/FrameworkControls.tsx | 29 +- .../components/tables/frameworks/columns.tsx | 11 + .../tables/frameworks/data-table-header.tsx | 20 +- apps/app/src/env.mjs | 2 + bun.lockb | Bin 0 -> 633256 bytes packages/data/controls/soc2.json | 86 + .../migration.sql | 17 + .../migration.sql | 19 + .../migration.sql | 11 + packages/db/prisma/schema.prisma | 39 +- packages/db/prisma/seed.ts | 2 + yarn.lock | 1603 ++++++++--------- 17 files changed, 1166 insertions(+), 836 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Actions/getOrganizationControlRequirements.ts rename apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/{useOrganizationControl.tsx => useOrganizationControl.ts} (100%) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControlRequirements.ts create mode 100755 bun.lockb create mode 100644 packages/db/prisma/migrations/20250217233112_add_organization_control_requirement_table/migration.sql create mode 100644 packages/db/prisma/migrations/20250218002311_add_missing_columns_to_org_control_req/migration.sql create mode 100644 packages/db/prisma/migrations/20250218002808_removed_policy_id_column/migration.sql diff --git a/apps/app/src/actions/framework/select-frameworks-action.ts b/apps/app/src/actions/framework/select-frameworks-action.ts index 7939a7d4a7..04134c15d4 100644 --- a/apps/app/src/actions/framework/select-frameworks-action.ts +++ b/apps/app/src/actions/framework/select-frameworks-action.ts @@ -95,7 +95,7 @@ const createOrganizationFramework = async (user: User, frameworkId: string) => { if (!organizationCategory) continue; - await db.organizationControl.createMany({ + const controls = await db.organizationControl.createManyAndReturn({ data: frameworkCategory.controls.map((control) => ({ organizationFrameworkId: organizationFramework.id, controlId: control.id, @@ -104,6 +104,28 @@ const createOrganizationFramework = async (user: User, frameworkId: string) => { organizationCategoryId: organizationCategory.id, })), }); + + // Create control requirements for each control + const controlRequirements = await db.controlRequirement.findMany({ + where: { + controlId: { in: controls.map((control) => control.controlId) }, + }, + }); + + for (const control of controls) { + const requirements = controlRequirements.filter( + (req) => req.controlId === control.controlId + ); + + await db.organizationControlRequirement.createMany({ + data: requirements.map((requirement) => ({ + organizationControlId: control.id, + controlRequirementId: requirement.id, + type: requirement.type, + description: requirement.description, + })), + }); + } } }; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Actions/getOrganizationControlRequirements.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Actions/getOrganizationControlRequirements.ts new file mode 100644 index 0000000000..571070d7bd --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Actions/getOrganizationControlRequirements.ts @@ -0,0 +1,56 @@ +"use server"; + +import { authActionClient } from "@/actions/safe-action"; +import type { OrganizationControlRequirement } from "@bubba/db"; +import { db } from "@bubba/db"; +import { z } from "zod"; + +export interface OrganizationControlRequirementsResponse { + organizationControlRequirements: OrganizationControlRequirement[]; +} + +export const getOrganizationControlRequirements = authActionClient + .schema(z.object({ controlId: z.string() })) + .metadata({ + name: "getOrganizationControlRequirements", + track: { + event: "get-organization-control-requirements", + channel: "server", + }, + }) + .action(async ({ ctx, parsedInput }) => { + const { user } = ctx; + const { controlId } = parsedInput; + + if (!user.organizationId) { + return { + error: "Not authorized - no organization found", + }; + } + + try { + const organizationControlRequirements = + await db.organizationControlRequirement.findMany({ + where: { + organizationControlId: controlId, + }, + }); + + if (!organizationControlRequirements) { + return { + error: "Organization control requirements not found", + }; + } + + return { + data: { + organizationControlRequirements, + }, + }; + } catch (error) { + console.error("Error fetching organization control:", error); + return { + error: "Failed to fetch organization control", + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Components/SingleControl.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Components/SingleControl.tsx index c21286c5f4..47c1fb37be 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Components/SingleControl.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/Components/SingleControl.tsx @@ -2,11 +2,15 @@ import { DisplayFrameworkStatus, - StatusType, + type StatusType, } from "@/components/frameworks/framework-status"; import { useOrganizationControl } from "../hooks/useOrganizationControl"; import { Card } from "@bubba/ui/card"; import { Label } from "@bubba/ui/label"; +import { Button } from "@bubba/ui/button"; +import { ArrowLeft } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useOrganizationControlRequirements } from "../hooks/useOrganizationControlRequirements"; interface SingleControlProps { controlId: string; @@ -14,10 +18,19 @@ interface SingleControlProps { export const SingleControl = ({ controlId }: SingleControlProps) => { const { data: control } = useOrganizationControl(controlId); + const { data: requirements } = useOrganizationControlRequirements(controlId); if (!control) return null; + const router = useRouter(); + return ( -
+
+
+ +

{control?.control.name}

{
+ +
+

Requirements

+ + + + + + + + + + {requirements?.map((requirement) => ( + + + + + + ))} + +
TypeDescriptionPublished
{requirement.type} + {requirement.description} + {requirement.published ? "Yes" : "No"}
+
); }; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControl.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControl.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControl.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControl.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControlRequirements.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControlRequirements.ts new file mode 100644 index 0000000000..beab6a2d56 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/hooks/useOrganizationControlRequirements.ts @@ -0,0 +1,42 @@ +"use client"; + +import type { OrganizationControlRequirement } from "@bubba/db"; +import useSWR from "swr"; +import { getOrganizationControlRequirements } from "../Actions/getOrganizationControlRequirements"; + +async function fetchOrganizationControlRequirements( + controlId: string +): Promise { + const result = await getOrganizationControlRequirements({ controlId }); + + if (!result) { + throw new Error("Failed to fetch control"); + } + + const data = result.data?.data; + if (!data) { + throw new Error("Invalid response from server"); + } + + return data.organizationControlRequirements; +} + +export function useOrganizationControlRequirements(controlId: string) { + const { data, error, isLoading, mutate } = useSWR< + OrganizationControlRequirement[] + >( + ["organization-control-requirements", controlId], + () => fetchOrganizationControlRequirements(controlId), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + + return { + data: data, + isLoading, + error, + mutate, + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/Components/FrameworkControls.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/Components/FrameworkControls.tsx index 139b897688..f4e51b13df 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/Components/FrameworkControls.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/Components/FrameworkControls.tsx @@ -10,7 +10,6 @@ import { } from "@bubba/ui/card"; import { DataTable } from "@/components/tables/frameworks/data-table"; -import { useOrganizationFramework } from "@/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/hooks/useOrganizationFramework"; import { useOrganizationCategories } from "@/app/[locale]/(app)/(dashboard)/frameworks/[frameworkId]/hooks/useOrganizationCategories"; interface FrameworkControlsProps { @@ -25,6 +24,18 @@ export function FrameworkControls({ frameworkId }: FrameworkControlsProps) { return null; } + const allControls = organizationCategories.flatMap((category) => + category.organizationControl.map((control) => ({ + code: control.control.code, + description: control.control.description, + name: control.control.name, + status: control.status, + id: control.id, + frameworkId, + category: category.name, + })) + ); + return ( @@ -32,21 +43,7 @@ export function FrameworkControls({ frameworkId }: FrameworkControlsProps) { Review and manage compliance controls - {organizationCategories.map((category) => ( -
-

{category.name}

- ({ - code: control.control.code, - description: control.control.description, - name: control.control.name, - status: control.status, - id: control.id, - frameworkId, - }))} - /> -
- ))} +
); diff --git a/apps/app/src/components/tables/frameworks/columns.tsx b/apps/app/src/components/tables/frameworks/columns.tsx index fdb0a11127..60ea9adfe9 100644 --- a/apps/app/src/components/tables/frameworks/columns.tsx +++ b/apps/app/src/components/tables/frameworks/columns.tsx @@ -16,6 +16,7 @@ export type OrganizationControlType = { description: string | null; status: ComplianceStatus; frameworkId: string; + category: string; }; export function columns(): ColumnDef[] { @@ -42,6 +43,16 @@ export function columns(): ColumnDef[] { ); }, }, + { + id: "category", + accessorKey: "category", + header: t("risk.vendor.table.category"), + cell: ({ row }) => ( +
+ {row.original.category} +
+ ), + }, { id: "status", accessorKey: "status", diff --git a/apps/app/src/components/tables/frameworks/data-table-header.tsx b/apps/app/src/components/tables/frameworks/data-table-header.tsx index 4861fbd7b0..c54fbf9292 100644 --- a/apps/app/src/components/tables/frameworks/data-table-header.tsx +++ b/apps/app/src/components/tables/frameworks/data-table-header.tsx @@ -45,7 +45,7 @@ export function DataTableHeader({ table, loading }: Props) { router.replace(`${pathname}?${params.toString()}`); }, - [searchParams, router, pathname], + [searchParams, router, pathname] ); const isVisible = (id: string) => @@ -72,6 +72,24 @@ export function DataTableHeader({ table, loading }: Props) { )} + {isVisible("category") && ( + + + + )} + {isVisible("status") && (