Add implemented state and justification to controls
Introduce `implemented` enum (IMPLEMENTED/NOT_IMPLEMENTED) and `not_implemented_justification` (nullable text) fields on the Control entity across all API surfaces (GraphQL, MCP, CLI), database, frontend, and SOA export. The database stores implementation state as a PostgreSQL enum `control_implementation_state`. Controls default to IMPLEMENTED during migration. The SOA list and PDF export show implementation status alongside applicability, with "-" for non-applicable controls. Justification columns are renamed for clarity: "Justification for non-applicability" and "Justification for non-implementation". Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -103,6 +103,9 @@ export const frameworkControlNodeQuery = graphql`
|
||||
name
|
||||
sectionTitle
|
||||
description
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
canUpdate: permission(action: "core:control:update")
|
||||
canDelete: permission(action: "core:control:delete")
|
||||
canCreateMeasureMapping: permission(
|
||||
|
||||
@@ -3,7 +3,9 @@ import { promisifyMutation } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
DropdownItem,
|
||||
IconPencil,
|
||||
IconTrashCan,
|
||||
@@ -354,6 +356,28 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
{control.description}
|
||||
</div>
|
||||
)}
|
||||
<Card padded className="mb-6 mt-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-txt-secondary">{__("Best Practice")}</span>
|
||||
<Badge variant={control.bestPractice ? "success" : "neutral"} size="sm">
|
||||
{control.bestPractice ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-txt-secondary">{__("Implemented")}</span>
|
||||
<Badge variant={control.implemented === "IMPLEMENTED" ? "success" : "warning"} size="sm">
|
||||
{control.implemented === "IMPLEMENTED" ? __("Implemented") : __("Not Implemented")}
|
||||
</Badge>
|
||||
</div>
|
||||
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
|
||||
<div>
|
||||
<span className="text-xs text-txt-secondary">{__("Justification for non-implementation")}</span>
|
||||
<div className="text-sm mt-0.5 whitespace-pre-wrap">{control.notImplementedJustification}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="mb-4">
|
||||
<LinkedMeasuresCard
|
||||
variant="card"
|
||||
|
||||
@@ -34,6 +34,8 @@ const controlFragment = graphql`
|
||||
description
|
||||
sectionTitle
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -67,6 +69,8 @@ const schema = z.object({
|
||||
description: z.string().optional().nullable(),
|
||||
sectionTitle: z.string(),
|
||||
bestPractice: z.boolean(),
|
||||
implemented: z.enum(["IMPLEMENTED", "NOT_IMPLEMENTED"]),
|
||||
notImplementedJustification: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export function FrameworkControlDialog(props: Props) {
|
||||
@@ -91,6 +95,8 @@ export function FrameworkControlDialog(props: Props) {
|
||||
description: frameworkControl?.description ?? "",
|
||||
sectionTitle: frameworkControl?.sectionTitle ?? "",
|
||||
bestPractice: frameworkControl?.bestPractice ?? true,
|
||||
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
|
||||
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
|
||||
}),
|
||||
[frameworkControl],
|
||||
);
|
||||
@@ -105,10 +111,10 @@ export function FrameworkControlDialog(props: Props) {
|
||||
}, [defaultValues, reset]);
|
||||
|
||||
const bestPracticeValue = watch("bestPractice");
|
||||
const implementedValue = watch("implemented");
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
if (frameworkControl) {
|
||||
// Update the control
|
||||
await mutate({
|
||||
variables: {
|
||||
input: {
|
||||
@@ -117,11 +123,12 @@ export function FrameworkControlDialog(props: Props) {
|
||||
description: data.description || null,
|
||||
sectionTitle: data.sectionTitle,
|
||||
bestPractice: data.bestPractice,
|
||||
implemented: data.implemented,
|
||||
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create a new control
|
||||
await mutate({
|
||||
variables: {
|
||||
input: {
|
||||
@@ -130,6 +137,8 @@ export function FrameworkControlDialog(props: Props) {
|
||||
description: data.description || null,
|
||||
sectionTitle: data.sectionTitle,
|
||||
bestPractice: data.bestPractice ?? true,
|
||||
implemented: data.implemented ?? "IMPLEMENTED",
|
||||
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
|
||||
},
|
||||
connections: [props.connectionId!],
|
||||
},
|
||||
@@ -167,7 +176,7 @@ export function FrameworkControlDialog(props: Props) {
|
||||
id="title"
|
||||
required
|
||||
variant="title"
|
||||
placeholder={__("Document title")}
|
||||
placeholder={__("Control name")}
|
||||
{...register("name")}
|
||||
/>
|
||||
<Textarea
|
||||
@@ -177,14 +186,33 @@ export function FrameworkControlDialog(props: Props) {
|
||||
placeholder={__("Add description")}
|
||||
{...register("description")}
|
||||
/>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={bestPracticeValue}
|
||||
onChange={checked =>
|
||||
setValue("bestPractice", checked)}
|
||||
/>
|
||||
<span className="text-sm">{__("Best Practice")}</span>
|
||||
</label>
|
||||
<div className="border border-border-low rounded-xl p-3 space-y-3 mt-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={bestPracticeValue}
|
||||
onChange={checked =>
|
||||
setValue("bestPractice", checked)}
|
||||
/>
|
||||
<span className="text-sm">{__("Best Practice")}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={implementedValue === "IMPLEMENTED"}
|
||||
onChange={checked =>
|
||||
setValue("implemented", checked ? "IMPLEMENTED" : "NOT_IMPLEMENTED")}
|
||||
/>
|
||||
<span className="text-sm">{__("Implemented")}</span>
|
||||
</label>
|
||||
{implementedValue === "NOT_IMPLEMENTED" && (
|
||||
<Textarea
|
||||
id="notImplementedJustification"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
placeholder={__("Justification for non-implementation")}
|
||||
{...register("notImplementedJustification")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isMutating}>
|
||||
|
||||
@@ -79,6 +79,8 @@ const createApplicabilityStatementMutation = graphql`
|
||||
sectionTitle
|
||||
name
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
regulatory
|
||||
contractual
|
||||
riskAssessment
|
||||
|
||||
@@ -71,6 +71,8 @@ export const controlsFragment = graphql`
|
||||
sectionTitle
|
||||
name
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
regulatory
|
||||
contractual
|
||||
riskAssessment
|
||||
@@ -135,6 +137,8 @@ export default function StateOfApplicabilityControlsTab({
|
||||
applicability: edge.node.applicability,
|
||||
justification: edge.node.justification,
|
||||
bestPractice: edge.node.control.bestPractice,
|
||||
implemented: edge.node.control.implemented,
|
||||
notImplementedJustification: edge.node.control.notImplementedJustification,
|
||||
regulatory: edge.node.control.regulatory,
|
||||
contractual: edge.node.control.contractual,
|
||||
riskAssessment: edge.node.control.riskAssessment,
|
||||
@@ -204,29 +208,19 @@ export default function StateOfApplicabilityControlsTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Table>
|
||||
<Table className="table-fixed w-full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th className="w-32">{__("Framework")}</Th>
|
||||
<Th>{__("Control")}</Th>
|
||||
<Th className="w-28 text-center">
|
||||
{__("Applicability")}
|
||||
</Th>
|
||||
<Th className="min-w-48">{__("Justification")}</Th>
|
||||
<Th className="w-24 text-center">
|
||||
{__("Regulatory")}
|
||||
</Th>
|
||||
<Th className="w-24 text-center">
|
||||
{__("Contractual")}
|
||||
</Th>
|
||||
<Th className="w-32 text-center">
|
||||
{__("Best Practice")}
|
||||
</Th>
|
||||
<Th className="w-36 text-center">
|
||||
{__("Risk Assessment")}
|
||||
</Th>
|
||||
<Th className="w-[10%]">{__("Framework")}</Th>
|
||||
<Th className="w-[20%]">{__("Control")}</Th>
|
||||
<Th className="w-[15%]">{__("Applicability")}</Th>
|
||||
<Th className="w-[15%]">{__("Implemented")}</Th>
|
||||
<Th className="w-[8%]">{__("Regulatory")}</Th>
|
||||
<Th className="w-[8%]">{__("Contractual")}</Th>
|
||||
<Th className="w-[8%]">{__("Best Practice")}</Th>
|
||||
<Th className="w-[8%]">{__("Risk Assessment")}</Th>
|
||||
{(canUpdate || canDelete) && (
|
||||
<Th className="w-12"></Th>
|
||||
<Th className="w-[4%]"></Th>
|
||||
)}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -250,103 +244,82 @@ export default function StateOfApplicabilityControlsTab({
|
||||
{control.frameworkName}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-txt-tertiary">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-xs text-txt-tertiary">
|
||||
{control.sectionTitle}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="text-xs">
|
||||
{control.name}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
<div className="space-y-1">
|
||||
{control.applicability !== null
|
||||
? (
|
||||
<Badge
|
||||
variant={
|
||||
control.applicability
|
||||
? "success"
|
||||
: "danger"
|
||||
}
|
||||
variant={control.applicability ? "success" : "danger"}
|
||||
size="sm"
|
||||
>
|
||||
{control.applicability
|
||||
? __("Yes")
|
||||
: __("No")}
|
||||
{control.applicability ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
)
|
||||
: (
|
||||
<span className="text-txt-tertiary">
|
||||
-
|
||||
</span>
|
||||
<span className="text-txt-tertiary">-</span>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="text-sm text-txt-secondary line-clamp-2">
|
||||
{control.justification || (
|
||||
<span className="text-txt-tertiary italic">
|
||||
-
|
||||
</span>
|
||||
{control.justification && (
|
||||
<p className="text-xs text-txt-secondary break-words">
|
||||
{control.justification}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
<div className="space-y-1">
|
||||
<Badge
|
||||
variant={control.regulatory ? "success" : "danger"}
|
||||
variant={control.implemented === "IMPLEMENTED" ? "success" : "danger"}
|
||||
size="sm"
|
||||
>
|
||||
{control.regulatory ? __("Yes") : __("No")}
|
||||
{control.implemented === "IMPLEMENTED" ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
|
||||
<p className="text-xs text-txt-secondary break-words">
|
||||
{control.notImplementedJustification}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
<Badge
|
||||
variant={control.contractual ? "success" : "danger"}
|
||||
size="sm"
|
||||
>
|
||||
{control.contractual ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: control.regulatory
|
||||
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
|
||||
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
<Badge
|
||||
variant={control.bestPractice ? "success" : "danger"}
|
||||
size="sm"
|
||||
>
|
||||
{control.bestPractice ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: control.contractual
|
||||
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
|
||||
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
<Badge
|
||||
variant={control.riskAssessment ? "success" : "danger"}
|
||||
size="sm"
|
||||
>
|
||||
{control.riskAssessment ? __("Yes") : __("No")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: control.bestPractice
|
||||
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
|
||||
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||
</Td>
|
||||
<Td>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: control.riskAssessment
|
||||
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
|
||||
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||
</Td>
|
||||
{(canUpdate || canDelete) && (
|
||||
<Td noLink className="text-end">
|
||||
|
||||
Reference in New Issue
Block a user