-
Notifications
You must be signed in to change notification settings - Fork 422
Mariano/fixes #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mariano/fixes #161
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,178 +1,177 @@ | ||||||
| "use client"; | ||||||
|
|
||||||
| import { useI18n } from "@/locales/client"; | ||||||
| import type { | ||||||
| Framework, | ||||||
| OrganizationFramework, | ||||||
| } from "@bubba/db/types"; | ||||||
| import type { Framework, OrganizationFramework } from "@bubba/db/types"; | ||||||
| import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; | ||||||
| import { Progress } from "@bubba/ui/progress"; | ||||||
| import Link from "next/link"; | ||||||
| import { useMemo } from "react"; | ||||||
| import type { OrganizationControlType } from "../overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns"; | ||||||
| import { useOrganizationCategories } from "../overview/frameworks/[frameworkId]/hooks/useOrganizationCategories"; | ||||||
| import { useParams } from "next/navigation"; | ||||||
|
|
||||||
| interface Props { | ||||||
| frameworks: (OrganizationFramework & { | ||||||
| framework: Framework; | ||||||
| })[]; | ||||||
| frameworks: (OrganizationFramework & { | ||||||
| framework: Framework; | ||||||
| })[]; | ||||||
| } | ||||||
|
|
||||||
| // Helper function to check if a control is compliant based on its requirements | ||||||
| const isControlCompliant = (control: OrganizationControlType) => { | ||||||
| // First, check if the control has the direct status of "compliant" | ||||||
| if (control.status === "compliant") { | ||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| // Then check the requirements if they exist | ||||||
| const requirements = control.requirements; | ||||||
|
|
||||||
| if (!requirements || requirements.length === 0) { | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| const totalRequirements = requirements.length; | ||||||
| const completedRequirements = requirements.filter((req) => { | ||||||
| let isCompleted = false; | ||||||
|
|
||||||
| switch (req.type) { | ||||||
| case "policy": | ||||||
| isCompleted = req.organizationPolicy?.status === "published"; | ||||||
| break; | ||||||
| case "file": | ||||||
| isCompleted = !!req.fileUrl; | ||||||
| break; | ||||||
| case "evidence": | ||||||
| isCompleted = req.organizationEvidence?.published === true; | ||||||
| break; | ||||||
| default: | ||||||
| isCompleted = req.published || false; | ||||||
| } | ||||||
|
|
||||||
| return isCompleted; | ||||||
| }).length; | ||||||
|
|
||||||
| return completedRequirements === totalRequirements; | ||||||
| // First, check if the control has the direct status of "compliant" | ||||||
| if (control.status === "compliant") { | ||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| // Then check the requirements if they exist | ||||||
| const requirements = control.requirements; | ||||||
|
|
||||||
| if (!requirements || requirements.length === 0) { | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| const totalRequirements = requirements.length; | ||||||
| const completedRequirements = requirements.filter((req) => { | ||||||
| let isCompleted = false; | ||||||
|
|
||||||
| switch (req.type) { | ||||||
| case "policy": | ||||||
| isCompleted = req.organizationPolicy?.status === "published"; | ||||||
| break; | ||||||
| case "file": | ||||||
| isCompleted = !!req.fileUrl; | ||||||
| break; | ||||||
| case "evidence": | ||||||
| isCompleted = req.organizationEvidence?.published === true; | ||||||
| break; | ||||||
| default: | ||||||
| isCompleted = req.published || false; | ||||||
| } | ||||||
|
|
||||||
| return isCompleted; | ||||||
| }).length; | ||||||
|
|
||||||
| return completedRequirements === totalRequirements; | ||||||
| }; | ||||||
|
|
||||||
| // Individual FrameworkCard component | ||||||
| function FrameworkCard({ | ||||||
| framework, | ||||||
| framework, | ||||||
| }: { framework: OrganizationFramework & { framework: Framework } }) { | ||||||
| const { data: organizationCategories, isLoading } = useOrganizationCategories( | ||||||
| framework.framework.id, | ||||||
| ); | ||||||
|
|
||||||
| // Transform the organizationCategories into controls | ||||||
| const controls = useMemo(() => { | ||||||
| if (!organizationCategories) return []; | ||||||
|
|
||||||
| return 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: framework.framework.id, | ||||||
| category: category.name, | ||||||
| requirements: control.OrganizationControlRequirement, | ||||||
| })), | ||||||
| ); | ||||||
| }, [ | ||||||
| organizationCategories, | ||||||
| framework.framework.id, | ||||||
| ]) as OrganizationControlType[]; | ||||||
|
|
||||||
| // Calculate framework compliance based on controls | ||||||
| const compliance = useMemo(() => { | ||||||
| if (isLoading || controls.length === 0) return 0; | ||||||
|
|
||||||
| const totalControls = controls.length; | ||||||
| const compliantControls = controls.filter(isControlCompliant).length; | ||||||
|
|
||||||
| return totalControls > 0 | ||||||
| ? Math.round((compliantControls / totalControls) * 100) | ||||||
| : 0; | ||||||
| }, [controls, isLoading]); | ||||||
|
|
||||||
| if (isLoading) { | ||||||
| return ( | ||||||
| <div className="flex items-start gap-4 rounded-lg p-4 animate-pulse bg-zinc-800/20"> | ||||||
| <div className="flex-shrink-0 h-12 w-12 rounded-full bg-zinc-800/40" /> | ||||||
| <div className="flex-1 space-y-2"> | ||||||
| <div className="h-5 w-2/3 bg-zinc-800/40 rounded" /> | ||||||
| <div className="h-2 bg-zinc-800/40 rounded" /> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <Link | ||||||
| href={`/overview/frameworks/${framework.framework.id}`} | ||||||
| className="flex items-start gap-4 rounded-lg p-4 hover:bg-zinc-800/40 transition-colors duration-200" | ||||||
| > | ||||||
| <div className="flex-shrink-0 h-12 w-12 rounded-full overflow-hidden bg-zinc-800 flex items-center justify-center"> | ||||||
| <div className="text-lg font-bold text-zinc-400"> | ||||||
| {framework.framework.name.substring(0, 2).toUpperCase()} | ||||||
| </div> | ||||||
| </div> | ||||||
| <div className="flex-1 space-y-2"> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <h3 className="font-medium">{framework.framework.name}</h3> | ||||||
| <span className="text-sm font-medium text-muted-foreground"> | ||||||
| {compliance}% Compliant | ||||||
| </span> | ||||||
| </div> | ||||||
| <Progress | ||||||
| value={compliance} | ||||||
| className="h-2 bg-zinc-800 [&>div]:bg-emerald-500" | ||||||
| /> | ||||||
| </div> | ||||||
| </Link> | ||||||
| ); | ||||||
| const { data: organizationCategories, isLoading } = useOrganizationCategories( | ||||||
| framework.framework.id, | ||||||
| ); | ||||||
| const { orgId } = useParams<{ orgId: string }>(); | ||||||
|
|
||||||
| // Transform the organizationCategories into controls | ||||||
| const controls = useMemo(() => { | ||||||
| if (!organizationCategories) return []; | ||||||
|
|
||||||
| return 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: framework.framework.id, | ||||||
| category: category.name, | ||||||
| requirements: control.OrganizationControlRequirement, | ||||||
| })), | ||||||
| ); | ||||||
| }, [ | ||||||
| organizationCategories, | ||||||
| framework.framework.id, | ||||||
| ]) as OrganizationControlType[]; | ||||||
|
|
||||||
| // Calculate framework compliance based on controls | ||||||
| const compliance = useMemo(() => { | ||||||
| if (isLoading || controls.length === 0) return 0; | ||||||
|
|
||||||
| const totalControls = controls.length; | ||||||
| const compliantControls = controls.filter(isControlCompliant).length; | ||||||
|
|
||||||
| return totalControls > 0 | ||||||
| ? Math.round((compliantControls / totalControls) * 100) | ||||||
| : 0; | ||||||
| }, [controls, isLoading]); | ||||||
|
|
||||||
| if (isLoading) { | ||||||
| return ( | ||||||
| <div className="flex items-start gap-4 rounded-lg p-4 animate-pulse bg-zinc-800/20"> | ||||||
| <div className="flex-shrink-0 h-12 w-12 rounded-full bg-zinc-800/40" /> | ||||||
| <div className="flex-1 space-y-2"> | ||||||
| <div className="h-5 w-2/3 bg-zinc-800/40 rounded" /> | ||||||
| <div className="h-2 bg-zinc-800/40 rounded" /> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <Link | ||||||
| href={`${orgId}/overview/frameworks/${framework.framework.id}`} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing leading slash in URL path The URL construction appears to be missing a leading slash before the Apply this change: -href={`${orgId}/overview/frameworks/${framework.framework.id}`}
+href={`/${orgId}/overview/frameworks/${framework.framework.id}`}📝 Committable suggestion
Suggested change
|
||||||
| className="flex items-start gap-4 rounded-lg p-4 hover:bg-zinc-800/40 transition-colors duration-200" | ||||||
| > | ||||||
| <div className="flex-shrink-0 h-12 w-12 rounded-full overflow-hidden bg-zinc-800 flex items-center justify-center"> | ||||||
| <div className="text-lg font-bold text-zinc-400"> | ||||||
| {framework.framework.name.substring(0, 2).toUpperCase()} | ||||||
| </div> | ||||||
| </div> | ||||||
| <div className="flex-1 space-y-2"> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <h3 className="font-medium">{framework.framework.name}</h3> | ||||||
| <span className="text-sm font-medium text-muted-foreground"> | ||||||
| {compliance}% Compliant | ||||||
| </span> | ||||||
| </div> | ||||||
| <Progress | ||||||
| value={compliance} | ||||||
| className="h-2 bg-zinc-800 [&>div]:bg-emerald-500" | ||||||
| /> | ||||||
| </div> | ||||||
| </Link> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Main component | ||||||
| export function RequirementStatus({ frameworks }: Props) { | ||||||
| const t = useI18n(); | ||||||
| const isLoading = !frameworks; | ||||||
|
|
||||||
| return ( | ||||||
| <Card className="border border-border bg-background select-none"> | ||||||
| <CardHeader className="flex flex-row items-center justify-between"> | ||||||
| <CardTitle>{t("frameworks.title")}</CardTitle> | ||||||
| </CardHeader> | ||||||
| <CardContent> | ||||||
| {isLoading ? ( | ||||||
| <div className="flex flex-col items-center justify-center py-8 text-center"> | ||||||
| <div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" /> | ||||||
| <p className="mt-4 text-sm text-muted-foreground"> | ||||||
| Loading compliance data... | ||||||
| </p> | ||||||
| </div> | ||||||
| ) : frameworks.length === 0 ? ( | ||||||
| <div className="flex flex-col items-center justify-center py-8 text-center"> | ||||||
| <p className="text-sm text-muted-foreground"> | ||||||
| {t("frameworks.overview.empty.description")} | ||||||
| </p> | ||||||
| </div> | ||||||
| ) : ( | ||||||
| <div className="space-y-8"> | ||||||
| {/* Framework List */} | ||||||
| <div className="space-y-6"> | ||||||
| {frameworks.map((framework) => ( | ||||||
| <FrameworkCard | ||||||
| key={framework.framework.id} | ||||||
| framework={framework} | ||||||
| /> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
| </CardContent> | ||||||
| </Card> | ||||||
| ); | ||||||
| const t = useI18n(); | ||||||
| const isLoading = !frameworks; | ||||||
|
|
||||||
| return ( | ||||||
| <Card className="border border-border bg-background select-none"> | ||||||
| <CardHeader className="flex flex-row items-center justify-between"> | ||||||
| <CardTitle>{t("frameworks.title")}</CardTitle> | ||||||
| </CardHeader> | ||||||
| <CardContent> | ||||||
| {isLoading ? ( | ||||||
| <div className="flex flex-col items-center justify-center py-8 text-center"> | ||||||
| <div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" /> | ||||||
| <p className="mt-4 text-sm text-muted-foreground"> | ||||||
| Loading compliance data... | ||||||
| </p> | ||||||
| </div> | ||||||
| ) : frameworks.length === 0 ? ( | ||||||
| <div className="flex flex-col items-center justify-center py-8 text-center"> | ||||||
| <p className="text-sm text-muted-foreground"> | ||||||
| {t("frameworks.overview.empty.description")} | ||||||
| </p> | ||||||
| </div> | ||||||
| ) : ( | ||||||
| <div className="space-y-8"> | ||||||
| {/* Framework List */} | ||||||
| <div className="space-y-6"> | ||||||
| {frameworks.map((framework) => ( | ||||||
| <FrameworkCard | ||||||
| key={framework.framework.id} | ||||||
| framework={framework} | ||||||
| /> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
| </CardContent> | ||||||
| </Card> | ||||||
| ); | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add leading slash to prevent incorrect navigation
The href attribute is missing a leading slash, which could lead to incorrect navigation. This is inconsistent with other links in the same file (lines 156, 166, 176) that correctly use a leading slash.
📝 Committable suggestion