Add controls snapshots
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import { useNavigate, useOutletContext } from "react-router";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard";
|
||||
import { LinkedAuditsCard } from "/components/audits/LinkedAuditsCard";
|
||||
import { LinkedSnapshotsCard } from "/components/snapshots/LinkedSnapshotsCard";
|
||||
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
|
||||
import { promisifyMutation } from "@probo/helpers";
|
||||
import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql";
|
||||
@@ -105,6 +106,33 @@ const detachAuditMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const attachSnapshotMutation = graphql`
|
||||
mutation FrameworkControlPageAttachSnapshotMutation(
|
||||
$input: CreateControlSnapshotMappingInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createControlSnapshotMapping(input: $input) {
|
||||
snapshotEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
...LinkedSnapshotsCardFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const detachSnapshotMutation = graphql`
|
||||
mutation FrameworkControlPageDetachSnapshotMutation(
|
||||
$input: DeleteControlSnapshotMappingInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteControlSnapshotMapping(input: $input) {
|
||||
deletedSnapshotId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteControlMutation = graphql`
|
||||
mutation FrameworkControlPageDeleteControlMutation(
|
||||
$input: DeleteControlInput!
|
||||
@@ -148,6 +176,8 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
);
|
||||
const [detachAudit, isDetachingAudit] = useMutation(detachAuditMutation);
|
||||
const [attachAudit, isAttachingAudit] = useMutation(attachAuditMutation);
|
||||
const [detachSnapshot, isDetachingSnapshot] = useMutation(detachSnapshotMutation);
|
||||
const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation);
|
||||
const [deleteControl] = useMutation(deleteControlMutation);
|
||||
|
||||
const onDelete = () => {
|
||||
@@ -216,34 +246,51 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
</div>
|
||||
)}
|
||||
<div className={control.status === "EXCLUDED" ? "opacity-60" : ""}>
|
||||
<div className="text-base">{control.name}</div>
|
||||
<LinkedMeasuresCard
|
||||
variant="card"
|
||||
measures={control.measures?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.measures?.__id!}
|
||||
onAttach={attachMeasure}
|
||||
onDetach={detachMeasure}
|
||||
disabled={isAttachingMeasure || isDetachingMeasure}
|
||||
/>
|
||||
<LinkedDocumentsCard
|
||||
variant="card"
|
||||
documents={control.documents?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.documents?.__id!}
|
||||
onAttach={attachDocument}
|
||||
onDetach={detachDocument}
|
||||
disabled={isAttachingDocument || isDetachingDocument}
|
||||
/>
|
||||
<LinkedAuditsCard
|
||||
variant="card"
|
||||
audits={control.audits?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.audits?.__id!}
|
||||
onAttach={attachAudit}
|
||||
onDetach={detachAudit}
|
||||
disabled={isAttachingAudit || isDetachingAudit}
|
||||
/>
|
||||
<div className="text-base mb-4">{control.name}</div>
|
||||
<div className="mb-4">
|
||||
<LinkedMeasuresCard
|
||||
variant="card"
|
||||
measures={control.measures?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.measures?.__id!}
|
||||
onAttach={attachMeasure}
|
||||
onDetach={detachMeasure}
|
||||
disabled={isAttachingMeasure || isDetachingMeasure}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<LinkedDocumentsCard
|
||||
variant="card"
|
||||
documents={control.documents?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.documents?.__id!}
|
||||
onAttach={attachDocument}
|
||||
onDetach={detachDocument}
|
||||
disabled={isAttachingDocument || isDetachingDocument}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<LinkedAuditsCard
|
||||
variant="card"
|
||||
audits={control.audits?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.audits?.__id!}
|
||||
onAttach={attachAudit}
|
||||
onDetach={detachAudit}
|
||||
disabled={isAttachingAudit || isDetachingAudit}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<LinkedSnapshotsCard
|
||||
variant="card"
|
||||
snapshots={control.snapshots?.edges.map((edge) => edge.node) ?? []}
|
||||
params={{ controlId: control.id }}
|
||||
connectionId={control.snapshots?.__id!}
|
||||
onAttach={attachSnapshot}
|
||||
onDetach={detachSnapshot}
|
||||
disabled={isAttachingSnapshot || isDetachingSnapshot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* @generated SignedSource<<a4b20010a96dac4779e4b1576b947e45>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type CreateControlSnapshotMappingInput = {
|
||||
controlId: string;
|
||||
snapshotId: string;
|
||||
};
|
||||
export type FrameworkControlPageAttachSnapshotMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: CreateControlSnapshotMappingInput;
|
||||
};
|
||||
export type FrameworkControlPageAttachSnapshotMutation$data = {
|
||||
readonly createControlSnapshotMapping: {
|
||||
readonly snapshotEdge: {
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsCardFragment">;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FrameworkControlPageAttachSnapshotMutation = {
|
||||
response: FrameworkControlPageAttachSnapshotMutation$data;
|
||||
variables: FrameworkControlPageAttachSnapshotMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkControlPageAttachSnapshotMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateControlSnapshotMappingPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createControlSnapshotMapping",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "SnapshotEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "snapshotEdge",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Snapshot",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "LinkedSnapshotsCardFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkControlPageAttachSnapshotMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateControlSnapshotMappingPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createControlSnapshotMapping",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "SnapshotEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "snapshotEdge",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Snapshot",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "prependEdge",
|
||||
"key": "",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "snapshotEdge",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "52c76e336bb7f38dc2d2f6620817100a",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "FrameworkControlPageAttachSnapshotMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation FrameworkControlPageAttachSnapshotMutation(\n $input: CreateControlSnapshotMappingInput!\n) {\n createControlSnapshotMapping(input: $input) {\n snapshotEdge {\n node {\n id\n ...LinkedSnapshotsCardFragment\n }\n }\n }\n}\n\nfragment LinkedSnapshotsCardFragment on Snapshot {\n id\n name\n description\n type\n createdAt\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "9e8ad62cc8d9f3672e1f785c31a91624";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @generated SignedSource<<d4c60256aa53236cfb4c3a01f4fd35c1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteControlSnapshotMappingInput = {
|
||||
controlId: string;
|
||||
snapshotId: string;
|
||||
};
|
||||
export type FrameworkControlPageDetachSnapshotMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: DeleteControlSnapshotMappingInput;
|
||||
};
|
||||
export type FrameworkControlPageDetachSnapshotMutation$data = {
|
||||
readonly deleteControlSnapshotMapping: {
|
||||
readonly deletedSnapshotId: string;
|
||||
};
|
||||
};
|
||||
export type FrameworkControlPageDetachSnapshotMutation = {
|
||||
response: FrameworkControlPageDetachSnapshotMutation$data;
|
||||
variables: FrameworkControlPageDetachSnapshotMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "deletedSnapshotId",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkControlPageDetachSnapshotMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "DeleteControlSnapshotMappingPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "deleteControlSnapshotMapping",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkControlPageDetachSnapshotMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "DeleteControlSnapshotMappingPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "deleteControlSnapshotMapping",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "deleteEdge",
|
||||
"key": "",
|
||||
"kind": "ScalarHandle",
|
||||
"name": "deletedSnapshotId",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "610636745f2c0441c54bd0028fc52cf0",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "FrameworkControlPageDetachSnapshotMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation FrameworkControlPageDetachSnapshotMutation(\n $input: DeleteControlSnapshotMappingInput!\n) {\n deleteControlSnapshotMapping(input: $input) {\n deletedSnapshotId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "be8d40beadabe60d8c9bee4d99b885b7";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
|
||||
import { snapshotNodeQuery } from "/hooks/graph/SnapshotGraph";
|
||||
import type { SnapshotGraphNodeQuery } from "/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { PageError } from "/components/PageError";
|
||||
import { getSnapshotTypeUrlPath } from "@probo/helpers";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<SnapshotGraphNodeQuery>;
|
||||
};
|
||||
|
||||
export default function SnapshotDetailPage({ queryRef }: Props) {
|
||||
const navigate = useNavigate();
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams();
|
||||
const data = usePreloadedQuery(snapshotNodeQuery, queryRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.node || !data.node.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = data.node;
|
||||
const snapshotType = snapshot.type;
|
||||
const urlPath = getSnapshotTypeUrlPath(snapshotType);
|
||||
|
||||
navigate(`/organizations/${organizationId}/snapshots/${snapshotId}${urlPath}`, {
|
||||
replace: true,
|
||||
});
|
||||
}, [data.node, navigate, organizationId, snapshotId]);
|
||||
|
||||
if (!data.node || !data.node.type) {
|
||||
return <PageError />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
type PreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { getSnapshotTypeLabel } from "@probo/helpers";
|
||||
import { getSnapshotTypeLabel, getSnapshotTypeUrlPath } from "@probo/helpers";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
Button,
|
||||
DropdownItem,
|
||||
IconPlusLarge,
|
||||
@@ -131,28 +132,15 @@ function SnapshotRow(props: SnapshotRowProps) {
|
||||
const { __, dateFormat } = useTranslate();
|
||||
const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId);
|
||||
|
||||
const getSnapshotUrl = (snapshot: SnapshotRowProps["snapshot"]) => {
|
||||
const baseUrl = `/organizations/${props.organizationId}/snapshots/${snapshot.id}`;
|
||||
|
||||
switch (snapshot.type) {
|
||||
case "DATA":
|
||||
return `${baseUrl}/data`;
|
||||
case "VENDORS":
|
||||
return `${baseUrl}/vendors`;
|
||||
case "RISKS":
|
||||
return `${baseUrl}/risks`;
|
||||
case "ASSETS":
|
||||
return `${baseUrl}/assets`;
|
||||
default:
|
||||
return baseUrl;
|
||||
}
|
||||
};
|
||||
const typePath = getSnapshotTypeUrlPath(props.snapshot.type);
|
||||
|
||||
return (
|
||||
<Tr to={getSnapshotUrl(props.snapshot)}>
|
||||
<Tr to={`/organizations/${props.organizationId}/snapshots/${props.snapshot.id}${typePath}`}>
|
||||
<Td className="font-medium">{props.snapshot.name}</Td>
|
||||
<Td className="text-txt-secondary">
|
||||
{getSnapshotTypeLabel(__, props.snapshot.type)}
|
||||
<Td>
|
||||
<Badge variant="neutral">
|
||||
{getSnapshotTypeLabel(__, props.snapshot.type)}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="text-txt-secondary">
|
||||
{props.snapshot.description || __("No description")}
|
||||
|
||||
Reference in New Issue
Block a user