Add risk snapshots

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-02 18:52:07 +02:00
parent 781026d3e7
commit 16ced9638d
22 changed files with 639 additions and 253 deletions

View File

@@ -3,7 +3,7 @@ import { useTranslate } from "@probo/i18n";
import { useLazyLoadQuery, graphql } from "react-relay"; import { useLazyLoadQuery, graphql } from "react-relay";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import type { SnapshotBannerQuery } from "./__generated__/SnapshotBannerQuery.graphql"; import type { SnapshotBannerQuery } from "./__generated__/SnapshotBannerQuery.graphql";
import { getSnapshotTypeUrlPath } from "@probo/helpers"; import { getSnapshotTypeUrlPath, getSnapshotTypeLabel, sprintf } from "@probo/helpers";
const snapshotQuery = graphql` const snapshotQuery = graphql`
query SnapshotBannerQuery($snapshotId: ID!) { query SnapshotBannerQuery($snapshotId: ID!) {
@@ -50,7 +50,11 @@ export function SnapshotBanner({ snapshotId }: Props) {
<span className="font-medium text-warning-800">{__("Snapshot")} {snapshot.name}</span> <span className="font-medium text-warning-800">{__("Snapshot")} {snapshot.name}</span>
</div> </div>
<p className="text-sm text-warning-700"> <p className="text-sm text-warning-700">
{__("You are viewing a snapshot from")} {dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })} {sprintf(
__("You are viewing a %s snapshot from %s"),
getSnapshotTypeLabel(__, snapshot.type).toLocaleLowerCase(),
dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })
)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -31,10 +31,10 @@ export function useDeleteRiskMutation() {
} }
export const risksQuery = graphql` export const risksQuery = graphql`
query RiskGraphListQuery($organizationId: ID!) { query RiskGraphListQuery($organizationId: ID!, $snapshotId: ID) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
id id
...RiskGraphFragment ...RiskGraphFragment @arguments(snapshotId: $snapshotId)
} }
} }
`; `;
@@ -48,6 +48,7 @@ const risksFragment = graphql`
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
snapshotId: { type: "ID", defaultValue: null }
) { ) {
risks( risks(
first: $first first: $first
@@ -55,10 +56,12 @@ const risksFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "RisksListQuery_risks") { filter: { snapshotId: $snapshotId }
) @connection(key: "RisksListQuery_risks", filters: ["filter"]) {
edges { edges {
node { node {
id id
snapshotId
name name
category category
treatment treatment
@@ -77,7 +80,7 @@ const risksFragment = graphql`
export const RisksConnectionKey = "RisksListQuery_risks"; export const RisksConnectionKey = "RisksListQuery_risks";
export function useRisksQuery(queryRef: PreloadedQuery<RiskGraphListQuery>) { export function useRisksQuery(queryRef: PreloadedQuery<RiskGraphListQuery>, snapshotId?: string | null) {
const data = usePreloadedQuery(risksQuery, queryRef); const data = usePreloadedQuery(risksQuery, queryRef);
const [dataFragment, refetch] = useRefetchableFragment( const [dataFragment, refetch] = useRefetchableFragment(
risksFragment, risksFragment,
@@ -90,6 +93,7 @@ export function useRisksQuery(queryRef: PreloadedQuery<RiskGraphListQuery>) {
connectionId: ConnectionHandler.getConnectionID( connectionId: ConnectionHandler.getConnectionID(
data.organization.id, data.organization.id,
RisksConnectionKey, RisksConnectionKey,
{ filter: { snapshotId: snapshotId || null } }
), ),
}; };
} }
@@ -98,6 +102,8 @@ export const riskNodeQuery = graphql`
query RiskGraphNodeQuery($riskId: ID!) { query RiskGraphNodeQuery($riskId: ID!) {
node(id: $riskId) { node(id: $riskId) {
... on Risk { ... on Risk {
id
snapshotId
name name
description description
treatment treatment

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<d65dc38bcf11079a304f8a5cc4070389>> * @generated SignedSource<<2b964bdee12b49230932678f583c7401>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -25,6 +25,7 @@ export type RiskGraphFragment$data = {
readonly residualImpact: number; readonly residualImpact: number;
readonly residualLikelihood: number; readonly residualLikelihood: number;
readonly residualRiskScore: number; readonly residualRiskScore: number;
readonly snapshotId: string | null | undefined;
readonly treatment: RiskTreatment; readonly treatment: RiskTreatment;
readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">; readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">;
}; };
@@ -76,6 +77,11 @@ return {
"defaultValue": null, "defaultValue": null,
"kind": "LocalArgument", "kind": "LocalArgument",
"name": "order" "name": "order"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "snapshotId"
} }
], ],
"kind": "Fragment", "kind": "Fragment",
@@ -116,9 +122,15 @@ return {
"alias": "risks", "alias": "risks",
"args": [ "args": [
{ {
"kind": "Variable", "fields": [
"name": "orderBy", {
"variableName": "order" "kind": "Variable",
"name": "snapshotId",
"variableName": "snapshotId"
}
],
"kind": "ObjectValue",
"name": "filter"
} }
], ],
"concreteType": "RiskConnection", "concreteType": "RiskConnection",
@@ -143,6 +155,13 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v1/*: any*/), (v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "snapshotId",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -280,6 +299,6 @@ return {
}; };
})(); })();
(node as any).hash = "dc662098b9aceaa2400f4656ed1b2d01"; (node as any).hash = "95c68a022cbf075eede9b7417393a369";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<8ae6d82b30ec5657f8a55bd47fab4e7e>> * @generated SignedSource<<af89b7b79cdb083497eabebd82b51991>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type RiskGraphListQuery$variables = { export type RiskGraphListQuery$variables = {
organizationId: string; organizationId: string;
snapshotId?: string | null | undefined;
}; };
export type RiskGraphListQuery$data = { export type RiskGraphListQuery$data = {
readonly organization: { readonly organization: {
@@ -30,6 +31,11 @@ var v0 = [
"defaultValue": null, "defaultValue": null,
"kind": "LocalArgument", "kind": "LocalArgument",
"name": "organizationId" "name": "organizationId"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "snapshotId"
} }
], ],
v1 = [ v1 = [
@@ -46,14 +52,26 @@ v2 = {
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
v3 = { v3 = [
{
"kind": "Variable",
"name": "snapshotId",
"variableName": "snapshotId"
}
],
v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v4 = [ v5 = [
{
"fields": (v3/*: any*/),
"kind": "ObjectValue",
"name": "filter"
},
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -77,7 +95,7 @@ return {
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
{ {
"args": null, "args": (v3/*: any*/),
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "RiskGraphFragment" "name": "RiskGraphFragment"
} }
@@ -102,14 +120,14 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v4/*: any*/),
(v2/*: any*/), (v2/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v4/*: any*/), "args": (v5/*: any*/),
"concreteType": "RiskConnection", "concreteType": "RiskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "risks", "name": "risks",
@@ -132,6 +150,13 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "snapshotId",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -221,7 +246,7 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
(v3/*: any*/) (v4/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -275,13 +300,13 @@ return {
"storageKey": null "storageKey": null
} }
], ],
"storageKey": "risks(first:50)" "storageKey": null
}, },
{ {
"alias": null, "alias": null,
"args": (v4/*: any*/), "args": (v5/*: any*/),
"filters": [ "filters": [
"orderBy" "filter"
], ],
"handle": "connection", "handle": "connection",
"key": "RisksListQuery_risks", "key": "RisksListQuery_risks",
@@ -298,16 +323,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "36e237e142f201c74ebc23ff46d7345a", "cacheID": "b2114c476ed7e095fc9d2317b6b1c554",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "RiskGraphListQuery", "name": "RiskGraphListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query RiskGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...RiskGraphFragment\n }\n}\n\nfragment RiskGraphFragment on Organization {\n risks(first: 50) {\n edges {\n node {\n id\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" "text": "query RiskGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...RiskGraphFragment_3iomuz\n }\n}\n\nfragment RiskGraphFragment_3iomuz on Organization {\n risks(first: 50, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "5d41ad11b598f63829f481ff0ae8aec7"; (node as any).hash = "a6e146d7d0f9d2713796935d0d615311";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<a4b7866dde478255ad0bb04a93ada596>> * @generated SignedSource<<494ff8512e4a50c2bbc2cb3453dbab1c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -23,6 +23,7 @@ export type RiskGraphNodeQuery$data = {
readonly documentsInfo?: { readonly documentsInfo?: {
readonly totalCount: number; readonly totalCount: number;
}; };
readonly id?: string;
readonly inherentRiskScore?: number; readonly inherentRiskScore?: number;
readonly measuresInfo?: { readonly measuresInfo?: {
readonly totalCount: number; readonly totalCount: number;
@@ -34,6 +35,7 @@ export type RiskGraphNodeQuery$data = {
readonly id: string; readonly id: string;
} | null | undefined; } | null | undefined;
readonly residualRiskScore?: number; readonly residualRiskScore?: number;
readonly snapshotId?: string | null | undefined;
readonly treatment?: RiskTreatment; readonly treatment?: RiskTreatment;
readonly " $fragmentSpreads": FragmentRefs<"RiskControlsTabFragment" | "RiskDocumentsTabFragment" | "RiskMeasuresTabFragment" | "RiskOverviewTabFragment" | "useRiskFormFragment">; readonly " $fragmentSpreads": FragmentRefs<"RiskControlsTabFragment" | "RiskDocumentsTabFragment" | "RiskMeasuresTabFragment" | "RiskOverviewTabFragment" | "useRiskFormFragment">;
}; };
@@ -62,31 +64,38 @@ v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "name", "name": "id",
"storageKey": null "storageKey": null
}, },
v3 = { v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "description", "name": "snapshotId",
"storageKey": null "storageKey": null
}, },
v4 = { v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "treatment", "name": "name",
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "description",
"storageKey": null "storageKey": null
}, },
v6 = { v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "treatment",
"storageKey": null
},
v7 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "People", "concreteType": "People",
@@ -94,7 +103,7 @@ v6 = {
"name": "owner", "name": "owner",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -105,35 +114,35 @@ v6 = {
], ],
"storageKey": null "storageKey": null
}, },
v7 = { v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "note", "name": "note",
"storageKey": null "storageKey": null
}, },
v8 = { v9 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "inherentRiskScore", "name": "inherentRiskScore",
"storageKey": null "storageKey": null
}, },
v9 = { v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "residualRiskScore", "name": "residualRiskScore",
"storageKey": null "storageKey": null
}, },
v10 = [ v11 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 0 "value": 0
} }
], ],
v11 = [ v12 = [
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -142,72 +151,72 @@ v11 = [
"storageKey": null "storageKey": null
} }
], ],
v12 = { v13 = {
"alias": "measuresInfo", "alias": "measuresInfo",
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "MeasureConnection", "concreteType": "MeasureConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "measures", "name": "measures",
"plural": false, "plural": false,
"selections": (v11/*: any*/), "selections": (v12/*: any*/),
"storageKey": "measures(first:0)" "storageKey": "measures(first:0)"
}, },
v13 = { v14 = {
"alias": "documentsInfo", "alias": "documentsInfo",
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "DocumentConnection", "concreteType": "DocumentConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "documents", "name": "documents",
"plural": false, "plural": false,
"selections": (v11/*: any*/), "selections": (v12/*: any*/),
"storageKey": "documents(first:0)" "storageKey": "documents(first:0)"
}, },
v14 = { v15 = {
"alias": "controlsInfo", "alias": "controlsInfo",
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "ControlConnection", "concreteType": "ControlConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "controls", "name": "controls",
"plural": false, "plural": false,
"selections": (v11/*: any*/), "selections": (v12/*: any*/),
"storageKey": "controls(first:0)" "storageKey": "controls(first:0)"
}, },
v15 = { v16 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v16 = [ v17 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 100 "value": 100
} }
], ],
v17 = { v18 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "cursor",
"storageKey": null "storageKey": null
}, },
v18 = { v19 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "endCursor", "name": "endCursor",
"storageKey": null "storageKey": null
}, },
v19 = { v20 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "hasNextPage", "name": "hasNextPage",
"storageKey": null "storageKey": null
}, },
v20 = { v21 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -215,12 +224,12 @@ v20 = {
"name": "pageInfo", "name": "pageInfo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v18/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v20/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
v21 = { v22 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -232,7 +241,7 @@ v21 = {
} }
] ]
}, },
v22 = [ v23 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -260,13 +269,15 @@ return {
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/), (v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v12/*: any*/), (v10/*: any*/),
(v13/*: any*/), (v13/*: any*/),
(v14/*: any*/), (v14/*: any*/),
(v15/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -317,21 +328,22 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v15/*: any*/), (v16/*: any*/),
(v5/*: any*/), (v2/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/), (v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v12/*: any*/), (v10/*: any*/),
(v13/*: any*/), (v13/*: any*/),
(v14/*: any*/), (v14/*: any*/),
(v15/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -369,7 +381,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v16/*: any*/), "args": (v17/*: any*/),
"concreteType": "MeasureConnection", "concreteType": "MeasureConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "measures", "name": "measures",
@@ -391,8 +403,8 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v4/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -400,22 +412,22 @@ return {
"name": "state", "name": "state",
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v16/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v17/*: any*/) (v18/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v20/*: any*/), (v21/*: any*/),
(v21/*: any*/) (v22/*: any*/)
], ],
"storageKey": "measures(first:100)" "storageKey": "measures(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v16/*: any*/), "args": (v17/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "Risk__measures", "key": "Risk__measures",
@@ -424,7 +436,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v16/*: any*/), "args": (v17/*: any*/),
"concreteType": "DocumentConnection", "concreteType": "DocumentConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "documents", "name": "documents",
@@ -446,7 +458,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -498,7 +510,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -515,22 +527,22 @@ return {
], ],
"storageKey": "versions(first:1)" "storageKey": "versions(first:1)"
}, },
(v15/*: any*/) (v16/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v17/*: any*/) (v18/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v20/*: any*/), (v21/*: any*/),
(v21/*: any*/) (v22/*: any*/)
], ],
"storageKey": "documents(first:100)" "storageKey": "documents(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v16/*: any*/), "args": (v17/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "Risk__documents", "key": "Risk__documents",
@@ -539,7 +551,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v22/*: any*/), "args": (v23/*: any*/),
"concreteType": "ControlConnection", "concreteType": "ControlConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "controls", "name": "controls",
@@ -561,7 +573,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -569,7 +581,7 @@ return {
"name": "sectionTitle", "name": "sectionTitle",
"storageKey": null "storageKey": null
}, },
(v2/*: any*/), (v4/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -578,16 +590,16 @@ return {
"name": "framework", "name": "framework",
"plural": false, "plural": false,
"selections": [ "selections": [
(v5/*: any*/), (v2/*: any*/),
(v2/*: any*/) (v4/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v16/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v17/*: any*/) (v18/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -599,8 +611,8 @@ return {
"name": "pageInfo", "name": "pageInfo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v18/*: any*/),
(v19/*: any*/), (v19/*: any*/),
(v20/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -623,7 +635,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v22/*: any*/), "args": (v23/*: any*/),
"filters": [ "filters": [
"orderBy", "orderBy",
"filter" "filter"
@@ -643,16 +655,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "5cb3fdf2f48c4b0d2714a06ac2a8fc4d", "cacheID": "03ef938803c52b572e23e691d50c4188",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "RiskGraphNodeQuery", "name": "RiskGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query RiskGraphNodeQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n note\n inherentRiskScore\n residualRiskScore\n measuresInfo: measures(first: 0) {\n totalCount\n }\n documentsInfo: documents(first: 0) {\n totalCount\n }\n controlsInfo: controls(first: 0) {\n totalCount\n }\n ...useRiskFormFragment\n ...RiskOverviewTabFragment\n ...RiskMeasuresTabFragment\n ...RiskDocumentsTabFragment\n ...RiskControlsTabFragment\n }\n id\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n\nfragment RiskControlsTabFragment on Risk {\n id\n controls(first: 20) {\n edges {\n node {\n id\n sectionTitle\n name\n framework {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment RiskDocumentsTabFragment on Risk {\n id\n documents(first: 100) {\n edges {\n node {\n id\n ...LinkedDocumentsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskMeasuresTabFragment on Risk {\n id\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskOverviewTabFragment on Risk {\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" "text": "query RiskGraphNodeQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n ... on Risk {\n id\n snapshotId\n name\n description\n treatment\n owner {\n id\n fullName\n }\n note\n inherentRiskScore\n residualRiskScore\n measuresInfo: measures(first: 0) {\n totalCount\n }\n documentsInfo: documents(first: 0) {\n totalCount\n }\n controlsInfo: controls(first: 0) {\n totalCount\n }\n ...useRiskFormFragment\n ...RiskOverviewTabFragment\n ...RiskMeasuresTabFragment\n ...RiskDocumentsTabFragment\n ...RiskControlsTabFragment\n }\n id\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n\nfragment RiskControlsTabFragment on Risk {\n id\n controls(first: 20) {\n edges {\n node {\n id\n sectionTitle\n name\n framework {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment RiskDocumentsTabFragment on Risk {\n id\n documents(first: 100) {\n edges {\n node {\n id\n ...LinkedDocumentsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskMeasuresTabFragment on Risk {\n id\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskOverviewTabFragment on Risk {\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "a190e4ead5b381da07e69f00650d8372"; (node as any).hash = "43dcdf60f1d1b28414650fe5f9294faa";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6369aba92b58ca2a67d57110a829bde4>> * @generated SignedSource<<4a0922eabd4e20c574db91e75f45a562>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -23,6 +23,7 @@ export type RisksListQuery$variables = {
id: string; id: string;
last?: number | null | undefined; last?: number | null | undefined;
order?: RiskOrder | null | undefined; order?: RiskOrder | null | undefined;
snapshotId?: string | null | undefined;
}; };
export type RisksListQuery$data = { export type RisksListQuery$data = {
readonly node: { readonly node: {
@@ -65,52 +66,69 @@ v5 = {
"kind": "LocalArgument", "kind": "LocalArgument",
"name": "order" "name": "order"
}, },
v6 = [ v6 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "snapshotId"
},
v7 = [
{ {
"kind": "Variable", "kind": "Variable",
"name": "id", "name": "id",
"variableName": "id" "variableName": "id"
} }
], ],
v7 = { v8 = {
"kind": "Variable", "kind": "Variable",
"name": "after", "name": "after",
"variableName": "after" "variableName": "after"
}, },
v8 = { v9 = {
"kind": "Variable", "kind": "Variable",
"name": "before", "name": "before",
"variableName": "before" "variableName": "before"
}, },
v9 = { v10 = {
"kind": "Variable", "kind": "Variable",
"name": "first", "name": "first",
"variableName": "first" "variableName": "first"
}, },
v10 = { v11 = {
"kind": "Variable", "kind": "Variable",
"name": "last", "name": "last",
"variableName": "last" "variableName": "last"
}, },
v11 = { v12 = {
"kind": "Variable",
"name": "snapshotId",
"variableName": "snapshotId"
},
v13 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v12 = { v14 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
v13 = [ v15 = [
(v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
{
"fields": [
(v12/*: any*/)
],
"kind": "ObjectValue",
"name": "filter"
},
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/),
{ {
"kind": "Variable", "kind": "Variable",
"name": "orderBy", "name": "orderBy",
@@ -125,7 +143,8 @@ return {
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/) (v5/*: any*/),
(v6/*: any*/)
], ],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
@@ -133,7 +152,7 @@ return {
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v6/*: any*/), "args": (v7/*: any*/),
"concreteType": null, "concreteType": null,
"kind": "LinkedField", "kind": "LinkedField",
"name": "node", "name": "node",
@@ -141,15 +160,16 @@ return {
"selections": [ "selections": [
{ {
"args": [ "args": [
(v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/),
{ {
"kind": "Variable", "kind": "Variable",
"name": "order", "name": "order",
"variableName": "order" "variableName": "order"
} },
(v12/*: any*/)
], ],
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "RiskGraphFragment" "name": "RiskGraphFragment"
@@ -169,6 +189,7 @@ return {
(v2/*: any*/), (v2/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/),
(v3/*: any*/) (v3/*: any*/)
], ],
"kind": "Operation", "kind": "Operation",
@@ -176,20 +197,20 @@ return {
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v6/*: any*/), "args": (v7/*: any*/),
"concreteType": null, "concreteType": null,
"kind": "LinkedField", "kind": "LinkedField",
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v11/*: any*/), (v13/*: any*/),
(v12/*: any*/), (v14/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v15/*: any*/),
"concreteType": "RiskConnection", "concreteType": "RiskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "risks", "name": "risks",
@@ -211,7 +232,14 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/), (v14/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "snapshotId",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -297,11 +325,11 @@ return {
"name": "owner", "name": "owner",
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/) (v13/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -359,9 +387,9 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v15/*: any*/),
"filters": [ "filters": [
"orderBy" "filter"
], ],
"handle": "connection", "handle": "connection",
"key": "RisksListQuery_risks", "key": "RisksListQuery_risks",
@@ -378,16 +406,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "ca8ad189a3d134eecaaf9adfe090277e", "cacheID": "161eb78d1d5100120c68db5d8106aa7e",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "RisksListQuery", "name": "RisksListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query RisksListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: RiskOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RiskGraphFragment_16fISc\n id\n }\n}\n\nfragment RiskGraphFragment_16fISc on Organization {\n risks(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" "text": "query RisksListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: RiskOrder = null\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RiskGraphFragment_25MC8O\n id\n }\n}\n\nfragment RiskGraphFragment_25MC8O on Organization {\n risks(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "dc662098b9aceaa2400f4656ed1b2d01"; (node as any).hash = "95c68a022cbf075eede9b7417393a369";
export default node; export default node;

View File

@@ -91,7 +91,7 @@ export default function MeasureEvidencesTab() {
const { measure } = useOutletContext<{ const { measure } = useOutletContext<{
measure: MeasureEvidencesTabFragment$key & { id: string; name: string }; measure: MeasureEvidencesTabFragment$key & { id: string; name: string };
}>(); }>();
const { evidenceId } = useParams<{ evidenceId: string }>(); const { evidenceId, snapshotId } = useParams<{ evidenceId: string; snapshotId?: string }>();
const pagination = usePaginationFragment(evidencesFragment, measure); const pagination = usePaginationFragment(evidencesFragment, measure);
const connectionId = pagination.data.evidences.__id; const connectionId = pagination.data.evidences.__id;
const evidences = const evidences =
@@ -101,6 +101,7 @@ export default function MeasureEvidencesTab() {
const evidence = evidences.find((e) => e.id === evidenceId); const evidence = evidences.find((e) => e.id === evidenceId);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const isSnapshotMode = Boolean(snapshotId);
usePageTitle(measure.name + " - " + __("Evidences")); usePageTitle(measure.name + " - " + __("Evidences"));
@@ -124,34 +125,41 @@ export default function MeasureEvidencesTab() {
measureId={measure.id} measureId={measure.id}
organizationId={organizationId} organizationId={organizationId}
connectionId={connectionId} connectionId={connectionId}
hideActions={isSnapshotMode}
snapshotId={snapshotId}
/> />
))} ))}
<TrButton {!isSnapshotMode && (
colspan={5} <TrButton
onClick={() => dialogRef.current?.open()} colspan={5}
icon={IconPlusLarge} onClick={() => dialogRef.current?.open()}
> icon={IconPlusLarge}
{__("Add evidence")} >
</TrButton> {__("Add evidence")}
</TrButton>
)}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
{evidence && ( {evidence && (
<EvidencePreviewDialog <EvidencePreviewDialog
key={evidence?.id} key={evidence?.id}
onClose={() => onClose={() => {
navigate( const baseUrl = isSnapshotMode
`/organizations/${organizationId}/measures/${measure.id}/evidences` ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/measures/${measure.id}/evidences`
) : `/organizations/${organizationId}/measures/${measure.id}/evidences`;
} navigate(baseUrl);
}}
evidenceId={evidence.id} evidenceId={evidence.id}
filename={evidence.filename} filename={evidence.filename}
/> />
)} )}
<CreateEvidenceDialog {!isSnapshotMode && (
ref={dialogRef} <CreateEvidenceDialog
measureId={measure.id} ref={dialogRef}
connectionId={connectionId} measureId={measure.id}
/> connectionId={connectionId}
/>
)}
</div> </div>
); );
} }
@@ -161,6 +169,8 @@ function EvidenceRow(props: {
measureId: string; measureId: string;
organizationId: string; organizationId: string;
connectionId: string; connectionId: string;
hideActions?: boolean;
snapshotId?: string;
}) { }) {
const evidence = useFragment(evidenceFragment, props.evidenceKey); const evidence = useFragment(evidenceFragment, props.evidenceKey);
const { __, dateFormat } = useTranslate(); const { __, dateFormat } = useTranslate();
@@ -203,6 +213,10 @@ function EvidenceRow(props: {
); );
}; };
const evidenceUrl = props.snapshotId
? `/organizations/${props.organizationId}/snapshots/${props.snapshotId}/risks/measures/${props.measureId}/evidences/${evidence.id}`
: `/organizations/${props.organizationId}/measures/${props.measureId}/evidences/${evidence.id}`;
return ( return (
<> <>
{isDownloading && ( {isDownloading && (
@@ -211,30 +225,30 @@ function EvidenceRow(props: {
onClose={() => setIsDownloading(false)} onClose={() => setIsDownloading(false)}
/> />
)} )}
<Tr <Tr to={evidenceUrl}>
to={`/organizations/${props.organizationId}/measures/${props.measureId}/evidences/${evidence.id}`}
>
<Td>{evidence.filename}</Td> <Td>{evidence.filename}</Td>
<Td>{fileType(__, evidence)}</Td> <Td>{fileType(__, evidence)}</Td>
<Td>{fileSize(__, evidence.size)}</Td> <Td>{fileSize(__, evidence.size)}</Td>
<Td>{dateFormat(evidence.createdAt)}</Td> <Td>{dateFormat(evidence.createdAt)}</Td>
<Td noLink> <Td noLink>
<div className="flex gap-2"> {!props.hideActions && (
<ActionDropdown> <div className="flex gap-2">
<DropdownItem onClick={() => setIsDownloading(true)}> <ActionDropdown>
<IconArrowInbox size={16} /> <DropdownItem onClick={() => setIsDownloading(true)}>
{__("Download")} <IconArrowInbox size={16} />
</DropdownItem> {__("Download")}
<DropdownItem </DropdownItem>
variant="danger" <DropdownItem
icon={IconTrashCan} variant="danger"
onClick={handleDelete} icon={IconTrashCan}
disabled={isDeleting} onClick={handleDelete}
> disabled={isDeleting}
{__("Delete")} >
</DropdownItem> {__("Delete")}
</ActionDropdown> </DropdownItem>
</div> </ActionDropdown>
</div>
)}
</Td> </Td>
</Tr> </Tr>
</> </>

View File

@@ -17,7 +17,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { Outlet, useNavigate, useParams } from "react-router"; import { Outlet, useNavigate, useParams } from "react-router";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { getTreatment, sprintf } from "@probo/helpers"; import { getTreatment, sprintf, validateSnapshotConsistency } from "@probo/helpers";
import { ConnectionHandler } from "relay-runtime"; import { ConnectionHandler } from "relay-runtime";
import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import FormRiskDialog from "./FormRiskDialog"; import FormRiskDialog from "./FormRiskDialog";
@@ -29,23 +29,27 @@ import {
useDeleteRiskMutation, useDeleteRiskMutation,
} from "/hooks/graph/RiskGraph"; } from "/hooks/graph/RiskGraph";
import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Props = { type Props = {
queryRef: PreloadedQuery<RiskGraphNodeQuery>; queryRef: PreloadedQuery<RiskGraphNodeQuery>;
}; };
export default function RiskDetailPage(props: Props) { export default function RiskDetailPage(props: Props) {
const { riskId } = useParams<{ riskId: string }>(); const { riskId, snapshotId } = useParams<{ riskId: string; snapshotId?: string }>();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const isSnapshotMode = Boolean(snapshotId);
if (!riskId) { if (!riskId) {
throw new Error("Cannot load risk detail page without riskId parameter"); throw new Error("Cannot load risk detail page without riskId parameter");
} }
const { __ } = useTranslate(); const { __ } = useTranslate();
const data = usePreloadedQuery(riskNodeQuery, props.queryRef); const data = usePreloadedQuery<RiskGraphNodeQuery>(riskNodeQuery, props.queryRef);
const risk = data.node; const risk = data.node;
validateSnapshotConsistency(risk, snapshotId);
const [deleteRisk] = useDeleteRiskMutation(); const [deleteRisk] = useDeleteRiskMutation();
usePageTitle(risk.name ?? "Risk detail"); usePageTitle(risk.name ?? "Risk detail");
@@ -54,7 +58,8 @@ export default function RiskDetailPage(props: Props) {
const onDelete = () => { const onDelete = () => {
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId, organizationId,
RisksConnectionKey RisksConnectionKey,
{ filter: { snapshotId: snapshotId || null } }
); );
confirm( confirm(
() => () =>
@@ -65,7 +70,10 @@ export default function RiskDetailPage(props: Props) {
connections: [connectionId], connections: [connectionId],
}, },
onSuccess() { onSuccess() {
navigate(`/organizations/${organizationId}/risks`); const risksUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks`
: `/organizations/${organizationId}/risks`;
navigate(risksUrl);
resolve(); resolve();
}, },
}); });
@@ -85,68 +93,75 @@ export default function RiskDetailPage(props: Props) {
const measuresCount = risk.measuresInfo?.totalCount ?? 0; const measuresCount = risk.measuresInfo?.totalCount ?? 0;
const controlsCount = risk.controlsInfo?.totalCount ?? 0; const controlsCount = risk.controlsInfo?.totalCount ?? 0;
const risksUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks`
: `/organizations/${organizationId}/risks`;
const baseTabUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${riskId}`
: `/organizations/${organizationId}/risks/${riskId}`;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
{/* Header */} {/* Header */}
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("Risks"), label: __("Risks"),
to: `/organizations/${organizationId}/risks`, to: risksUrl,
}, },
{ {
label: __("Risk detail"), label: __("Risk detail"),
}, },
]} ]}
/> />
<div className="flex gap-2"> {!isSnapshotMode && (
<FormRiskDialog <div className="flex gap-2">
trigger={ <FormRiskDialog
<Button icon={IconPencil} variant="secondary"> trigger={
{__("Edit")} <Button icon={IconPencil} variant="secondary">
</Button> {__("Edit")}
} </Button>
risk={{ id: riskId, ...risk }} }
/> risk={{ id: riskId, ...risk }}
<ActionDropdown variant="secondary"> />
<DropdownItem <ActionDropdown variant="secondary">
variant="danger" <DropdownItem
icon={IconTrashCan} variant="danger"
onClick={onDelete} icon={IconTrashCan}
> onClick={onDelete}
{__("Delete")} >
</DropdownItem> {__("Delete")}
</ActionDropdown> </DropdownItem>
</div> </ActionDropdown>
</div>
)}
</div> </div>
<PageHeader title={risk.name} /> <PageHeader title={risk.name} />
<Tabs> <Tabs>
<TabLink <TabLink to={`${baseTabUrl}/overview`}>
to={`/organizations/${organizationId}/risks/${riskId}/overview`}
>
{__("Overview")} {__("Overview")}
</TabLink> </TabLink>
<TabLink {!isSnapshotMode && (
to={`/organizations/${organizationId}/risks/${riskId}/measures`} <>
> <TabLink to={`${baseTabUrl}/measures`}>
{__("Measures")} {__("Measures")}
<TabBadge>{measuresCount}</TabBadge> <TabBadge>{measuresCount}</TabBadge>
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseTabUrl}/documents`}>
to={`/organizations/${organizationId}/risks/${riskId}/documents`} {__("Documents")}
> <TabBadge>{documentsCount}</TabBadge>
{__("Documents")} </TabLink>
<TabBadge>{documentsCount}</TabBadge> <TabLink to={`${baseTabUrl}/controls`}>
</TabLink> {__("Controls")}
<TabLink <TabBadge>{controlsCount}</TabBadge>
to={`/organizations/${organizationId}/risks/${riskId}/controls`} </TabLink>
> </>
{__("Controls")} )}
<TabBadge>{controlsCount}</TabBadge>
</TabLink>
</Tabs> </Tabs>
<Outlet context={{ risk }} /> <Outlet context={{ risk }} />

View File

@@ -27,6 +27,8 @@ import { SortableTable, SortableTh } from "/components/SortableTable";
import type { PreloadedQuery } from "react-relay"; import type { PreloadedQuery } from "react-relay";
import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphListQuery.graphql"; import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphListQuery.graphql";
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql"; import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
import { useParams } from "react-router";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Props = { type Props = {
queryRef: PreloadedQuery<RiskGraphListQuery>; queryRef: PreloadedQuery<RiskGraphListQuery>;
@@ -35,18 +37,28 @@ type Props = {
export default function RisksPage(props: Props) { export default function RisksPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { connectionId, risks, refetch } = useRisksQuery(props.queryRef); const { connectionId, risks, refetch } = useRisksQuery(props.queryRef, snapshotId);
usePageTitle(__("Risks")); usePageTitle(__("Risks"));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader title={__("Risks")}> {snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<FormRiskDialog <PageHeader
connection={connectionId} title={__("Risks")}
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>} description={__(
/> "Risks are potential threats to your organization. Manage them by identifying, assessing, and implementing mitigation measures."
)}
>
{!isSnapshotMode && (
<FormRiskDialog
connection={connectionId}
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
/>
)}
</PageHeader> </PageHeader>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -100,6 +112,8 @@ type RowProps = {
function RiskRow(props: RowProps) { function RiskRow(props: RowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { risk, connectionId, organizationId } = props; const { risk, connectionId, organizationId } = props;
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [deleteRisk] = useDeleteRiskMutation(); const [deleteRisk] = useDeleteRiskMutation();
const confirm = useConfirm(); const confirm = useConfirm();
const onDelete = () => { const onDelete = () => {
@@ -125,14 +139,21 @@ function RiskRow(props: RowProps) {
); );
}; };
const formDialogRef = useDialogRef(); const formDialogRef = useDialogRef();
const riskUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${risk.id}/overview`
: `/organizations/${organizationId}/risks/${risk.id}/overview`;
return ( return (
<> <>
<FormRiskDialog {!isSnapshotMode && (
ref={formDialogRef} <FormRiskDialog
risk={risk} ref={formDialogRef}
connection={connectionId} risk={risk}
/> connection={connectionId}
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}> />
)}
<Tr to={riskUrl}>
<Td>{risk.name}</Td> <Td>{risk.name}</Td>
<Td>{risk.category}</Td> <Td>{risk.category}</Td>
<Td>{getTreatment(__, risk.treatment)}</Td> <Td>{getTreatment(__, risk.treatment)}</Td>
@@ -143,22 +164,24 @@ function RiskRow(props: RowProps) {
<SeverityBadge score={risk.residualRiskScore} /> <SeverityBadge score={risk.residualRiskScore} />
</Td> </Td>
<Td noLink className="text-end"> <Td noLink className="text-end">
<ActionDropdown> {!isSnapshotMode && (
<DropdownItem <ActionDropdown>
icon={IconPencil} <DropdownItem
onClick={() => formDialogRef.current?.open()} icon={IconPencil}
> onClick={() => formDialogRef.current?.open()}
{__("Edit")} >
</DropdownItem> {__("Edit")}
</DropdownItem>
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
onClick={onDelete} onClick={onDelete}
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
)}
</Td> </Td>
</Tr> </Tr>
</> </>

View File

@@ -14,7 +14,20 @@ export const riskRoutes = [
path: "risks", path: "risks",
fallback: RisksPageSkeleton, fallback: RisksPageSkeleton,
queryLoader: ({ organizationId }) => queryLoader: ({ organizationId }) =>
loadQuery(relayEnvironment, risksQuery, { organizationId }), loadQuery(relayEnvironment, risksQuery, {
organizationId,
snapshotId: null
}),
Component: lazy(() => import("/pages/organizations/risks/RisksPage")),
},
{
path: "snapshots/:snapshotId/risks",
fallback: RisksPageSkeleton,
queryLoader: ({ organizationId, snapshotId }) =>
loadQuery(relayEnvironment, risksQuery, {
organizationId,
snapshotId
}),
Component: lazy(() => import("/pages/organizations/risks/RisksPage")), Component: lazy(() => import("/pages/organizations/risks/RisksPage")),
}, },
{ {
@@ -61,4 +74,27 @@ export const riskRoutes = [
}, },
], ],
}, },
{
path: "snapshots/:snapshotId/risks/:riskId",
fallback: PageSkeleton,
queryLoader: ({ riskId }) =>
loadQuery(relayEnvironment, riskNodeQuery, { riskId }),
Component: lazy(() => import("/pages/organizations/risks/RiskDetailPage")),
children: [
{
path: "",
loader: () => {
throw redirect("overview");
},
Component: Fragment,
},
{
path: "overview",
fallback: LinkCardSkeleton,
Component: lazy(
() => import("/pages/organizations/risks/tabs/RiskOverviewTab.tsx")
),
},
],
},
] satisfies AppRoute[]; ] satisfies AppRoute[];

View File

@@ -1,6 +1,7 @@
type Translator = (s: string) => string; type Translator = (s: string) => string;
export const snapshotTypes = [ export const snapshotTypes = [
"RISKS",
"VENDORS", "VENDORS",
"ASSETS", "ASSETS",
"DATA", "DATA",
@@ -39,6 +40,8 @@ export function getSnapshotTypeLabel(__: Translator, type: string | null | undef
export function getSnapshotTypeUrlPath(type?: string): string { export function getSnapshotTypeUrlPath(type?: string): string {
switch (type) { switch (type) {
case "RISKS":
return "/risks";
case "VENDORS": case "VENDORS":
return "/vendors"; return "/vendors";
case "ASSETS": case "ASSETS":

View File

@@ -0,0 +1,11 @@
ALTER TABLE risks ADD COLUMN snapshot_id TEXT;
ALTER TABLE risks ADD COLUMN source_id TEXT;
ALTER TABLE risks ADD CONSTRAINT risks_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE risks ADD CONSTRAINT risks_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);

View File

@@ -42,11 +42,17 @@ type (
ResidualLikelihood int `db:"residual_likelihood"` ResidualLikelihood int `db:"residual_likelihood"`
ResidualImpact int `db:"residual_impact"` ResidualImpact int `db:"residual_impact"`
ResidualRiskScore int `db:"residual_risk_score"` ResidualRiskScore int `db:"residual_risk_score"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at"`
} }
Risks []*Risk Risks []*Risk
RiskSnapshotter interface {
InsertRiskSnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error
}
) )
func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey { func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
@@ -79,7 +85,9 @@ func (r *Risks) CountByMeasureID(
WITH rsks AS ( WITH rsks AS (
SELECT SELECT
r.id, r.id,
r.tenant_id r.tenant_id,
r.search_vector,
r.snapshot_id
FROM FROM
risks r risks r
INNER JOIN INNER JOIN
@@ -136,6 +144,8 @@ WITH rsks AS (
r.residual_likelihood, r.residual_likelihood,
r.residual_impact, r.residual_impact,
r.residual_risk_score, r.residual_risk_score,
r.snapshot_id,
r.source_id,
r.search_vector, r.search_vector,
r.created_at, r.created_at,
r.updated_at r.updated_at
@@ -161,6 +171,8 @@ SELECT
residual_likelihood, residual_likelihood,
residual_impact, residual_impact,
residual_risk_score, residual_risk_score,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -246,6 +258,8 @@ SELECT
residual_impact, residual_impact,
residual_risk_score, residual_risk_score,
category, category,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM risks FROM risks
@@ -298,6 +312,8 @@ SELECT
residual_likelihood, residual_likelihood,
residual_impact, residual_impact,
residual_risk_score, residual_risk_score,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM risks FROM risks
@@ -378,6 +394,7 @@ SET
updated_at = @updated_at updated_at = @updated_at
WHERE %s WHERE %s
AND id = @risk_id AND id = @risk_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -408,7 +425,7 @@ func (r *Risk) Delete(
riskID gid.GID, riskID gid.GID,
) error { ) error {
q := ` q := `
DELETE FROM risks WHERE %s AND id = @id DELETE FROM risks WHERE %s AND id = @id AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -430,7 +447,9 @@ func (r *Risks) CountByDocumentID(
WITH rsks AS ( WITH rsks AS (
SELECT SELECT
r.id, r.id,
r.tenant_id r.tenant_id,
r.search_vector,
r.snapshot_id
FROM FROM
risks r risks r
INNER JOIN INNER JOIN
@@ -460,3 +479,78 @@ WHERE %s
return count, nil return count, nil
} }
func (r Risks) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
if err := r.InsertRiskSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
return fmt.Errorf("cannot create risk snapshots: %w", err)
}
return nil
}
func (r Risks) InsertRiskSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
INSERT INTO risks (
tenant_id,
id,
snapshot_id,
source_id,
organization_id,
name,
description,
category,
treatment,
note,
owner_id,
inherent_likelihood,
inherent_impact,
residual_likelihood,
residual_impact,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @risk_entity_type),
@snapshot_id,
r.id,
r.organization_id,
r.name,
r.description,
r.category,
r.treatment,
r.note,
r.owner_id,
r.inherent_likelihood,
r.inherent_impact,
r.residual_likelihood,
r.residual_impact,
r.created_at,
r.updated_at
FROM risks r
WHERE %s AND organization_id = @organization_id AND snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"risk_entity_type": RiskEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert risk snapshots: %w", err)
}
return nil
}

View File

@@ -15,36 +15,62 @@
package coredata package coredata
import ( import (
"github.com/getprobo/probo/pkg/gid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
type ( type (
RiskFilter struct { RiskFilter struct {
query *string query *string
snapshotID **gid.GID
} }
) )
func NewRiskFilter(query *string) *RiskFilter { func NewRiskFilter(query *string, snapshotID **gid.GID) *RiskFilter {
return &RiskFilter{ return &RiskFilter{
query: query, query: query,
snapshotID: snapshotID,
} }
} }
func (f *RiskFilter) SQLArguments() pgx.NamedArgs { func (f *RiskFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.NamedArgs{ args := pgx.StrictNamedArgs{
"query": f.query, "query": f.query,
} }
if f.snapshotID == nil {
args["has_snapshot_filter"] = false
args["filter_snapshot_id"] = nil
} else if *f.snapshotID == nil {
args["has_snapshot_filter"] = true
args["filter_snapshot_id"] = nil
} else {
args["has_snapshot_filter"] = true
args["filter_snapshot_id"] = **f.snapshotID
}
return args
} }
func (f *RiskFilter) SQLFragment() string { func (f *RiskFilter) SQLFragment() string {
if f.query == nil || *f.query == "" {
return "TRUE"
}
return ` return `
search_vector @@ ( (
SELECT to_tsquery('simple', string_agg(lexeme || ':*', ' & ')) CASE
FROM unnest(regexp_split_to_array(trim(@query), '\s+')) AS lexeme WHEN @query::text IS NOT NULL AND @query::text != '' THEN
) search_vector @@ (
` SELECT to_tsquery('simple', string_agg(lexeme || ':*', ' & '))
FROM unnest(regexp_split_to_array(trim(@query), '\s+')) AS lexeme
)
ELSE TRUE
END
AND
CASE
WHEN @has_snapshot_filter::boolean = false THEN TRUE
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NOT NULL THEN
snapshot_id = @filter_snapshot_id::text
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NULL THEN
snapshot_id IS NULL
ELSE TRUE
END
)`
} }

View File

@@ -30,6 +30,8 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
switch snapshotType { switch snapshotType {
case SnapshotsTypeAssets: case SnapshotsTypeAssets:
return Assets{}, nil return Assets{}, nil
case SnapshotsTypeRisks:
return Risks{}, nil
case SnapshotsTypeData: case SnapshotsTypeData:
return Data{}, nil return Data{}, nil
case SnapshotsTypeNonConformityRegistries: case SnapshotsTypeNonConformityRegistries:

View File

@@ -364,12 +364,13 @@ func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID
for _, measure := range measures { for _, measure := range measures {
risks := coredata.Risks{} risks := coredata.Risks{}
var nilSnapshotID *gid.GID = nil
risksCount, err := risks.CountByMeasureID( risksCount, err := risks.CountByMeasureID(
ctx, ctx,
conn, conn,
s.svc.scope, s.svc.scope,
measure.ID, measure.ID,
coredata.NewRiskFilter(nil), coredata.NewRiskFilter(nil, &nilSnapshotID),
) )
if err != nil { if err != nil {
return fmt.Errorf("cannot count risks: %w", err) return fmt.Errorf("cannot count risks: %w", err)
@@ -405,12 +406,13 @@ func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID
for _, document := range documents { for _, document := range documents {
risks := coredata.Risks{} risks := coredata.Risks{}
var nilSnapshotID *gid.GID = nil
risksCount, err := risks.CountByDocumentID( risksCount, err := risks.CountByDocumentID(
ctx, ctx,
conn, conn,
s.svc.scope, s.svc.scope,
document.ID, document.ID,
coredata.NewRiskFilter(nil), coredata.NewRiskFilter(nil, &nilSnapshotID),
) )
if err != nil { if err != nil {
return fmt.Errorf("cannot count risks: %w", err) return fmt.Errorf("cannot count risks: %w", err)

View File

@@ -186,7 +186,6 @@ func (s RiskService) CreateDocumentMapping(
riskDocument := &coredata.RiskDocument{ riskDocument := &coredata.RiskDocument{
RiskID: risk.ID, RiskID: risk.ID,
DocumentID: document.ID, DocumentID: document.ID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
@@ -254,7 +253,6 @@ func (s RiskService) CreateMeasureMapping(
riskMeasure := &coredata.RiskMeasure{ riskMeasure := &coredata.RiskMeasure{
RiskID: risk.ID, RiskID: risk.ID,
MeasureID: measure.ID, MeasureID: measure.ID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
@@ -291,7 +289,6 @@ func (s RiskService) DeleteMeasureMapping(
riskMeasure := &coredata.RiskMeasure{ riskMeasure := &coredata.RiskMeasure{
RiskID: riskID, RiskID: riskID,
MeasureID: measureID, MeasureID: measureID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }

View File

@@ -1100,6 +1100,7 @@ input MeasureFilter {
input RiskFilter { input RiskFilter {
query: String query: String
snapshotId: ID
} }
input PeopleFilter { input PeopleFilter {
@@ -1236,7 +1237,7 @@ type Organization implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: RiskOrder orderBy: RiskOrder
filter: RiskFilter filter: RiskFilter = { snapshotId: null }
): RiskConnection! @goField(forceResolver: true) ): RiskConnection! @goField(forceResolver: true)
tasks( tasks(
@@ -1664,6 +1665,7 @@ type Document implements Node {
type Risk implements Node { type Risk implements Node {
id: ID! id: ID!
snapshotId: ID
name: String! name: String!
description: String! description: String!
category: String! category: String!

View File

@@ -1027,6 +1027,7 @@ type ComplexityRoot struct {
ResidualImpact func(childComplexity int) int ResidualImpact func(childComplexity int) int
ResidualLikelihood func(childComplexity int) int ResidualLikelihood func(childComplexity int) int
ResidualRiskScore func(childComplexity int) int ResidualRiskScore func(childComplexity int) int
SnapshotID func(childComplexity int) int
Treatment func(childComplexity int) int Treatment func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -6224,6 +6225,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Risk.ResidualRiskScore(childComplexity), true return e.complexity.Risk.ResidualRiskScore(childComplexity), true
case "Risk.snapshotId":
if e.complexity.Risk.SnapshotID == nil {
break
}
return e.complexity.Risk.SnapshotID(childComplexity), true
case "Risk.treatment": case "Risk.treatment":
if e.complexity.Risk.Treatment == nil { if e.complexity.Risk.Treatment == nil {
break break
@@ -9061,6 +9069,7 @@ input MeasureFilter {
input RiskFilter { input RiskFilter {
query: String query: String
snapshotId: ID
} }
input PeopleFilter { input PeopleFilter {
@@ -9197,7 +9206,7 @@ type Organization implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: RiskOrder orderBy: RiskOrder
filter: RiskFilter filter: RiskFilter = { snapshotId: null }
): RiskConnection! @goField(forceResolver: true) ): RiskConnection! @goField(forceResolver: true)
tasks( tasks(
@@ -9625,6 +9634,7 @@ type Document implements Node {
type Risk implements Node { type Risk implements Node {
id: ID! id: ID!
snapshotId: ID
name: String! name: String!
description: String! description: String!
category: String! category: String!
@@ -45984,6 +45994,47 @@ func (ec *executionContext) fieldContext_Risk_id(_ context.Context, field graphq
return fc, nil return fc, nil
} }
func (ec *executionContext) _Risk_snapshotId(ctx context.Context, field graphql.CollectedField, obj *types.Risk) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Risk_snapshotId(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 obj.SnapshotID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*gid.GID)
fc.Result = res
return ec.marshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_snapshotId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Risk",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Risk_name(ctx context.Context, field graphql.CollectedField, obj *types.Risk) (ret graphql.Marshaler) { func (ec *executionContext) _Risk_name(ctx context.Context, field graphql.CollectedField, obj *types.Risk) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Risk_name(ctx, field) fc, err := ec.fieldContext_Risk_name(ctx, field)
if err != nil { if err != nil {
@@ -47135,6 +47186,8 @@ func (ec *executionContext) fieldContext_RiskEdge_node(_ context.Context, field
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Risk_id(ctx, field) return ec.fieldContext_Risk_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Risk_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Risk_name(ctx, field) return ec.fieldContext_Risk_name(ctx, field)
case "description": case "description":
@@ -51015,6 +51068,8 @@ func (ec *executionContext) fieldContext_UpdateRiskPayload_risk(_ context.Contex
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Risk_id(ctx, field) return ec.fieldContext_Risk_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Risk_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Risk_name(ctx, field) return ec.fieldContext_Risk_name(ctx, field)
case "description": case "description":
@@ -63958,7 +64013,7 @@ func (ec *executionContext) unmarshalInputRiskFilter(ctx context.Context, obj an
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"query"} fieldsInOrder := [...]string{"query", "snapshotId"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -63972,6 +64027,13 @@ func (ec *executionContext) unmarshalInputRiskFilter(ctx context.Context, obj an
return it, err return it, err
} }
it.Query = data it.Query = data
case "snapshotId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("snapshotId"))
data, err := ec.unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.SnapshotID = data
} }
} }
@@ -76125,6 +76187,8 @@ func (ec *executionContext) _Risk(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "snapshotId":
out.Values[i] = ec._Risk_snapshotId(ctx, field, obj)
case "name": case "name":
out.Values[i] = ec._Risk_name(ctx, field, obj) out.Values[i] = ec._Risk_name(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {

View File

@@ -67,6 +67,7 @@ func NewRisk(r *coredata.Risk) *Risk {
return &Risk{ return &Risk{
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.Name,
SnapshotID: r.SnapshotID,
Description: r.Description, Description: r.Description,
Treatment: r.Treatment, Treatment: r.Treatment,
InherentLikelihood: r.InherentLikelihood, InherentLikelihood: r.InherentLikelihood,

View File

@@ -1336,6 +1336,7 @@ type RequestSignaturePayload struct {
type Risk struct { type Risk struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Category string `json:"category"` Category string `json:"category"`
@@ -1365,7 +1366,8 @@ type RiskEdge struct {
} }
type RiskFilter struct { type RiskFilter struct {
Query *string `json:"query,omitempty"` Query *string `json:"query,omitempty"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
} }
type SendSigningNotificationsInput struct { type SendSigningNotificationsInput struct {

View File

@@ -1034,9 +1034,9 @@ func (r *measureResolver) Risks(ctx context.Context, obj *types.Measure, first *
cursor := types.NewCursor(first, after, last, before, pageOrderBy) cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var riskFilter = coredata.NewRiskFilter(nil) var riskFilter = coredata.NewRiskFilter(nil, nil)
if filter != nil { if filter != nil {
riskFilter = coredata.NewRiskFilter(filter.Query) riskFilter = coredata.NewRiskFilter(filter.Query, &filter.SnapshotID)
} }
page, err := prb.Risks.ListForMeasureID(ctx, obj.ID, cursor, riskFilter) page, err := prb.Risks.ListForMeasureID(ctx, obj.ID, cursor, riskFilter)
@@ -3595,9 +3595,9 @@ func (r *organizationResolver) Risks(ctx context.Context, obj *types.Organizatio
cursor := types.NewCursor(first, after, last, before, pageOrderBy) cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var riskFilter = coredata.NewRiskFilter(nil) var riskFilter = coredata.NewRiskFilter(nil, nil)
if filter != nil { if filter != nil {
riskFilter = coredata.NewRiskFilter(filter.Query) riskFilter = coredata.NewRiskFilter(filter.Query, &filter.SnapshotID)
} }
page, err := prb.Risks.ListForOrganizationID(ctx, obj.ID, cursor, riskFilter) page, err := prb.Risks.ListForOrganizationID(ctx, obj.ID, cursor, riskFilter)