diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/RequirementRow.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/RequirementRow.tsx new file mode 100644 index 0000000000..8bebb19c07 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/RequirementRow.tsx @@ -0,0 +1,34 @@ +import type { FrameworkId, RequirementMap } from "@bubba/db/types"; +import { TableCell, TableRow } from "@bubba/ui/table"; +import { getRequirementDetails } from "../../../frameworks/lib/getRequirementDetails"; +import { useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; + +interface RequirementRowProps { + requirement: RequirementMap; +} + +export function RequirementRow({ requirement }: RequirementRowProps) { + const router = useRouter(); + const params = useParams(); + const [frameworkId, requirementId] = requirement.requirementId.split("_"); + const orgId = params.orgId as string; + + const requirementDetails = getRequirementDetails( + frameworkId as FrameworkId, + requirementId, + ); + + const href = `/${orgId}/frameworks/${requirement.frameworkInstanceId}/requirements/${requirementId}`; + + return ( + router.push(href)} + className="group hover:bg-muted/50 transition-colors cursor-pointer" + > + {requirementId} + {requirementDetails?.name} + {requirementDetails?.description} + + ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/SingleControl.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/SingleControl.tsx index 14695bdaff..0de8ffd9c1 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/SingleControl.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/SingleControl.tsx @@ -15,6 +15,7 @@ import { useMemo } from "react"; import type { ControlProgressResponse } from "../data/getOrganizationControlProgress"; import { SingleControlSkeleton } from "./SingleControlSkeleton"; import { useI18n } from "@/locales/client"; +import { RequirementRow } from "./RequirementRow"; interface SingleControlProps { control: Control & { @@ -80,17 +81,18 @@ export const SingleControl = ({ {t("frameworks.requirements.table.name")} + + {t("frameworks.requirements.table.description")} + {control.requirementsMapped.length > 0 ? ( control.requirementsMapped.map((requirement) => ( - - - {requirement.requirementId} - - {requirement.requirementId} - + )) ) : ( diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx index f478bbe214..33a0b28de0 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx @@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { Progress } from "@bubba/ui/progress"; import { FrameworkInstanceWithControls } from "../../types"; import { getControlStatus } from "../../lib/utils"; -import { getFrameworkName } from "../../lib/getFrameworkName"; +import { getFrameworkDetails } from "../../lib/getFrameworkDetails"; interface FrameworkOverviewProps { frameworkInstanceWithControls: FrameworkInstanceWithControls; } @@ -34,7 +34,10 @@ export function FrameworkOverview({ - {getFrameworkName(frameworkInstanceWithControls.frameworkId)} + { + getFrameworkDetails(frameworkInstanceWithControls.frameworkId) + .name + } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index 339c9b7599..ded9bfa760 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -76,7 +76,7 @@ export function FrameworkRequirements({ > {requirement.id} @@ -84,7 +84,7 @@ export function FrameworkRequirements({ {requirement.name} @@ -100,7 +100,7 @@ export function FrameworkRequirements({ {requirement.mappedControlsCount} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx index 5cfbb91527..2c875240c3 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx @@ -4,7 +4,8 @@ import { redirect } from "next/navigation"; import { getSingleFrameworkInstanceWithControls } from "../data/getSingleFrameworkInstanceWithControls"; import { FrameworkOverview } from "./components/FrameworkOverview"; import { FrameworkRequirements } from "./components/FrameworkRequirements"; - +import PageWithBreadcrumb from "../../../../../../../components/pages/PageWithBreadcrumb"; +import { getFrameworkDetails } from "../lib/getFrameworkDetails"; interface PageProps { params: Promise<{ frameworkInstanceId: string; @@ -38,15 +39,26 @@ export default async function FrameworkPage({ params }: PageProps) { redirect("/"); } + const framework = getFrameworkDetails( + frameworkInstanceWithControls.frameworkId, + ).name; + return ( -
- - -
+ +
+ + +
+
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/RequirementControls.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/RequirementControls.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/table/RequirementControlsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/table/RequirementControlsTable.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/table/RequirementControlsTableColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/components/table/RequirementControlsTableColumns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/loading.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/loading.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/loading.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx similarity index 54% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx index 0c0df27943..8d642f870d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirement/[requirementKey]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx @@ -4,6 +4,8 @@ import { redirect } from "next/navigation"; import { getSingleFrameworkInstanceWithControls } from "../../../data/getSingleFrameworkInstanceWithControls"; import { getFrameworkRequirements } from "../../../lib/getFrameworkRequirements"; import { RequirementControls } from "./components/RequirementControls"; +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; +import { getFrameworkDetails } from "../../../lib/getFrameworkDetails"; interface PageProps { params: Promise<{ @@ -48,13 +50,37 @@ export default async function RequirementPage({ params }: PageProps) { redirect(`/${organizationId}/frameworks/${frameworkInstanceId}`); } + const frameworkName = getFrameworkDetails( + frameworkInstanceWithControls.frameworkId, + ).name; + + const siblingRequirements = Object.keys(requirements).filter( + (req) => req !== requirementKey, + ); + + const siblingRequirementsDropdown = siblingRequirements.map((req) => ({ + label: requirements[req as keyof typeof requirements].name, + href: `/${organizationId}/frameworks/${frameworkInstanceId}/requirements/${req}`, + })); + return ( -
- -
+ +
+ +
+
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/components/FrameworkCard.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/components/FrameworkCard.tsx index c9d63f260f..2f03491ad7 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/components/FrameworkCard.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/components/FrameworkCard.tsx @@ -7,7 +7,7 @@ import { cn } from "@bubba/ui/cn"; import { Progress } from "@bubba/ui/progress"; import Link from "next/link"; import { useParams } from "next/navigation"; -import { getFrameworkName } from "../lib/getFrameworkName"; +import { getFrameworkDetails } from "../lib/getFrameworkDetails"; import { FrameworkInstanceWithControls } from "../types"; interface FrameworkCardProps { @@ -44,7 +44,7 @@ export function FrameworkCard({

- {getFrameworkName(frameworkInstance.frameworkId)} + {getFrameworkDetails(frameworkInstance.frameworkId).name}

-
- -
+
+
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/layout.tsx index b8e126b65b..5287c8fc59 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/layout.tsx @@ -1,8 +1,7 @@ -import { auth } from "@bubba/auth"; import { getI18n } from "@/locales/server"; -import { redirect } from "next/navigation"; -import { SecondaryMenu } from "@bubba/ui/secondary-menu"; +import { auth } from "@bubba/auth"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; export default async function Layout({ children, @@ -21,15 +20,6 @@ export default async function Layout({ return (
- -
{children}
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkDetails.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkDetails.ts new file mode 100644 index 0000000000..4759675219 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkDetails.ts @@ -0,0 +1,6 @@ +import { frameworks } from "@bubba/data"; +import { FrameworkId } from "@bubba/db/types"; + +export function getFrameworkDetails(frameworkId: FrameworkId) { + return frameworks[frameworkId as keyof typeof frameworks]; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkName.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkName.ts deleted file mode 100644 index dbe01fc448..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getFrameworkName.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { frameworks } from "@bubba/data"; -import { FrameworkId } from "@bubba/db/types"; - -export function getFrameworkName(frameworkId: FrameworkId) { - return ( - frameworks[frameworkId as keyof typeof frameworks]?.name || frameworkId - ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getRequirementDetails.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getRequirementDetails.ts new file mode 100644 index 0000000000..983dc8454c --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/lib/getRequirementDetails.ts @@ -0,0 +1,14 @@ +import { requirements } from "@bubba/data"; + +export function getRequirementDetails( + frameworkId: keyof typeof requirements, + requirementId: string, +) { + const frameworkRequirements = + requirements[frameworkId as keyof typeof requirements] || []; + + const requirement = + frameworkRequirements[requirementId as keyof typeof frameworkRequirements]; + + return requirement; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/page.tsx index ccdd75926e..d0aa1c3ea4 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/page.tsx @@ -4,6 +4,7 @@ import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { FrameworksOverview } from "./components/FrameworksOverview"; import { getAllFrameworkInstancesWithControls } from "./data/getAllFrameworkInstancesWithControls"; +import PageWithBreadcrumb from "../../../../../../components/pages/PageWithBreadcrumb"; export async function generateMetadata() { const t = await getI18n(); @@ -30,9 +31,8 @@ export default async function DashboardPage() { }); return ( - + + + ); } diff --git a/apps/app/src/components/pages/PageWithBreadcrumb.tsx b/apps/app/src/components/pages/PageWithBreadcrumb.tsx new file mode 100644 index 0000000000..43f0b6cf42 --- /dev/null +++ b/apps/app/src/components/pages/PageWithBreadcrumb.tsx @@ -0,0 +1,124 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +} from "@bubba/ui/breadcrumb"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@bubba/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import Link from "next/link"; +import React from "react"; + +interface BreadcrumbDropdownItem { + label: string; + href: string; +} + +interface BreadcrumbItemType { + label: string; + href?: string; + dropdown?: BreadcrumbDropdownItem[]; + current?: boolean; +} + +interface PageLayoutProps { + children: React.ReactNode; + breadcrumbs: BreadcrumbItemType[]; + /** + * Maximum number of visible items before collapsing + * @default 3 + */ + maxItems?: number; +} + +export default function PageWithBreadcrumb({ + children, + breadcrumbs, + maxItems = 3, +}: PageLayoutProps) { + const totalItems = breadcrumbs.length; + const shouldCollapse = totalItems > maxItems; + + const visibleItems = shouldCollapse + ? [breadcrumbs[0], ...breadcrumbs.slice(totalItems - (maxItems - 1))] + : breadcrumbs; + + const hiddenItems = shouldCollapse + ? breadcrumbs.slice(1, totalItems - (maxItems - 1)) + : []; + + return ( +
+ + + {visibleItems.map((item, index) => { + const isFirst = index === 0; + const isLast = index === visibleItems.length - 1; + const showEllipsis = shouldCollapse && index === 1; + + return ( + + + {item.dropdown ? ( + + + {item.label} + + + + {item.dropdown.map((dropdownItem) => ( + + + {dropdownItem.label} + + + ))} + + + ) : item.current ? ( + {item.label} + ) : ( + + {item.label} + + )} + + {!isLast && } + {showEllipsis && hiddenItems.length > 0 && ( + <> + + + + + + + {hiddenItems.map((hiddenItem) => ( + + + {hiddenItem.label} + + + ))} + + + + + + )} + + ); + })} + + + {children} +
+ ); +}