From 8bbb4f59f18338553d7295bdad6ae61285f0d6c2 Mon Sep 17 00:00:00 2001 From: gearnode Date: Sat, 12 Apr 2025 11:18:20 -0700 Subject: [PATCH] Add risk ownership Signed-off-by: gearnode --- CHANGELOG.md | 1 + .../organizations/risks/EditRiskView.tsx | 43 ++- .../pages/organizations/risks/NewRiskView.tsx | 66 ++++- .../organizations/risks/ShowRiskView.tsx | 67 ++++- .../EditRiskViewQuery.graphql.ts | 259 +++++++++++++++--- .../EditRiskViewUpdateRiskMutation.graphql.ts | 53 +++- .../NewRiskViewCreateRiskMutation.graphql.ts | 3 +- .../__generated__/NewRiskViewQuery.graphql.ts | 230 ++++++++++++++++ .../ShowRiskViewQuery.graphql.ts | 120 +++++--- pkg/coredata/migrations/20250412T105200Z.sql | 1 + pkg/coredata/risk.go | 12 +- pkg/probo/risk_service.go | 7 + pkg/server/api/console/v1/schema.graphql | 4 + pkg/server/api/console/v1/schema/schema.go | 125 ++++++++- pkg/server/api/console/v1/types/types.go | 3 + pkg/server/api/console/v1/v1_resolver.go | 23 ++ 16 files changed, 911 insertions(+), 106 deletions(-) create mode 100644 apps/console/src/pages/organizations/risks/__generated__/NewRiskViewQuery.graphql.ts create mode 100644 pkg/coredata/migrations/20250412T105200Z.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f949f091..10bcabb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - New risk treatment strategy options: Mitigate, Accept, Avoid, Transfer +- Risk ownership functionality ## [0.7.0] - 2025-04-12 diff --git a/apps/console/src/pages/organizations/risks/EditRiskView.tsx b/apps/console/src/pages/organizations/risks/EditRiskView.tsx index 00981bea9..7370e99a2 100644 --- a/apps/console/src/pages/organizations/risks/EditRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/EditRiskView.tsx @@ -32,13 +32,15 @@ import { import { Separator } from "@/components/ui/separator"; import { Suspense } from "react"; import { EditRiskViewSkeleton } from "./EditRiskPage"; +import PeopleSelector from "@/components/PeopleSelector"; +import { User } from "lucide-react"; import type { EditRiskViewQuery } from "./__generated__/EditRiskViewQuery.graphql"; import type { EditRiskViewUpdateRiskMutation } from "./__generated__/EditRiskViewUpdateRiskMutation.graphql"; import type { RiskTreatment } from "./__generated__/EditRiskViewUpdateRiskMutation.graphql"; // Query to get risk details const editRiskViewQuery = graphql` - query EditRiskViewQuery($riskId: ID!) { + query EditRiskViewQuery($riskId: ID!, $organizationId: ID!) { risk: node(id: $riskId) { ... on Risk { id @@ -49,8 +51,15 @@ const editRiskViewQuery = graphql` residualLikelihood residualImpact treatment + owner { + id + fullName + } } } + organization: node(id: $organizationId) { + ...PeopleSelector_organization + } } `; @@ -68,6 +77,10 @@ const updateRiskMutation = graphql` residualImpact treatment updatedAt + owner { + id + fullName + } } } } @@ -98,6 +111,7 @@ function EditRiskViewContent({ useState("MEDIUM"); const [residualImpact, setResidualImpact] = useState("MEDIUM"); const [treatment, setTreatment] = useState("MITIGATED"); + const [ownerId, setOwnerId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [updateRisk, isInFlight] = @@ -166,6 +180,7 @@ function EditRiskViewContent({ setResidualLikelihood(floatToLikelihood(risk.residualLikelihood || 0.5)); setResidualImpact(floatToImpact(risk.residualImpact || 0.5)); setTreatment(risk.treatment || "MITIGATED"); + setOwnerId(risk.owner?.id || null); } }, [risk]); @@ -192,6 +207,7 @@ function EditRiskViewContent({ residualLikelihood: likelihoodToFloat(residualLikelihood), residualImpact: impactToFloat(residualImpact), treatment, + ownerId: ownerId || undefined, }; updateRisk({ @@ -263,6 +279,20 @@ function EditRiskViewContent({ /> +
+ + +
+
@@ -411,15 +441,18 @@ function EditRiskViewContent({ // Main component that loads the query export function EditRiskView() { - const { riskId } = useParams<{ riskId: string }>(); + const { riskId, organizationId } = useParams<{ + riskId: string; + organizationId: string; + }>(); const [queryRef, loadQuery] = useQueryLoader(editRiskViewQuery); useEffect(() => { - if (riskId) { - loadQuery({ riskId }); + if (riskId && organizationId) { + loadQuery({ riskId, organizationId }); } - }, [loadQuery, riskId]); + }, [loadQuery, riskId, organizationId]); if (!queryRef) { return ; diff --git a/apps/console/src/pages/organizations/risks/NewRiskView.tsx b/apps/console/src/pages/organizations/risks/NewRiskView.tsx index 47ca465f4..74b7f228d 100644 --- a/apps/console/src/pages/organizations/risks/NewRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/NewRiskView.tsx @@ -1,6 +1,13 @@ import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router"; -import { ConnectionHandler, graphql, useMutation } from "react-relay"; +import { + ConnectionHandler, + graphql, + useMutation, + useQueryLoader, + usePreloadedQuery, + PreloadedQuery, +} from "react-relay"; import { Card, CardContent, @@ -22,6 +29,11 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; +import PeopleSelector from "@/components/PeopleSelector"; +import { User } from "lucide-react"; +import { Suspense } from "react"; +import type { NewRiskViewQuery } from "./__generated__/NewRiskViewQuery.graphql"; +import type { NewRiskViewCreateRiskMutation } from "./__generated__/NewRiskViewCreateRiskMutation.graphql"; interface RiskTemplate { name: string; @@ -34,6 +46,14 @@ interface RiskTemplate { }[]; } +const newRiskQuery = graphql` + query NewRiskViewQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + ...PeopleSelector_organization + } + } +`; + const createRiskMutation = graphql` mutation NewRiskViewCreateRiskMutation( $input: CreateRiskInput! @@ -58,9 +78,14 @@ const createRiskMutation = graphql` } `; -export default function NewRiskView() { +function NewRiskForm({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { const navigate = useNavigate(); const { organizationId } = useParams<{ organizationId: string }>(); + const data = usePreloadedQuery(newRiskQuery, queryRef); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [inherentLikelihood, setinherentLikelihood] = @@ -70,6 +95,7 @@ export default function NewRiskView() { useState("MEDIUM"); const [residualImpact, setResidualImpact] = useState("MEDIUM"); const [treatment, setTreatment] = useState("MITIGATED"); + const [ownerId, setOwnerId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(""); const [riskTemplates, setRiskTemplates] = useState([]); @@ -210,6 +236,7 @@ export default function NewRiskView() { residualLikelihood: likelihoodToFloat(residualLikelihood), residualImpact: impactToFloat(residualImpact), treatment, + ownerId: ownerId || undefined, }; createRisk({ @@ -313,6 +340,20 @@ export default function NewRiskView() { />
+
+ + +
+
@@ -442,3 +483,24 @@ export default function NewRiskView() { ); } + +export default function NewRiskView() { + const { organizationId } = useParams<{ organizationId: string }>(); + const [queryRef, loadQuery] = useQueryLoader(newRiskQuery); + + useEffect(() => { + if (organizationId) { + loadQuery({ organizationId }); + } + }, [organizationId, loadQuery]); + + if (!queryRef) { + return
Loading...
; + } + + return ( + Loading...
}> + + + ); +} diff --git a/apps/console/src/pages/organizations/risks/ShowRiskView.tsx b/apps/console/src/pages/organizations/risks/ShowRiskView.tsx index 96068e2a3..8a77210bc 100644 --- a/apps/console/src/pages/organizations/risks/ShowRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/ShowRiskView.tsx @@ -16,7 +16,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; -import { Plus, Trash2, Search, Tag, Edit, ShieldCheck } from "lucide-react"; +import { + Plus, + Trash2, + Search, + Tag, + Edit, + ShieldCheck, + Shield, + Handshake, + Ban, + User, +} from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { Dialog, @@ -62,6 +73,11 @@ const showRiskViewQuery = graphql` ... on Risk { name description + treatment + owner { + id + fullName + } inherentLikelihood inherentImpact residualLikelihood @@ -759,6 +775,55 @@ function ShowRiskViewContent({
+ {/* Treatment and Owner section */} +
+
+
+

+ Treatment +

+
+ {risk.treatment === "MITIGATED" && ( + + )} + {risk.treatment === "TRANSFERRED" && ( + + )} + {risk.treatment === "AVOIDED" && ( + + )} + {risk.treatment === "ACCEPTED" && ( + + )} + + {risk.treatment ? risk.treatment.toLowerCase() : "N/A"} + +
+
+
+

+ Risk Owner +

+
+ + + {risk.owner ? ( + + {risk.owner.fullName} + + ) : ( + "Unassigned" + )} + +
+
+
+
+
+ {/* Residual risk section */}

Residual Risk

diff --git a/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts b/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts index 2c4491103..e665fcaaf 100644 --- a/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5ae729150c7f534725306ecd1cf786af>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,17 +9,26 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED"; export type EditRiskViewQuery$variables = { + organizationId: string; riskId: string; }; export type EditRiskViewQuery$data = { + readonly organization: { + readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">; + }; readonly risk: { readonly description?: string; readonly id?: string; readonly inherentImpact?: number; readonly inherentLikelihood?: number; readonly name?: string; + readonly owner?: { + readonly fullName: string; + readonly id: string; + } | null | undefined; readonly residualImpact?: number; readonly residualLikelihood?: number; readonly treatment?: RiskTreatment; @@ -31,86 +40,141 @@ export type EditRiskViewQuery = { }; const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "riskId" - } -], -v1 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "riskId" +}, +v2 = [ { "kind": "Variable", "name": "id", "variableName": "riskId" } ], -v2 = { +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v3 = { +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "name", "storageKey": null }, -v4 = { +v5 = { "alias": null, "args": null, "kind": "ScalarField", "name": "description", "storageKey": null }, -v5 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "inherentLikelihood", "storageKey": null }, -v6 = { +v7 = { "alias": null, "args": null, "kind": "ScalarField", "name": "inherentImpact", "storageKey": null }, -v7 = { +v8 = { "alias": null, "args": null, "kind": "ScalarField", "name": "residualLikelihood", "storageKey": null }, -v8 = { +v9 = { "alias": null, "args": null, "kind": "ScalarField", "name": "residualImpact", "storageKey": null }, -v9 = { +v10 = { "alias": null, "args": null, "kind": "ScalarField", "name": "treatment", "storageKey": null -}; +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v3/*: any*/), + (v11/*: any*/) + ], + "storageKey": null +}, +v13 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v15 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "FULL_NAME" + } + } +]; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], "kind": "Fragment", "metadata": null, "name": "EditRiskViewQuery", "selections": [ { "alias": "risk", - "args": (v1/*: any*/), + "args": (v2/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", @@ -119,20 +183,37 @@ return { { "kind": "InlineFragment", "selections": [ - (v2/*: any*/), (v3/*: any*/), (v4/*: any*/), (v5/*: any*/), (v6/*: any*/), (v7/*: any*/), (v8/*: any*/), - (v9/*: any*/) + (v9/*: any*/), + (v10/*: any*/), + (v12/*: any*/) ], "type": "Risk", "abstractKey": null } ], "storageKey": null + }, + { + "alias": "organization", + "args": (v13/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "PeopleSelector_organization" + } + ], + "storageKey": null } ], "type": "Query", @@ -140,56 +221,160 @@ return { }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], "kind": "Operation", "name": "EditRiskViewQuery", "selections": [ { "alias": "risk", - "args": (v1/*: any*/), + "args": (v2/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, - (v2/*: any*/), + (v14/*: any*/), + (v3/*: any*/), { "kind": "InlineFragment", "selections": [ - (v3/*: any*/), (v4/*: any*/), (v5/*: any*/), (v6/*: any*/), (v7/*: any*/), (v8/*: any*/), - (v9/*: any*/) + (v9/*: any*/), + (v10/*: any*/), + (v12/*: any*/) ], "type": "Risk", "abstractKey": null } ], "storageKey": null + }, + { + "alias": "organization", + "args": (v13/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v14/*: any*/), + (v3/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v15/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + (v14/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" + }, + { + "alias": null, + "args": (v15/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "PeopleSelector_organization_peoples", + "kind": "LinkedHandle", + "name": "peoples" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null } ] }, "params": { - "cacheID": "cd7c9a0e53b9c1a2291e488756da9908", + "cacheID": "5d84f3bb7bcc5123adcf8fe776baea73", "id": null, "metadata": {}, "name": "EditRiskViewQuery", "operationKind": "query", - "text": "query EditRiskViewQuery(\n $riskId: ID!\n) {\n risk: node(id: $riskId) {\n __typename\n ... on Risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n treatment\n }\n id\n }\n}\n" + "text": "query EditRiskViewQuery(\n $riskId: ID!\n $organizationId: ID!\n) {\n risk: node(id: $riskId) {\n __typename\n ... on Risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n treatment\n owner {\n id\n fullName\n }\n }\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); -(node as any).hash = "cce4cb6856fe45d8ab37fe690ac8c720"; +(node as any).hash = "616261dd100b7243f955cd94e39e7ac9"; export default node; diff --git a/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts b/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts index 3246c7acf..bb112d4ba 100644 --- a/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts +++ b/apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<92886e4193283cb894a864edc361a575>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -16,6 +16,7 @@ export type UpdateRiskInput = { inherentImpact?: number | null | undefined; inherentLikelihood?: number | null | undefined; name?: string | null | undefined; + ownerId?: string | null | undefined; residualImpact?: number | null | undefined; residualLikelihood?: number | null | undefined; treatment?: RiskTreatment | null | undefined; @@ -31,6 +32,10 @@ export type EditRiskViewUpdateRiskMutation$data = { readonly inherentImpact: number; readonly inherentLikelihood: number; readonly name: string; + readonly owner: { + readonly fullName: string; + readonly id: string; + } | null | undefined; readonly residualImpact: number; readonly residualLikelihood: number; readonly treatment: RiskTreatment; @@ -51,7 +56,14 @@ var v0 = [ "name": "input" } ], -v1 = [ +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = [ { "alias": null, "args": [ @@ -74,13 +86,7 @@ v1 = [ "name": "risk", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, + (v1/*: any*/), { "alias": null, "args": null, @@ -136,6 +142,25 @@ v1 = [ "kind": "ScalarField", "name": "updatedAt", "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null } ], "storageKey": null @@ -150,7 +175,7 @@ return { "kind": "Fragment", "metadata": null, "name": "EditRiskViewUpdateRiskMutation", - "selections": (v1/*: any*/), + "selections": (v2/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -159,19 +184,19 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "EditRiskViewUpdateRiskMutation", - "selections": (v1/*: any*/) + "selections": (v2/*: any*/) }, "params": { - "cacheID": "e1bdac5dea224d79834ac952692a9bda", + "cacheID": "31fc731869a56e3bdc73aae7c114a385", "id": null, "metadata": {}, "name": "EditRiskViewUpdateRiskMutation", "operationKind": "mutation", - "text": "mutation EditRiskViewUpdateRiskMutation(\n $input: UpdateRiskInput!\n) {\n updateRisk(input: $input) {\n risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n treatment\n updatedAt\n }\n }\n}\n" + "text": "mutation EditRiskViewUpdateRiskMutation(\n $input: UpdateRiskInput!\n) {\n updateRisk(input: $input) {\n risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n treatment\n updatedAt\n owner {\n id\n fullName\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "6394ecc50b1e1f039b11ecdcf2724571"; +(node as any).hash = "777e70d85a1de9db8b625b1f5b109c7e"; export default node; diff --git a/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewCreateRiskMutation.graphql.ts b/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewCreateRiskMutation.graphql.ts index 80feb9c43..776a0f898 100644 --- a/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewCreateRiskMutation.graphql.ts +++ b/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewCreateRiskMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -16,6 +16,7 @@ export type CreateRiskInput = { inherentLikelihood: number; name: string; organizationId: string; + ownerId?: string | null | undefined; residualImpact?: number | null | undefined; residualLikelihood?: number | null | undefined; treatment: RiskTreatment; diff --git a/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewQuery.graphql.ts b/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewQuery.graphql.ts new file mode 100644 index 000000000..34df77b67 --- /dev/null +++ b/apps/console/src/pages/organizations/risks/__generated__/NewRiskViewQuery.graphql.ts @@ -0,0 +1,230 @@ +/** + * @generated SignedSource<<6c7503b4c94cbe6c062d634f0192a3ef>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type NewRiskViewQuery$variables = { + organizationId: string; +}; +export type NewRiskViewQuery$data = { + readonly organization: { + readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">; + }; +}; +export type NewRiskViewQuery = { + response: NewRiskViewQuery$data; + variables: NewRiskViewQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "FULL_NAME" + } + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "NewRiskViewQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "PeopleSelector_organization" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "NewRiskViewQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "PeopleSelector_organization_peoples", + "kind": "LinkedHandle", + "name": "peoples" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "395a2ea0b09e5fdc165a19757d0b637a", + "id": null, + "metadata": {}, + "name": "NewRiskViewQuery", + "operationKind": "query", + "text": "query NewRiskViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f62e2420e6fd2b9d90840df308fb6c1c"; + +export default node; diff --git a/apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts b/apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts index 1a3da0000..7edf517fd 100644 --- a/apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<486518520656d7e1392a34ee1877fe22>> * @lightSyntaxTransform * @nogrep */ @@ -11,6 +11,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type PolicyStatus = "ACTIVE" | "DRAFT"; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED"; export type ShowRiskViewQuery$variables = { riskId: string; }; @@ -45,6 +46,10 @@ export type ShowRiskViewQuery$data = { }>; }; readonly name?: string; + readonly owner?: { + readonly fullName: string; + readonly id: string; + } | null | undefined; readonly policies?: { readonly edges: ReadonlyArray<{ readonly node: { @@ -57,6 +62,7 @@ export type ShowRiskViewQuery$data = { }; readonly residualImpact?: number; readonly residualLikelihood?: number; + readonly treatment?: RiskTreatment; readonly updatedAt?: string; }; }; @@ -105,59 +111,85 @@ v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "inherentLikelihood", + "name": "treatment", "storageKey": null }, v6 = { "alias": null, "args": null, - "kind": "ScalarField", - "name": "inherentImpact", + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], "storageKey": null }, v7 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "residualLikelihood", + "name": "inherentLikelihood", "storageKey": null }, v8 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "residualImpact", + "name": "inherentImpact", "storageKey": null }, v9 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "createdAt", + "name": "residualLikelihood", "storageKey": null }, v10 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "updatedAt", + "name": "residualImpact", "storageKey": null }, v11 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "__typename", + "name": "createdAt", "storageKey": null }, v12 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "cursor", + "name": "updatedAt", "storageKey": null }, v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v15 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -182,7 +214,7 @@ v13 = { ], "storageKey": null }, -v14 = [ +v16 = [ { "alias": null, "args": null, @@ -209,7 +241,7 @@ v14 = [ "name": "category", "storageKey": null }, - (v9/*: any*/), + (v11/*: any*/), { "alias": null, "args": null, @@ -217,17 +249,17 @@ v14 = [ "name": "state", "storageKey": null }, - (v11/*: any*/) + (v13/*: any*/) ], "storageKey": null }, - (v12/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v15/*: any*/) ], -v15 = [ +v17 = [ { "alias": null, "args": null, @@ -253,18 +285,18 @@ v15 = [ "name": "status", "storageKey": null }, - (v9/*: any*/), - (v11/*: any*/) + (v11/*: any*/), + (v13/*: any*/) ], "storageKey": null }, - (v12/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v15/*: any*/) ], -v16 = [ +v18 = [ { "alias": null, "args": null, @@ -291,18 +323,18 @@ v16 = [ }, (v3/*: any*/), (v4/*: any*/), - (v9/*: any*/), - (v11/*: any*/) + (v11/*: any*/), + (v13/*: any*/) ], "storageKey": null }, - (v12/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v15/*: any*/) ], -v17 = [ +v19 = [ { "kind": "Literal", "name": "first", @@ -336,6 +368,8 @@ return { (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), { "alias": "mitigations", "args": null, @@ -343,7 +377,7 @@ return { "kind": "LinkedField", "name": "__Risk__mitigations_connection", "plural": false, - "selections": (v14/*: any*/), + "selections": (v16/*: any*/), "storageKey": null }, { @@ -353,7 +387,7 @@ return { "kind": "LinkedField", "name": "__Risk__policies_connection", "plural": false, - "selections": (v15/*: any*/), + "selections": (v17/*: any*/), "storageKey": null }, { @@ -363,7 +397,7 @@ return { "kind": "LinkedField", "name": "__Risk__controls_connection", "plural": false, - "selections": (v16/*: any*/), + "selections": (v18/*: any*/), "storageKey": null } ], @@ -391,7 +425,7 @@ return { "name": "node", "plural": false, "selections": [ - (v11/*: any*/), + (v13/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", @@ -404,19 +438,21 @@ return { (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "concreteType": "MitigationConnection", "kind": "LinkedField", "name": "mitigations", "plural": false, - "selections": (v14/*: any*/), + "selections": (v16/*: any*/), "storageKey": "mitigations(first:100)" }, { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "filters": null, "handle": "connection", "key": "Risk__mitigations", @@ -425,17 +461,17 @@ return { }, { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "concreteType": "PolicyConnection", "kind": "LinkedField", "name": "policies", "plural": false, - "selections": (v15/*: any*/), + "selections": (v17/*: any*/), "storageKey": "policies(first:100)" }, { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "filters": null, "handle": "connection", "key": "Risk__policies", @@ -444,17 +480,17 @@ return { }, { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "concreteType": "ControlConnection", "kind": "LinkedField", "name": "controls", "plural": false, - "selections": (v16/*: any*/), + "selections": (v18/*: any*/), "storageKey": "controls(first:100)" }, { "alias": null, - "args": (v17/*: any*/), + "args": (v19/*: any*/), "filters": null, "handle": "connection", "key": "Risk__controls", @@ -471,7 +507,7 @@ return { ] }, "params": { - "cacheID": "4463fac13f91564f513bfdabcc3e0063", + "cacheID": "8867786ee5bfa23bfd16d52d0023beeb", "id": null, "metadata": { "connection": [ @@ -506,11 +542,11 @@ return { }, "name": "ShowRiskViewQuery", "operationKind": "query", - "text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n createdAt\n updatedAt\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + "text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n createdAt\n updatedAt\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "68db9a4877abc338711844de6da38cb7"; +(node as any).hash = "264f7dd097e487eda41cdc4d04a14274"; export default node; diff --git a/pkg/coredata/migrations/20250412T105200Z.sql b/pkg/coredata/migrations/20250412T105200Z.sql new file mode 100644 index 000000000..0379d406a --- /dev/null +++ b/pkg/coredata/migrations/20250412T105200Z.sql @@ -0,0 +1 @@ +ALTER TABLE risks ADD COLUMN owner_id TEXT REFERENCES peoples(id) ON DELETE SET NULL; \ No newline at end of file diff --git a/pkg/coredata/risk.go b/pkg/coredata/risk.go index 4d7fd0772..9ee0ed5fc 100644 --- a/pkg/coredata/risk.go +++ b/pkg/coredata/risk.go @@ -33,6 +33,7 @@ type ( Name string `db:"name"` Description string `db:"description"` Treatment RiskTreatment `db:"treatment"` + OwnerID *gid.GID `db:"owner_id"` InherentLikelihood float64 `db:"inherent_likelihood"` InherentImpact float64 `db:"inherent_impact"` ResidualLikelihood float64 `db:"residual_likelihood"` @@ -75,6 +76,7 @@ WITH rsks AS ( r.organization_id, r.name, r.description, + r.owner_id, r.treatment, r.inherent_likelihood, r.inherent_impact, @@ -94,6 +96,7 @@ SELECT organization_id, name, description, + owner_id, treatment, inherent_likelihood, inherent_impact, @@ -139,6 +142,7 @@ SELECT organization_id, name, description, + owner_id, treatment, inherent_likelihood, inherent_impact, @@ -183,6 +187,7 @@ SELECT organization_id, name, description, + owner_id, treatment, inherent_likelihood, inherent_impact, @@ -221,8 +226,8 @@ func (r *Risk) Insert( scope Scoper, ) error { q := ` -INSERT INTO risks (id, tenant_id, organization_id, name, description, treatment, inherent_likelihood, inherent_impact, residual_likelihood, residual_impact, created_at, updated_at) -VALUES (@id, @tenant_id, @organization_id, @name, @description, @treatment, @inherent_likelihood, @inherent_impact, @residual_likelihood, @residual_impact, @created_at, @updated_at) +INSERT INTO risks (id, tenant_id, organization_id, name, description, owner_id, treatment, inherent_likelihood, inherent_impact, residual_likelihood, residual_impact, created_at, updated_at) +VALUES (@id, @tenant_id, @organization_id, @name, @description, @owner_id, @treatment, @inherent_likelihood, @inherent_impact, @residual_likelihood, @residual_impact, @created_at, @updated_at) ` args := pgx.StrictNamedArgs{ @@ -231,6 +236,7 @@ VALUES (@id, @tenant_id, @organization_id, @name, @description, @treatment, @inh "organization_id": r.OrganizationID, "name": r.Name, "description": r.Description, + "owner_id": r.OwnerID, "treatment": r.Treatment, "inherent_likelihood": r.InherentLikelihood, "inherent_impact": r.InherentImpact, @@ -254,6 +260,7 @@ UPDATE risks SET name = @name, description = @description, + owner_id = @owner_id, treatment = @treatment, inherent_likelihood = @inherent_likelihood, inherent_impact = @inherent_impact, @@ -269,6 +276,7 @@ WHERE %s "risk_id": r.ID, "name": r.Name, "description": r.Description, + "owner_id": r.OwnerID, "treatment": r.Treatment, "inherent_likelihood": r.InherentLikelihood, "inherent_impact": r.InherentImpact, diff --git a/pkg/probo/risk_service.go b/pkg/probo/risk_service.go index 71ff14c3e..2ac3e6aff 100644 --- a/pkg/probo/risk_service.go +++ b/pkg/probo/risk_service.go @@ -35,6 +35,7 @@ type ( Name string Description string Treatment coredata.RiskTreatment + OwnerID *gid.GID InherentLikelihood float64 InherentImpact float64 ResidualLikelihood *float64 @@ -46,6 +47,7 @@ type ( Name *string Description *string Treatment *coredata.RiskTreatment + OwnerID *gid.GID InherentLikelihood *float64 InherentImpact *float64 ResidualLikelihood *float64 @@ -169,6 +171,7 @@ func (s RiskService) Create( OrganizationID: req.OrganizationID, Name: req.Name, Description: req.Description, + OwnerID: req.OwnerID, InherentLikelihood: req.InherentLikelihood, InherentImpact: req.InherentImpact, Treatment: req.Treatment, @@ -261,6 +264,10 @@ func (s RiskService) Update( risk.Treatment = *req.Treatment } + if req.OwnerID != nil { + risk.OwnerID = req.OwnerID + } + risk.UpdatedAt = time.Now() if err := risk.Update(ctx, conn, s.svc.scope); err != nil { diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index b0fe2cc01..89ccd4b54 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -636,6 +636,8 @@ type Risk implements Node { residualImpact: Float! residualSeverity: Float! + owner: People @goField(forceResolver: true) + mitigations( first: Int after: CursorKey @@ -1085,6 +1087,7 @@ input CreateRiskInput { organizationId: ID! name: String! description: String! + ownerId: ID treatment: RiskTreatment! inherentLikelihood: Float! inherentImpact: Float! @@ -1096,6 +1099,7 @@ input UpdateRiskInput { id: ID! name: String description: String + ownerId: ID treatment: RiskTreatment inherentLikelihood: Float inherentImpact: Float diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 05a9017fe..8d2e534bb 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -421,6 +421,7 @@ type ComplexityRoot struct { InherentSeverity func(childComplexity int) int Mitigations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) int Name func(childComplexity int) int + Owner func(childComplexity int) int Policies func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PolicyOrderBy) int ResidualImpact func(childComplexity int) int ResidualLikelihood func(childComplexity int) int @@ -667,6 +668,7 @@ type QueryResolver interface { Viewer(ctx context.Context) (*types.Viewer, error) } type RiskResolver interface { + Owner(ctx context.Context, obj *types.Risk) (*types.People, error) Mitigations(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) (*types.MitigationConnection, error) Policies(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PolicyOrderBy) (*types.PolicyConnection, error) Controls(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) @@ -2306,6 +2308,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Risk.Name(childComplexity), true + case "Risk.owner": + if e.complexity.Risk.Owner == nil { + break + } + + return e.complexity.Risk.Owner(childComplexity), true + case "Risk.policies": if e.complexity.Risk.Policies == nil { break @@ -3720,6 +3729,8 @@ type Risk implements Node { residualImpact: Float! residualSeverity: Float! + owner: People @goField(forceResolver: true) + mitigations( first: Int after: CursorKey @@ -4169,6 +4180,7 @@ input CreateRiskInput { organizationId: ID! name: String! description: String! + ownerId: ID treatment: RiskTreatment! inherentLikelihood: Float! inherentImpact: Float! @@ -4180,6 +4192,7 @@ input UpdateRiskInput { id: ID! name: String description: String + ownerId: ID treatment: RiskTreatment inherentLikelihood: Float inherentImpact: Float @@ -17111,6 +17124,63 @@ func (ec *executionContext) fieldContext_Risk_residualSeverity(_ context.Context return fc, nil } +func (ec *executionContext) _Risk_owner(ctx context.Context, field graphql.CollectedField, obj *types.Risk) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Risk_owner(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Risk().Owner(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*types.People) + fc.Result = res + return ec.marshalOPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Risk_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Risk", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_People_id(ctx, field) + case "fullName": + return ec.fieldContext_People_fullName(ctx, field) + case "primaryEmailAddress": + return ec.fieldContext_People_primaryEmailAddress(ctx, field) + case "additionalEmailAddresses": + return ec.fieldContext_People_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_People_kind(ctx, field) + case "createdAt": + return ec.fieldContext_People_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_People_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type People", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Risk_mitigations(ctx context.Context, field graphql.CollectedField, obj *types.Risk) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Risk_mitigations(ctx, field) if err != nil { @@ -17589,6 +17659,8 @@ func (ec *executionContext) fieldContext_RiskEdge_node(_ context.Context, field return ec.fieldContext_Risk_residualImpact(ctx, field) case "residualSeverity": return ec.fieldContext_Risk_residualSeverity(ctx, field) + case "owner": + return ec.fieldContext_Risk_owner(ctx, field) case "mitigations": return ec.fieldContext_Risk_mitigations(ctx, field) case "policies": @@ -18772,6 +18844,8 @@ func (ec *executionContext) fieldContext_UpdateRiskPayload_risk(_ context.Contex return ec.fieldContext_Risk_residualImpact(ctx, field) case "residualSeverity": return ec.fieldContext_Risk_residualSeverity(ctx, field) + case "owner": + return ec.fieldContext_Risk_owner(ctx, field) case "mitigations": return ec.fieldContext_Risk_mitigations(ctx, field) case "policies": @@ -23789,7 +23863,7 @@ func (ec *executionContext) unmarshalInputCreateRiskInput(ctx context.Context, o asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "name", "description", "treatment", "inherentLikelihood", "inherentImpact", "residualLikelihood", "residualImpact"} + fieldsInOrder := [...]string{"organizationId", "name", "description", "ownerId", "treatment", "inherentLikelihood", "inherentImpact", "residualLikelihood", "residualImpact"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -23817,6 +23891,13 @@ func (ec *executionContext) unmarshalInputCreateRiskInput(ctx context.Context, o return it, err } it.Description = data + case "ownerId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ownerId")) + data, err := ec.unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OwnerID = data case "treatment": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("treatment")) data, err := ec.unmarshalNRiskTreatment2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTreatment(ctx, v) @@ -25319,7 +25400,7 @@ func (ec *executionContext) unmarshalInputUpdateRiskInput(ctx context.Context, o asMap[k] = v } - fieldsInOrder := [...]string{"id", "name", "description", "treatment", "inherentLikelihood", "inherentImpact", "residualLikelihood", "residualImpact"} + fieldsInOrder := [...]string{"id", "name", "description", "ownerId", "treatment", "inherentLikelihood", "inherentImpact", "residualLikelihood", "residualImpact"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -25347,6 +25428,13 @@ func (ec *executionContext) unmarshalInputUpdateRiskInput(ctx context.Context, o return it, err } it.Description = data + case "ownerId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ownerId")) + data, err := ec.unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OwnerID = data case "treatment": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("treatment")) data, err := ec.unmarshalORiskTreatment2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRiskTreatment(ctx, v) @@ -29422,6 +29510,39 @@ func (ec *executionContext) _Risk(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "owner": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Risk_owner(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "mitigations": field := field diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 874a32a06..ee9c9a766 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -150,6 +150,7 @@ type CreateRiskInput struct { OrganizationID gid.GID `json:"organizationId"` Name string `json:"name"` Description string `json:"description"` + OwnerID *gid.GID `json:"ownerId,omitempty"` Treatment coredata.RiskTreatment `json:"treatment"` InherentLikelihood float64 `json:"inherentLikelihood"` InherentImpact float64 `json:"inherentImpact"` @@ -565,6 +566,7 @@ type Risk struct { ResidualLikelihood float64 `json:"residualLikelihood"` ResidualImpact float64 `json:"residualImpact"` ResidualSeverity float64 `json:"residualSeverity"` + Owner *People `json:"owner,omitempty"` Mitigations *MitigationConnection `json:"mitigations"` Policies *PolicyConnection `json:"policies"` Controls *ControlConnection `json:"controls"` @@ -685,6 +687,7 @@ type UpdateRiskInput struct { ID gid.GID `json:"id"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` + OwnerID *gid.GID `json:"ownerId,omitempty"` Treatment *coredata.RiskTreatment `json:"treatment,omitempty"` InherentLikelihood *float64 `json:"inherentLikelihood,omitempty"` InherentImpact *float64 `json:"inherentImpact,omitempty"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 6b0b0e56e..d09e8b628 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -727,6 +727,7 @@ func (r *mutationResolver) CreateRisk(ctx context.Context, input types.CreateRis Name: input.Name, Description: input.Description, Treatment: input.Treatment, + OwnerID: input.OwnerID, InherentLikelihood: input.InherentLikelihood, InherentImpact: input.InherentImpact, ResidualLikelihood: input.ResidualLikelihood, @@ -753,6 +754,7 @@ func (r *mutationResolver) UpdateRisk(ctx context.Context, input types.UpdateRis Name: input.Name, Description: input.Description, Treatment: input.Treatment, + OwnerID: input.OwnerID, InherentLikelihood: input.InherentLikelihood, InherentImpact: input.InherentImpact, ResidualLikelihood: input.ResidualLikelihood, @@ -1345,6 +1347,27 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) { }, nil } +// Owner is the resolver for the owner field. +func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.People, error) { + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) + + risk, err := svc.Risks.Get(ctx, obj.ID) + if err != nil { + panic(fmt.Errorf("cannot get risk: %w", err)) + } + + if risk.OwnerID == nil { + return nil, nil + } + + owner, err := svc.Peoples.Get(ctx, *risk.OwnerID) + if err != nil { + panic(fmt.Errorf("cannot get owner: %w", err)) + } + + return types.NewPeople(owner), nil +} + // Mitigations is the resolver for the mitigations field. func (r *riskResolver) Mitigations(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) (*types.MitigationConnection, error) { svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())