Replace implemented column with CMMI maturity level
Drop the boolean implemented/not-implemented state in favor of a mandatory CMMI maturity level enum (NONE, INITIAL, MANAGED, DEFINED, QUANTITATIVELY_MANAGED, OPTIMIZING) stored as a Postgres enum type. The migration backfills existing rows (NOT_IMPLEMENTED → NONE, IMPLEMENTED → INITIAL), makes the column NOT NULL, and drops the old implemented column and its enum type. - maturityLevel is required on CreateControlInput and non-nullable (!) in the GraphQL schema - CLI displays human-readable CMMI labels instead of raw enum tokens - SOA table and published document use a single Maturity column in place of the old Implemented + Maturity columns - Remove ControlImplementationState type and all implemented references across backend, frontend, CLI, MCP, n8n, and E2E tests Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -19,14 +19,11 @@ import {
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Option } from "@probo/ui";
|
||||
|
||||
export const MATURITY_LEVEL_UNSET = "__unset__" as const;
|
||||
|
||||
export function ControlMaturityLevelOptions() {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Option value={MATURITY_LEVEL_UNSET}>{__("Not set")}</Option>
|
||||
{controlMaturityLevels.map(level => (
|
||||
<Option key={level} value={level}>
|
||||
{getControlMaturityLevelLabel(__, level)}
|
||||
|
||||
@@ -118,7 +118,6 @@ export const frameworkControlNodeQuery = graphql`
|
||||
sectionTitle
|
||||
description
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
maturityLevel
|
||||
canUpdate: permission(action: "core:control:update")
|
||||
|
||||
@@ -383,25 +383,17 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
</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")}
|
||||
<span className="text-sm text-txt-secondary">{__("Maturity level")}</span>
|
||||
<Badge variant="neutral" size="sm">
|
||||
{getControlMaturityLevelLabel(__, control.maturityLevel ?? "NONE")}
|
||||
</Badge>
|
||||
</div>
|
||||
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
|
||||
{control.maturityLevel === "NONE" && 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 className="flex items-center gap-2">
|
||||
<span className="text-sm text-txt-secondary">{__("Maturity level")}</span>
|
||||
<Badge variant="neutral" size="sm">
|
||||
{control.maturityLevel
|
||||
? getControlMaturityLevelLabel(__, control.maturityLevel)
|
||||
: __("Not set")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="mb-4">
|
||||
|
||||
@@ -32,10 +32,7 @@ import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { FrameworkControlDialogFragment$key } from "#/__generated__/core/FrameworkControlDialogFragment.graphql";
|
||||
import {
|
||||
ControlMaturityLevelOptions,
|
||||
MATURITY_LEVEL_UNSET,
|
||||
} from "#/components/form/ControlMaturityLevelOptions";
|
||||
import { ControlMaturityLevelOptions } from "#/components/form/ControlMaturityLevelOptions";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
|
||||
@@ -53,7 +50,6 @@ const controlFragment = graphql`
|
||||
description
|
||||
sectionTitle
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
maturityLevel
|
||||
}
|
||||
@@ -89,10 +85,7 @@ 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(),
|
||||
maturityLevel: z.enum([
|
||||
MATURITY_LEVEL_UNSET,
|
||||
"NONE",
|
||||
"INITIAL",
|
||||
"MANAGED",
|
||||
@@ -100,6 +93,7 @@ const schema = z.object({
|
||||
"QUANTITATIVELY_MANAGED",
|
||||
"OPTIMIZING",
|
||||
]),
|
||||
notImplementedJustification: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export function FrameworkControlDialog(props: Props) {
|
||||
@@ -124,9 +118,8 @@ export function FrameworkControlDialog(props: Props) {
|
||||
description: frameworkControl?.description ?? "",
|
||||
sectionTitle: frameworkControl?.sectionTitle ?? "",
|
||||
bestPractice: frameworkControl?.bestPractice ?? true,
|
||||
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
|
||||
maturityLevel: frameworkControl?.maturityLevel ?? "INITIAL",
|
||||
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
|
||||
maturityLevel: frameworkControl?.maturityLevel ?? MATURITY_LEVEL_UNSET,
|
||||
}),
|
||||
[frameworkControl],
|
||||
);
|
||||
@@ -141,14 +134,9 @@ export function FrameworkControlDialog(props: Props) {
|
||||
}, [defaultValues, reset]);
|
||||
|
||||
const bestPracticeValue = watch("bestPractice");
|
||||
const implementedValue = watch("implemented");
|
||||
const maturityLevelValue = watch("maturityLevel");
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
const maturityLevel = data.maturityLevel === MATURITY_LEVEL_UNSET
|
||||
? null
|
||||
: data.maturityLevel;
|
||||
|
||||
if (frameworkControl) {
|
||||
await mutate({
|
||||
variables: {
|
||||
@@ -158,9 +146,8 @@ 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),
|
||||
maturityLevel,
|
||||
maturityLevel: data.maturityLevel,
|
||||
notImplementedJustification: data.maturityLevel === "NONE" ? (data.notImplementedJustification || null) : null,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -173,9 +160,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),
|
||||
maturityLevel,
|
||||
maturityLevel: data.maturityLevel,
|
||||
notImplementedJustification: data.maturityLevel === "NONE" ? (data.notImplementedJustification || null) : null,
|
||||
},
|
||||
connections: [props.connectionId!],
|
||||
},
|
||||
@@ -232,23 +218,6 @@ export function FrameworkControlDialog(props: Props) {
|
||||
/>
|
||||
<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 className="flex items-center gap-2">
|
||||
<span className="text-sm">{__("Maturity level")}</span>
|
||||
<Select
|
||||
@@ -260,6 +229,15 @@ export function FrameworkControlDialog(props: Props) {
|
||||
<ControlMaturityLevelOptions />
|
||||
</Select>
|
||||
</div>
|
||||
{maturityLevelValue === "NONE" && (
|
||||
<Textarea
|
||||
id="notImplementedJustification"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
placeholder={__("Justification for non-implementation")}
|
||||
{...register("notImplementedJustification")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -93,7 +93,6 @@ const createApplicabilityStatementMutation = graphql`
|
||||
sectionTitle
|
||||
name
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
regulatory
|
||||
contractual
|
||||
|
||||
@@ -86,7 +86,6 @@ export const controlsFragment = graphql`
|
||||
sectionTitle
|
||||
name
|
||||
bestPractice
|
||||
implemented
|
||||
notImplementedJustification
|
||||
maturityLevel
|
||||
regulatory
|
||||
@@ -151,7 +150,6 @@ export default function StatementOfApplicabilityControlsTab({
|
||||
applicability: edge.node.applicability,
|
||||
justification: edge.node.justification,
|
||||
bestPractice: edge.node.control.bestPractice,
|
||||
implemented: edge.node.control.implemented,
|
||||
notImplementedJustification: edge.node.control.notImplementedJustification,
|
||||
maturityLevel: edge.node.control.maturityLevel,
|
||||
regulatory: edge.node.control.regulatory,
|
||||
@@ -226,7 +224,6 @@ export default function StatementOfApplicabilityControlsTab({
|
||||
<Th className="w-[9%]">{__("Framework")}</Th>
|
||||
<Th className="w-[17%]">{__("Control")}</Th>
|
||||
<Th className="w-[12%]">{__("Applicability")}</Th>
|
||||
<Th className="w-[12%]">{__("Implemented")}</Th>
|
||||
<Th className="w-[14%]">{__("Maturity")}</Th>
|
||||
<Th className="w-[7%]">{__("Regulatory")}</Th>
|
||||
<Th className="w-[7%]">{__("Contractual")}</Th>
|
||||
@@ -241,7 +238,7 @@ export default function StatementOfApplicabilityControlsTab({
|
||||
{linkedControls.length === 0 && (
|
||||
<Tr>
|
||||
<Td
|
||||
colSpan={canUpdate || canDelete ? 10 : 9}
|
||||
colSpan={canUpdate || canDelete ? 9 : 8}
|
||||
className="text-center text-txt-secondary py-12"
|
||||
>
|
||||
{__("No controls linked")}
|
||||
@@ -293,12 +290,12 @@ export default function StatementOfApplicabilityControlsTab({
|
||||
: (
|
||||
<div className="space-y-1">
|
||||
<Badge
|
||||
variant={control.implemented === "IMPLEMENTED" ? "success" : "danger"}
|
||||
variant={control.maturityLevel !== "NONE" ? "success" : "neutral"}
|
||||
size="sm"
|
||||
>
|
||||
{control.implemented === "IMPLEMENTED" ? __("Yes") : __("No")}
|
||||
{getControlMaturityLevelLabel(__, control.maturityLevel)}
|
||||
</Badge>
|
||||
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
|
||||
{control.maturityLevel === "NONE" && control.notImplementedJustification && (
|
||||
<p className="text-xs text-txt-secondary break-words">
|
||||
{control.notImplementedJustification}
|
||||
</p>
|
||||
@@ -306,17 +303,6 @@ export default function StatementOfApplicabilityControlsTab({
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
: (
|
||||
<Badge variant="neutral" size="sm">
|
||||
{control.maturityLevel
|
||||
? getControlMaturityLevelLabel(__, control.maturityLevel)
|
||||
: __("Not set")}
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{control.applicability === false
|
||||
? <span className="text-txt-tertiary">-</span>
|
||||
|
||||
Reference in New Issue
Block a user