Add vendor snapshots

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-01 10:24:17 +02:00
parent d7ffed9d7f
commit 078102db74
32 changed files with 1381 additions and 426 deletions

View File

@@ -89,11 +89,11 @@ export const useDeleteVendor = (
export const vendorConnectionKey = "VendorsPage_vendors"; export const vendorConnectionKey = "VendorsPage_vendors";
export const vendorsQuery = graphql` export const vendorsQuery = graphql`
query VendorGraphListQuery($organizationId: ID!) { query VendorGraphListQuery($organizationId: ID!, $snapshotId: ID) {
node(id: $organizationId) { node(id: $organizationId) {
... on Organization { ... on Organization {
id id
...VendorGraphPaginatedFragment ...VendorGraphPaginatedFragment @arguments(snapshotId: $snapshotId)
} }
} }
} }
@@ -108,6 +108,7 @@ export const paginatedVendorsFragment = 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 }
) { ) {
vendors( vendors(
first: $first first: $first
@@ -115,11 +116,13 @@ export const paginatedVendorsFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorsListQuery_vendors") { filter: { snapshotId: $snapshotId }
) @connection(key: "VendorsListQuery_vendors", filters: ["filter"]) {
__id __id
edges { edges {
node { node {
id id
snapshotId
name name
websiteUrl websiteUrl
updatedAt updatedAt
@@ -148,6 +151,7 @@ export const vendorNodeQuery = graphql`
node(id: $vendorId) { node(id: $vendorId) {
... on Vendor { ... on Vendor {
id id
snapshotId
name name
websiteUrl websiteUrl
...useVendorFormFragment ...useVendorFormFragment

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<b748739bef19b5c39b7c352f35332865>> * @generated SignedSource<<8b1614888dac560deff871232f91995d>>
* @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 VendorGraphListQuery$variables = { export type VendorGraphListQuery$variables = {
organizationId: string; organizationId: string;
snapshotId?: string | null | undefined;
}; };
export type VendorGraphListQuery$data = { export type VendorGraphListQuery$data = {
readonly node: { readonly node: {
@@ -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",
@@ -80,7 +98,7 @@ return {
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
{ {
"args": null, "args": (v3/*: any*/),
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "VendorGraphPaginatedFragment" "name": "VendorGraphPaginatedFragment"
} }
@@ -109,14 +127,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": "VendorConnection", "concreteType": "VendorConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "vendors", "name": "vendors",
@@ -139,6 +157,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,
@@ -236,7 +261,7 @@ return {
], ],
"storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})" "storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})"
}, },
(v3/*: any*/) (v4/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -302,13 +327,13 @@ return {
] ]
} }
], ],
"storageKey": "vendors(first:50)" "storageKey": null
}, },
{ {
"alias": null, "alias": null,
"args": (v4/*: any*/), "args": (v5/*: any*/),
"filters": [ "filters": [
"orderBy" "filter"
], ],
"handle": "connection", "handle": "connection",
"key": "VendorsListQuery_vendors", "key": "VendorsListQuery_vendors",
@@ -325,16 +350,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "e2eade63dcf3d7ea071463a2e5993293", "cacheID": "4b19f6f4be47c38d8b68601ec95315a6",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorGraphListQuery", "name": "VendorGraphListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query VendorGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...VendorGraphPaginatedFragment\n }\n id\n }\n}\n\nfragment VendorGraphPaginatedFragment on Organization {\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query VendorGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...VendorGraphPaginatedFragment_3iomuz\n }\n id\n }\n}\n\nfragment VendorGraphPaginatedFragment_3iomuz on Organization {\n vendors(first: 50, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();
(node as any).hash = "3bb12a4d7a49090154bb56951e69f4aa"; (node as any).hash = "3034ee812f89d0596f3f6d3094c1dfc8";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<46bd8a339fe6fd05256df0b1f806e0bd>> * @generated SignedSource<<c3a7e9de9c7f072bbca3ad552e5860cd>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -18,6 +18,7 @@ export type VendorGraphNodeQuery$data = {
readonly node: { readonly node: {
readonly id?: string; readonly id?: string;
readonly name?: string; readonly name?: string;
readonly snapshotId?: string | null | undefined;
readonly websiteUrl?: string | null | undefined; readonly websiteUrl?: string | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "VendorContactsTabFragment" | "VendorOverviewTabBusinessAssociateAgreementFragment" | "VendorOverviewTabDataPrivacyAgreementFragment" | "VendorRiskAssessmentTabFragment" | "VendorServicesTabFragment" | "useVendorFormFragment">; readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "VendorContactsTabFragment" | "VendorOverviewTabBusinessAssociateAgreementFragment" | "VendorOverviewTabDataPrivacyAgreementFragment" | "VendorRiskAssessmentTabFragment" | "VendorServicesTabFragment" | "useVendorFormFragment">;
}; };
@@ -63,20 +64,27 @@ v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "name", "name": "snapshotId",
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "websiteUrl", "name": "websiteUrl",
"storageKey": null "storageKey": null
}, },
v6 = [ v7 = [
(v3/*: any*/) (v3/*: any*/)
], ],
v7 = { v8 = {
"alias": null, "alias": null,
"args": [ "args": [
{ {
@@ -89,80 +97,80 @@ v7 = {
"kind": "LinkedField", "kind": "LinkedField",
"name": "people", "name": "people",
"plural": false, "plural": false,
"selections": (v6/*: any*/), "selections": (v7/*: any*/),
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null "storageKey": null
}, },
v9 = { v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "description", "name": "description",
"storageKey": null "storageKey": null
}, },
v10 = [ v11 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 50 "value": 50
} }
], ],
v11 = { v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "validUntil", "name": "validUntil",
"storageKey": null "storageKey": null
}, },
v12 = { v13 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fileUrl", "name": "fileUrl",
"storageKey": null "storageKey": null
}, },
v13 = { v14 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "cursor",
"storageKey": null "storageKey": null
}, },
v14 = { v15 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "endCursor", "name": "endCursor",
"storageKey": null "storageKey": null
}, },
v15 = { v16 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "hasNextPage", "name": "hasNextPage",
"storageKey": null "storageKey": null
}, },
v16 = { v17 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "hasPreviousPage", "name": "hasPreviousPage",
"storageKey": null "storageKey": null
}, },
v17 = { v18 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "startCursor", "name": "startCursor",
"storageKey": null "storageKey": null
}, },
v18 = { v19 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -170,14 +178,14 @@ v18 = {
"name": "pageInfo", "name": "pageInfo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v14/*: any*/),
(v15/*: any*/), (v15/*: any*/),
(v16/*: any*/), (v16/*: any*/),
(v17/*: any*/) (v17/*: any*/),
(v18/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
v19 = { v20 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -189,31 +197,31 @@ v19 = {
} }
] ]
}, },
v20 = [ v21 = [
"orderBy" "orderBy"
], ],
v21 = { v22 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
v22 = { v23 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
v23 = { v24 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "updatedAt", "name": "updatedAt",
"storageKey": null "storageKey": null
}, },
v24 = [ v25 = [
(v3/*: any*/), (v3/*: any*/),
{ {
"alias": null, "alias": null,
@@ -222,7 +230,7 @@ v24 = [
"name": "fileName", "name": "fileName",
"storageKey": null "storageKey": null
}, },
(v12/*: any*/), (v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -230,8 +238,8 @@ v24 = [
"name": "validFrom", "name": "validFrom",
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
(v22/*: any*/) (v23/*: any*/)
]; ];
return { return {
"fragment": { "fragment": {
@@ -257,6 +265,7 @@ return {
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -315,7 +324,7 @@ return {
"name": "user", "name": "user",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/) (v8/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -343,14 +352,15 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v8/*: any*/), (v9/*: any*/),
(v3/*: any*/), (v3/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v9/*: any*/), (v6/*: any*/),
(v10/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -428,7 +438,7 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "businessOwner", "name": "businessOwner",
"plural": false, "plural": false,
"selections": (v6/*: any*/), "selections": (v7/*: any*/),
"storageKey": null "storageKey": null
}, },
{ {
@@ -438,12 +448,12 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "securityOwner", "name": "securityOwner",
"plural": false, "plural": false,
"selections": (v6/*: any*/), "selections": (v7/*: any*/),
"storageKey": null "storageKey": null
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "VendorComplianceReportConnection", "concreteType": "VendorComplianceReportConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "complianceReports", "name": "complianceReports",
@@ -473,7 +483,7 @@ return {
"name": "reportDate", "name": "reportDate",
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -481,7 +491,7 @@ return {
"name": "reportName", "name": "reportName",
"storageKey": null "storageKey": null
}, },
(v12/*: any*/), (v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -489,23 +499,23 @@ return {
"name": "fileSize", "name": "fileSize",
"storageKey": null "storageKey": null
}, },
(v8/*: any*/) (v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v18/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v20/*: any*/)
], ],
"storageKey": "complianceReports(first:50)" "storageKey": "complianceReports(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"filters": (v20/*: any*/), "filters": (v21/*: any*/),
"handle": "connection", "handle": "connection",
"key": "VendorComplianceTabFragment_complianceReports", "key": "VendorComplianceTabFragment_complianceReports",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -513,7 +523,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "VendorContactConnection", "concreteType": "VendorContactConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "contacts", "name": "contacts",
@@ -536,7 +546,7 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v21/*: any*/), (v22/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -558,25 +568,25 @@ return {
"name": "role", "name": "role",
"storageKey": null "storageKey": null
}, },
(v22/*: any*/),
(v23/*: any*/), (v23/*: any*/),
(v8/*: any*/) (v24/*: any*/),
(v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v18/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v20/*: any*/)
], ],
"storageKey": "contacts(first:50)" "storageKey": "contacts(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"filters": (v20/*: any*/), "filters": (v21/*: any*/),
"handle": "connection", "handle": "connection",
"key": "VendorContactsTabFragment_contacts", "key": "VendorContactsTabFragment_contacts",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -584,7 +594,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "VendorServiceConnection", "concreteType": "VendorServiceConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "services", "name": "services",
@@ -607,27 +617,27 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v5/*: any*/),
(v9/*: any*/), (v10/*: any*/),
(v22/*: any*/),
(v23/*: any*/), (v23/*: any*/),
(v8/*: any*/) (v24/*: any*/),
(v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v18/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v20/*: any*/)
], ],
"storageKey": "services(first:50)" "storageKey": "services(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"filters": (v20/*: any*/), "filters": (v21/*: any*/),
"handle": "connection", "handle": "connection",
"key": "VendorServicesTabFragment_services", "key": "VendorServicesTabFragment_services",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -635,7 +645,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"concreteType": "VendorRiskAssessmentConnection", "concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "riskAssessments", "name": "riskAssessments",
@@ -674,7 +684,7 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v21/*: any*/) (v22/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -706,11 +716,11 @@ return {
"name": "notes", "name": "notes",
"storageKey": null "storageKey": null
}, },
(v8/*: any*/) (v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -722,21 +732,21 @@ return {
"name": "pageInfo", "name": "pageInfo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v15/*: any*/),
(v14/*: any*/),
(v16/*: any*/), (v16/*: any*/),
(v17/*: any*/) (v15/*: any*/),
(v17/*: any*/),
(v18/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v19/*: any*/) (v20/*: any*/)
], ],
"storageKey": "riskAssessments(first:50)" "storageKey": "riskAssessments(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v10/*: any*/), "args": (v11/*: any*/),
"filters": (v20/*: any*/), "filters": (v21/*: any*/),
"handle": "connection", "handle": "connection",
"key": "VendorRiskAssessmentTabFragment_riskAssessments", "key": "VendorRiskAssessmentTabFragment_riskAssessments",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -749,7 +759,7 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "businessAssociateAgreement", "name": "businessAssociateAgreement",
"plural": false, "plural": false,
"selections": (v24/*: any*/), "selections": (v25/*: any*/),
"storageKey": null "storageKey": null
}, },
{ {
@@ -759,7 +769,7 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "dataPrivacyAgreement", "name": "dataPrivacyAgreement",
"plural": false, "plural": false,
"selections": (v24/*: any*/), "selections": (v25/*: any*/),
"storageKey": null "storageKey": null
} }
], ],
@@ -785,7 +795,7 @@ return {
"name": "user", "name": "user",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/), (v8/*: any*/),
(v3/*: any*/) (v3/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -797,16 +807,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "7e1a7455861aea37eee2d799d8385586", "cacheID": "f0b506b57e84959deb0b1e3fa0481897",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorGraphNodeQuery", "name": "VendorGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n people(organizationId: $organizationId) {\n id\n }\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n assessedAt\n assessedBy {\n id\n fullName\n }\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n people(organizationId: $organizationId) {\n id\n }\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n assessedAt\n assessedBy {\n id\n fullName\n }\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "1eac0bd4bc5eccf51ea024d27f348e14"; (node as any).hash = "4af64d92bd85441554a2c6ad8d2ccd77";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<93638f7eeb1634fc19aaaa95cae0c7b8>> * @generated SignedSource<<b6975a3a740bdf7e2c9914fc64770638>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -31,6 +31,7 @@ export type VendorGraphPaginatedFragment$data = {
}; };
}>; }>;
}; };
readonly snapshotId: string | null | undefined;
readonly updatedAt: any; readonly updatedAt: any;
readonly websiteUrl: string | null | undefined; readonly websiteUrl: string | null | undefined;
}; };
@@ -82,6 +83,11 @@ return {
"defaultValue": null, "defaultValue": null,
"kind": "LocalArgument", "kind": "LocalArgument",
"name": "order" "name": "order"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "snapshotId"
} }
], ],
"kind": "Fragment", "kind": "Fragment",
@@ -121,10 +127,16 @@ return {
{ {
"alias": "vendors", "alias": "vendors",
"args": [ "args": [
{
"fields": [
{ {
"kind": "Variable", "kind": "Variable",
"name": "orderBy", "name": "snapshotId",
"variableName": "order" "variableName": "snapshotId"
}
],
"kind": "ObjectValue",
"name": "filter"
} }
], ],
"concreteType": "VendorConnection", "concreteType": "VendorConnection",
@@ -149,6 +161,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,
@@ -327,6 +346,6 @@ return {
}; };
})(); })();
(node as any).hash = "1d2bf7c29e77d60d0a955a62b98c85ab"; (node as any).hash = "578772ed7c83ae174d43b0be95a02fd2";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<bfe74e681960002105d6995d91548400>> * @generated SignedSource<<f98b10bd4f5fb03b55b46e2554480590>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -23,6 +23,7 @@ export type VendorsListQuery$variables = {
id: string; id: string;
last?: number | null | undefined; last?: number | null | undefined;
order?: VendorOrder | null | undefined; order?: VendorOrder | null | undefined;
snapshotId?: string | null | undefined;
}; };
export type VendorsListQuery$data = { export type VendorsListQuery$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": "VendorGraphPaginatedFragment" "name": "VendorGraphPaginatedFragment"
@@ -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": "VendorConnection", "concreteType": "VendorConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "vendors", "name": "vendors",
@@ -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,
@@ -271,7 +299,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/), (v14/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -309,7 +337,7 @@ return {
], ],
"storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})" "storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})"
}, },
(v11/*: any*/) (v13/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -379,9 +407,9 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v15/*: any*/),
"filters": [ "filters": [
"orderBy" "filter"
], ],
"handle": "connection", "handle": "connection",
"key": "VendorsListQuery_vendors", "key": "VendorsListQuery_vendors",
@@ -398,16 +426,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "70fb3d49e78996ae4cefbaa8f5605c2c", "cacheID": "6570f4601f6dea5b7fe738d851a7f1ba",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorsListQuery", "name": "VendorsListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query VendorsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorGraphPaginatedFragment_16fISc\n id\n }\n}\n\nfragment VendorGraphPaginatedFragment_16fISc on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query VendorsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorOrder = null\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorGraphPaginatedFragment_25MC8O\n id\n }\n}\n\nfragment VendorGraphPaginatedFragment_25MC8O on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();
(node as any).hash = "1d2bf7c29e77d60d0a955a62b98c85ab"; (node as any).hash = "578772ed7c83ae174d43b0be95a02fd2";
export default node; export default node;

View File

@@ -192,6 +192,7 @@ export default function AssetDetailsPage(props: Props) {
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="vendorIds"
selectedVendors={vendors}
label={__("Vendors")} label={__("Vendors")}
disabled={isSnapshotMode} disabled={isSnapshotMode}
/> />

View File

@@ -23,11 +23,12 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { Outlet } from "react-router"; import { Outlet, useParams } from "react-router";
import { faviconUrl } from "@probo/helpers"; import { faviconUrl, validateSnapshotConsistency } from "@probo/helpers";
import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog"; import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
import { complianceReportsFragment } from "./tabs/VendorComplianceTab"; import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql"; import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Props = { type Props = {
queryRef: PreloadedQuery<VendorGraphNodeQuery>; queryRef: PreloadedQuery<VendorGraphNodeQuery>;
@@ -38,6 +39,14 @@ export default function VendorDetailPage(props: Props) {
const vendor = data.node; const vendor = data.node;
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
if (!vendor) {
return <div>{__("Vendor not found")}</div>;
}
validateSnapshotConsistency(vendor, snapshotId);
const deleteVendor = useDeleteVendor( const deleteVendor = useDeleteVendor(
vendor, vendor,
ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey) ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey)
@@ -48,13 +57,22 @@ export default function VendorDetailPage(props: Props) {
vendor as VendorComplianceTabFragment$key vendor as VendorComplianceTabFragment$key
).complianceReports.edges.length; ).complianceReports.edges.length;
const vendorsUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors`
: `/organizations/${organizationId}/vendors`;
const baseVendorUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}`
: `/organizations/${organizationId}/vendors/${vendor.id}`;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("Vendors"), label: __("Vendors"),
to: `/organizations/${organizationId}/vendors`, to: vendorsUrl,
}, },
{ {
label: vendor.name ?? "", label: vendor.name ?? "",
@@ -72,6 +90,7 @@ export default function VendorDetailPage(props: Props) {
)} )}
<div className="text-2xl">{vendor.name}</div> <div className="text-2xl">{vendor.name}</div>
</div> </div>
{!isSnapshotMode && (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<ImportAssessmentDialog vendorId={vendor.id!}> <ImportAssessmentDialog vendorId={vendor.id!}>
<Button icon={IconPageTextLine} variant="secondary"> <Button icon={IconPageTextLine} variant="secondary">
@@ -88,38 +107,27 @@ export default function VendorDetailPage(props: Props) {
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</div> </div>
)}
</div> </div>
<Tabs> <Tabs>
<TabLink <TabLink to={`${baseVendorUrl}/overview`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}
>
{__("Overview")} {__("Overview")}
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseVendorUrl}/certifications`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/certifications`}
>
{__("Certifications")} {__("Certifications")}
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseVendorUrl}/compliance`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/compliance`}
>
{__("Compliance reports")} {__("Compliance reports")}
{reportsCount > 0 && <TabBadge>{reportsCount}</TabBadge>} {reportsCount > 0 && <TabBadge>{reportsCount}</TabBadge>}
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseVendorUrl}/risks`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/risks`}
>
{__("Risk Assessment")} {__("Risk Assessment")}
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseVendorUrl}/contacts`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/contacts`}
>
{__("Contacts")} {__("Contacts")}
</TabLink> </TabLink>
<TabLink <TabLink to={`${baseVendorUrl}/services`}>
to={`/organizations/${organizationId}/vendors/${vendor.id}/services`}
>
{__("Services")} {__("Services")}
</TabLink> </TabLink>
</Tabs> </Tabs>

View File

@@ -21,6 +21,7 @@ import {
type PreloadedQuery, type PreloadedQuery,
} from "react-relay"; } from "react-relay";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { useParams } from "react-router";
import { faviconUrl } from "@probo/helpers"; import { faviconUrl } from "@probo/helpers";
import type { NodeOf } from "/types"; import type { NodeOf } from "/types";
import { CreateVendorDialog } from "./dialogs/CreateVendorDialog"; import { CreateVendorDialog } from "./dialogs/CreateVendorDialog";
@@ -35,6 +36,7 @@ import type {
VendorGraphPaginatedFragment$key, VendorGraphPaginatedFragment$key,
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql"; } from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>; type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
@@ -45,6 +47,8 @@ type Props = {
export default function VendorsPage(props: Props) { export default function VendorsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const data = usePreloadedQuery(vendorsQuery, props.queryRef); const data = usePreloadedQuery(vendorsQuery, props.queryRef);
const pagination = usePaginationFragment( const pagination = usePaginationFragment(
@@ -59,18 +63,21 @@ export default function VendorsPage(props: Props) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<PageHeader <PageHeader
title={__("Vendors")} title={__("Vendors")}
description={__( description={__(
"Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status." "Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status."
)} )}
> >
{!isSnapshotMode && (
<CreateVendorDialog <CreateVendorDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
> >
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button> <Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
</CreateVendorDialog> </CreateVendorDialog>
)}
</PageHeader> </PageHeader>
<SortableTable {...pagination}> <SortableTable {...pagination}>
<Thead> <Thead>
@@ -106,14 +113,20 @@ function VendorRow({
organizationId: string; organizationId: string;
connectionId: string; connectionId: string;
}) { }) {
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { __, dateFormat } = useTranslate(); const { __, dateFormat } = useTranslate();
const latestAssessment = vendor.riskAssessments?.edges[0]?.node; const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
const deleteVendor = useDeleteVendor(vendor, connectionId); const deleteVendor = useDeleteVendor(vendor, connectionId);
const vendorUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}/overview`
: `/organizations/${organizationId}/vendors/${vendor.id}/overview`;
return ( return (
<> <>
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}> <Tr to={vendorUrl}>
<Td> <Td>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} /> <Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} />
@@ -136,6 +149,7 @@ function VendorRow({
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} /> <RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
</Td> </Td>
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
{!isSnapshotMode && (
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
onClick={deleteVendor} onClick={deleteVendor}
@@ -145,6 +159,7 @@ function VendorRow({
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
)}
</Td> </Td>
</Tr> </Tr>
</> </>

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql"; import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
@@ -98,6 +98,8 @@ export default function VendorComplianceTab() {
const connectionId = data.complianceReports.__id; const connectionId = data.complianceReports.__id;
const reports = data.complianceReports.edges.map((edge) => edge.node); const reports = data.complianceReports.edges.map((edge) => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
usePageTitle(vendor.name + " - " + __("Compliance reports")); usePageTitle(vendor.name + " - " + __("Compliance reports"));
@@ -125,6 +127,7 @@ export default function VendorComplianceTab() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{!isSnapshotMode && (
<Dropzone <Dropzone
description={__("Only PDF files up to 10MB are allowed")} description={__("Only PDF files up to 10MB are allowed")}
isUploading={isMutating} isUploading={isMutating}
@@ -134,6 +137,7 @@ export default function VendorComplianceTab() {
}} }}
maxSize={10} maxSize={10}
/> />
)}
<SortableTable refetch={refetch}> <SortableTable refetch={refetch}>
<Thead> <Thead>
<Tr> <Tr>
@@ -141,7 +145,7 @@ export default function VendorComplianceTab() {
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh> <SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
<Th>{__("Valid until")}</Th> <Th>{__("Valid until")}</Th>
<Th>{__("File size")}</Th> <Th>{__("File size")}</Th>
<Th>{__("Actions")}</Th> {!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -150,6 +154,7 @@ export default function VendorComplianceTab() {
key={report.id} key={report.id}
reportKey={report} reportKey={report}
connectionId={connectionId} connectionId={connectionId}
isSnapshotMode={isSnapshotMode}
/> />
))} ))}
</Tbody> </Tbody>
@@ -161,6 +166,7 @@ export default function VendorComplianceTab() {
type ReportRowProps = { type ReportRowProps = {
reportKey: VendorComplianceTabFragment_report$key; reportKey: VendorComplianceTabFragment_report$key;
connectionId: string; connectionId: string;
isSnapshotMode: boolean;
}; };
function ReportRow(props: ReportRowProps) { function ReportRow(props: ReportRowProps) {
@@ -204,6 +210,7 @@ function ReportRow(props: ReportRowProps) {
<Td>{dateFormat(report.reportDate)}</Td> <Td>{dateFormat(report.reportDate)}</Td>
<Td>{dateFormat(report.validUntil)}</Td> <Td>{dateFormat(report.validUntil)}</Td>
<Td>{fileSize(__, report.fileSize)}</Td> <Td>{fileSize(__, report.fileSize)}</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
@@ -215,6 +222,7 @@ function ReportRow(props: ReportRowProps) {
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorContactsTabFragment$key } from "./__generated__/VendorContactsTabFragment.graphql"; import type { VendorContactsTabFragment$key } from "./__generated__/VendorContactsTabFragment.graphql";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
@@ -89,6 +89,8 @@ export default function VendorContactsTab() {
const connectionId = data.contacts.__id; const connectionId = data.contacts.__id;
const contacts = data.contacts.edges.map((edge) => edge.node); const contacts = data.contacts.edges.map((edge) => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [editingContact, setEditingContact] = useState<{ const [editingContact, setEditingContact] = useState<{
id: string; id: string;
fullName?: string | null; fullName?: string | null;
@@ -105,12 +107,14 @@ export default function VendorContactsTab() {
title={__("Contacts")} title={__("Contacts")}
description={__("Manage vendor contacts and their information.")} description={__("Manage vendor contacts and their information.")}
> >
{!isSnapshotMode && (
<CreateContactDialog <CreateContactDialog
vendorId={vendor.id} vendorId={vendor.id}
connectionId={connectionId} connectionId={connectionId}
> >
<Button icon={IconPlusLarge}>{__("Add contact")}</Button> <Button icon={IconPlusLarge}>{__("Add contact")}</Button>
</CreateContactDialog> </CreateContactDialog>
)}
</PageHeader> </PageHeader>
<SortableTable refetch={refetch}> <SortableTable refetch={refetch}>
@@ -120,7 +124,7 @@ export default function VendorContactsTab() {
<SortableTh field="EMAIL">{__("Email")}</SortableTh> <SortableTh field="EMAIL">{__("Email")}</SortableTh>
<Th>{__("Phone")}</Th> <Th>{__("Phone")}</Th>
<Th>{__("Role")}</Th> <Th>{__("Role")}</Th>
<Th>{__("Actions")}</Th> {!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -130,12 +134,13 @@ export default function VendorContactsTab() {
contactKey={contact} contactKey={contact}
connectionId={connectionId} connectionId={connectionId}
onEdit={setEditingContact} onEdit={setEditingContact}
isSnapshotMode={isSnapshotMode}
/> />
))} ))}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
{editingContact && ( {editingContact && !isSnapshotMode && (
<EditContactDialog <EditContactDialog
contactId={editingContact.id} contactId={editingContact.id}
contact={editingContact} contact={editingContact}
@@ -156,6 +161,7 @@ type ContactRowProps = {
phone?: string | null; phone?: string | null;
role?: string | null; role?: string | null;
}) => void; }) => void;
isSnapshotMode: boolean;
}; };
function ContactRow(props: ContactRowProps) { function ContactRow(props: ContactRowProps) {
@@ -220,6 +226,7 @@ function ContactRow(props: ContactRowProps) {
)} )}
</Td> </Td>
<Td>{contact.role || __("—")}</Td> <Td>{contact.role || __("—")}</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
@@ -243,6 +250,7 @@ function ContactRow(props: ContactRowProps) {
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -1,6 +1,6 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useVendorForm } from "/hooks/forms/useVendorForm"; import { useVendorForm } from "/hooks/forms/useVendorForm";
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { Button, Card, Field, Input, IconPlusLarge, IconTrashCan, IconPencil } from "@probo/ui"; import { Button, Card, Field, Input, IconPlusLarge, IconTrashCan, IconPencil } from "@probo/ui";
import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { PeopleSelectField } from "/components/form/PeopleSelectField";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
@@ -55,6 +55,8 @@ export default function VendorOverviewTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { const {
control, control,
@@ -98,7 +100,7 @@ export default function VendorOverviewTab() {
usePageTitle(vendor.name + " - " + __("Overview")); usePageTitle(vendor.name + " - " + __("Overview"));
return ( return (
<form onSubmit={handleSubmit} className="space-y-12"> <form onSubmit={isSnapshotMode ? undefined : handleSubmit} className="space-y-12">
{/* Vendor Details */} {/* Vendor Details */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-base font-medium">{__("Vendor details")}</h2> <h2 className="text-base font-medium">{__("Vendor details")}</h2>
@@ -108,30 +110,35 @@ export default function VendorOverviewTab() {
label={__("Name")} label={__("Name")}
type="text" type="text"
error={errors.name?.message} error={errors.name?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
<Field <Field
{...register("description")} {...register("description")}
label={__("Description")} label={__("Description")}
type="textarea" type="textarea"
error={errors.description?.message} error={errors.description?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
<Field <Field
{...register("legalName")} {...register("legalName")}
label={__("Legal name")} label={__("Legal name")}
type="text" type="text"
error={errors.legalName?.message} error={errors.legalName?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
<Field <Field
{...register("headquarterAddress")} {...register("headquarterAddress")}
label={__("Headquarter address")} label={__("Headquarter address")}
type="textarea" type="textarea"
error={errors.headquarterAddress?.message} error={errors.headquarterAddress?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
<Field <Field
{...register("websiteUrl")} {...register("websiteUrl")}
label={__("Website URL")} label={__("Website URL")}
type="text" type="text"
error={errors.websiteUrl?.message} error={errors.websiteUrl?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
</Card> </Card>
</div> </div>
@@ -146,6 +153,7 @@ export default function VendorOverviewTab() {
name="businessOwnerId" name="businessOwnerId"
label={__("Business owner")} label={__("Business owner")}
error={errors.businessOwnerId?.message} error={errors.businessOwnerId?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
<PeopleSelectField <PeopleSelectField
organizationId={organizationId} organizationId={organizationId}
@@ -153,6 +161,7 @@ export default function VendorOverviewTab() {
name="securityOwnerId" name="securityOwnerId"
label={__("Security owner")} label={__("Security owner")}
error={errors.securityOwnerId?.message} error={errors.securityOwnerId?.message}
disabled={isSubmitting || isSnapshotMode}
/> />
</Card> </Card>
</div> </div>
@@ -180,6 +189,7 @@ export default function VendorOverviewTab() {
type="text" type="text"
placeholder="https://..." placeholder="https://..."
variant="ghost" variant="ghost"
disabled={isSubmitting || isSnapshotMode}
/> />
</div> </div>
))} ))}
@@ -220,6 +230,8 @@ export default function VendorOverviewTab() {
> >
{__("Download PDF")} {__("Download PDF")}
</Button> </Button>
{!isSnapshotMode && (
<>
<EditBusinessAssociateAgreementDialog <EditBusinessAssociateAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
agreement={{ agreement={{
@@ -238,7 +250,10 @@ export default function VendorOverviewTab() {
<Button variant="quaternary" icon={IconTrashCan} /> <Button variant="quaternary" icon={IconTrashCan} />
</DeleteBusinessAssociateAgreementDialog> </DeleteBusinessAssociateAgreementDialog>
</> </>
)}
</>
) : ( ) : (
!isSnapshotMode && (
<UploadBusinessAssociateAgreementDialog <UploadBusinessAssociateAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
@@ -247,6 +262,7 @@ export default function VendorOverviewTab() {
{__("Upload")} {__("Upload")}
</Button> </Button>
</UploadBusinessAssociateAgreementDialog> </UploadBusinessAssociateAgreementDialog>
)
)} )}
</div> </div>
</div> </div>
@@ -281,6 +297,8 @@ export default function VendorOverviewTab() {
> >
{__("Download PDF")} {__("Download PDF")}
</Button> </Button>
{!isSnapshotMode && (
<>
<EditDataPrivacyAgreementDialog <EditDataPrivacyAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
agreement={{ agreement={{
@@ -299,8 +317,10 @@ export default function VendorOverviewTab() {
<Button variant="quaternary" icon={IconTrashCan} /> <Button variant="quaternary" icon={IconTrashCan} />
</DeleteDataPrivacyAgreementDialog> </DeleteDataPrivacyAgreementDialog>
</> </>
)}
</>
) : ( ) : (
<> !isSnapshotMode && (
<UploadDataPrivacyAgreementDialog <UploadDataPrivacyAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
@@ -309,7 +329,7 @@ export default function VendorOverviewTab() {
{__("Upload")} {__("Upload")}
</Button> </Button>
</UploadDataPrivacyAgreementDialog> </UploadDataPrivacyAgreementDialog>
</> )
)} )}
</div> </div>
</div> </div>
@@ -317,11 +337,13 @@ export default function VendorOverviewTab() {
</div> </div>
{/* Submit */} {/* Submit */}
{!isSnapshotMode && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{__("Update vendor")} {__("Update vendor")}
</Button> </Button>
</div> </div>
)}
</form> </form>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorRiskAssessmentTabFragment$key } from "./__generated__/VendorRiskAssessmentTabFragment.graphql"; import type { VendorRiskAssessmentTabFragment$key } from "./__generated__/VendorRiskAssessmentTabFragment.graphql";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
@@ -82,6 +82,8 @@ export default function VendorRiskAssessmentTab() {
); );
const assessments = data.riskAssessments.edges.map((edge) => edge.node); const assessments = data.riskAssessments.edges.map((edge) => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
usePageTitle(vendor.name + " - " + __("Risk Assessments")); usePageTitle(vendor.name + " - " + __("Risk Assessments"));
@@ -90,6 +92,7 @@ export default function VendorRiskAssessmentTab() {
return ( return (
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2"> <div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
{__("No risk assessments found")} {__("No risk assessments found")}
{!isSnapshotMode && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} vendorId={vendor.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}
@@ -99,6 +102,7 @@ export default function VendorRiskAssessmentTab() {
{__("Add Risk Assessment")} {__("Add Risk Assessment")}
</Button> </Button>
</CreateRiskAssessmentDialog> </CreateRiskAssessmentDialog>
)}
</div> </div>
); );
} }
@@ -117,6 +121,7 @@ export default function VendorRiskAssessmentTab() {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{!isSnapshotMode && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} vendorId={vendor.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}
@@ -126,6 +131,7 @@ export default function VendorRiskAssessmentTab() {
{__("Add Risk Assessment")} {__("Add Risk Assessment")}
</TrButton> </TrButton>
</CreateRiskAssessmentDialog> </CreateRiskAssessmentDialog>
)}
{assessments.map((assessment) => ( {assessments.map((assessment) => (
<AssessmentRow <AssessmentRow
key={assessment.id} key={assessment.id}

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorServicesTabFragment$key } from "./__generated__/VendorServicesTabFragment.graphql"; import type { VendorServicesTabFragment$key } from "./__generated__/VendorServicesTabFragment.graphql";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
@@ -87,6 +87,8 @@ export default function VendorServicesTab() {
const connectionId = data.services.__id; const connectionId = data.services.__id;
const services = data.services.edges.map((edge) => edge.node); const services = data.services.edges.map((edge) => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [editingService, setEditingService] = useState<{ const [editingService, setEditingService] = useState<{
id: string; id: string;
name: string; name: string;
@@ -101,12 +103,14 @@ export default function VendorServicesTab() {
title={__("Services")} title={__("Services")}
description={__("Manage services provided by this vendor.")} description={__("Manage services provided by this vendor.")}
> >
{!isSnapshotMode && (
<CreateServiceDialog <CreateServiceDialog
vendorId={vendor.id} vendorId={vendor.id}
connectionId={connectionId} connectionId={connectionId}
> >
<Button icon={IconPlusLarge}>{__("Add service")}</Button> <Button icon={IconPlusLarge}>{__("Add service")}</Button>
</CreateServiceDialog> </CreateServiceDialog>
)}
</PageHeader> </PageHeader>
<SortableTable refetch={refetch}> <SortableTable refetch={refetch}>
@@ -114,7 +118,7 @@ export default function VendorServicesTab() {
<Tr> <Tr>
<SortableTh field="NAME">{__("Name")}</SortableTh> <SortableTh field="NAME">{__("Name")}</SortableTh>
<Th>{__("Description")}</Th> <Th>{__("Description")}</Th>
<Th>{__("Actions")}</Th> {!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -124,12 +128,13 @@ export default function VendorServicesTab() {
serviceKey={service} serviceKey={service}
connectionId={connectionId} connectionId={connectionId}
onEdit={setEditingService} onEdit={setEditingService}
isSnapshotMode={isSnapshotMode}
/> />
))} ))}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
{editingService && ( {editingService && !isSnapshotMode && (
<EditServiceDialog <EditServiceDialog
serviceId={editingService.id} serviceId={editingService.id}
service={editingService} service={editingService}
@@ -148,6 +153,7 @@ type ServiceRowProps = {
name: string; name: string;
description?: string | null; description?: string | null;
}) => void; }) => void;
isSnapshotMode: boolean;
}; };
function ServiceRow(props: ServiceRowProps) { function ServiceRow(props: ServiceRowProps) {
@@ -188,6 +194,7 @@ function ServiceRow(props: ServiceRowProps) {
<Tr> <Tr>
<Td>{service.name}</Td> <Td>{service.name}</Td>
<Td>{service.description || __("—")}</Td> <Td>{service.description || __("—")}</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
@@ -209,6 +216,7 @@ function ServiceRow(props: ServiceRowProps) {
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -11,7 +11,20 @@ export const vendorRoutes = [
path: "vendors", path: "vendors",
fallback: PageSkeleton, fallback: PageSkeleton,
queryLoader: ({ organizationId }) => queryLoader: ({ organizationId }) =>
loadQuery(relayEnvironment, vendorsQuery, { organizationId }), loadQuery(relayEnvironment, vendorsQuery, {
organizationId,
snapshotId: null
}),
Component: lazy(() => import("/pages/organizations/vendors/VendorsPage")),
},
{
path: "snapshots/:snapshotId/vendors",
fallback: PageSkeleton,
queryLoader: ({ organizationId, snapshotId }) =>
loadQuery(relayEnvironment, vendorsQuery, {
organizationId,
snapshotId
}),
Component: lazy(() => import("/pages/organizations/vendors/VendorsPage")), Component: lazy(() => import("/pages/organizations/vendors/VendorsPage")),
}, },
{ {
@@ -79,4 +92,69 @@ export const vendorRoutes = [
}, },
], ],
}, },
{
path: "snapshots/:snapshotId/vendors/:vendorId",
fallback: PageSkeleton,
queryLoader: ({ vendorId, organizationId }) =>
loadQuery(relayEnvironment, vendorNodeQuery, {
vendorId,
organizationId,
}),
Component: lazy(
() => import("../pages/organizations/vendors/VendorDetailPage")
),
children: [
{
path: "overview",
fallback: LinkCardSkeleton,
Component: lazy(
() => import("../pages/organizations/vendors/tabs/VendorOverviewTab")
),
},
{
path: "certifications",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import(
"../pages/organizations/vendors/tabs/VendorCertificationsTab"
)
),
},
{
path: "compliance",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/vendors/tabs/VendorComplianceTab")
),
},
{
path: "risks",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import(
"../pages/organizations/vendors/tabs/VendorRiskAssessmentTab"
)
),
},
{
path: "contacts",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/vendors/tabs/VendorContactsTab")
),
},
{
path: "services",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/vendors/tabs/VendorServicesTab")
),
},
],
},
] 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 = [
"VENDORS",
"ASSETS", "ASSETS",
"DATA", "DATA",
"NONCONFORMITY_REGISTRIES", "NONCONFORMITY_REGISTRIES",
@@ -38,6 +39,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 "VENDORS":
return "/vendors";
case "ASSETS": case "ASSETS":
return "/assets"; return "/assets";
case "DATA": case "DATA":

View File

@@ -135,42 +135,3 @@ VALUES (
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
} }
func (f File) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE FROM files WHERE %s AND id = @file_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"file_id": f.ID}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}
func (f File) SoftDelete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE files SET deleted_at = @deleted_at, updated_at = @updated_at WHERE %s AND id = @file_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"file_id": f.ID,
"updated_at": time.Now(),
"deleted_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -0,0 +1,84 @@
ALTER TABLE vendor_business_associate_agreements ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_business_associate_agreements ADD COLUMN source_id TEXT;
ALTER TABLE vendor_business_associate_agreements DROP CONSTRAINT vendor_business_associate_agreements_file_id_unique;
ALTER TABLE vendor_business_associate_agreements DROP CONSTRAINT vendor_business_associate_agreements_file_id_fkey;
ALTER TABLE vendor_business_associate_agreements ADD CONSTRAINT vendor_business_associate_agreements_file_id_fkey
FOREIGN KEY (file_id)
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE RESTRICT;
ALTER TABLE vendor_business_associate_agreements ADD CONSTRAINT vendor_business_associate_agreements_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE vendor_business_associate_agreements ADD CONSTRAINT vendor_business_associate_agreements_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);
ALTER TABLE vendor_contacts ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_contacts ADD COLUMN source_id TEXT;
ALTER TABLE vendor_contacts ADD CONSTRAINT vendor_contacts_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE vendor_contacts ADD CONSTRAINT vendor_contacts_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);
ALTER TABLE vendor_data_privacy_agreements ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_data_privacy_agreements ADD COLUMN source_id TEXT;
ALTER TABLE vendor_data_privacy_agreements DROP CONSTRAINT vendor_data_privacy_agreements_file_id_unique;
ALTER TABLE vendor_data_privacy_agreements DROP CONSTRAINT vendor_data_privacy_agreements_file_id_fkey;
ALTER TABLE vendor_data_privacy_agreements ADD CONSTRAINT vendor_data_privacy_agreements_file_id_fkey
FOREIGN KEY (file_id)
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE RESTRICT;
ALTER TABLE vendor_data_privacy_agreements ADD CONSTRAINT vendor_data_privacy_agreements_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE vendor_data_privacy_agreements ADD CONSTRAINT vendor_data_privacy_agreements_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);
ALTER TABLE vendor_risk_assessments ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_risk_assessments ADD COLUMN source_id TEXT;
ALTER TABLE vendor_risk_assessments ADD CONSTRAINT vendor_risk_assessments_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE vendor_risk_assessments ADD CONSTRAINT vendor_risk_assessments_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);
ALTER TABLE vendor_services ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_services ADD COLUMN source_id TEXT;
ALTER TABLE vendor_services ADD CONSTRAINT vendor_services_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE vendor_services ADD CONSTRAINT vendor_services_source_id_snapshot_id_key
UNIQUE (source_id, snapshot_id);
ALTER TABLE vendor_compliance_reports ADD COLUMN snapshot_id TEXT;
ALTER TABLE vendor_compliance_reports ADD COLUMN source_id TEXT;
ALTER TABLE vendor_compliance_reports ADD CONSTRAINT vendor_compliance_reports_snapshot_id_fkey
FOREIGN KEY (snapshot_id)
REFERENCES snapshots(id)
ON UPDATE CASCADE
ON DELETE CASCADE;

View File

@@ -40,6 +40,8 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
return ContinualImprovementRegistries{}, nil return ContinualImprovementRegistries{}, nil
case SnapshotsTypeProcessingActivityRegistries: case SnapshotsTypeProcessingActivityRegistries:
return ProcessingActivityRegistries{}, nil return ProcessingActivityRegistries{}, nil
case SnapshotsTypeVendors:
return Vendors{}, nil
default: default:
return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType) return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType)
} }

View File

@@ -57,6 +57,10 @@ type (
} }
Vendors []*Vendor Vendors []*Vendor
VendorSnapshotter interface {
InsertVendorSnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error
}
) )
func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey { func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey {
@@ -914,3 +918,106 @@ FROM source_vendors v
return nil return nil
} }
func (v Vendors) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
for _, snapshotter := range []VendorSnapshotter{
Vendors{},
VendorServices{},
VendorContacts{},
VendorRiskAssessments{},
VendorComplianceReports{},
VendorBusinessAssociateAgreements{},
VendorDataPrivacyAgreements{},
} {
if err := snapshotter.InsertVendorSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
return fmt.Errorf("cannot create vendor snapshots: (%T) %w", snapshotter, err)
}
}
return nil
}
func (v Vendors) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
INSERT INTO vendors (
tenant_id,
id,
snapshot_id,
source_id,
organization_id,
name,
description,
category,
headquarter_address,
legal_name,
website_url,
privacy_policy_url,
service_level_agreement_url,
data_processing_agreement_url,
business_associate_agreement_url,
subprocessors_list_url,
certifications,
business_owner_id,
security_owner_id,
status_page_url,
terms_of_service_url,
security_page_url,
trust_page_url,
show_on_trust_center,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_entity_type),
@snapshot_id,
v.id,
v.organization_id,
v.name,
v.description,
v.category,
v.headquarter_address,
v.legal_name,
v.website_url,
v.privacy_policy_url,
v.service_level_agreement_url,
v.data_processing_agreement_url,
v.business_associate_agreement_url,
v.subprocessors_list_url,
v.certifications,
v.business_owner_id,
v.security_owner_id,
v.status_page_url,
v.terms_of_service_url,
v.security_page_url,
v.trust_page_url,
v.show_on_trust_center,
v.created_at,
v.updated_at
FROM vendors v
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,
"vendor_entity_type": VendorEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor snapshots: %w", err)
}
return nil
}

View File

@@ -34,6 +34,8 @@ type (
ValidFrom *time.Time `db:"valid_from"` ValidFrom *time.Time `db:"valid_from"`
ValidUntil *time.Time `db:"valid_until"` ValidUntil *time.Time `db:"valid_until"`
FileID gid.GID `db:"file_id"` FileID gid.GID `db:"file_id"`
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"`
} }
@@ -66,6 +68,8 @@ SELECT
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -110,6 +114,8 @@ SELECT
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -156,6 +162,7 @@ SET
WHERE WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -192,6 +199,8 @@ INSERT INTO
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
) )
@@ -203,6 +212,8 @@ VALUES (
@valid_from, @valid_from,
@valid_until, @valid_until,
@file_id, @file_id,
@snapshot_id,
@source_id,
@created_at, @created_at,
@updated_at @updated_at
) )
@@ -211,6 +222,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
valid_from = EXCLUDED.valid_from, valid_from = EXCLUDED.valid_from,
valid_until = EXCLUDED.valid_until, valid_until = EXCLUDED.valid_until,
file_id = EXCLUDED.file_id, file_id = EXCLUDED.file_id,
snapshot_id = EXCLUDED.snapshot_id,
source_id = EXCLUDED.source_id,
updated_at = EXCLUDED.updated_at updated_at = EXCLUDED.updated_at
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
@@ -221,6 +234,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
"valid_from": vbaa.ValidFrom, "valid_from": vbaa.ValidFrom,
"valid_until": vbaa.ValidUntil, "valid_until": vbaa.ValidUntil,
"file_id": vbaa.FileID, "file_id": vbaa.FileID,
"snapshot_id": vbaa.SnapshotID,
"source_id": vbaa.SourceID,
"created_at": vbaa.CreatedAt, "created_at": vbaa.CreatedAt,
"updated_at": vbaa.UpdatedAt, "updated_at": vbaa.UpdatedAt,
} }
@@ -241,6 +256,7 @@ FROM
WHERE WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -265,6 +281,7 @@ FROM
WHERE WHERE
%s %s
AND vendor_id = @vendor_id AND vendor_id = @vendor_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -275,3 +292,65 @@ WHERE
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
} }
func (v VendorBusinessAssociateAgreements) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_business_associate_agreements (
tenant_id,
id,
snapshot_id,
source_id,
organization_id,
vendor_id,
valid_from,
valid_until,
file_id,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_business_associate_agreement_entity_type),
@snapshot_id,
vbaa.id,
vbaa.organization_id,
sv.id,
vbaa.valid_from,
vbaa.valid_until,
vbaa.file_id,
vbaa.created_at,
vbaa.updated_at
FROM vendor_business_associate_agreements vbaa
INNER JOIN snapshot_vendors sv ON sv.source_id = vbaa.vendor_id
WHERE %s AND vbaa.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_business_associate_agreement_entity_type": VendorBusinessAssociateAgreementEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor business associate agreement snapshots: %w", err)
}
return nil
}

View File

@@ -35,6 +35,8 @@ type (
ReportName string ReportName string
FileKey string FileKey string
FileSize int FileSize int
SnapshotID *gid.GID
SourceID *gid.GID
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
@@ -69,6 +71,8 @@ SELECT
report_name, report_name,
file_key, file_key,
file_size, file_size,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -115,6 +119,8 @@ SELECT
report_name, report_name,
file_key, file_key,
file_size, file_size,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -206,6 +212,7 @@ FROM
WHERE WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -216,3 +223,67 @@ WHERE
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
} }
func (vcrs VendorComplianceReports) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_compliance_reports (
tenant_id,
id,
snapshot_id,
source_id,
vendor_id,
report_date,
valid_until,
report_name,
file_key,
file_size,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_compliance_report_entity_type),
@snapshot_id,
vcr.id,
sv.id,
vcr.report_date,
vcr.valid_until,
vcr.report_name,
vcr.file_key,
vcr.file_size,
vcr.created_at,
vcr.updated_at
FROM vendor_compliance_reports vcr
INNER JOIN snapshot_vendors sv ON sv.source_id = vcr.vendor_id
WHERE %s AND vcr.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_compliance_report_entity_type": VendorComplianceReportEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor compliance report snapshots: %w", err)
}
return nil
}

View File

@@ -35,6 +35,8 @@ type (
Email *string `db:"email"` Email *string `db:"email"`
Phone *string `db:"phone"` Phone *string `db:"phone"`
Role *string `db:"role"` Role *string `db:"role"`
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"`
} }
@@ -77,6 +79,8 @@ SELECT
email, email,
phone, phone,
role, role,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -127,6 +131,8 @@ SELECT
email, email,
phone, phone,
role, role,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -228,6 +234,7 @@ SET
WHERE WHERE
%s %s
AND id = @vendor_contact_id AND id = @vendor_contact_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -261,6 +268,7 @@ DELETE FROM
WHERE WHERE
%s %s
AND id = @vendor_contact_id AND id = @vendor_contact_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -275,3 +283,65 @@ WHERE
return nil return nil
} }
func (vc VendorContacts) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_contacts (
tenant_id,
id,
snapshot_id,
source_id,
vendor_id,
full_name,
email,
phone,
role,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_contact_entity_type),
@snapshot_id,
vc.id,
sv.id,
vc.full_name,
vc.email,
vc.phone,
vc.role,
vc.created_at,
vc.updated_at
FROM vendor_contacts vc
INNER JOIN snapshot_vendors sv ON sv.source_id = vc.vendor_id
WHERE %s AND vc.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_contact_entity_type": VendorContactEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor contact snapshots: %w", err)
}
return nil
}

View File

@@ -29,12 +29,13 @@ import (
type ( type (
VendorDataPrivacyAgreement struct { VendorDataPrivacyAgreement struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
TenantID gid.TenantID `db:"tenant_id"`
OrganizationID gid.GID `db:"organization_id"` OrganizationID gid.GID `db:"organization_id"`
VendorID gid.GID `db:"vendor_id"` VendorID gid.GID `db:"vendor_id"`
ValidFrom *time.Time `db:"valid_from"` ValidFrom *time.Time `db:"valid_from"`
ValidUntil *time.Time `db:"valid_until"` ValidUntil *time.Time `db:"valid_until"`
FileID gid.GID `db:"file_id"` FileID gid.GID `db:"file_id"`
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"`
} }
@@ -62,12 +63,13 @@ func (vdpa *VendorDataPrivacyAgreement) LoadByVendorID(
q := ` q := `
SELECT SELECT
id, id,
tenant_id,
organization_id, organization_id,
vendor_id, vendor_id,
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -107,12 +109,13 @@ func (vdpa *VendorDataPrivacyAgreement) LoadByID(
q := ` q := `
SELECT SELECT
id, id,
tenant_id,
organization_id, organization_id,
vendor_id, vendor_id,
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -159,6 +162,7 @@ SET
WHERE WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -195,6 +199,8 @@ INSERT INTO
valid_from, valid_from,
valid_until, valid_until,
file_id, file_id,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
) )
@@ -206,6 +212,8 @@ VALUES (
@valid_from, @valid_from,
@valid_until, @valid_until,
@file_id, @file_id,
@snapshot_id,
@source_id,
@created_at, @created_at,
@updated_at @updated_at
) )
@@ -214,6 +222,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
valid_from = EXCLUDED.valid_from, valid_from = EXCLUDED.valid_from,
valid_until = EXCLUDED.valid_until, valid_until = EXCLUDED.valid_until,
file_id = EXCLUDED.file_id, file_id = EXCLUDED.file_id,
snapshot_id = EXCLUDED.snapshot_id,
source_id = EXCLUDED.source_id,
updated_at = EXCLUDED.updated_at updated_at = EXCLUDED.updated_at
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
@@ -224,6 +234,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
"valid_from": vdpa.ValidFrom, "valid_from": vdpa.ValidFrom,
"valid_until": vdpa.ValidUntil, "valid_until": vdpa.ValidUntil,
"file_id": vdpa.FileID, "file_id": vdpa.FileID,
"snapshot_id": vdpa.SnapshotID,
"source_id": vdpa.SourceID,
"created_at": vdpa.CreatedAt, "created_at": vdpa.CreatedAt,
"updated_at": vdpa.UpdatedAt, "updated_at": vdpa.UpdatedAt,
} }
@@ -244,6 +256,7 @@ FROM
WHERE WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -278,3 +291,65 @@ WHERE
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
} }
func (vdpa VendorDataPrivacyAgreements) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_data_privacy_agreements (
tenant_id,
id,
snapshot_id,
source_id,
organization_id,
vendor_id,
valid_from,
valid_until,
file_id,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_data_privacy_agreement_entity_type),
@snapshot_id,
vdpa.id,
vdpa.organization_id,
sv.id,
vdpa.valid_from,
vdpa.valid_until,
vdpa.file_id,
vdpa.created_at,
vdpa.updated_at
FROM vendor_data_privacy_agreements vdpa
INNER JOIN snapshot_vendors sv ON sv.source_id = vdpa.vendor_id
WHERE %s AND vdpa.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_data_privacy_agreement_entity_type": VendorDataPrivacyAgreementEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor data privacy agreement snapshots: %w", err)
}
return nil
}

View File

@@ -39,6 +39,8 @@ type (
DataSensitivity DataSensitivity `db:"data_sensitivity"` DataSensitivity DataSensitivity `db:"data_sensitivity"`
BusinessImpact BusinessImpact `db:"business_impact"` BusinessImpact BusinessImpact `db:"business_impact"`
Notes *string `db:"notes"` Notes *string `db:"notes"`
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"`
} }
@@ -137,6 +139,8 @@ SELECT
data_sensitivity, data_sensitivity,
business_impact, business_impact,
notes, notes,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -187,6 +191,8 @@ SELECT
data_sensitivity, data_sensitivity,
business_impact, business_impact,
notes, notes,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -240,6 +246,8 @@ SELECT
data_sensitivity, data_sensitivity,
business_impact, business_impact,
notes, notes,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -270,3 +278,73 @@ WHERE
return nil return nil
} }
func (v VendorRiskAssessments) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_risk_assessments (
tenant_id,
id,
snapshot_id,
source_id,
vendor_id,
assessed_at,
assessed_by,
approved_by,
approved_at,
expires_at,
data_sensitivity,
business_impact,
notes,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_risk_assessment_entity_type),
@snapshot_id,
vra.id,
sv.id,
vra.assessed_at,
vra.assessed_by,
vra.approved_by,
vra.approved_at,
vra.expires_at,
vra.data_sensitivity,
vra.business_impact,
vra.notes,
vra.created_at,
vra.updated_at
FROM vendor_risk_assessments vra
INNER JOIN snapshot_vendors sv ON sv.source_id = vra.vendor_id
WHERE %s AND vra.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_risk_assessment_entity_type": VendorRiskAssessmentEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor risk assessment snapshots: %w", err)
}
return nil
}

View File

@@ -33,6 +33,8 @@ type (
VendorID gid.GID `db:"vendor_id"` VendorID gid.GID `db:"vendor_id"`
Name string `db:"name"` Name string `db:"name"`
Description *string `db:"description"` Description *string `db:"description"`
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"`
} }
@@ -71,6 +73,8 @@ SELECT
vendor_id, vendor_id,
name, name,
description, description,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -119,6 +123,8 @@ SELECT
vendor_id, vendor_id,
name, name,
description, description,
snapshot_id,
source_id,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -212,6 +218,7 @@ SET
WHERE WHERE
%s %s
AND id = @vendor_service_id AND id = @vendor_service_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -243,6 +250,7 @@ DELETE FROM
WHERE WHERE
%s %s
AND id = @vendor_service_id AND id = @vendor_service_id
AND snapshot_id IS NULL
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -257,3 +265,61 @@ WHERE
return nil return nil
} }
func (vs VendorServices) InsertVendorSnapshots(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
snapshotID gid.GID,
) error {
query := `
WITH
snapshot_vendors AS (
SELECT id, source_id
FROM vendors
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
)
INSERT INTO vendor_services (
tenant_id,
id,
snapshot_id,
source_id,
vendor_id,
name,
description,
created_at,
updated_at
)
SELECT
@tenant_id,
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_service_entity_type),
@snapshot_id,
vs.id,
sv.id,
vs.name,
vs.description,
vs.created_at,
vs.updated_at
FROM vendor_services vs
INNER JOIN snapshot_vendors sv ON sv.source_id = vs.vendor_id
WHERE %s AND vs.snapshot_id IS NULL
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"snapshot_id": snapshotID,
"organization_id": organizationID,
"vendor_service_entity_type": VendorServiceEntityType,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot insert vendor service snapshots: %w", err)
}
return nil
}

View File

@@ -302,16 +302,10 @@ func (s VendorBusinessAssociateAgreementService) Delete(
return fmt.Errorf("cannot load vendor business associate agreement: %w", err) return fmt.Errorf("cannot load vendor business associate agreement: %w", err)
} }
file := &coredata.File{ID: vendorBusinessAssociateAgreement.FileID}
if err := vendorBusinessAssociateAgreement.Delete(ctx, conn, s.svc.scope); err != nil { if err := vendorBusinessAssociateAgreement.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete vendor business associate agreement: %w", err) return fmt.Errorf("cannot delete vendor business associate agreement: %w", err)
} }
if err := file.SoftDelete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot soft delete file: %w", err)
}
return nil return nil
}, },
) )
@@ -329,16 +323,10 @@ func (s VendorBusinessAssociateAgreementService) DeleteByVendorID(
return fmt.Errorf("cannot load vendor business associate agreement: %w", err) return fmt.Errorf("cannot load vendor business associate agreement: %w", err)
} }
file := &coredata.File{ID: vendorBusinessAssociateAgreement.FileID}
if err := vendorBusinessAssociateAgreement.DeleteByVendorID(ctx, conn, s.svc.scope, vendorID); err != nil { if err := vendorBusinessAssociateAgreement.DeleteByVendorID(ctx, conn, s.svc.scope, vendorID); err != nil {
return fmt.Errorf("cannot delete vendor business associate agreement: %w", err) return fmt.Errorf("cannot delete vendor business associate agreement: %w", err)
} }
if err := file.SoftDelete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot soft delete file: %w", err)
}
return nil return nil
}, },
) )

View File

@@ -139,7 +139,6 @@ func (s VendorDataPrivacyAgreementService) Upload(
vendorDataPrivacyAgreement = &coredata.VendorDataPrivacyAgreement{ vendorDataPrivacyAgreement = &coredata.VendorDataPrivacyAgreement{
ID: vendorDataPrivacyAgreementID, ID: vendorDataPrivacyAgreementID,
TenantID: s.svc.scope.GetTenantID(),
OrganizationID: vendor.OrganizationID, OrganizationID: vendor.OrganizationID,
VendorID: vendorID, VendorID: vendorID,
ValidFrom: req.ValidFrom, ValidFrom: req.ValidFrom,
@@ -303,16 +302,10 @@ func (s VendorDataPrivacyAgreementService) Delete(
return fmt.Errorf("cannot load vendor data privacy agreement: %w", err) return fmt.Errorf("cannot load vendor data privacy agreement: %w", err)
} }
file := &coredata.File{ID: vendorDataPrivacyAgreement.FileID}
if err := vendorDataPrivacyAgreement.Delete(ctx, conn, s.svc.scope); err != nil { if err := vendorDataPrivacyAgreement.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete vendor data privacy agreement: %w", err) return fmt.Errorf("cannot delete vendor data privacy agreement: %w", err)
} }
if err := file.SoftDelete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot soft delete file: %w", err)
}
return nil return nil
}, },
) )
@@ -330,16 +323,10 @@ func (s VendorDataPrivacyAgreementService) DeleteByVendorID(
return fmt.Errorf("cannot load vendor data privacy agreement: %w", err) return fmt.Errorf("cannot load vendor data privacy agreement: %w", err)
} }
file := &coredata.File{ID: vendorDataPrivacyAgreement.FileID}
if err := vendorDataPrivacyAgreement.DeleteByVendorID(ctx, conn, s.svc.scope, vendorID); err != nil { if err := vendorDataPrivacyAgreement.DeleteByVendorID(ctx, conn, s.svc.scope, vendorID); err != nil {
return fmt.Errorf("cannot delete vendor data privacy agreement: %w", err) return fmt.Errorf("cannot delete vendor data privacy agreement: %w", err)
} }
if err := file.SoftDelete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot soft delete file: %w", err)
}
return nil return nil
}, },
) )

View File

@@ -1134,6 +1134,10 @@ input AssetFilter {
snapshotId: ID snapshotId: ID
} }
input VendorFilter {
snapshotId: ID
}
# Core Types # Core Types
type TrustCenter implements Node { type TrustCenter implements Node {
id: ID! id: ID!
@@ -1196,6 +1200,7 @@ type Organization implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: VendorOrder orderBy: VendorOrder
filter: VendorFilter = { snapshotId: null }
): VendorConnection! @goField(forceResolver: true) ): VendorConnection! @goField(forceResolver: true)
peoples( peoples(
@@ -1351,6 +1356,7 @@ type People implements Node {
type Vendor implements Node { type Vendor implements Node {
id: ID! id: ID!
snapshotId: ID
name: String! name: String!
category: VendorCategory! category: VendorCategory!
description: String description: String

View File

@@ -897,7 +897,7 @@ type ComplexityRoot struct {
TrustCenter func(childComplexity int) int TrustCenter func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
Users func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) int Users func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) int
Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) int
} }
OrganizationConnection struct { OrganizationConnection struct {
@@ -1300,6 +1300,7 @@ type ComplexityRoot struct {
ServiceLevelAgreementURL func(childComplexity int) int ServiceLevelAgreementURL func(childComplexity int) int
Services func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorServiceOrderBy) int Services func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorServiceOrderBy) int
ShowOnTrustCenter func(childComplexity int) int ShowOnTrustCenter func(childComplexity int) int
SnapshotID func(childComplexity int) int
StatusPageURL func(childComplexity int) int StatusPageURL func(childComplexity int) int
SubprocessorsListURL func(childComplexity int) int SubprocessorsListURL func(childComplexity int) int
TermsOfServiceURL func(childComplexity int) int TermsOfServiceURL func(childComplexity int) int
@@ -1666,7 +1667,7 @@ type OrganizationResolver interface {
Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error)
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error)
Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error)
Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) (*types.VendorConnection, error)
Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) (*types.PeopleConnection, error) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) (*types.PeopleConnection, error)
Documents(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy, filter *types.DocumentFilter) (*types.DocumentConnection, error) Documents(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy, filter *types.DocumentFilter) (*types.DocumentConnection, error)
Measures(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error) Measures(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error)
@@ -5634,7 +5635,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false return 0, false
} }
return e.complexity.Organization.Vendors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.VendorOrderBy)), true return e.complexity.Organization.Vendors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.VendorOrderBy), args["filter"].(*types.VendorFilter)), true
case "OrganizationConnection.edges": case "OrganizationConnection.edges":
if e.complexity.OrganizationConnection.Edges == nil { if e.complexity.OrganizationConnection.Edges == nil {
@@ -7124,6 +7125,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Vendor.ShowOnTrustCenter(childComplexity), true return e.complexity.Vendor.ShowOnTrustCenter(childComplexity), true
case "Vendor.snapshotId":
if e.complexity.Vendor.SnapshotID == nil {
break
}
return e.complexity.Vendor.SnapshotID(childComplexity), true
case "Vendor.statusPageUrl": case "Vendor.statusPageUrl":
if e.complexity.Vendor.StatusPageURL == nil { if e.complexity.Vendor.StatusPageURL == nil {
break break
@@ -7850,6 +7858,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputUserOrder, ec.unmarshalInputUserOrder,
ec.unmarshalInputVendorComplianceReportOrder, ec.unmarshalInputVendorComplianceReportOrder,
ec.unmarshalInputVendorContactOrder, ec.unmarshalInputVendorContactOrder,
ec.unmarshalInputVendorFilter,
ec.unmarshalInputVendorOrder, ec.unmarshalInputVendorOrder,
ec.unmarshalInputVendorRiskAssessmentOrder, ec.unmarshalInputVendorRiskAssessmentOrder,
ec.unmarshalInputVendorServiceOrder, ec.unmarshalInputVendorServiceOrder,
@@ -9086,6 +9095,10 @@ input AssetFilter {
snapshotId: ID snapshotId: ID
} }
input VendorFilter {
snapshotId: ID
}
# Core Types # Core Types
type TrustCenter implements Node { type TrustCenter implements Node {
id: ID! id: ID!
@@ -9148,6 +9161,7 @@ type Organization implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: VendorOrder orderBy: VendorOrder
filter: VendorFilter = { snapshotId: null }
): VendorConnection! @goField(forceResolver: true) ): VendorConnection! @goField(forceResolver: true)
peoples( peoples(
@@ -9303,6 +9317,7 @@ type People implements Node {
type Vendor implements Node { type Vendor implements Node {
id: ID! id: ID!
snapshotId: ID
name: String! name: String!
category: VendorCategory! category: VendorCategory!
description: String description: String
@@ -17711,6 +17726,11 @@ func (ec *executionContext) field_Organization_vendors_args(ctx context.Context,
return nil, err return nil, err
} }
args["orderBy"] = arg4 args["orderBy"] = arg4
arg5, err := ec.field_Organization_vendors_argsFilter(ctx, rawArgs)
if err != nil {
return nil, err
}
args["filter"] = arg5
return args, nil return args, nil
} }
func (ec *executionContext) field_Organization_vendors_argsFirst( func (ec *executionContext) field_Organization_vendors_argsFirst(
@@ -17778,6 +17798,19 @@ func (ec *executionContext) field_Organization_vendors_argsOrderBy(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Organization_vendors_argsFilter(
ctx context.Context,
rawArgs map[string]any,
) (*types.VendorFilter, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter"))
if tmp, ok := rawArgs["filter"]; ok {
return ec.unmarshalOVendorFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorFilter(ctx, tmp)
}
var zeroVal *types.VendorFilter
return zeroVal, nil
}
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -19123,6 +19156,8 @@ func (ec *executionContext) fieldContext_AssessVendorPayload_vendor(_ context.Co
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -41697,7 +41732,7 @@ func (ec *executionContext) _Organization_vendors(ctx context.Context, field gra
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Organization().Vendors(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.VendorOrderBy)) return ec.resolvers.Organization().Vendors(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.VendorOrderBy), fc.Args["filter"].(*types.VendorFilter))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -51442,6 +51477,8 @@ func (ec *executionContext) fieldContext_UpdateVendorPayload_vendor(_ context.Co
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -52457,6 +52494,47 @@ func (ec *executionContext) fieldContext_Vendor_id(_ context.Context, field grap
return fc, nil return fc, nil
} }
func (ec *executionContext) _Vendor_snapshotId(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_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_Vendor_snapshotId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
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) _Vendor_name(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) { func (ec *executionContext) _Vendor_name(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_name(ctx, field) fc, err := ec.fieldContext_Vendor_name(ctx, field)
if err != nil { if err != nil {
@@ -53925,6 +54003,8 @@ func (ec *executionContext) fieldContext_VendorBusinessAssociateAgreement_vendor
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -54375,6 +54455,8 @@ func (ec *executionContext) fieldContext_VendorComplianceReport_vendor(_ context
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -55188,6 +55270,8 @@ func (ec *executionContext) fieldContext_VendorContact_vendor(_ context.Context,
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -55798,6 +55882,8 @@ func (ec *executionContext) fieldContext_VendorDataPrivacyAgreement_vendor(_ con
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -56248,6 +56334,8 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -56396,6 +56484,8 @@ func (ec *executionContext) fieldContext_VendorRiskAssessment_vendor(_ context.C
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -57129,6 +57219,8 @@ func (ec *executionContext) fieldContext_VendorService_vendor(_ context.Context,
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Vendor_id(ctx, field) return ec.fieldContext_Vendor_id(ctx, field)
case "snapshotId":
return ec.fieldContext_Vendor_snapshotId(ctx, field)
case "name": case "name":
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "category": case "category":
@@ -66011,6 +66103,33 @@ func (ec *executionContext) unmarshalInputVendorContactOrder(ctx context.Context
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputVendorFilter(ctx context.Context, obj any) (types.VendorFilter, error) {
var it types.VendorFilter
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"snapshotId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
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
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputVendorOrder(ctx context.Context, obj any) (types.VendorOrderBy, error) { func (ec *executionContext) unmarshalInputVendorOrder(ctx context.Context, obj any) (types.VendorOrderBy, error) {
var it types.VendorOrderBy var it types.VendorOrderBy
asMap := map[string]any{} asMap := map[string]any{}
@@ -78797,6 +78916,8 @@ func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, o
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._Vendor_snapshotId(ctx, field, obj)
case "name": case "name":
out.Values[i] = ec._Vendor_name(ctx, field, obj) out.Values[i] = ec._Vendor_name(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -88481,6 +88602,14 @@ func (ec *executionContext) marshalOVendorDataPrivacyAgreement2ᚖgithubᚗcom
return ec._VendorDataPrivacyAgreement(ctx, sel, v) return ec._VendorDataPrivacyAgreement(ctx, sel, v)
} }
func (ec *executionContext) unmarshalOVendorFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorFilter(ctx context.Context, v any) (*types.VendorFilter, error) {
if v == nil {
return nil, nil
}
res, err := ec.unmarshalInputVendorFilter(ctx, v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalOVendorOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorOrderBy(ctx context.Context, v any) (*types.VendorOrderBy, error) { func (ec *executionContext) unmarshalOVendorOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorOrderBy(ctx context.Context, v any) (*types.VendorOrderBy, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil

View File

@@ -1881,6 +1881,7 @@ type UserEdge struct {
type Vendor struct { type Vendor 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"`
Category coredata.VendorCategory `json:"category"` Category coredata.VendorCategory `json:"category"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
@@ -1998,6 +1999,10 @@ type VendorEdge struct {
Node *Vendor `json:"node"` Node *Vendor `json:"node"`
} }
type VendorFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type VendorRiskAssessment struct { type VendorRiskAssessment struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Vendor *Vendor `json:"vendor"` Vendor *Vendor `json:"vendor"`

View File

@@ -80,6 +80,7 @@ func NewVendor(v *coredata.Vendor) *Vendor {
WebsiteURL: v.WebsiteURL, WebsiteURL: v.WebsiteURL,
Category: v.Category, Category: v.Category,
ShowOnTrustCenter: v.ShowOnTrustCenter, ShowOnTrustCenter: v.ShowOnTrustCenter,
SnapshotID: v.SnapshotID,
UpdatedAt: v.UpdatedAt, UpdatedAt: v.UpdatedAt,
CreatedAt: v.CreatedAt, CreatedAt: v.CreatedAt,
} }

View File

@@ -3459,7 +3459,7 @@ func (r *organizationResolver) Controls(ctx context.Context, obj *types.Organiza
} }
// Vendors is the resolver for the vendors field. // Vendors is the resolver for the vendors field.
func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) { func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) (*types.VendorConnection, error) {
prb := r.ProboService(ctx, obj.ID.TenantID()) prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.VendorOrderField]{ pageOrderBy := page.OrderBy[coredata.VendorOrderField]{
@@ -3474,8 +3474,11 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat
} }
cursor := types.NewCursor(first, after, last, before, pageOrderBy) cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var nilSnapshotID *gid.GID = nil
vendorFilter := coredata.NewVendorFilter(&nilSnapshotID, nil) var vendorFilter = coredata.NewVendorFilter(nil, nil)
if filter != nil {
vendorFilter = coredata.NewVendorFilter(&filter.SnapshotID, nil)
}
page, err := prb.Vendors.ListForOrganizationID(ctx, obj.ID, cursor, vendorFilter) page, err := prb.Vendors.ListForOrganizationID(ctx, obj.ID, cursor, vendorFilter)
if err != nil { if err != nil {