Add control importance

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-13 08:46:51 +01:00
parent 4ede969b04
commit d4280b1111
16 changed files with 1162 additions and 186 deletions

View File

@@ -64,6 +64,7 @@ const controlOverviewPageQuery = graphql`
name name
description description
state state
importance
category category
tasks(first: 100) @connection(key: "ControlOverviewPage_tasks") { tasks(first: 100) @connection(key: "ControlOverviewPage_tasks") {
__id __id
@@ -195,6 +196,60 @@ function ControlOverviewPageContent({
const { organizationId, frameworkId, controlId } = useParams(); const { organizationId, frameworkId, controlId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
const formatImportance = (importance: string | undefined): string => {
if (!importance) return "";
const upperImportance = importance.toUpperCase();
if (upperImportance === "MANDATORY") return "Mandatory";
if (upperImportance === "PREFERRED") return "Preferred";
if (upperImportance === "ADVANCED") return "Advanced";
const formatted = importance.toLowerCase();
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
};
const formatState = (state: string | undefined): string => {
if (!state) return "";
const upperState = state.toUpperCase();
if (upperState === "NOT_STARTED") return "Not Started";
if (upperState === "IN_PROGRESS") return "In Progress";
if (upperState === "NOT_APPLICABLE") return "Not Applicable";
if (upperState === "IMPLEMENTED") return "Implemented";
// Fallback for any other states
const formatted = state.toLowerCase();
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
};
const getStateColor = (state: string | undefined): string => {
if (!state) return "bg-gray-100 text-gray-800";
const upperState = state.toUpperCase();
if (upperState === "NOT_STARTED") return "bg-gray-100 text-gray-800";
if (upperState === "IN_PROGRESS") return "bg-blue-100 text-blue-800";
if (upperState === "NOT_APPLICABLE") return "bg-purple-100 text-purple-800";
if (upperState === "IMPLEMENTED") return "bg-green-100 text-green-800";
return "bg-gray-100 text-gray-800";
};
const getImportanceColor = (importance: string | undefined): string => {
if (!importance) return "bg-gray-100 text-gray-800";
const upperImportance = importance.toUpperCase();
if (upperImportance === "MANDATORY") return "bg-red-100 text-red-800";
if (upperImportance === "PREFERRED") return "bg-orange-100 text-orange-800";
if (upperImportance === "ADVANCED") return "bg-blue-100 text-blue-800";
return "bg-gray-100 text-gray-800";
};
const [updateTask] = const [updateTask] =
useMutation<ControlOverviewPageUpdateTaskStateMutationType>( useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
updateTaskStateMutation updateTaskStateMutation
@@ -542,7 +597,6 @@ function ControlOverviewPageContent({
}); });
}; };
// Function to format file size
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
@@ -551,7 +605,6 @@ function ControlOverviewPageContent({
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}; };
// Function to format date
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString("en-US", { return date.toLocaleDateString("en-US", {
@@ -681,40 +734,25 @@ function ControlOverviewPageContent({
<Button variant="outline" size="sm" onClick={handleEditControl}> <Button variant="outline" size="sm" onClick={handleEditControl}>
Edit Control Edit Control
</Button> </Button>
<div className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm"> <div
30 min className={`${getStateColor(
data.control.state
)} px-3 py-1 rounded-full text-sm`}
>
{formatState(data.control.state)}
</div> </div>
<div className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm"> <div
Mandatory className={`${getImportanceColor(
data.control.importance
)} px-3 py-1 rounded-full text-sm`}
>
{formatImportance(data.control.importance)}
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 max-w-3xl">{data.control.description}</p> <p className="text-gray-600 max-w-3xl">{data.control.description}</p>
</div> </div>
<Card className="bg-gray-50 border border-gray-200">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div
className={`w-4 h-4 rounded-full bg-white flex items-center justify-center border border-gray-200`}
>
<div
className={`w-2 h-2 rounded-full ${
data.control.state === "IMPLEMENTED"
? "bg-green-500"
: "bg-gray-300"
}`}
/>
</div>
<span className="text-sm text-gray-700">
{data.control.state === "IMPLEMENTED"
? "Validated"
: "Not validated"}
</span>
</div>
</CardContent>
</Card>
<div> <div>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold">Tasks</h2> <h2 className="text-xl font-semibold">Tasks</h2>

View File

@@ -28,6 +28,7 @@ const FrameworkOverviewPageQuery = graphql`
description description
state state
category category
importance
} }
} }
} }
@@ -209,7 +210,10 @@ function FrameworkOverviewPageContent({
30 min 30 min
</div> </div>
<div className="bg-[#2A2A2A] text-white px-2 py-1 rounded-full text-xs"> <div className="bg-[#2A2A2A] text-white px-2 py-1 rounded-full text-xs">
Mandatory {
controlCards[hoveredCard]?.controls[hoveredControl]
?.importance
}
</div> </div>
</div> </div>
<MoveUpRight className="w-4 h-4 cursor-pointer hover:text-[#A3E635] transition-colors" /> <MoveUpRight className="w-4 h-4 cursor-pointer hover:text-[#A3E635] transition-colors" />

View File

@@ -23,10 +23,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import type { UpdateControlPageUpdateControlMutation as UpdateControlPageUpdateControlMutationType } from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
// Type imports will be available after Relay compiler runs import type {
// import type { UpdateControlPageQuery as UpdateControlPageQueryType } from "./__generated__/UpdateControlPageQuery.graphql"; ControlState,
// import type { UpdateControlPageUpdateControlMutation as UpdateControlPageUpdateControlMutationType } from "./__generated__/UpdateControlPageUpdateControlMutation.graphql"; ControlImportance,
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
const updateControlMutation = graphql` const updateControlMutation = graphql`
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) { mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
@@ -36,6 +37,7 @@ const updateControlMutation = graphql`
name name
description description
category category
importance
state state
version version
} }
@@ -51,6 +53,7 @@ const updateControlQuery = graphql`
name name
description description
category category
importance
state state
version version
} }
@@ -98,7 +101,7 @@ function EditableField({
} }
className={cn( className={cn(
"w-full resize-none", "w-full resize-none",
required && !value && "border-red-500", required && !value && "border-red-500"
)} )}
placeholder={`Enter ${label.toLowerCase()}`} placeholder={`Enter ${label.toLowerCase()}`}
rows={4} rows={4}
@@ -135,6 +138,7 @@ function UpdateControlPageContent({
description: "", description: "",
category: "", category: "",
state: "", state: "",
importance: "",
}); });
useEffect(() => { useEffect(() => {
@@ -144,12 +148,15 @@ function UpdateControlPageContent({
description: data.node.description || "", description: data.node.description || "",
category: data.node.category || "", category: data.node.category || "",
state: data.node.state || "", state: data.node.state || "",
importance: data.node.importance || "",
}); });
} }
}, [data.node]); }, [data.node]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any const [commit, isInFlight] =
const [commit, isInFlight] = useMutation<any>(updateControlMutation); useMutation<UpdateControlPageUpdateControlMutationType>(
updateControlMutation
);
const handleFieldChange = (field: keyof typeof formData, value: string) => { const handleFieldChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ setFormData((prev) => ({
@@ -161,7 +168,7 @@ function UpdateControlPageContent({
const handleCancel = () => { const handleCancel = () => {
navigate( navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`, `/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
); );
}; };
@@ -185,7 +192,8 @@ function UpdateControlPageContent({
name?: string; name?: string;
description?: string; description?: string;
category?: string; category?: string;
state?: string; state?: ControlState;
importance?: ControlImportance;
} = { } = {
id: controlId!, id: controlId!,
expectedVersion: data.node.version, expectedVersion: data.node.version,
@@ -201,7 +209,10 @@ function UpdateControlPageContent({
input.category = formData.category; input.category = formData.category;
} }
if (editedFields.has("state")) { if (editedFields.has("state")) {
input.state = formData.state; input.state = formData.state as ControlState;
}
if (editedFields.has("importance")) {
input.importance = formData.importance as ControlImportance;
} }
commit({ commit({
@@ -224,7 +235,7 @@ function UpdateControlPageContent({
}); });
navigate( navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`, `/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
); );
}, },
onError(error) { onError(error) {
@@ -273,6 +284,27 @@ function UpdateControlPageContent({
required required
/> />
<div className="space-y-2">
<Label htmlFor="importance" className="text-sm font-medium">
Importance
</Label>
<Select
value={formData.importance}
onValueChange={(value) =>
handleFieldChange("importance", value)
}
>
<SelectTrigger>
<SelectValue placeholder="Select importance" />
</SelectTrigger>
<SelectContent>
<SelectItem value="MANDATORY">Mandatory</SelectItem>
<SelectItem value="PREFERRED">Preferred</SelectItem>
<SelectItem value="ADVANCED">Advanced</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="state" className="text-sm font-medium"> <Label htmlFor="state" className="text-sm font-medium">
State State

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<b26c1748d5c4ae346ebbe98ea97b7459>> * @generated SignedSource<<5f28c9407a05834be16d66330e268caa>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,6 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID"; export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type TaskState = "DONE" | "TODO"; export type TaskState = "DONE" | "TODO";
@@ -20,6 +21,7 @@ export type ControlOverviewPageQuery$data = {
readonly category?: string; readonly category?: string;
readonly description?: string; readonly description?: string;
readonly id: string; readonly id: string;
readonly importance?: ControlImportance;
readonly name?: string; readonly name?: string;
readonly state?: ControlState; readonly state?: ControlState;
readonly tasks?: { readonly tasks?: {
@@ -101,31 +103,38 @@ v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "category", "name": "importance",
"storageKey": null "storageKey": null
}, },
v7 = { v7 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "version", "name": "category",
"storageKey": null "storageKey": null
}, },
v8 = { v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "version",
"storageKey": null "storageKey": null
}, },
v9 = { v9 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v10 = { v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v11 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -150,7 +159,7 @@ v10 = {
], ],
"storageKey": null "storageKey": null
}, },
v11 = { v12 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -162,7 +171,7 @@ v11 = {
} }
] ]
}, },
v12 = [ v13 = [
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -209,25 +218,25 @@ v12 = [
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
(v8/*: any*/) (v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/), (v11/*: any*/),
(v11/*: any*/) (v12/*: any*/)
], ],
v13 = [ v14 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 100 "value": 100
} }
], ],
v14 = [ v15 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -257,6 +266,7 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/),
{ {
"alias": "tasks", "alias": "tasks",
"args": null, "args": null,
@@ -285,7 +295,7 @@ return {
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v7/*: any*/), (v8/*: any*/),
{ {
"alias": "evidences", "alias": "evidences",
"args": null, "args": null,
@@ -293,19 +303,19 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "__ControlOverviewPage_evidences_connection", "name": "__ControlOverviewPage_evidences_connection",
"plural": false, "plural": false,
"selections": (v12/*: any*/), "selections": (v13/*: any*/),
"storageKey": null "storageKey": null
}, },
(v8/*: any*/) (v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/), (v11/*: any*/),
(v11/*: any*/) (v12/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -334,7 +344,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v8/*: any*/), (v9/*: any*/),
(v2/*: any*/), (v2/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
@@ -343,9 +353,10 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v14/*: any*/),
"concreteType": "TaskConnection", "concreteType": "TaskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "tasks", "name": "tasks",
@@ -371,42 +382,42 @@ return {
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v7/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"concreteType": "EvidenceConnection", "concreteType": "EvidenceConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "evidences", "name": "evidences",
"plural": false, "plural": false,
"selections": (v12/*: any*/), "selections": (v13/*: any*/),
"storageKey": "evidences(first:50)" "storageKey": "evidences(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "ControlOverviewPage_evidences", "key": "ControlOverviewPage_evidences",
"kind": "LinkedHandle", "kind": "LinkedHandle",
"name": "evidences" "name": "evidences"
}, },
(v8/*: any*/) (v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/), (v11/*: any*/),
(v11/*: any*/) (v12/*: any*/)
], ],
"storageKey": "tasks(first:100)" "storageKey": "tasks(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v14/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "ControlOverviewPage_tasks", "key": "ControlOverviewPage_tasks",
@@ -423,7 +434,7 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "c8fb4975f792904f5106fb8518406063", "cacheID": "1dc4bdedef3b39fd2e441f5259f83d29",
"id": null, "id": null,
"metadata": { "metadata": {
"connection": [ "connection": [
@@ -446,11 +457,11 @@ return {
}, },
"name": "ControlOverviewPageQuery", "name": "ControlOverviewPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n version\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" "text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n version\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "bd552dffe1a33e64e693a9170074989f"; (node as any).hash = "ff1a734556fc0d56e90ddae161a93408";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6f707328d181f9b362855a7b6023acc0>> * @generated SignedSource<<f5c15295df1bbb71ef99c60ec2417464>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,6 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type FrameworkOverviewPageQuery$variables = { export type FrameworkOverviewPageQuery$variables = {
frameworkId: string; frameworkId: string;
@@ -21,6 +22,7 @@ export type FrameworkOverviewPageQuery$data = {
readonly category: string; readonly category: string;
readonly description: string; readonly description: string;
readonly id: string; readonly id: string;
readonly importance: ControlImportance;
readonly name: string; readonly name: string;
readonly state: ControlState; readonly state: ControlState;
}; };
@@ -113,6 +115,13 @@ v6 = [
"name": "category", "name": "category",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
(v5/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -252,7 +261,7 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "77f347f74c922dbf3c1dd0baf0e37efd", "cacheID": "6a4fc22ad7657a6861027975b620b08e",
"id": null, "id": null,
"metadata": { "metadata": {
"connection": [ "connection": [
@@ -269,11 +278,11 @@ return {
}, },
"name": "FrameworkOverviewPageQuery", "name": "FrameworkOverviewPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query FrameworkOverviewPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 90) {\n edges {\n node {\n id\n name\n description\n state\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" "text": "query FrameworkOverviewPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 90) {\n edges {\n node {\n id\n name\n description\n state\n category\n importance\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "de913888fcb5a475baf829a5773326a3"; (node as any).hash = "0a7d8e6c782dae1777ee687c768d0ad6";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<daddf46b2039f01b80967778f24dd7f4>> * @generated SignedSource<<86f7062f21c125e478c5a7a58b495d6c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,6 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type UpdateControlPageQuery$variables = { export type UpdateControlPageQuery$variables = {
controlId: string; controlId: string;
@@ -18,6 +19,7 @@ export type UpdateControlPageQuery$data = {
readonly category?: string; readonly category?: string;
readonly description?: string; readonly description?: string;
readonly id?: string; readonly id?: string;
readonly importance?: ControlImportance;
readonly name?: string; readonly name?: string;
readonly state?: ControlState; readonly state?: ControlState;
readonly version?: number; readonly version?: number;
@@ -75,10 +77,17 @@ v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "state", "name": "importance",
"storageKey": null "storageKey": null
}, },
v7 = { v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
@@ -108,7 +117,8 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/) (v7/*: any*/),
(v8/*: any*/)
], ],
"type": "Control", "type": "Control",
"abstractKey": null "abstractKey": null
@@ -149,7 +159,8 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/) (v7/*: any*/),
(v8/*: any*/)
], ],
"type": "Control", "type": "Control",
"abstractKey": null "abstractKey": null
@@ -160,16 +171,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "c44526d8079e0c539d634841a1f14923", "cacheID": "8b9ee499b6fc5726d6ce679205b59501",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "UpdateControlPageQuery", "name": "UpdateControlPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query UpdateControlPageQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n description\n category\n state\n version\n }\n id\n }\n}\n" "text": "query UpdateControlPageQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n description\n category\n importance\n state\n version\n }\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "d0a9cc6ebcb5b97ea57567edea1f7d5f"; (node as any).hash = "f3336cff42059e52e983e4cd9d6c2f1e";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<21747064508d0bb73d846aed7ed6b97a>> * @generated SignedSource<<8eaa34f32a83e626da3331f2e0c6a7cf>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -29,6 +29,7 @@ export type UpdateControlPageUpdateControlMutation$data = {
readonly category: string; readonly category: string;
readonly description: string; readonly description: string;
readonly id: string; readonly id: string;
readonly importance: ControlImportance;
readonly name: string; readonly name: string;
readonly state: ControlState; readonly state: ControlState;
readonly version: number; readonly version: number;
@@ -99,6 +100,13 @@ v1 = [
"name": "category", "name": "category",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -138,16 +146,16 @@ return {
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "1e9e2b8000a1b3061fcfda07260413ed", "cacheID": "3d05aa40f4dba0b24241235c54b85c4f",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "UpdateControlPageUpdateControlMutation", "name": "UpdateControlPageUpdateControlMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation UpdateControlPageUpdateControlMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n name\n description\n category\n state\n version\n }\n }\n}\n" "text": "mutation UpdateControlPageUpdateControlMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n name\n description\n category\n importance\n state\n version\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "861813b150cef2977f8d9455a9bcf4fa"; (node as any).hash = "0de0b5b9560213e95af60cc1732dee4c";
export default node; export default node;

View File

@@ -29,16 +29,17 @@ import (
type ( type (
Control struct { Control struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
FrameworkID gid.GID `db:"framework_id"` FrameworkID gid.GID `db:"framework_id"`
Category string `db:"category"` Category string `db:"category"`
Name string `db:"name"` Name string `db:"name"`
Description string `db:"description"` Description string `db:"description"`
State ControlState `db:"state"` Importance ControlImportance `db:"importance"`
ContentRef string `db:"content_ref"` State ControlState `db:"state"`
CreatedAt time.Time `db:"created_at"` ContentRef string `db:"content_ref"`
UpdatedAt time.Time `db:"updated_at"` CreatedAt time.Time `db:"created_at"`
Version int `db:"version"` UpdatedAt time.Time `db:"updated_at"`
Version int `db:"version"`
} }
Controls []*Control Controls []*Control
@@ -49,6 +50,7 @@ type (
Description *string Description *string
Category *string Category *string
State *ControlState State *ControlState
Importance *ControlImportance
} }
) )
@@ -70,6 +72,7 @@ SELECT
name, name,
description, description,
state, state,
importance,
content_ref, content_ref,
created_at, created_at,
updated_at, updated_at,
@@ -115,6 +118,7 @@ INSERT INTO
framework_id, framework_id,
category, category,
name, name,
importance,
state, state,
description, description,
content_ref, content_ref,
@@ -128,6 +132,7 @@ VALUES (
@framework_id, @framework_id,
@category, @category,
@name, @name,
@importance,
@state, @state,
@description, @description,
@content_ref, @content_ref,
@@ -149,6 +154,7 @@ VALUES (
"created_at": c.CreatedAt, "created_at": c.CreatedAt,
"updated_at": c.UpdatedAt, "updated_at": c.UpdatedAt,
"state": c.State, "state": c.State,
"importance": c.Importance,
} }
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
@@ -169,6 +175,7 @@ SELECT
name, name,
description, description,
state, state,
importance,
content_ref, content_ref,
created_at, created_at,
updated_at, updated_at,
@@ -213,6 +220,7 @@ UPDATE controls SET
description = COALESCE(@description, description), description = COALESCE(@description, description),
category = COALESCE(@category, category), category = COALESCE(@category, category),
state = COALESCE(@state, state), state = COALESCE(@state, state),
importance = COALESCE(@importance, importance),
updated_at = @updated_at, updated_at = @updated_at,
version = version + 1 version = version + 1
WHERE %s WHERE %s
@@ -224,6 +232,7 @@ RETURNING
category, category,
name, name,
description, description,
importance,
state, state,
content_ref, content_ref,
created_at, created_at,
@@ -235,23 +244,16 @@ RETURNING
args := pgx.NamedArgs{ args := pgx.NamedArgs{
"control_id": c.ID, "control_id": c.ID,
"expected_version": params.ExpectedVersion, "expected_version": params.ExpectedVersion,
"name": params.Name,
"description": params.Description,
"category": params.Category,
"state": params.State,
"importance": params.Importance,
"updated_at": time.Now(), "updated_at": time.Now(),
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
if params.Name != nil {
args["name"] = *params.Name
}
if params.Description != nil {
args["description"] = *params.Description
}
if params.Category != nil {
args["category"] = *params.Category
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args) rows, err := conn.Query(ctx, q, args)
if err != nil { if err != nil {
return fmt.Errorf("cannot query controls: %w", err) return fmt.Errorf("cannot query controls: %w", err)

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
type ControlImportance uint8
const (
ControlImportanceMandatory ControlImportance = iota
ControlImportancePreferred
ControlImportanceAdvanced
)
func (i ControlImportance) String() string {
return []string{"MANDATORY", "PREFERRED", "ADVANCED"}[i]
}
func (i *ControlImportance) Scan(value interface{}) error {
switch v := value.(type) {
case uint8:
*i = ControlImportance(v)
case string:
switch v {
case "MANDATORY":
*i = ControlImportanceMandatory
case "PREFERRED":
*i = ControlImportancePreferred
case "ADVANCED":
*i = ControlImportanceAdvanced
default:
return fmt.Errorf("invalid ControlImportance value: %q", v)
}
default:
return fmt.Errorf("unsupported type for ControlImportance: %T", value)
}
return nil
}
func (i ControlImportance) Value() (driver.Value, error) {
return i.String(), nil
}
func (i ControlImportance) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
func (i *ControlImportance) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case "MANDATORY":
*i = ControlImportanceMandatory
case "PREFERRED":
*i = ControlImportancePreferred
case "ADVANCED":
*i = ControlImportanceAdvanced
default:
return fmt.Errorf("invalid ControlImportance value: %q", s)
}
return nil
}
func (i *ControlImportance) UnmarshalText(text []byte) error {
var s string
if err := json.Unmarshal(text, &s); err != nil {
return err
}
switch s {
case "MANDATORY":
*i = ControlImportanceMandatory
case "PREFERRED":
*i = ControlImportancePreferred
case "ADVANCED":
*i = ControlImportanceAdvanced
default:
return fmt.Errorf("invalid ControlImportance value: %q", s)
}
return nil
}

View File

@@ -0,0 +1,8 @@
CREATE TYPE control_importance AS ENUM ('MANDATORY', 'PREFERRED', 'ADVANCED');
ALTER TABLE controls ADD COLUMN importance control_importance NOT NULL DEFAULT 'MANDATORY';
ALTER TABLE controls ALTER COLUMN importance DROP DEFAULT;
ALTER TABLE tasks ADD COLUMN time_estimate INTERVAL NOT NULL DEFAULT '00:30:00';
ALTER TABLE tasks ALTER COLUMN time_estimate DROP DEFAULT;

View File

@@ -36,6 +36,7 @@ type (
Description string Description string
ContentRef string ContentRef string
Category string Category string
Importance coredata.ControlImportance
} }
UpdateControlRequest struct { UpdateControlRequest struct {
@@ -45,6 +46,7 @@ type (
Description *string Description *string
Category *string Category *string
State *coredata.ControlState State *coredata.ControlState
Importance *coredata.ControlImportance
} }
) )
@@ -78,6 +80,7 @@ func (s ControlService) Update(
Description: req.Description, Description: req.Description,
Category: req.Category, Category: req.Category,
State: req.State, State: req.State,
Importance: req.Importance,
} }
control := &coredata.Control{ID: req.ID} control := &coredata.Control{ID: req.ID}
@@ -139,6 +142,7 @@ func (s ControlService) Create(
Description: req.Description, Description: req.Description,
Category: req.Category, Category: req.Category,
State: coredata.ControlStateNotStarted, State: coredata.ControlStateNotStarted,
Importance: req.Importance,
ContentRef: req.ContentRef, ContentRef: req.ContentRef,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,

View File

@@ -70,6 +70,22 @@ enum PeopleKind
) )
} }
enum ControlImportance
@goModel(model: "github.com/getprobo/probo/pkg/coredata.ControlImportance") {
MANDATORY
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.ControlImportanceMandatory"
)
PREFERRED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.ControlImportancePreferred"
)
ADVANCED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.ControlImportanceAdvanced"
)
}
type PageInfo { type PageInfo {
hasNextPage: Boolean! hasNextPage: Boolean!
hasPreviousPage: Boolean! hasPreviousPage: Boolean!
@@ -216,6 +232,7 @@ type Control implements Node {
name: String! name: String!
description: String! description: String!
state: ControlState! state: ControlState!
importance: ControlImportance!
tasks( tasks(
first: Int first: Int
@@ -308,24 +325,35 @@ type Mutation {
createVendor(input: CreateVendorInput!): CreateVendorPayload! createVendor(input: CreateVendorInput!): CreateVendorPayload!
updateVendor(input: UpdateVendorInput!): UpdateVendorPayload! updateVendor(input: UpdateVendorInput!): UpdateVendorPayload!
deleteVendor(input: DeleteVendorInput!): DeleteVendorPayload! deleteVendor(input: DeleteVendorInput!): DeleteVendorPayload!
createPeople(input: CreatePeopleInput!): CreatePeoplePayload! createPeople(input: CreatePeopleInput!): CreatePeoplePayload!
updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload! updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload!
deletePeople(input: DeletePeopleInput!): DeletePeoplePayload! deletePeople(input: DeletePeopleInput!): DeletePeoplePayload!
createOrganization( createOrganization(
input: CreateOrganizationInput! input: CreateOrganizationInput!
): CreateOrganizationPayload! ): CreateOrganizationPayload!
updateOrganization(
input: UpdateOrganizationInput!
): UpdateOrganizationPayload!
deleteOrganization( deleteOrganization(
input: DeleteOrganizationInput! input: DeleteOrganizationInput!
): DeleteOrganizationPayload! ): DeleteOrganizationPayload!
createTask(input: CreateTaskInput!): CreateTaskPayload! createTask(input: CreateTaskInput!): CreateTaskPayload!
updateTask(input: UpdateTaskInput!): UpdateTaskPayload! updateTask(input: UpdateTaskInput!): UpdateTaskPayload!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload! createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload!
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload!
updateControl(input: UpdateControlInput!): UpdateControlPayload! updateControl(input: UpdateControlInput!): UpdateControlPayload!
uploadEvidence(input: UploadEvidenceInput!): UploadEvidencePayload! uploadEvidence(input: UploadEvidenceInput!): UploadEvidencePayload!
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload! deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
createPolicy(input: CreatePolicyInput!): CreatePolicyPayload! createPolicy(input: CreatePolicyInput!): CreatePolicyPayload!
updatePolicy(input: UpdatePolicyInput!): UpdatePolicyPayload! updatePolicy(input: UpdatePolicyInput!): UpdatePolicyPayload!
deletePolicy(input: DeletePolicyInput!): DeletePolicyPayload! deletePolicy(input: DeletePolicyInput!): DeletePolicyPayload!
@@ -431,6 +459,12 @@ input CreateOrganizationInput {
name: String! name: String!
} }
input UpdateOrganizationInput {
organizationId: ID!
name: String
logoUrl: String
}
input DeleteOrganizationInput { input DeleteOrganizationInput {
organizationId: ID! organizationId: ID!
} }
@@ -439,6 +473,10 @@ type CreateOrganizationPayload {
organizationEdge: OrganizationEdge! organizationEdge: OrganizationEdge!
} }
type UpdateOrganizationPayload {
organization: Organization!
}
type DeleteOrganizationPayload { type DeleteOrganizationPayload {
deletedOrganizationId: ID! deletedOrganizationId: ID!
} }
@@ -483,6 +521,7 @@ input CreateControlInput {
name: String! name: String!
description: String! description: String!
category: String! category: String!
importance: ControlImportance!
} }
type CreateControlPayload { type CreateControlPayload {
@@ -508,6 +547,7 @@ input UpdateControlInput {
description: String description: String
category: String category: String
state: ControlState state: ControlState
importance: ControlImportance
} }
type UpdateControlPayload { type UpdateControlPayload {
@@ -616,3 +656,12 @@ input ConfirmEmailInput {
type ConfirmEmailPayload { type ConfirmEmailPayload {
success: Boolean! success: Boolean!
} }
input ImportFrameworkInput {
organizationId: ID!
file: Upload!
}
type ImportFrameworkPayload {
frameworkEdge: FrameworkEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,7 @@ func NewControl(c *coredata.Control) *Control {
Name: c.Name, Name: c.Name,
Description: c.Description, Description: c.Description,
State: c.State, State: c.State,
Importance: c.Importance,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
} }

View File

@@ -25,15 +25,16 @@ type ConfirmEmailPayload struct {
} }
type Control struct { type Control struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Version int `json:"version"` Version int `json:"version"`
Category string `json:"category"` Category string `json:"category"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
State coredata.ControlState `json:"state"` State coredata.ControlState `json:"state"`
Tasks *TaskConnection `json:"tasks"` Importance coredata.ControlImportance `json:"importance"`
CreatedAt time.Time `json:"createdAt"` Tasks *TaskConnection `json:"tasks"`
UpdatedAt time.Time `json:"updatedAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} }
func (Control) IsNode() {} func (Control) IsNode() {}
@@ -50,10 +51,11 @@ type ControlEdge struct {
} }
type CreateControlInput struct { type CreateControlInput struct {
FrameworkID gid.GID `json:"frameworkId"` FrameworkID gid.GID `json:"frameworkId"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Category string `json:"category"` Category string `json:"category"`
Importance coredata.ControlImportance `json:"importance"`
} }
type CreateControlPayload struct { type CreateControlPayload struct {
@@ -225,6 +227,15 @@ type FrameworkEdge struct {
Node *Framework `json:"node"` Node *Framework `json:"node"`
} }
type ImportFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"`
}
type ImportFrameworkPayload struct {
FrameworkEdge *FrameworkEdge `json:"frameworkEdge"`
}
type Mutation struct { type Mutation struct {
} }
@@ -342,12 +353,13 @@ type TaskEdge struct {
} }
type UpdateControlInput struct { type UpdateControlInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"` ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty"` Category *string `json:"category,omitempty"`
State *coredata.ControlState `json:"state,omitempty"` State *coredata.ControlState `json:"state,omitempty"`
Importance *coredata.ControlImportance `json:"importance,omitempty"`
} }
type UpdateControlPayload struct { type UpdateControlPayload struct {
@@ -365,6 +377,16 @@ type UpdateFrameworkPayload struct {
Framework *Framework `json:"framework"` Framework *Framework `json:"framework"`
} }
type UpdateOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`
LogoURL *string `json:"logoUrl,omitempty"`
}
type UpdateOrganizationPayload struct {
Organization *Organization `json:"organization"`
}
type UpdatePeopleInput struct { type UpdatePeopleInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"` ExpectedVersion int `json:"expectedVersion"`

View File

@@ -321,6 +321,7 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
Description: input.Description, Description: input.Description,
Category: input.Category, Category: input.Category,
State: input.State, State: input.State,
Importance: input.Importance,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot update control: %w", err) return nil, fmt.Errorf("cannot update control: %w", err)