Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { useI18n } from "@/locales/client";
import { Input } from "@comp/ui/input";
import { ColumnDef } from "@tanstack/react-table";
import { useMemo, useState } from "react";
import type { RelatedArtifact } from "../data/getRelatedArtifacts";
import { Card, CardTitle, CardHeader, CardContent } from "@comp/ui/card";
import { Artifact, Evidence, Policy } from "@comp/db/types";

interface ArtifactsTableProps {
artifacts: RelatedArtifact[];
artifacts: (Artifact & {
evidence: Evidence | null;
policy: Policy | null;
})[];
orgId: string;
controlId: string;
}
Expand All @@ -26,7 +29,14 @@ export function ArtifactsTable({
const [searchTerm, setSearchTerm] = useState("");

// Define columns for artifacts table
const columns = useMemo<ColumnDef<RelatedArtifact>[]>(
const columns = useMemo<
ColumnDef<
Artifact & {
evidence: Evidence | null;
policy: Policy | null;
}
>[]
>(
() => [
{
accessorKey: "name",
Expand All @@ -36,10 +46,24 @@ export function ArtifactsTable({
title={t("frameworks.artifacts.table.name")}
/>
),
cell: ({ row }) => <span>{row.original.name}</span>,
cell: ({ row }) => {
const name =
row.original.type === "evidence"
? row.original.evidence?.name
: row.original.policy?.name;
return <span>{name}</span>;
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
return rowA.original.name.localeCompare(rowB.original.name);
const nameA =
rowA.original.type === "evidence"
? rowA.original.evidence?.name || ""
: rowA.original.policy?.name || "";
const nameB =
rowB.original.type === "evidence"
? rowB.original.evidence?.name || ""
: rowB.original.policy?.name || "";
return nameA.localeCompare(nameB);
},
},
{
Expand Down Expand Up @@ -88,7 +112,11 @@ export function ArtifactsTable({
return artifacts.filter(
(artifact) =>
artifact.id.toLowerCase().includes(searchLower) ||
artifact.name.toLowerCase().includes(searchLower) ||
(artifact.type === "evidence"
? artifact.evidence?.name?.toLowerCase().includes(searchLower) ||
false
: artifact.policy?.name?.toLowerCase().includes(searchLower) ||
false) ||
artifact.type.toLowerCase().includes(searchLower),
);
}, [artifacts, searchTerm]);
Expand Down Expand Up @@ -127,8 +155,18 @@ export function ArtifactsTable({
</div>
<DataTable
table={table.table}
rowClickBasePath={`/${orgId}/controls/${controlId}/artifacts`}
getRowId={(row) => row.id}
rowClickBasePath={`/${orgId}/`}
getRowId={(row) => {
if (row.type === "policy") {
return `/policies/${row.policyId}`;
}

if (row.type === "evidence") {
return `/evidence/${row.evidenceId}`;
}

return `/artifacts/${row.id}`;
}}
tableId={"a"}
/>
</CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import { DataTableColumnHeader } from "@/components/data-table/data-table-column
import { DataTableSortList } from "@/components/data-table/data-table-sort-list";
import { useDataTable } from "@/hooks/use-data-table";
import { useI18n } from "@/locales/client";
import type { FrameworkId, RequirementMap } from "@comp/db/types";
import type {
FrameworkId,
FrameworkInstance,
RequirementMap,
} from "@comp/db/types";
import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card";
import { Input } from "@comp/ui/input";
import { ColumnDef } from "@tanstack/react-table";
import { useMemo, useState } from "react";
import { getFrameworkDetails } from "../../../frameworks/lib/getFrameworkDetails";
import { getRequirementDetails } from "../../../frameworks/lib/getRequirementDetails";
import { Card, CardTitle, CardHeader } from "@comp/ui/card";
import { CardContent } from "@comp/ui/card";

interface RequirementsTableProps {
requirements: RequirementMap[];
requirements: (RequirementMap & { frameworkInstance: FrameworkInstance })[];
orgId: string;
}

Expand All @@ -26,29 +30,12 @@ export function RequirementsTable({
const [searchTerm, setSearchTerm] = useState("");

// Define columns for requirements table
const columns = useMemo<ColumnDef<RequirementMap>[]>(
const columns = useMemo<
ColumnDef<RequirementMap & { frameworkInstance: FrameworkInstance }>[]
>(
() => [
{
accessorKey: "requirementId",
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t("frameworks.requirements.table.id")}
/>
),
cell: ({ row }) => {
const requirementId = row.original.requirementId.split("_").pop();
return <span className="font-mono text-xs">{requirementId}</span>;
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.original.requirementId.split("_").pop() || "";
const b = rowB.original.requirementId.split("_").pop() || "";
return a.localeCompare(b);
},
},
{
accessorKey: "name",
accessorKey: "id",
header: ({ column }) => (
<DataTableColumnHeader
column={column}
Expand All @@ -62,7 +49,15 @@ export function RequirementsTable({
frameworkId as FrameworkId,
requirementId,
);
return <span>{details?.name}</span>;

const frameworkDetails = getFrameworkDetails(
row.original.frameworkInstance.frameworkId,
);
return (
<span>
{frameworkDetails?.name} - {details?.name}
</span>
);
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
Expand Down Expand Up @@ -147,6 +142,7 @@ export function RequirementsTable({

// Search in ID, name, and description
return (
frameworkId.toLowerCase().includes(searchLower) ||
requirementId.toLowerCase().includes(searchLower) ||
details?.name?.toLowerCase().includes(searchLower) ||
false ||
Expand All @@ -164,7 +160,7 @@ export function RequirementsTable({
shallow: false,
getRowId: (row) => row.id,
initialState: {
sorting: [{ id: "requirementId", desc: false }],
sorting: [{ id: "id", desc: false }],
},
tableId: "r",
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
"use client";

import { DisplayFrameworkStatus } from "@/components/frameworks/framework-status";
import type { Control } from "@comp/db/types";
import { useI18n } from "@/locales/client";
import type {
Artifact,
Control,
Evidence,
FrameworkInstance,
Policy,
RequirementMap,
} from "@comp/db/types";
import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card";
import { useParams } from "next/navigation";
import { useMemo } from "react";
import type { ControlProgressResponse } from "../data/getOrganizationControlProgress";
import { SingleControlSkeleton } from "./SingleControlSkeleton";
import { useI18n } from "@/locales/client";
import { useParams } from "next/navigation";
import { RequirementsTable } from "./RequirementsTable";
import { ArtifactsTable } from "./ArtifactsTable";
import type { RelatedArtifact } from "../data/getRelatedArtifacts";
import { Separator } from "@comp/ui/separator";
import { RequirementsTable } from "./RequirementsTable";
import { SingleControlSkeleton } from "./SingleControlSkeleton";

interface SingleControlProps {
control: Control & {
requirementsMapped: any[];
requirementsMapped: (RequirementMap & {
frameworkInstance: FrameworkInstance;
})[];
};
controlProgress: ControlProgressResponse;
relatedArtifacts: RelatedArtifact[];
relatedArtifacts: (Artifact & {
evidence: Evidence | null;
policy: Policy | null;
})[];
}

export const SingleControl = ({
Expand Down Expand Up @@ -63,13 +73,10 @@ export const SingleControl = ({
<p className="text-sm text-muted-foreground">{control.description}</p>
</CardContent>
</Card>

<RequirementsTable
requirements={control.requirementsMapped}
orgId={orgId}
/>
<Separator />

<ArtifactsTable
artifacts={relatedArtifacts}
orgId={orgId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export const getControl = async (id: string) => {
id,
},
include: {
requirementsMapped: true,
requirementsMapped: {
include: {
frameworkInstance: true,
},
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@

import { auth } from "@comp/auth";
import { db } from "@comp/db";
import { Artifact, Evidence, Policy } from "@comp/db/types";
import { headers } from "next/headers";
import { cache } from "react";

export interface RelatedArtifact {
id: string;
name: string;
type: string;
createdAt: string;
}

interface GetRelatedArtifactsParams {
organizationId: string;
controlId: string;
Expand All @@ -21,7 +15,12 @@ export const getRelatedArtifacts = cache(
async ({
organizationId,
controlId,
}: GetRelatedArtifactsParams): Promise<RelatedArtifact[]> => {
}: GetRelatedArtifactsParams): Promise<
(Artifact & {
evidence: Evidence | null;
policy: Policy | null;
})[]
> => {
try {
const session = await auth.api.getSession({
headers: await headers(),
Expand Down Expand Up @@ -52,27 +51,9 @@ export const getRelatedArtifacts = cache(
}

// Transform the artifacts into the format expected by the UI
return control.artifacts.map((artifact) => {
let name = "Unknown";
let displayType = artifact.type;

if (artifact.policy) {
name = artifact.policy.name;
displayType = "policy";
} else if (artifact.evidence) {
name = artifact.evidence.name;
displayType = "evidence";
}

return {
id: artifact.id,
name,
type: displayType,
createdAt: artifact.createdAt.toISOString(),
};
});
return control.artifacts;
} catch (error) {
console.error("Error fetching related artifacts:", error);
console.error("Error fetching Linked Artifacts:", error);
return [];
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getOrganizationControlProgress } from "./data/getOrganizationControlPro
import type { ControlProgressResponse } from "./data/getOrganizationControlProgress";
import { headers } from "next/headers";
import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb";
import { getRelatedArtifacts } from "./data/getRelatedArtifacts";
import { getRelatedArtifacts } from "@/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/data/getRelatedArtifacts";

interface ControlPageProps {
params: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function getControlColumns(): ColumnDef<
id: "mappedRequirements",
accessorKey: "mappedRequirements",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Mapped Requirements" />
<DataTableColumnHeader column={column} title="Linked Requirements" />
),
cell: ({ row }) => {
const control = row.original;
Expand All @@ -94,8 +94,8 @@ export function getControlColumns(): ColumnDef<
);
},
meta: {
label: "Mapped Requirements",
placeholder: "Search mapped requirements...",
label: "Linked Requirements",
placeholder: "Search Linked Requirements...",
variant: "text",
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ export function EvidenceDetails({ assignees, evidence }: EvidenceDetailsProps) {
in compliance reports.
</div>
)}
<Alert>
<FileIcon className="h-4 w-4" />
<AlertTitle>{evidence.name} Evidence</AlertTitle>
<AlertDescription className="mt-4">
{evidence.description || "No description provided."}
</AlertDescription>
</Alert>
<ReviewSection
evidence={evidence}
lastPublishedAt={evidence.lastPublishedAt}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,9 @@ export function ReviewSection({
<CardHeader>
<CardTitle className="text-base flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h2 className="text-lg font-medium">Evidence Overview</h2>
<h2 className="text-lg font-medium">{evidence.name}</h2>
<h3 className="text-sm text-muted-foreground">
Manage review frequency, department assignment, and track upcoming
review dates
{evidence.description}
</h3>
</div>
</CardTitle>
Expand Down
Loading