diff --git a/apps/console/src/hooks/graph/VendorGraph.ts b/apps/console/src/hooks/graph/VendorGraph.ts index e879175d8..35065bafb 100644 --- a/apps/console/src/hooks/graph/VendorGraph.ts +++ b/apps/console/src/hooks/graph/VendorGraph.ts @@ -89,11 +89,11 @@ export const useDeleteVendor = ( export const vendorConnectionKey = "VendorsPage_vendors"; export const vendorsQuery = graphql` - query VendorGraphListQuery($organizationId: ID!) { + query VendorGraphListQuery($organizationId: ID!, $snapshotId: ID) { node(id: $organizationId) { ... on Organization { id - ...VendorGraphPaginatedFragment + ...VendorGraphPaginatedFragment @arguments(snapshotId: $snapshotId) } } } @@ -108,6 +108,7 @@ export const paginatedVendorsFragment = graphql` after: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null } last: { type: "Int", defaultValue: null } + snapshotId: { type: "ID", defaultValue: null } ) { vendors( first: $first @@ -115,11 +116,13 @@ export const paginatedVendorsFragment = graphql` last: $last before: $before orderBy: $order - ) @connection(key: "VendorsListQuery_vendors") { + filter: { snapshotId: $snapshotId } + ) @connection(key: "VendorsListQuery_vendors", filters: ["filter"]) { __id edges { node { id + snapshotId name websiteUrl updatedAt @@ -148,6 +151,7 @@ export const vendorNodeQuery = graphql` node(id: $vendorId) { ... on Vendor { id + snapshotId name websiteUrl ...useVendorFormFragment diff --git a/apps/console/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts index 2855e7d53..ffa964d23 100644 --- a/apps/console/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<8b1614888dac560deff871232f91995d>> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type VendorGraphListQuery$variables = { organizationId: string; + snapshotId?: string | null | undefined; }; export type VendorGraphListQuery$data = { readonly node: { @@ -30,6 +31,11 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "organizationId" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], v1 = [ @@ -46,14 +52,26 @@ v2 = { "name": "id", "storageKey": null }, -v3 = { +v3 = [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } +], +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v4 = [ +v5 = [ + { + "fields": (v3/*: any*/), + "kind": "ObjectValue", + "name": "filter" + }, { "kind": "Literal", "name": "first", @@ -80,7 +98,7 @@ return { "selections": [ (v2/*: any*/), { - "args": null, + "args": (v3/*: any*/), "kind": "FragmentSpread", "name": "VendorGraphPaginatedFragment" } @@ -109,14 +127,14 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": "VendorConnection", "kind": "LinkedField", "name": "vendors", @@ -139,6 +157,13 @@ return { "plural": false, "selections": [ (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, { "alias": null, "args": null, @@ -236,7 +261,7 @@ return { ], "storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})" }, - (v3/*: any*/) + (v4/*: any*/) ], "storageKey": null }, @@ -302,13 +327,13 @@ return { ] } ], - "storageKey": "vendors(first:50)" + "storageKey": null }, { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "filters": [ - "orderBy" + "filter" ], "handle": "connection", "key": "VendorsListQuery_vendors", @@ -325,16 +350,16 @@ return { ] }, "params": { - "cacheID": "e2eade63dcf3d7ea071463a2e5993293", + "cacheID": "4b19f6f4be47c38d8b68601ec95315a6", "id": null, "metadata": {}, "name": "VendorGraphListQuery", "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; diff --git a/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts index c9848c258..3fc407fe4 100644 --- a/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<46bd8a339fe6fd05256df0b1f806e0bd>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -18,6 +18,7 @@ export type VendorGraphNodeQuery$data = { readonly node: { readonly id?: string; readonly name?: string; + readonly snapshotId?: string | null | undefined; readonly websiteUrl?: string | null | undefined; readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "VendorContactsTabFragment" | "VendorOverviewTabBusinessAssociateAgreementFragment" | "VendorOverviewTabDataPrivacyAgreementFragment" | "VendorRiskAssessmentTabFragment" | "VendorServicesTabFragment" | "useVendorFormFragment">; }; @@ -63,20 +64,27 @@ v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "snapshotId", "storageKey": null }, v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "websiteUrl", "storageKey": null }, -v6 = [ +v7 = [ (v3/*: any*/) ], -v7 = { +v8 = { "alias": null, "args": [ { @@ -89,80 +97,80 @@ v7 = { "kind": "LinkedField", "name": "people", "plural": false, - "selections": (v6/*: any*/), - "storageKey": null -}, -v8 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", + "selections": (v7/*: any*/), "storageKey": null }, v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v10 = { "alias": null, "args": null, "kind": "ScalarField", "name": "description", "storageKey": null }, -v10 = [ +v11 = [ { "kind": "Literal", "name": "first", "value": 50 } ], -v11 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "validUntil", "storageKey": null }, -v12 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fileUrl", "storageKey": null }, -v13 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v14 = { +v15 = { "alias": null, "args": null, "kind": "ScalarField", "name": "endCursor", "storageKey": null }, -v15 = { +v16 = { "alias": null, "args": null, "kind": "ScalarField", "name": "hasNextPage", "storageKey": null }, -v16 = { +v17 = { "alias": null, "args": null, "kind": "ScalarField", "name": "hasPreviousPage", "storageKey": null }, -v17 = { +v18 = { "alias": null, "args": null, "kind": "ScalarField", "name": "startCursor", "storageKey": null }, -v18 = { +v19 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -170,14 +178,14 @@ v18 = { "name": "pageInfo", "plural": false, "selections": [ - (v14/*: any*/), (v15/*: any*/), (v16/*: any*/), - (v17/*: any*/) + (v17/*: any*/), + (v18/*: any*/) ], "storageKey": null }, -v19 = { +v20 = { "kind": "ClientExtension", "selections": [ { @@ -189,31 +197,31 @@ v19 = { } ] }, -v20 = [ +v21 = [ "orderBy" ], -v21 = { +v22 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v22 = { +v23 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v23 = { +v24 = { "alias": null, "args": null, "kind": "ScalarField", "name": "updatedAt", "storageKey": null }, -v24 = [ +v25 = [ (v3/*: any*/), { "alias": null, @@ -222,7 +230,7 @@ v24 = [ "name": "fileName", "storageKey": null }, - (v12/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -230,8 +238,8 @@ v24 = [ "name": "validFrom", "storageKey": null }, - (v11/*: any*/), - (v22/*: any*/) + (v12/*: any*/), + (v23/*: any*/) ]; return { "fragment": { @@ -257,6 +265,7 @@ return { (v3/*: any*/), (v4/*: any*/), (v5/*: any*/), + (v6/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -315,7 +324,7 @@ return { "name": "user", "plural": false, "selections": [ - (v7/*: any*/) + (v8/*: any*/) ], "storageKey": null } @@ -343,14 +352,15 @@ return { "name": "node", "plural": false, "selections": [ - (v8/*: any*/), + (v9/*: any*/), (v3/*: any*/), { "kind": "InlineFragment", "selections": [ (v4/*: any*/), (v5/*: any*/), - (v9/*: any*/), + (v6/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, @@ -428,7 +438,7 @@ return { "kind": "LinkedField", "name": "businessOwner", "plural": false, - "selections": (v6/*: any*/), + "selections": (v7/*: any*/), "storageKey": null }, { @@ -438,12 +448,12 @@ return { "kind": "LinkedField", "name": "securityOwner", "plural": false, - "selections": (v6/*: any*/), + "selections": (v7/*: any*/), "storageKey": null }, { "alias": null, - "args": (v10/*: any*/), + "args": (v11/*: any*/), "concreteType": "VendorComplianceReportConnection", "kind": "LinkedField", "name": "complianceReports", @@ -473,7 +483,7 @@ return { "name": "reportDate", "storageKey": null }, - (v11/*: any*/), + (v12/*: any*/), { "alias": null, "args": null, @@ -481,7 +491,7 @@ return { "name": "reportName", "storageKey": null }, - (v12/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -489,23 +499,23 @@ return { "name": "fileSize", "storageKey": null }, - (v8/*: any*/) + (v9/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v18/*: any*/), - (v19/*: any*/) + (v19/*: any*/), + (v20/*: any*/) ], "storageKey": "complianceReports(first:50)" }, { "alias": null, - "args": (v10/*: any*/), - "filters": (v20/*: any*/), + "args": (v11/*: any*/), + "filters": (v21/*: any*/), "handle": "connection", "key": "VendorComplianceTabFragment_complianceReports", "kind": "LinkedHandle", @@ -513,7 +523,7 @@ return { }, { "alias": null, - "args": (v10/*: any*/), + "args": (v11/*: any*/), "concreteType": "VendorContactConnection", "kind": "LinkedField", "name": "contacts", @@ -536,7 +546,7 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v21/*: any*/), + (v22/*: any*/), { "alias": null, "args": null, @@ -558,25 +568,25 @@ return { "name": "role", "storageKey": null }, - (v22/*: any*/), (v23/*: any*/), - (v8/*: any*/) + (v24/*: any*/), + (v9/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v18/*: any*/), - (v19/*: any*/) + (v19/*: any*/), + (v20/*: any*/) ], "storageKey": "contacts(first:50)" }, { "alias": null, - "args": (v10/*: any*/), - "filters": (v20/*: any*/), + "args": (v11/*: any*/), + "filters": (v21/*: any*/), "handle": "connection", "key": "VendorContactsTabFragment_contacts", "kind": "LinkedHandle", @@ -584,7 +594,7 @@ return { }, { "alias": null, - "args": (v10/*: any*/), + "args": (v11/*: any*/), "concreteType": "VendorServiceConnection", "kind": "LinkedField", "name": "services", @@ -607,27 +617,27 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v4/*: any*/), - (v9/*: any*/), - (v22/*: any*/), + (v5/*: any*/), + (v10/*: any*/), (v23/*: any*/), - (v8/*: any*/) + (v24/*: any*/), + (v9/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v18/*: any*/), - (v19/*: any*/) + (v19/*: any*/), + (v20/*: any*/) ], "storageKey": "services(first:50)" }, { "alias": null, - "args": (v10/*: any*/), - "filters": (v20/*: any*/), + "args": (v11/*: any*/), + "filters": (v21/*: any*/), "handle": "connection", "key": "VendorServicesTabFragment_services", "kind": "LinkedHandle", @@ -635,7 +645,7 @@ return { }, { "alias": null, - "args": (v10/*: any*/), + "args": (v11/*: any*/), "concreteType": "VendorRiskAssessmentConnection", "kind": "LinkedField", "name": "riskAssessments", @@ -674,7 +684,7 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v21/*: any*/) + (v22/*: any*/) ], "storageKey": null }, @@ -706,11 +716,11 @@ return { "name": "notes", "storageKey": null }, - (v8/*: any*/) + (v9/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v14/*: any*/) ], "storageKey": null }, @@ -722,21 +732,21 @@ return { "name": "pageInfo", "plural": false, "selections": [ - (v15/*: any*/), - (v14/*: any*/), (v16/*: any*/), - (v17/*: any*/) + (v15/*: any*/), + (v17/*: any*/), + (v18/*: any*/) ], "storageKey": null }, - (v19/*: any*/) + (v20/*: any*/) ], "storageKey": "riskAssessments(first:50)" }, { "alias": null, - "args": (v10/*: any*/), - "filters": (v20/*: any*/), + "args": (v11/*: any*/), + "filters": (v21/*: any*/), "handle": "connection", "key": "VendorRiskAssessmentTabFragment_riskAssessments", "kind": "LinkedHandle", @@ -749,7 +759,7 @@ return { "kind": "LinkedField", "name": "businessAssociateAgreement", "plural": false, - "selections": (v24/*: any*/), + "selections": (v25/*: any*/), "storageKey": null }, { @@ -759,7 +769,7 @@ return { "kind": "LinkedField", "name": "dataPrivacyAgreement", "plural": false, - "selections": (v24/*: any*/), + "selections": (v25/*: any*/), "storageKey": null } ], @@ -785,7 +795,7 @@ return { "name": "user", "plural": false, "selections": [ - (v7/*: any*/), + (v8/*: any*/), (v3/*: any*/) ], "storageKey": null @@ -797,16 +807,16 @@ return { ] }, "params": { - "cacheID": "7e1a7455861aea37eee2d799d8385586", + "cacheID": "f0b506b57e84959deb0b1e3fa0481897", "id": null, "metadata": {}, "name": "VendorGraphNodeQuery", "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; diff --git a/apps/console/src/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql.ts b/apps/console/src/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql.ts index d89cfefb5..f8d7253e9 100644 --- a/apps/console/src/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<93638f7eeb1634fc19aaaa95cae0c7b8>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -31,6 +31,7 @@ export type VendorGraphPaginatedFragment$data = { }; }>; }; + readonly snapshotId: string | null | undefined; readonly updatedAt: any; readonly websiteUrl: string | null | undefined; }; @@ -82,6 +83,11 @@ return { "defaultValue": null, "kind": "LocalArgument", "name": "order" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], "kind": "Fragment", @@ -122,9 +128,15 @@ return { "alias": "vendors", "args": [ { - "kind": "Variable", - "name": "orderBy", - "variableName": "order" + "fields": [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } + ], + "kind": "ObjectValue", + "name": "filter" } ], "concreteType": "VendorConnection", @@ -149,6 +161,13 @@ return { "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, { "alias": null, "args": null, @@ -327,6 +346,6 @@ return { }; })(); -(node as any).hash = "1d2bf7c29e77d60d0a955a62b98c85ab"; +(node as any).hash = "578772ed7c83ae174d43b0be95a02fd2"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/VendorsListQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/VendorsListQuery.graphql.ts index 7e9193589..c4ac738f8 100644 --- a/apps/console/src/hooks/graph/__generated__/VendorsListQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/VendorsListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -23,6 +23,7 @@ export type VendorsListQuery$variables = { id: string; last?: number | null | undefined; order?: VendorOrder | null | undefined; + snapshotId?: string | null | undefined; }; export type VendorsListQuery$data = { readonly node: { @@ -65,52 +66,69 @@ v5 = { "kind": "LocalArgument", "name": "order" }, -v6 = [ +v6 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" +}, +v7 = [ { "kind": "Variable", "name": "id", "variableName": "id" } ], -v7 = { +v8 = { "kind": "Variable", "name": "after", "variableName": "after" }, -v8 = { +v9 = { "kind": "Variable", "name": "before", "variableName": "before" }, -v9 = { +v10 = { "kind": "Variable", "name": "first", "variableName": "first" }, -v10 = { +v11 = { "kind": "Variable", "name": "last", "variableName": "last" }, -v11 = { +v12 = { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" +}, +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v12 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v13 = [ - (v7/*: any*/), +v15 = [ (v8/*: any*/), (v9/*: any*/), + { + "fields": [ + (v12/*: any*/) + ], + "kind": "ObjectValue", + "name": "filter" + }, (v10/*: any*/), + (v11/*: any*/), { "kind": "Variable", "name": "orderBy", @@ -125,7 +143,8 @@ return { (v2/*: any*/), (v3/*: any*/), (v4/*: any*/), - (v5/*: any*/) + (v5/*: any*/), + (v6/*: any*/) ], "kind": "Fragment", "metadata": null, @@ -133,7 +152,7 @@ return { "selections": [ { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", @@ -141,15 +160,16 @@ return { "selections": [ { "args": [ - (v7/*: any*/), (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), + (v11/*: any*/), { "kind": "Variable", "name": "order", "variableName": "order" - } + }, + (v12/*: any*/) ], "kind": "FragmentSpread", "name": "VendorGraphPaginatedFragment" @@ -169,6 +189,7 @@ return { (v2/*: any*/), (v4/*: any*/), (v5/*: any*/), + (v6/*: any*/), (v3/*: any*/) ], "kind": "Operation", @@ -176,20 +197,20 @@ return { "selections": [ { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v11/*: any*/), - (v12/*: any*/), + (v13/*: any*/), + (v14/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v13/*: any*/), + "args": (v15/*: any*/), "concreteType": "VendorConnection", "kind": "LinkedField", "name": "vendors", @@ -211,7 +232,14 @@ return { "name": "node", "plural": false, "selections": [ - (v12/*: any*/), + (v14/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, { "alias": null, "args": null, @@ -271,7 +299,7 @@ return { "name": "node", "plural": false, "selections": [ - (v12/*: any*/), + (v14/*: any*/), { "alias": null, "args": null, @@ -309,7 +337,7 @@ return { ], "storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})" }, - (v11/*: any*/) + (v13/*: any*/) ], "storageKey": null }, @@ -379,9 +407,9 @@ return { }, { "alias": null, - "args": (v13/*: any*/), + "args": (v15/*: any*/), "filters": [ - "orderBy" + "filter" ], "handle": "connection", "key": "VendorsListQuery_vendors", @@ -398,16 +426,16 @@ return { ] }, "params": { - "cacheID": "70fb3d49e78996ae4cefbaa8f5605c2c", + "cacheID": "6570f4601f6dea5b7fe738d851a7f1ba", "id": null, "metadata": {}, "name": "VendorsListQuery", "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; diff --git a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx index 191a59956..6e4158ebf 100644 --- a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx @@ -192,6 +192,7 @@ export default function AssetDetailsPage(props: Props) { organizationId={organizationId} control={control} name="vendorIds" + selectedVendors={vendors} label={__("Vendors")} disabled={isSnapshotMode} /> diff --git a/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx b/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx index 230beb9f1..880589049 100644 --- a/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx +++ b/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx @@ -23,11 +23,12 @@ import { } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import { Outlet } from "react-router"; -import { faviconUrl } from "@probo/helpers"; +import { Outlet, useParams } from "react-router"; +import { faviconUrl, validateSnapshotConsistency } from "@probo/helpers"; import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog"; import { complianceReportsFragment } from "./tabs/VendorComplianceTab"; import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql"; +import { SnapshotBanner } from "/components/SnapshotBanner"; type Props = { queryRef: PreloadedQuery; @@ -38,6 +39,14 @@ export default function VendorDetailPage(props: Props) { const vendor = data.node; const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); + + if (!vendor) { + return
{__("Vendor not found")}
; + } + + validateSnapshotConsistency(vendor, snapshotId); const deleteVendor = useDeleteVendor( vendor, ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey) @@ -48,13 +57,22 @@ export default function VendorDetailPage(props: Props) { vendor as VendorComplianceTabFragment$key ).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 (
+ {snapshotId && } {vendor.name}
-
- - - - - - {__("Delete")} - - -
+ {!isSnapshotMode && ( +
+ + + + + + {__("Delete")} + + +
+ )} - + {__("Overview")} - + {__("Certifications")} - + {__("Compliance reports")} {reportsCount > 0 && {reportsCount}} - + {__("Risk Assessment")} - + {__("Contacts")} - + {__("Services")} diff --git a/apps/console/src/pages/organizations/vendors/VendorsPage.tsx b/apps/console/src/pages/organizations/vendors/VendorsPage.tsx index 6be97b319..84e76f4c7 100644 --- a/apps/console/src/pages/organizations/vendors/VendorsPage.tsx +++ b/apps/console/src/pages/organizations/vendors/VendorsPage.tsx @@ -21,6 +21,7 @@ import { type PreloadedQuery, } from "react-relay"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useParams } from "react-router"; import { faviconUrl } from "@probo/helpers"; import type { NodeOf } from "/types"; import { CreateVendorDialog } from "./dialogs/CreateVendorDialog"; @@ -35,6 +36,7 @@ import type { VendorGraphPaginatedFragment$key, } from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql"; import { SortableTable, SortableTh } from "/components/SortableTable"; +import { SnapshotBanner } from "/components/SnapshotBanner"; type Vendor = NodeOf; @@ -45,6 +47,8 @@ type Props = { export default function VendorsPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const data = usePreloadedQuery(vendorsQuery, props.queryRef); const pagination = usePaginationFragment( @@ -59,18 +63,21 @@ export default function VendorsPage(props: Props) { return (
+ {snapshotId && } - - - + {!isSnapshotMode && ( + + + + )} @@ -106,14 +113,20 @@ function VendorRow({ organizationId: string; connectionId: string; }) { + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const { __, dateFormat } = useTranslate(); const latestAssessment = vendor.riskAssessments?.edges[0]?.node; const deleteVendor = useDeleteVendor(vendor, connectionId); + const vendorUrl = isSnapshotMode && snapshotId + ? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}/overview` + : `/organizations/${organizationId}/vendors/${vendor.id}/overview`; + return ( <> - +
@@ -136,15 +149,17 @@ function VendorRow({ - - - {__("Delete")} - - + {!isSnapshotMode && ( + + + {__("Delete")} + + + )} diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx index e11f7b577..d64555708 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx @@ -1,4 +1,4 @@ -import { useOutletContext } from "react-router"; +import { useOutletContext, useParams } from "react-router"; import { graphql } from "relay-runtime"; import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql"; import { useTranslate } from "@probo/i18n"; @@ -98,6 +98,8 @@ export default function VendorComplianceTab() { const connectionId = data.complianceReports.__id; const reports = data.complianceReports.edges.map((edge) => edge.node); const { __ } = useTranslate(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); usePageTitle(vendor.name + " - " + __("Compliance reports")); @@ -125,15 +127,17 @@ export default function VendorComplianceTab() { return (
- + {!isSnapshotMode && ( + + )} @@ -141,7 +145,7 @@ export default function VendorComplianceTab() { {__("Report date")} {__("Valid until")} {__("File size")} - {__("Actions")} + {!isSnapshotMode && {__("Actions")}} @@ -150,6 +154,7 @@ export default function VendorComplianceTab() { key={report.id} reportKey={report} connectionId={connectionId} + isSnapshotMode={isSnapshotMode} /> ))} @@ -161,6 +166,7 @@ export default function VendorComplianceTab() { type ReportRowProps = { reportKey: VendorComplianceTabFragment_report$key; connectionId: string; + isSnapshotMode: boolean; }; function ReportRow(props: ReportRowProps) { @@ -204,17 +210,19 @@ function ReportRow(props: ReportRowProps) { {dateFormat(report.reportDate)} {dateFormat(report.validUntil)} {fileSize(__, report.fileSize)} - - - - {__("Delete")} - - - + {!props.isSnapshotMode && ( + + + + {__("Delete")} + + + + )} ); } diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorContactsTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorContactsTab.tsx index 604026f9d..f62554174 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorContactsTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorContactsTab.tsx @@ -1,4 +1,4 @@ -import { useOutletContext } from "react-router"; +import { useOutletContext, useParams } from "react-router"; import { graphql } from "relay-runtime"; import type { VendorContactsTabFragment$key } from "./__generated__/VendorContactsTabFragment.graphql"; import { useTranslate } from "@probo/i18n"; @@ -89,6 +89,8 @@ export default function VendorContactsTab() { const connectionId = data.contacts.__id; const contacts = data.contacts.edges.map((edge) => edge.node); const { __ } = useTranslate(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const [editingContact, setEditingContact] = useState<{ id: string; fullName?: string | null; @@ -105,12 +107,14 @@ export default function VendorContactsTab() { title={__("Contacts")} description={__("Manage vendor contacts and their information.")} > - - - + {!isSnapshotMode && ( + + + + )} @@ -120,7 +124,7 @@ export default function VendorContactsTab() { {__("Email")} {__("Phone")} {__("Role")} - {__("Actions")} + {!isSnapshotMode && {__("Actions")}} @@ -130,12 +134,13 @@ export default function VendorContactsTab() { contactKey={contact} connectionId={connectionId} onEdit={setEditingContact} + isSnapshotMode={isSnapshotMode} /> ))} - {editingContact && ( + {editingContact && !isSnapshotMode && ( void; + isSnapshotMode: boolean; }; function ContactRow(props: ContactRowProps) { @@ -220,29 +226,31 @@ function ContactRow(props: ContactRowProps) { )} {contact.role || __("—")} - - - props.onEdit({ - id: contact.id, - fullName: contact.fullName, - email: contact.email, - phone: contact.phone, - role: contact.role, - })} - > - {__("Edit")} - - - {__("Delete")} - - - + {!props.isSnapshotMode && ( + + + props.onEdit({ + id: contact.id, + fullName: contact.fullName, + email: contact.email, + phone: contact.phone, + role: contact.role, + })} + > + {__("Edit")} + + + {__("Delete")} + + + + )} ); } diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx index cf6b4649a..ae20f9a8f 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx @@ -1,6 +1,6 @@ import { useTranslate } from "@probo/i18n"; 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 { PeopleSelectField } from "/components/form/PeopleSelectField"; import { useOrganizationId } from "/hooks/useOrganizationId"; @@ -55,6 +55,8 @@ export default function VendorOverviewTab() { const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const { control, @@ -98,7 +100,7 @@ export default function VendorOverviewTab() { usePageTitle(vendor.name + " - " + __("Overview")); return ( -
+ {/* Vendor Details */}

{__("Vendor details")}

@@ -108,30 +110,35 @@ export default function VendorOverviewTab() { label={__("Name")} type="text" error={errors.name?.message} + disabled={isSubmitting || isSnapshotMode} />
@@ -146,6 +153,7 @@ export default function VendorOverviewTab() { name="businessOwnerId" label={__("Business owner")} error={errors.businessOwnerId?.message} + disabled={isSubmitting || isSnapshotMode} />
@@ -180,6 +189,7 @@ export default function VendorOverviewTab() { type="text" placeholder="https://..." variant="ghost" + disabled={isSubmitting || isSnapshotMode} />
))} @@ -220,33 +230,39 @@ export default function VendorOverviewTab() { > {__("Download PDF")} - window.location.reload()} - > - - + !isSnapshotMode && ( + window.location.reload()} + > + + + ) )}
@@ -281,26 +297,30 @@ export default function VendorOverviewTab() { > {__("Download PDF")} - window.location.reload()} - > - - + ) )} @@ -317,11 +337,13 @@ export default function VendorOverviewTab() { {/* Submit */} -
- -
+ {!isSnapshotMode && ( +
+ +
+ )} ); } diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorRiskAssessmentTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorRiskAssessmentTab.tsx index 417323ebf..b074f6724 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorRiskAssessmentTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorRiskAssessmentTab.tsx @@ -1,4 +1,4 @@ -import { useOutletContext } from "react-router"; +import { useOutletContext, useParams } from "react-router"; import { graphql } from "relay-runtime"; import type { VendorRiskAssessmentTabFragment$key } from "./__generated__/VendorRiskAssessmentTabFragment.graphql"; import { useTranslate } from "@probo/i18n"; @@ -82,6 +82,8 @@ export default function VendorRiskAssessmentTab() { ); const assessments = data.riskAssessments.edges.map((edge) => edge.node); const { __ } = useTranslate(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const [expanded, setExpanded] = useState(null); usePageTitle(vendor.name + " - " + __("Risk Assessments")); @@ -90,15 +92,17 @@ export default function VendorRiskAssessmentTab() { return (
{__("No risk assessments found")} - - - + {!isSnapshotMode && ( + + + + )}
); } @@ -117,15 +121,17 @@ export default function VendorRiskAssessmentTab() { - - {}}> - {__("Add Risk Assessment")} - - + {!isSnapshotMode && ( + + {}}> + {__("Add Risk Assessment")} + + + )} {assessments.map((assessment) => ( edge.node); const { __ } = useTranslate(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const [editingService, setEditingService] = useState<{ id: string; name: string; @@ -101,12 +103,14 @@ export default function VendorServicesTab() { title={__("Services")} description={__("Manage services provided by this vendor.")} > - - - + {!isSnapshotMode && ( + + + + )} @@ -114,7 +118,7 @@ export default function VendorServicesTab() { {__("Name")} {__("Description")} - {__("Actions")} + {!isSnapshotMode && {__("Actions")}} @@ -124,12 +128,13 @@ export default function VendorServicesTab() { serviceKey={service} connectionId={connectionId} onEdit={setEditingService} + isSnapshotMode={isSnapshotMode} /> ))} - {editingService && ( + {editingService && !isSnapshotMode && ( void; + isSnapshotMode: boolean; }; function ServiceRow(props: ServiceRowProps) { @@ -188,27 +194,29 @@ function ServiceRow(props: ServiceRowProps) { {service.name} {service.description || __("—")} - - - props.onEdit({ - id: service.id, - name: service.name, - description: service.description, - })} - > - {__("Edit")} - - - {__("Delete")} - - - + {!props.isSnapshotMode && ( + + + props.onEdit({ + id: service.id, + name: service.name, + description: service.description, + })} + > + {__("Edit")} + + + {__("Delete")} + + + + )} ); } diff --git a/apps/console/src/routes/vendorRoutes.ts b/apps/console/src/routes/vendorRoutes.ts index e1515de2a..29a653c2b 100644 --- a/apps/console/src/routes/vendorRoutes.ts +++ b/apps/console/src/routes/vendorRoutes.ts @@ -11,7 +11,20 @@ export const vendorRoutes = [ path: "vendors", fallback: PageSkeleton, 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")), }, { @@ -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[]; diff --git a/packages/helpers/src/snapshots.ts b/packages/helpers/src/snapshots.ts index 22a807630..3cb628424 100644 --- a/packages/helpers/src/snapshots.ts +++ b/packages/helpers/src/snapshots.ts @@ -1,6 +1,7 @@ type Translator = (s: string) => string; export const snapshotTypes = [ + "VENDORS", "ASSETS", "DATA", "NONCONFORMITY_REGISTRIES", @@ -38,6 +39,8 @@ export function getSnapshotTypeLabel(__: Translator, type: string | null | undef export function getSnapshotTypeUrlPath(type?: string): string { switch (type) { + case "VENDORS": + return "/vendors"; case "ASSETS": return "/assets"; case "DATA": diff --git a/pkg/coredata/file.go b/pkg/coredata/file.go index 45fe35132..59c126ee3 100644 --- a/pkg/coredata/file.go +++ b/pkg/coredata/file.go @@ -135,42 +135,3 @@ VALUES ( _, err := conn.Exec(ctx, q, args) 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 -} diff --git a/pkg/coredata/migrations/20250829T165122Z.sql b/pkg/coredata/migrations/20250829T165122Z.sql new file mode 100644 index 000000000..5a568a4ba --- /dev/null +++ b/pkg/coredata/migrations/20250829T165122Z.sql @@ -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; diff --git a/pkg/coredata/snapshottable.go b/pkg/coredata/snapshottable.go index 7e299eedf..6aed421ee 100644 --- a/pkg/coredata/snapshottable.go +++ b/pkg/coredata/snapshottable.go @@ -40,6 +40,8 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) { return ContinualImprovementRegistries{}, nil case SnapshotsTypeProcessingActivityRegistries: return ProcessingActivityRegistries{}, nil + case SnapshotsTypeVendors: + return Vendors{}, nil default: return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType) } diff --git a/pkg/coredata/vendor.go b/pkg/coredata/vendor.go index c925e97f6..179dc3bf8 100644 --- a/pkg/coredata/vendor.go +++ b/pkg/coredata/vendor.go @@ -57,6 +57,10 @@ type ( } 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 { @@ -914,3 +918,106 @@ FROM source_vendors v 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 +} diff --git a/pkg/coredata/vendor_business_associate_agreement.go b/pkg/coredata/vendor_business_associate_agreement.go index 085dd829d..c872344a9 100644 --- a/pkg/coredata/vendor_business_associate_agreement.go +++ b/pkg/coredata/vendor_business_associate_agreement.go @@ -34,6 +34,8 @@ type ( ValidFrom *time.Time `db:"valid_from"` ValidUntil *time.Time `db:"valid_until"` FileID gid.GID `db:"file_id"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -66,6 +68,8 @@ SELECT valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at FROM @@ -110,6 +114,8 @@ SELECT valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at FROM @@ -156,6 +162,7 @@ SET WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -192,6 +199,8 @@ INSERT INTO valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at ) @@ -203,6 +212,8 @@ VALUES ( @valid_from, @valid_until, @file_id, + @snapshot_id, + @source_id, @created_at, @updated_at ) @@ -211,6 +222,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET valid_from = EXCLUDED.valid_from, valid_until = EXCLUDED.valid_until, file_id = EXCLUDED.file_id, + snapshot_id = EXCLUDED.snapshot_id, + source_id = EXCLUDED.source_id, updated_at = EXCLUDED.updated_at ` args := pgx.StrictNamedArgs{ @@ -221,6 +234,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET "valid_from": vbaa.ValidFrom, "valid_until": vbaa.ValidUntil, "file_id": vbaa.FileID, + "snapshot_id": vbaa.SnapshotID, + "source_id": vbaa.SourceID, "created_at": vbaa.CreatedAt, "updated_at": vbaa.UpdatedAt, } @@ -241,6 +256,7 @@ FROM WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -265,6 +281,7 @@ FROM WHERE %s AND vendor_id = @vendor_id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -275,3 +292,65 @@ WHERE _, err := conn.Exec(ctx, q, args) 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 +} diff --git a/pkg/coredata/vendor_compliance_report.go b/pkg/coredata/vendor_compliance_report.go index 8402fed08..b7e8c766d 100644 --- a/pkg/coredata/vendor_compliance_report.go +++ b/pkg/coredata/vendor_compliance_report.go @@ -35,6 +35,8 @@ type ( ReportName string FileKey string FileSize int + SnapshotID *gid.GID + SourceID *gid.GID CreatedAt time.Time UpdatedAt time.Time } @@ -69,6 +71,8 @@ SELECT report_name, file_key, file_size, + snapshot_id, + source_id, created_at, updated_at FROM @@ -115,6 +119,8 @@ SELECT report_name, file_key, file_size, + snapshot_id, + source_id, created_at, updated_at FROM @@ -206,6 +212,7 @@ FROM WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -216,3 +223,67 @@ WHERE _, err := conn.Exec(ctx, q, args) 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 +} diff --git a/pkg/coredata/vendor_contact.go b/pkg/coredata/vendor_contact.go index 7914d3909..d00f318cb 100644 --- a/pkg/coredata/vendor_contact.go +++ b/pkg/coredata/vendor_contact.go @@ -29,14 +29,16 @@ import ( type ( VendorContact struct { - ID gid.GID `db:"id"` - VendorID gid.GID `db:"vendor_id"` - FullName *string `db:"full_name"` - Email *string `db:"email"` - Phone *string `db:"phone"` - Role *string `db:"role"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + VendorID gid.GID `db:"vendor_id"` + FullName *string `db:"full_name"` + Email *string `db:"email"` + Phone *string `db:"phone"` + Role *string `db:"role"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } VendorContacts []*VendorContact @@ -77,6 +79,8 @@ SELECT email, phone, role, + snapshot_id, + source_id, created_at, updated_at FROM @@ -127,6 +131,8 @@ SELECT email, phone, role, + snapshot_id, + source_id, created_at, updated_at FROM @@ -228,6 +234,7 @@ SET WHERE %s AND id = @vendor_contact_id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -261,6 +268,7 @@ DELETE FROM WHERE %s AND id = @vendor_contact_id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -275,3 +283,65 @@ WHERE 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 +} diff --git a/pkg/coredata/vendor_data_privacy_agreement.go b/pkg/coredata/vendor_data_privacy_agreement.go index 3a2b6d1d2..3736a1ed1 100644 --- a/pkg/coredata/vendor_data_privacy_agreement.go +++ b/pkg/coredata/vendor_data_privacy_agreement.go @@ -28,15 +28,16 @@ import ( type ( VendorDataPrivacyAgreement struct { - ID gid.GID `db:"id"` - TenantID gid.TenantID `db:"tenant_id"` - OrganizationID gid.GID `db:"organization_id"` - VendorID gid.GID `db:"vendor_id"` - ValidFrom *time.Time `db:"valid_from"` - ValidUntil *time.Time `db:"valid_until"` - FileID gid.GID `db:"file_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + VendorID gid.GID `db:"vendor_id"` + ValidFrom *time.Time `db:"valid_from"` + ValidUntil *time.Time `db:"valid_until"` + FileID gid.GID `db:"file_id"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } VendorDataPrivacyAgreements []*VendorDataPrivacyAgreement @@ -62,12 +63,13 @@ func (vdpa *VendorDataPrivacyAgreement) LoadByVendorID( q := ` SELECT id, - tenant_id, organization_id, vendor_id, valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at FROM @@ -107,12 +109,13 @@ func (vdpa *VendorDataPrivacyAgreement) LoadByID( q := ` SELECT id, - tenant_id, organization_id, vendor_id, valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at FROM @@ -159,6 +162,7 @@ SET WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -195,6 +199,8 @@ INSERT INTO valid_from, valid_until, file_id, + snapshot_id, + source_id, created_at, updated_at ) @@ -206,6 +212,8 @@ VALUES ( @valid_from, @valid_until, @file_id, + @snapshot_id, + @source_id, @created_at, @updated_at ) @@ -214,6 +222,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET valid_from = EXCLUDED.valid_from, valid_until = EXCLUDED.valid_until, file_id = EXCLUDED.file_id, + snapshot_id = EXCLUDED.snapshot_id, + source_id = EXCLUDED.source_id, updated_at = EXCLUDED.updated_at ` args := pgx.StrictNamedArgs{ @@ -224,6 +234,8 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET "valid_from": vdpa.ValidFrom, "valid_until": vdpa.ValidUntil, "file_id": vdpa.FileID, + "snapshot_id": vdpa.SnapshotID, + "source_id": vdpa.SourceID, "created_at": vdpa.CreatedAt, "updated_at": vdpa.UpdatedAt, } @@ -244,6 +256,7 @@ FROM WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -278,3 +291,65 @@ WHERE _, err := conn.Exec(ctx, q, args) 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 +} diff --git a/pkg/coredata/vendor_risk_assessment.go b/pkg/coredata/vendor_risk_assessment.go index 5e9b79a97..f1226248e 100644 --- a/pkg/coredata/vendor_risk_assessment.go +++ b/pkg/coredata/vendor_risk_assessment.go @@ -39,6 +39,8 @@ type ( DataSensitivity DataSensitivity `db:"data_sensitivity"` BusinessImpact BusinessImpact `db:"business_impact"` Notes *string `db:"notes"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -137,6 +139,8 @@ SELECT data_sensitivity, business_impact, notes, + snapshot_id, + source_id, created_at, updated_at FROM @@ -187,6 +191,8 @@ SELECT data_sensitivity, business_impact, notes, + snapshot_id, + source_id, created_at, updated_at FROM @@ -240,6 +246,8 @@ SELECT data_sensitivity, business_impact, notes, + snapshot_id, + source_id, created_at, updated_at FROM @@ -270,3 +278,73 @@ WHERE 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 +} diff --git a/pkg/coredata/vendor_service.go b/pkg/coredata/vendor_service.go index b95646cf7..4ec3f6741 100644 --- a/pkg/coredata/vendor_service.go +++ b/pkg/coredata/vendor_service.go @@ -33,6 +33,8 @@ type ( VendorID gid.GID `db:"vendor_id"` Name string `db:"name"` Description *string `db:"description"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -71,6 +73,8 @@ SELECT vendor_id, name, description, + snapshot_id, + source_id, created_at, updated_at FROM @@ -119,6 +123,8 @@ SELECT vendor_id, name, description, + snapshot_id, + source_id, created_at, updated_at FROM @@ -212,6 +218,7 @@ SET WHERE %s AND id = @vendor_service_id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -243,6 +250,7 @@ DELETE FROM WHERE %s AND id = @vendor_service_id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -257,3 +265,61 @@ WHERE 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 +} diff --git a/pkg/probo/vendor_business_associate_agreement_service.go b/pkg/probo/vendor_business_associate_agreement_service.go index dc33a18be..450bbb50e 100644 --- a/pkg/probo/vendor_business_associate_agreement_service.go +++ b/pkg/probo/vendor_business_associate_agreement_service.go @@ -302,16 +302,10 @@ func (s VendorBusinessAssociateAgreementService) Delete( 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 { 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 }, ) @@ -329,16 +323,10 @@ func (s VendorBusinessAssociateAgreementService) DeleteByVendorID( 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 { 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 }, ) diff --git a/pkg/probo/vendor_data_privacy_agreement_service.go b/pkg/probo/vendor_data_privacy_agreement_service.go index c67b03bd7..c525b93b2 100644 --- a/pkg/probo/vendor_data_privacy_agreement_service.go +++ b/pkg/probo/vendor_data_privacy_agreement_service.go @@ -139,7 +139,6 @@ func (s VendorDataPrivacyAgreementService) Upload( vendorDataPrivacyAgreement = &coredata.VendorDataPrivacyAgreement{ ID: vendorDataPrivacyAgreementID, - TenantID: s.svc.scope.GetTenantID(), OrganizationID: vendor.OrganizationID, VendorID: vendorID, ValidFrom: req.ValidFrom, @@ -303,16 +302,10 @@ func (s VendorDataPrivacyAgreementService) Delete( 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 { 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 }, ) @@ -330,16 +323,10 @@ func (s VendorDataPrivacyAgreementService) DeleteByVendorID( 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 { 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 }, ) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index f826bd8d4..172cb496b 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1134,6 +1134,10 @@ input AssetFilter { snapshotId: ID } +input VendorFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -1196,6 +1200,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: VendorOrder + filter: VendorFilter = { snapshotId: null } ): VendorConnection! @goField(forceResolver: true) peoples( @@ -1351,6 +1356,7 @@ type People implements Node { type Vendor implements Node { id: ID! + snapshotId: ID name: String! category: VendorCategory! description: String diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index d21096190..a27a4dcb8 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -897,7 +897,7 @@ type ComplexityRoot struct { TrustCenter 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 - 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 { @@ -1300,6 +1300,7 @@ type ComplexityRoot struct { ServiceLevelAgreementURL func(childComplexity int) int Services func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorServiceOrderBy) int ShowOnTrustCenter func(childComplexity int) int + SnapshotID func(childComplexity int) int StatusPageURL func(childComplexity int) int SubprocessorsListURL 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) 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) - 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) 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) @@ -5634,7 +5635,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin 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": 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 + case "Vendor.snapshotId": + if e.complexity.Vendor.SnapshotID == nil { + break + } + + return e.complexity.Vendor.SnapshotID(childComplexity), true + case "Vendor.statusPageUrl": if e.complexity.Vendor.StatusPageURL == nil { break @@ -7850,6 +7858,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputUserOrder, ec.unmarshalInputVendorComplianceReportOrder, ec.unmarshalInputVendorContactOrder, + ec.unmarshalInputVendorFilter, ec.unmarshalInputVendorOrder, ec.unmarshalInputVendorRiskAssessmentOrder, ec.unmarshalInputVendorServiceOrder, @@ -9086,6 +9095,10 @@ input AssetFilter { snapshotId: ID } +input VendorFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -9148,6 +9161,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: VendorOrder + filter: VendorFilter = { snapshotId: null } ): VendorConnection! @goField(forceResolver: true) peoples( @@ -9303,6 +9317,7 @@ type People implements Node { type Vendor implements Node { id: ID! + snapshotId: ID name: String! category: VendorCategory! description: String @@ -17711,6 +17726,11 @@ func (ec *executionContext) field_Organization_vendors_args(ctx context.Context, return nil, err } args["orderBy"] = arg4 + arg5, err := ec.field_Organization_vendors_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } func (ec *executionContext) field_Organization_vendors_argsFirst( @@ -17778,6 +17798,19 @@ func (ec *executionContext) field_Organization_vendors_argsOrderBy( 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) { var err error args := map[string]any{} @@ -19123,6 +19156,8 @@ func (ec *executionContext) fieldContext_AssessVendorPayload_vendor(_ context.Co switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) 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) { 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 { ec.Error(ctx, err) @@ -51442,6 +51477,8 @@ func (ec *executionContext) fieldContext_UpdateVendorPayload_vendor(_ context.Co switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -52457,6 +52494,47 @@ func (ec *executionContext) fieldContext_Vendor_id(_ context.Context, field grap 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) { fc, err := ec.fieldContext_Vendor_name(ctx, field) if err != nil { @@ -53925,6 +54003,8 @@ func (ec *executionContext) fieldContext_VendorBusinessAssociateAgreement_vendor switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -54375,6 +54455,8 @@ func (ec *executionContext) fieldContext_VendorComplianceReport_vendor(_ context switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -55188,6 +55270,8 @@ func (ec *executionContext) fieldContext_VendorContact_vendor(_ context.Context, switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -55798,6 +55882,8 @@ func (ec *executionContext) fieldContext_VendorDataPrivacyAgreement_vendor(_ con switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -56248,6 +56334,8 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -56396,6 +56484,8 @@ func (ec *executionContext) fieldContext_VendorRiskAssessment_vendor(_ context.C switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -57129,6 +57219,8 @@ func (ec *executionContext) fieldContext_VendorService_vendor(_ context.Context, switch field.Name { case "id": return ec.fieldContext_Vendor_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Vendor_snapshotId(ctx, field) case "name": return ec.fieldContext_Vendor_name(ctx, field) case "category": @@ -66011,6 +66103,33 @@ func (ec *executionContext) unmarshalInputVendorContactOrder(ctx context.Context 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) { var it types.VendorOrderBy 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 { atomic.AddUint32(&out.Invalids, 1) } + case "snapshotId": + out.Values[i] = ec._Vendor_snapshotId(ctx, field, obj) case "name": out.Values[i] = ec._Vendor_name(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -88481,6 +88602,14 @@ func (ec *executionContext) marshalOVendorDataPrivacyAgreement2ᚖgithubᚗcom 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) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index cdfb01083..8b8209104 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1881,6 +1881,7 @@ type UserEdge struct { type Vendor struct { ID gid.GID `json:"id"` + SnapshotID *gid.GID `json:"snapshotId,omitempty"` Name string `json:"name"` Category coredata.VendorCategory `json:"category"` Description *string `json:"description,omitempty"` @@ -1998,6 +1999,10 @@ type VendorEdge struct { Node *Vendor `json:"node"` } +type VendorFilter struct { + SnapshotID *gid.GID `json:"snapshotId,omitempty"` +} + type VendorRiskAssessment struct { ID gid.GID `json:"id"` Vendor *Vendor `json:"vendor"` diff --git a/pkg/server/api/console/v1/types/vendor.go b/pkg/server/api/console/v1/types/vendor.go index de1ca6c8e..606656179 100644 --- a/pkg/server/api/console/v1/types/vendor.go +++ b/pkg/server/api/console/v1/types/vendor.go @@ -80,6 +80,7 @@ func NewVendor(v *coredata.Vendor) *Vendor { WebsiteURL: v.WebsiteURL, Category: v.Category, ShowOnTrustCenter: v.ShowOnTrustCenter, + SnapshotID: v.SnapshotID, UpdatedAt: v.UpdatedAt, CreatedAt: v.CreatedAt, } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 9671ce9cb..9da1569a3 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -3459,7 +3459,7 @@ func (r *organizationResolver) Controls(ctx context.Context, obj *types.Organiza } // 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()) 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) - 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) if err != nil {