diff --git a/apps/console/src/hooks/graph/AssetGraph.ts b/apps/console/src/hooks/graph/AssetGraph.ts index 3ceb7a1f5..0b66b6e38 100644 --- a/apps/console/src/hooks/graph/AssetGraph.ts +++ b/apps/console/src/hooks/graph/AssetGraph.ts @@ -5,10 +5,10 @@ import { useTranslate } from "@probo/i18n"; import { promisifyMutation, sprintf } from "@probo/helpers"; export const assetsQuery = graphql` - query AssetGraphListQuery($organizationId: ID!) { + query AssetGraphListQuery($organizationId: ID!, $snapshotId: ID) { node(id: $organizationId) { ... on Organization { - ...AssetsPageFragment + ...AssetsPageFragment @arguments(snapshotId: $snapshotId) } } } @@ -19,6 +19,7 @@ export const assetNodeQuery = graphql` node(id: $assetId) { ... on Asset { id + snapshotId name amount criticity @@ -54,6 +55,7 @@ export const createAssetMutation = graphql` assetEdge @prependEdge(connections: $connections) { node { id + snapshotId name amount criticity @@ -63,7 +65,7 @@ export const createAssetMutation = graphql` id fullName } - vendors(first: 10) { + vendors(first: 50) { edges { node { id @@ -84,6 +86,7 @@ export const updateAssetMutation = graphql` updateAsset(input: $input) { asset { id + snapshotId name amount criticity diff --git a/apps/console/src/hooks/graph/__generated__/AssetGraphCreateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/AssetGraphCreateMutation.graphql.ts index 575dc5d2f..9a22cc929 100644 --- a/apps/console/src/hooks/graph/__generated__/AssetGraphCreateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/AssetGraphCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8ddee1e0195c3af2818a78e1224ad297>> + * @generated SignedSource<<9f6605e9adebeee2a681da0278642501>> * @lightSyntaxTransform * @nogrep */ @@ -40,6 +40,7 @@ export type AssetGraphCreateMutation$data = { readonly fullName: string; readonly id: string; }; + readonly snapshotId: string | null | undefined; readonly vendors: { readonly edges: ReadonlyArray<{ readonly node: { @@ -107,6 +108,13 @@ v5 = { "plural": false, "selections": [ (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, (v4/*: any*/), { "alias": null, @@ -161,7 +169,7 @@ v5 = { { "kind": "Literal", "name": "first", - "value": 10 + "value": 50 } ], "concreteType": "VendorConnection", @@ -201,7 +209,7 @@ v5 = { "storageKey": null } ], - "storageKey": "vendors(first:10)" + "storageKey": "vendors(first:50)" }, { "alias": null, @@ -282,16 +290,16 @@ return { ] }, "params": { - "cacheID": "f1d2e4e85879e08931d9d6815cc51ec1", + "cacheID": "f11f5052ed6dd175bd701812c0cd136b", "id": null, "metadata": {}, "name": "AssetGraphCreateMutation", "operationKind": "mutation", - "text": "mutation AssetGraphCreateMutation(\n $input: CreateAssetInput!\n) {\n createAsset(input: $input) {\n assetEdge {\n node {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 10) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n }\n }\n }\n}\n" + "text": "mutation AssetGraphCreateMutation(\n $input: CreateAssetInput!\n) {\n createAsset(input: $input) {\n assetEdge {\n node {\n id\n snapshotId\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "5053ed385668e9f51d916a194ea8936d"; +(node as any).hash = "3cec119022d8dffb8536d93294e2633b"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts index 85e6ac34f..748e10d65 100644 --- a/apps/console/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8758668524dc47bc04757126d7f423b3>> + * @generated SignedSource<<8d257a13d23e2dd68542540dd12ca35b>> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type AssetGraphListQuery$variables = { organizationId: string; + snapshotId?: string | null | undefined; }; export type AssetGraphListQuery$data = { readonly node: { @@ -29,6 +30,11 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "organizationId" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], v1 = [ @@ -38,28 +44,40 @@ v1 = [ "variableName": "organizationId" } ], -v2 = { +v2 = [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } +], +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v3 = { +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v4 = [ +v5 = [ + { + "fields": (v2/*: any*/), + "kind": "ObjectValue", + "name": "filter" + }, { "kind": "Literal", "name": "first", "value": 10 } ], -v5 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", @@ -85,7 +103,7 @@ return { "kind": "InlineFragment", "selections": [ { - "args": null, + "args": (v2/*: any*/), "kind": "FragmentSpread", "name": "AssetsPageFragment" } @@ -114,14 +132,14 @@ return { "name": "node", "plural": false, "selections": [ - (v2/*: any*/), (v3/*: any*/), + (v4/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": "AssetConnection", "kind": "LinkedField", "name": "assets", @@ -143,8 +161,15 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, + (v6/*: any*/), { "alias": null, "args": null, @@ -181,7 +206,7 @@ return { "name": "owner", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -222,8 +247,8 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/), + (v4/*: any*/), + (v6/*: any*/), { "alias": null, "args": null, @@ -247,7 +272,7 @@ return { "name": "createdAt", "storageKey": null }, - (v2/*: any*/) + (v3/*: any*/) ], "storageKey": null }, @@ -313,13 +338,13 @@ return { ] } ], - "storageKey": "assets(first:10)" + "storageKey": null }, { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "filters": [ - "orderBy" + "filter" ], "handle": "connection", "key": "AssetsPage_assets", @@ -336,16 +361,16 @@ return { ] }, "params": { - "cacheID": "7e293e732d5ff8246a1ed772e1c33984", + "cacheID": "29eec1243d3cceb5d9931562635e1f24", "id": null, "metadata": {}, "name": "AssetGraphListQuery", "operationKind": "query", - "text": "query AssetGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...AssetsPageFragment\n }\n id\n }\n}\n\nfragment AssetsPageFragment on Organization {\n assets(first: 10) {\n edges {\n node {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query AssetGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...AssetsPageFragment_3iomuz\n }\n id\n }\n}\n\nfragment AssetsPageFragment_3iomuz on Organization {\n assets(first: 10, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\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 = "88644d5fcb5f2e017df4b5d6373ce27f"; +(node as any).hash = "4cbb1234b0884823285d3660334a2b29"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/AssetGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/AssetGraphNodeQuery.graphql.ts index 098ccf515..e08fec1de 100644 --- a/apps/console/src/hooks/graph/__generated__/AssetGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/AssetGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -28,6 +28,7 @@ export type AssetGraphNodeQuery$data = { readonly fullName: string; readonly id: string; }; + readonly snapshotId?: string | null | undefined; readonly updatedAt?: any; readonly vendors?: { readonly edges: ReadonlyArray<{ @@ -72,38 +73,45 @@ v3 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "snapshotId", "storageKey": null }, v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "amount", + "name": "name", "storageKey": null }, v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "criticity", + "name": "amount", "storageKey": null }, v6 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "assetType", + "name": "criticity", "storageKey": null }, v7 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "dataTypesStored", + "name": "assetType", "storageKey": null }, v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataTypesStored", + "storageKey": null +}, +v9 = { "alias": null, "args": null, "concreteType": "People", @@ -122,7 +130,7 @@ v8 = { ], "storageKey": null }, -v9 = { +v10 = { "alias": null, "args": [ { @@ -153,7 +161,7 @@ v9 = { "plural": false, "selections": [ (v2/*: any*/), - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -177,14 +185,14 @@ v9 = { ], "storageKey": "vendors(first:50)" }, -v10 = { +v11 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v11 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", @@ -218,7 +226,8 @@ return { (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), - (v11/*: any*/) + (v11/*: any*/), + (v12/*: any*/) ], "type": "Asset", "abstractKey": null @@ -263,7 +272,8 @@ return { (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), - (v11/*: any*/) + (v11/*: any*/), + (v12/*: any*/) ], "type": "Asset", "abstractKey": null @@ -274,16 +284,16 @@ return { ] }, "params": { - "cacheID": "cd1d7c48ccf01acee749af3b8948fb9a", + "cacheID": "6c6e10d25c9bd116703346393adb7a3f", "id": null, "metadata": {}, "name": "AssetGraphNodeQuery", "operationKind": "query", - "text": "query AssetGraphNodeQuery(\n $assetId: ID!\n) {\n node(id: $assetId) {\n __typename\n ... on Asset {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n category\n }\n }\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" + "text": "query AssetGraphNodeQuery(\n $assetId: ID!\n) {\n node(id: $assetId) {\n __typename\n ... on Asset {\n id\n snapshotId\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n category\n }\n }\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "47e84c9ea1c9ab9310ad493219c12f3f"; +(node as any).hash = "60053f70c9a65d4658be7b830a3dae72"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/AssetGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/AssetGraphUpdateMutation.graphql.ts index e0a577a98..8f301a08a 100644 --- a/apps/console/src/hooks/graph/__generated__/AssetGraphUpdateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/AssetGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<6bc692e9d9ab5b78611dc2a326227761>> * @lightSyntaxTransform * @nogrep */ @@ -37,6 +37,7 @@ export type AssetGraphUpdateMutation$data = { readonly fullName: string; readonly id: string; }; + readonly snapshotId: string | null | undefined; readonly updatedAt: any; readonly vendors: { readonly edges: ReadonlyArray<{ @@ -101,6 +102,13 @@ v3 = [ "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, (v2/*: any*/), { "alias": null, @@ -229,16 +237,16 @@ return { "selections": (v3/*: any*/) }, "params": { - "cacheID": "f5e4ccc38f842f4965c32676ae407012", + "cacheID": "1447ea6f965bbd6fcdba1ebc72e2d23f", "id": null, "metadata": {}, "name": "AssetGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation AssetGraphUpdateMutation(\n $input: UpdateAssetInput!\n) {\n updateAsset(input: $input) {\n asset {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n updatedAt\n }\n }\n}\n" + "text": "mutation AssetGraphUpdateMutation(\n $input: UpdateAssetInput!\n) {\n updateAsset(input: $input) {\n asset {\n id\n snapshotId\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n updatedAt\n }\n }\n}\n" } }; })(); -(node as any).hash = "257fa8f1a9996808403fa65a06b61ba2"; +(node as any).hash = "21b0c94b6245eb849e6118a58fb97490"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/ProcessingActivityRegistryGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/ProcessingActivityRegistryGraphNodeQuery.graphql.ts index 84871b775..3993bf393 100644 --- a/apps/console/src/hooks/graph/__generated__/ProcessingActivityRegistryGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/ProcessingActivityRegistryGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<4453e7e28a9c546de765219f287f129a>> + * @generated SignedSource<<676223ac3bde5a9edf7e3d68fe3ed981>> * @lightSyntaxTransform * @nogrep */ @@ -38,7 +38,6 @@ export type ProcessingActivityRegistryGraphNodeQuery$data = { readonly retentionPeriod?: string | null | undefined; readonly securityMeasures?: string | null | undefined; readonly snapshotId?: string | null | undefined; - readonly sourceId?: string | null | undefined; readonly specialOrCriminalData?: ProcessingActivityRegistrySpecialOrCriminalData; readonly transferImpactAssessment?: ProcessingActivityRegistryTransferImpactAssessment; readonly transferSafeguards?: ProcessingActivityRegistryTransferSafeguards | null | undefined; @@ -83,115 +82,108 @@ v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "sourceId", + "name": "name", "storageKey": null }, v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "purpose", "storageKey": null }, v6 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "purpose", + "name": "dataSubjectCategory", "storageKey": null }, v7 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "dataSubjectCategory", + "name": "personalDataCategory", "storageKey": null }, v8 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "personalDataCategory", + "name": "specialOrCriminalData", "storageKey": null }, v9 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "specialOrCriminalData", + "name": "consentEvidenceLink", "storageKey": null }, v10 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "consentEvidenceLink", + "name": "lawfulBasis", "storageKey": null }, v11 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "lawfulBasis", + "name": "recipients", "storageKey": null }, v12 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "recipients", + "name": "location", "storageKey": null }, v13 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "location", + "name": "internationalTransfers", "storageKey": null }, v14 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "internationalTransfers", + "name": "transferSafeguards", "storageKey": null }, v15 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "transferSafeguards", + "name": "retentionPeriod", "storageKey": null }, v16 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "retentionPeriod", + "name": "securityMeasures", "storageKey": null }, v17 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "securityMeasures", + "name": "dataProtectionImpactAssessment", "storageKey": null }, v18 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "dataProtectionImpactAssessment", - "storageKey": null -}, -v19 = { "alias": null, "args": null, "kind": "ScalarField", "name": "transferImpactAssessment", "storageKey": null }, -v20 = { +v19 = { "alias": null, "args": null, "concreteType": "Organization", @@ -200,18 +192,18 @@ v20 = { "plural": false, "selections": [ (v2/*: any*/), - (v5/*: any*/) + (v4/*: any*/) ], "storageKey": null }, -v21 = { +v20 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v22 = { +v21 = { "alias": null, "args": null, "kind": "ScalarField", @@ -255,8 +247,7 @@ return { (v18/*: any*/), (v19/*: any*/), (v20/*: any*/), - (v21/*: any*/), - (v22/*: any*/) + (v21/*: any*/) ], "type": "ProcessingActivityRegistry", "abstractKey": null @@ -311,8 +302,7 @@ return { (v18/*: any*/), (v19/*: any*/), (v20/*: any*/), - (v21/*: any*/), - (v22/*: any*/) + (v21/*: any*/) ], "type": "ProcessingActivityRegistry", "abstractKey": null @@ -323,16 +313,16 @@ return { ] }, "params": { - "cacheID": "b8a3edd61ea5d94cc3594f40f181d27f", + "cacheID": "10b9608f3b93e930c05f415a0ac9fbec", "id": null, "metadata": {}, "name": "ProcessingActivityRegistryGraphNodeQuery", "operationKind": "query", - "text": "query ProcessingActivityRegistryGraphNodeQuery(\n $processingActivityRegistryId: ID!\n) {\n node(id: $processingActivityRegistryId) {\n __typename\n ... on ProcessingActivityRegistry {\n id\n snapshotId\n sourceId\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" + "text": "query ProcessingActivityRegistryGraphNodeQuery(\n $processingActivityRegistryId: ID!\n) {\n node(id: $processingActivityRegistryId) {\n __typename\n ... on ProcessingActivityRegistry {\n id\n snapshotId\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "6714c1965f54321530f73a5b7ef86fa6"; +(node as any).hash = "c1728cff79a3773498b383bb32728cf6"; export default node; diff --git a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx index 644ba3768..191a59956 100644 --- a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx @@ -20,12 +20,14 @@ import { } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useParams } from "react-router"; import { ControlledField } from "/components/form/ControlledField"; import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectField"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import z from "zod"; -import { getAssetTypeVariant, getCriticityVariant } from "@probo/helpers"; +import { getAssetTypeVariant, getCriticityVariant, validateSnapshotConsistency } from "@probo/helpers"; +import { SnapshotBanner } from "/components/SnapshotBanner"; const updateAssetSchema = z.object({ name: z.string().min(1, "Name is required"), @@ -46,10 +48,21 @@ export default function AssetDetailsPage(props: Props) { const assetEntry = asset.node; const { __ } = useTranslate(); const organizationId = useOrganizationId(); - const deleteAsset = useDeleteAsset( - assetEntry, - ConnectionHandler.getConnectionID(organizationId, "AssetsPage_assets") + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); + + if (!assetEntry) { + return
{__("Asset not found")}
; + } + + validateSnapshotConsistency(assetEntry, snapshotId); + + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + "AssetsPage_assets", + { filter: { snapshotId: snapshotId || null } } ); + const deleteAsset = useDeleteAsset(assetEntry, connectionId); const vendors = assetEntry?.vendors?.edges.map((edge: any) => edge.node) ?? []; const vendorIds = vendors.map((vendor: any) => vendor.id); @@ -80,13 +93,18 @@ export default function AssetDetailsPage(props: Props) { } }); + const breadcrumbAssetsUrl = isSnapshotMode && snapshotId + ? `/organizations/${organizationId}/snapshots/${snapshotId}/assets` + : `/organizations/${organizationId}/assets`; + return (
+ {snapshotId && }
- - - {__("Delete")} - - + {!isSnapshotMode && ( + + + {__("Delete")} + + + )}
@@ -120,12 +140,14 @@ export default function AssetDetailsPage(props: Props) { label={__("Name")} {...register("name")} type="text" + disabled={isSnapshotMode} /> @@ -144,6 +167,7 @@ export default function AssetDetailsPage(props: Props) { name="assetType" type="select" label={__("Asset Type")} + disabled={isSnapshotMode} > @@ -153,6 +177,7 @@ export default function AssetDetailsPage(props: Props) { label={__("Data Types Stored")} {...register("dataTypesStored")} type="text" + disabled={isSnapshotMode} />
- {formState.isDirty && ( + {formState.isDirty && !isSnapshotMode && ( diff --git a/apps/console/src/pages/organizations/assets/AssetsPage.tsx b/apps/console/src/pages/organizations/assets/AssetsPage.tsx index 040c91342..ee8211da4 100644 --- a/apps/console/src/pages/organizations/assets/AssetsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetsPage.tsx @@ -22,6 +22,7 @@ import { type PreloadedQuery, } from "react-relay"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useParams } from "react-router"; import { CreateAssetDialog } from "./dialogs/CreateAssetDialog"; import { useDeleteAsset, assetsQuery } from "../../../hooks/graph/AssetGraph"; import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql"; @@ -33,6 +34,7 @@ import type { AssetsPageFragment$key, } from "./__generated__/AssetsPageFragment.graphql"; import { SortableTable } from "/components/SortableTable"; +import { SnapshotBanner } from "/components/SnapshotBanner"; const paginatedAssetsFragment = graphql` fragment AssetsPageFragment on Organization @@ -43,6 +45,7 @@ const paginatedAssetsFragment = graphql` after: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null } last: { type: "Int", defaultValue: null } + snapshotId: { type: "ID", defaultValue: null } ) { assets( first: $first @@ -50,11 +53,13 @@ const paginatedAssetsFragment = graphql` last: $last before: $before orderBy: $orderBy - ) @connection(key: "AssetsPage_assets") { + filter: { snapshotId: $snapshotId } + ) @connection(key: "AssetsPage_assets", filters: ["filter"]) { __id edges { node { id + snapshotId name amount criticity @@ -89,6 +94,8 @@ type Props = { export default function AssetsPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const data = usePreloadedQuery(assetsQuery, props.queryRef); const pagination = usePaginationFragment( @@ -102,18 +109,21 @@ export default function AssetsPage(props: Props) { return (
+ {snapshotId && } - - - + {!isSnapshotMode && ( + + + + )} @@ -150,11 +160,17 @@ function AssetRow({ }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const deleteAsset = useDeleteAsset(entry, connectionId); const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? []; + const assetUrl = isSnapshotMode && snapshotId + ? `/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}` + : `/organizations/${organizationId}/assets/${entry.id}`; + return ( - + {entry.name} @@ -196,15 +212,17 @@ function AssetRow({ )} - - - {__("Delete")} - - + {!isSnapshotMode && ( + + + {__("Delete")} + + + )} ); diff --git a/apps/console/src/pages/organizations/assets/__generated__/AssetsListQuery.graphql.ts b/apps/console/src/pages/organizations/assets/__generated__/AssetsListQuery.graphql.ts index c017075ac..8ddf2d40c 100644 --- a/apps/console/src/pages/organizations/assets/__generated__/AssetsListQuery.graphql.ts +++ b/apps/console/src/pages/organizations/assets/__generated__/AssetsListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<305c2e15c19e7ead66752260c29d8794>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -23,6 +23,7 @@ export type AssetsListQuery$variables = { id: string; last?: number | null | undefined; orderBy?: AssetOrder | null | undefined; + snapshotId?: string | null | undefined; }; export type AssetsListQuery$data = { readonly node: { @@ -65,55 +66,77 @@ v5 = { "kind": "LocalArgument", "name": "orderBy" }, -v6 = [ +v6 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" +}, +v7 = [ { "kind": "Variable", "name": "id", "variableName": "id" } ], -v7 = [ - { - "kind": "Variable", - "name": "after", - "variableName": "after" - }, - { - "kind": "Variable", - "name": "before", - "variableName": "before" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "first" - }, - { - "kind": "Variable", - "name": "last", - "variableName": "last" - }, - { - "kind": "Variable", - "name": "orderBy", - "variableName": "orderBy" - } -], v8 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v9 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v10 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v11 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v12 = { + "kind": "Variable", + "name": "orderBy", + "variableName": "orderBy" +}, +v13 = { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" +}, +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v9 = { +v15 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v10 = { +v16 = [ + (v8/*: any*/), + (v9/*: any*/), + { + "fields": [ + (v13/*: any*/) + ], + "kind": "ObjectValue", + "name": "filter" + }, + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/) +], +v17 = { "alias": null, "args": null, "kind": "ScalarField", @@ -128,7 +151,8 @@ return { (v2/*: any*/), (v3/*: any*/), (v4/*: any*/), - (v5/*: any*/) + (v5/*: any*/), + (v6/*: any*/) ], "kind": "Fragment", "metadata": null, @@ -136,14 +160,21 @@ return { "selections": [ { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ { - "args": (v7/*: any*/), + "args": [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + (v13/*: any*/) + ], "kind": "FragmentSpread", "name": "AssetsPageFragment" } @@ -162,6 +193,7 @@ return { (v2/*: any*/), (v4/*: any*/), (v5/*: any*/), + (v6/*: any*/), (v3/*: any*/) ], "kind": "Operation", @@ -169,20 +201,20 @@ return { "selections": [ { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v8/*: any*/), - (v9/*: any*/), + (v14/*: any*/), + (v15/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v7/*: any*/), + "args": (v16/*: any*/), "concreteType": "AssetConnection", "kind": "LinkedField", "name": "assets", @@ -204,8 +236,15 @@ return { "name": "node", "plural": false, "selections": [ - (v9/*: any*/), - (v10/*: any*/), + (v15/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, + (v17/*: any*/), { "alias": null, "args": null, @@ -242,7 +281,7 @@ return { "name": "owner", "plural": false, "selections": [ - (v9/*: any*/), + (v15/*: any*/), { "alias": null, "args": null, @@ -283,8 +322,8 @@ return { "name": "node", "plural": false, "selections": [ - (v9/*: any*/), - (v10/*: any*/), + (v15/*: any*/), + (v17/*: any*/), { "alias": null, "args": null, @@ -308,7 +347,7 @@ return { "name": "createdAt", "storageKey": null }, - (v8/*: any*/) + (v14/*: any*/) ], "storageKey": null }, @@ -378,9 +417,9 @@ return { }, { "alias": null, - "args": (v7/*: any*/), + "args": (v16/*: any*/), "filters": [ - "orderBy" + "filter" ], "handle": "connection", "key": "AssetsPage_assets", @@ -397,16 +436,16 @@ return { ] }, "params": { - "cacheID": "d712227ae7c1c1a4151005092f7eeaf9", + "cacheID": "2eab5fe4d402f48ac4dcd8220284ea41", "id": null, "metadata": {}, "name": "AssetsListQuery", "operationKind": "query", - "text": "query AssetsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $orderBy: AssetOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...AssetsPageFragment_sdb03\n id\n }\n}\n\nfragment AssetsPageFragment_sdb03 on Organization {\n assets(first: $first, after: $after, last: $last, before: $before, orderBy: $orderBy) {\n edges {\n node {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query AssetsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $orderBy: AssetOrder = null\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...AssetsPageFragment_38J9tm\n id\n }\n}\n\nfragment AssetsPageFragment_38J9tm on Organization {\n assets(first: $first, after: $after, last: $last, before: $before, orderBy: $orderBy, filter: {snapshotId: $snapshotId}) {\n edges {\n node {\n id\n snapshotId\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\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 = "e25e58aa9cf789614d280313c6059d5e"; +(node as any).hash = "4bc1af55d208f3c773c49b753a76aa5c"; export default node; diff --git a/apps/console/src/pages/organizations/assets/__generated__/AssetsPageFragment.graphql.ts b/apps/console/src/pages/organizations/assets/__generated__/AssetsPageFragment.graphql.ts index 80ed68700..33aa98ae2 100644 --- a/apps/console/src/pages/organizations/assets/__generated__/AssetsPageFragment.graphql.ts +++ b/apps/console/src/pages/organizations/assets/__generated__/AssetsPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8bacf647a39e3c33616be7e9159928df>> + * @generated SignedSource<<663b14d82ccaeebe2dda8dc256fddfc5>> * @lightSyntaxTransform * @nogrep */ @@ -28,6 +28,7 @@ export type AssetsPageFragment$data = { readonly fullName: string; readonly id: string; }; + readonly snapshotId: string | null | undefined; readonly vendors: { readonly edges: ReadonlyArray<{ readonly node: { @@ -94,6 +95,11 @@ return { "defaultValue": null, "kind": "LocalArgument", "name": "orderBy" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], "kind": "Fragment", @@ -134,9 +140,15 @@ return { "alias": "assets", "args": [ { - "kind": "Variable", - "name": "orderBy", - "variableName": "orderBy" + "fields": [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } + ], + "kind": "ObjectValue", + "name": "filter" } ], "concreteType": "AssetConnection", @@ -161,6 +173,13 @@ return { "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "snapshotId", + "storageKey": null + }, (v2/*: any*/), { "alias": null, @@ -345,6 +364,6 @@ return { }; })(); -(node as any).hash = "e25e58aa9cf789614d280313c6059d5e"; +(node as any).hash = "4bc1af55d208f3c773c49b753a76aa5c"; export default node; diff --git a/apps/console/src/routes/assetRoutes.ts b/apps/console/src/routes/assetRoutes.ts index 70a24b121..a26323fcd 100644 --- a/apps/console/src/routes/assetRoutes.ts +++ b/apps/console/src/routes/assetRoutes.ts @@ -3,13 +3,29 @@ import { relayEnvironment } from "/providers/RelayProviders"; import { PageSkeleton } from "/components/skeletons/PageSkeleton"; import { lazy } from "@probo/react-lazy"; import { assetsQuery, assetNodeQuery } from "../hooks/graph/AssetGraph"; +import type { AppRoute } from "/routes"; export const assetRoutes = [ { path: "assets", fallback: PageSkeleton, queryLoader: (params: Record) => - loadQuery(relayEnvironment, assetsQuery, { organizationId: params.organizationId }), + loadQuery(relayEnvironment, assetsQuery, { + organizationId: params.organizationId, + snapshotId: null + }), + Component: lazy( + () => import("/pages/organizations/assets/AssetsPage") + ), + }, + { + path: "snapshots/:snapshotId/assets", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, assetsQuery, { + organizationId: params.organizationId, + snapshotId: params.snapshotId + }), Component: lazy( () => import("/pages/organizations/assets/AssetsPage") ), @@ -23,4 +39,13 @@ export const assetRoutes = [ () => import("/pages/organizations/assets/AssetDetailsPage") ), }, -]; + { + path: "snapshots/:snapshotId/assets/:assetId", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, assetNodeQuery, { assetId: params.assetId }), + Component: lazy( + () => import("/pages/organizations/assets/AssetDetailsPage") + ), + }, +] satisfies AppRoute[]; diff --git a/packages/helpers/src/snapshots.ts b/packages/helpers/src/snapshots.ts index 8571c0a05..22a807630 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 = [ + "ASSETS", "DATA", "NONCONFORMITY_REGISTRIES", "COMPLIANCE_REGISTRIES", @@ -37,6 +38,8 @@ export function getSnapshotTypeLabel(__: Translator, type: string | null | undef export function getSnapshotTypeUrlPath(type?: string): string { switch (type) { + case "ASSETS": + return "/assets"; case "DATA": return "/data"; case "NONCONFORMITY_REGISTRIES": diff --git a/pkg/coredata/asset.go b/pkg/coredata/asset.go index 25d20130e..2c75f496f 100644 --- a/pkg/coredata/asset.go +++ b/pkg/coredata/asset.go @@ -29,6 +29,8 @@ import ( type ( Asset struct { ID gid.GID `db:"id"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` Name string `db:"name"` Amount int `db:"amount"` OwnerID gid.GID `db:"owner_id"` @@ -65,6 +67,8 @@ func (a *Asset) LoadByID( q := ` SELECT id, + snapshot_id, + source_id, name, organization_id, owner_id, @@ -110,6 +114,8 @@ func (a *Asset) LoadByOwnerID( q := ` SELECT id, + snapshot_id, + source_id, name, organization_id, owner_id, @@ -152,6 +158,7 @@ func (a *Assets) CountByOrganizationID( conn pg.Conn, scope Scoper, organizationID gid.GID, + filter *AssetFilter, ) (int, error) { q := ` SELECT @@ -161,12 +168,14 @@ FROM WHERE %s AND organization_id = @organization_id + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) @@ -184,10 +193,13 @@ func (a *Assets) LoadByOrganizationID( scope Scoper, organizationID gid.GID, cursor *page.Cursor[AssetOrderField], + filter *AssetFilter, ) error { q := ` SELECT id, + snapshot_id, + source_id, name, organization_id, owner_id, @@ -203,12 +215,14 @@ WHERE %s AND organization_id = @organization_id AND %s + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) @@ -299,8 +313,11 @@ SET WHERE %s AND id = @id + AND snapshot_id IS NULL RETURNING id, + snapshot_id, + source_id, name, organization_id, owner_id, @@ -351,6 +368,7 @@ DELETE FROM assets WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -365,3 +383,79 @@ WHERE return nil } + +func (assets Assets) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error { + snapshotters := []AssetSnapshotter{Assets{}, Vendors{}, AssetVendors{}} + + for _, snapshotter := range snapshotters { + if err := snapshotter.InsertAssetSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil { + return fmt.Errorf("cannot create asset snapshots: (%T) %w", snapshotter, err) + } + } + + return nil +} + +func (assets Assets) InsertAssetSnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { + query := ` +WITH + source_assets AS ( + SELECT * + FROM assets + WHERE %s AND organization_id = @organization_id AND snapshot_id IS NULL + ) +INSERT INTO assets ( + tenant_id, + id, + snapshot_id, + source_id, + name, + organization_id, + owner_id, + amount, + criticity, + asset_type, + data_types_stored, + created_at, + updated_at +) +SELECT + @tenant_id, + generate_gid(decode_base64_unpadded(@tenant_id), @asset_entity_type), + @snapshot_id, + a.id, + a.name, + a.organization_id, + a.owner_id, + a.amount, + a.criticity, + a.asset_type, + a.data_types_stored, + a.created_at, + a.updated_at +FROM source_assets a + ` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "snapshot_id": snapshotID, + "organization_id": organizationID, + "asset_entity_type": AssetEntityType, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot insert asset snapshots: %w", err) + } + + return nil +} diff --git a/pkg/coredata/asset_filter.go b/pkg/coredata/asset_filter.go new file mode 100644 index 000000000..a06ab0975 --- /dev/null +++ b/pkg/coredata/asset_filter.go @@ -0,0 +1,54 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" +) + +type ( + AssetFilter struct { + snapshotID **gid.GID + } +) + +func NewAssetFilter(snapshotID **gid.GID) *AssetFilter { + return &AssetFilter{ + snapshotID: snapshotID, + } +} + +func (f *AssetFilter) SQLArguments() pgx.NamedArgs { + args := pgx.NamedArgs{} + + if f.snapshotID != nil && *f.snapshotID != nil { + args["filter_snapshot_id"] = **f.snapshotID + } + + return args +} + +func (f *AssetFilter) SQLFragment() string { + if f.snapshotID == nil { + return "TRUE" + } + + if *f.snapshotID == nil { + return "snapshot_id IS NULL" + } else { + return "snapshot_id = @filter_snapshot_id" + } +} diff --git a/pkg/coredata/asset_vendor.go b/pkg/coredata/asset_vendor.go index b9424211e..e0e0884cf 100644 --- a/pkg/coredata/asset_vendor.go +++ b/pkg/coredata/asset_vendor.go @@ -17,6 +17,7 @@ package coredata import ( "context" "fmt" + "maps" "time" "github.com/getprobo/probo/pkg/gid" @@ -26,13 +27,18 @@ import ( type ( AssetVendor struct { - AssetID gid.GID `db:"asset_id"` - VendorID gid.GID `db:"vendor_id"` - TenantID gid.TenantID `db:"tenant_id"` - CreatedAt time.Time `db:"created_at"` + AssetID gid.GID `db:"asset_id"` + VendorID gid.GID `db:"vendor_id"` + TenantID gid.TenantID `db:"tenant_id"` + SnapshotID *gid.GID `db:"snapshot_id"` + CreatedAt time.Time `db:"created_at"` } AssetVendors []*AssetVendor + + AssetSnapshotter interface { + InsertAssetSnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error + } ) func (av AssetVendors) Merge( @@ -112,3 +118,61 @@ FROM vendor_ids return nil } + +func (av AssetVendors) InsertAssetSnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { + query := ` +WITH + source_assets AS ( + SELECT id + FROM assets + WHERE organization_id = @organization_id AND snapshot_id IS NULL + ), + snapshot_assets AS ( + SELECT id, source_id + FROM assets + WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id + ), + snapshot_vendors AS ( + SELECT id, source_id + FROM vendors + WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id + ), + source_asset_vendors AS ( + SELECT asset_id, vendor_id, snapshot_id, created_at + FROM asset_vendors + WHERE %s AND asset_id = ANY(SELECT id FROM source_assets) AND snapshot_id IS NULL + ) +INSERT INTO asset_vendors (tenant_id, asset_id, vendor_id, snapshot_id, created_at) +SELECT + @tenant_id, + sa.id, + sv.id, + @snapshot_id, + av.created_at +FROM source_asset_vendors av +JOIN snapshot_assets sa ON sa.source_id = av.asset_id +JOIN snapshot_vendors sv ON sv.source_id = av.vendor_id +` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "snapshot_id": snapshotID, + "organization_id": organizationID, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot insert asset vendor snapshots: %w", err) + } + + return nil +} diff --git a/pkg/coredata/migrations/20250829T155558Z.sql b/pkg/coredata/migrations/20250829T155558Z.sql new file mode 100644 index 000000000..e8ee2226b --- /dev/null +++ b/pkg/coredata/migrations/20250829T155558Z.sql @@ -0,0 +1,19 @@ +ALTER TABLE assets ADD COLUMN snapshot_id TEXT; +ALTER TABLE assets ADD COLUMN source_id TEXT; + +ALTER TABLE assets ADD CONSTRAINT assets_snapshot_id_fkey + FOREIGN KEY (snapshot_id) + REFERENCES snapshots(id) + ON UPDATE CASCADE + ON DELETE CASCADE; + +ALTER TABLE assets ADD CONSTRAINT assets_source_id_snapshot_id_key + UNIQUE (source_id, snapshot_id); + +ALTER TABLE asset_vendors ADD COLUMN snapshot_id TEXT; + +ALTER TABLE asset_vendors ADD CONSTRAINT asset_vendors_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 ce98b3273..7e299eedf 100644 --- a/pkg/coredata/snapshottable.go +++ b/pkg/coredata/snapshottable.go @@ -28,6 +28,8 @@ type Snapshottable interface { func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) { switch snapshotType { + case SnapshotsTypeAssets: + return Assets{}, nil case SnapshotsTypeData: return Data{}, nil case SnapshotsTypeNonConformityRegistries: diff --git a/pkg/coredata/vendor.go b/pkg/coredata/vendor.go index d6620aee0..c925e97f6 100644 --- a/pkg/coredata/vendor.go +++ b/pkg/coredata/vendor.go @@ -814,3 +814,103 @@ FROM source_vendors v return nil } + +func (vs Vendors) InsertAssetSnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { + query := ` +WITH + source_assets AS ( + SELECT id + FROM assets + WHERE organization_id = @organization_id AND snapshot_id IS NULL + ), + source_asset_vendors AS ( + SELECT asset_id, vendor_id, snapshot_id, created_at + FROM asset_vendors + WHERE asset_id = ANY(SELECT id FROM source_assets) + ), + source_vendors AS ( + SELECT * + FROM vendors + WHERE %s AND id = ANY(SELECT vendor_id FROM source_asset_vendors) + ) +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 source_vendors v + ` + + 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 for assets: %w", err) + } + + return nil +} diff --git a/pkg/probo/asset_service.go b/pkg/probo/asset_service.go index 9fcb33e35..7dff5fe75 100644 --- a/pkg/probo/asset_service.go +++ b/pkg/probo/asset_service.go @@ -80,6 +80,7 @@ func (s AssetService) GetByOwnerID( func (s AssetService) CountForOrganizationID( ctx context.Context, organizationID gid.GID, + filter *coredata.AssetFilter, ) (int, error) { var count int @@ -87,7 +88,7 @@ func (s AssetService) CountForOrganizationID( ctx, func(conn pg.Conn) (err error) { assets := coredata.Assets{} - count, err = assets.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + count, err = assets.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count assets: %w", err) } @@ -107,6 +108,7 @@ func (s AssetService) ListForOrganizationID( ctx context.Context, organizationID gid.GID, cursor *page.Cursor[coredata.AssetOrderField], + filter *coredata.AssetFilter, ) (*page.Page[*coredata.Asset, coredata.AssetOrderField], error) { var assets coredata.Assets @@ -119,6 +121,7 @@ func (s AssetService) ListForOrganizationID( s.svc.scope, organizationID, cursor, + filter, ) }, ) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 13ad4bee2..98a756a2e 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1130,6 +1130,10 @@ input ProcessingActivityRegistryFilter { snapshotId: ID } +input AssetFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -1244,6 +1248,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: AssetOrder + filter: AssetFilter ): AssetConnection! @goField(forceResolver: true) data( @@ -3639,6 +3644,7 @@ type AssessVendorPayload { type Asset implements Node { id: ID! + snapshotId: ID name: String! amount: Int! owner: People! @goField(forceResolver: true) diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 0956bc3fc..f1c46cce2 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -112,6 +112,7 @@ type ComplexityRoot struct { Name func(childComplexity int) int Organization func(childComplexity int) int Owner func(childComplexity int) int + SnapshotID func(childComplexity int) int UpdatedAt func(childComplexity int) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) int } @@ -873,7 +874,7 @@ type ComplexityRoot struct { } Organization struct { - Assets func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy) int + Assets func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy, filter *types.AssetFilter) int Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditOrderBy) int ComplianceRegistries func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ComplianceRegistryOrderBy, filter *types.ComplianceRegistryFilter) int Connectors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) int @@ -1671,7 +1672,7 @@ type OrganizationResolver interface { 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) Risks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) (*types.RiskConnection, error) Tasks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error) - Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy) (*types.AssetConnection, error) + Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy, filter *types.AssetFilter) (*types.AssetConnection, error) Data(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DatumOrderBy, filter *types.DatumFilter) (*types.DatumConnection, error) Audits(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditOrderBy) (*types.AuditConnection, error) NonconformityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy, filter *types.NonconformityRegistryFilter) (*types.NonconformityRegistryConnection, error) @@ -1864,6 +1865,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Asset.Owner(childComplexity), true + case "Asset.snapshotId": + if e.complexity.Asset.SnapshotID == nil { + break + } + + return e.complexity.Asset.SnapshotID(childComplexity), true + case "Asset.updatedAt": if e.complexity.Asset.UpdatedAt == nil { break @@ -5380,7 +5388,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Organization.Assets(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.AssetOrderBy)), true + return e.complexity.Organization.Assets(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.AssetOrderBy), args["filter"].(*types.AssetFilter)), true case "Organization.audits": if e.complexity.Organization.Audits == nil { @@ -7697,6 +7705,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputAssessVendorInput, + ec.unmarshalInputAssetFilter, ec.unmarshalInputAssetOrder, ec.unmarshalInputAssignTaskInput, ec.unmarshalInputAuditOrder, @@ -9073,6 +9082,10 @@ input ProcessingActivityRegistryFilter { snapshotId: ID } +input AssetFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -9187,6 +9200,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: AssetOrder + filter: AssetFilter ): AssetConnection! @goField(forceResolver: true) data( @@ -11582,6 +11596,7 @@ type AssessVendorPayload { type Asset implements Node { id: ID! + snapshotId: ID name: String! amount: Int! owner: People! @goField(forceResolver: true) @@ -15883,6 +15898,11 @@ func (ec *executionContext) field_Organization_assets_args(ctx context.Context, return nil, err } args["orderBy"] = arg4 + arg5, err := ec.field_Organization_assets_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } func (ec *executionContext) field_Organization_assets_argsFirst( @@ -15950,6 +15970,19 @@ func (ec *executionContext) field_Organization_assets_argsOrderBy( return zeroVal, nil } +func (ec *executionContext) field_Organization_assets_argsFilter( + ctx context.Context, + rawArgs map[string]any, +) (*types.AssetFilter, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + if tmp, ok := rawArgs["filter"]; ok { + return ec.unmarshalOAssetFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssetFilter(ctx, tmp) + } + + var zeroVal *types.AssetFilter + return zeroVal, nil +} + func (ec *executionContext) field_Organization_audits_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -19197,6 +19230,47 @@ func (ec *executionContext) fieldContext_Asset_id(_ context.Context, field graph return fc, nil } +func (ec *executionContext) _Asset_snapshotId(ctx context.Context, field graphql.CollectedField, obj *types.Asset) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Asset_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_Asset_snapshotId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Asset", + 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) _Asset_name(ctx context.Context, field graphql.CollectedField, obj *types.Asset) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Asset_name(ctx, field) if err != nil { @@ -19961,6 +20035,8 @@ func (ec *executionContext) fieldContext_AssetEdge_node(_ context.Context, field switch field.Name { case "id": return ec.fieldContext_Asset_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Asset_snapshotId(ctx, field) case "name": return ec.fieldContext_Asset_name(ctx, field) case "amount": @@ -41999,7 +42075,7 @@ func (ec *executionContext) _Organization_assets(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Organization().Assets(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.AssetOrderBy)) + return ec.resolvers.Organization().Assets(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.AssetOrderBy), fc.Args["filter"].(*types.AssetFilter)) }) if err != nil { ec.Error(ctx, err) @@ -49882,6 +49958,8 @@ func (ec *executionContext) fieldContext_UpdateAssetPayload_asset(_ context.Cont switch field.Name { case "id": return ec.fieldContext_Asset_id(ctx, field) + case "snapshotId": + return ec.fieldContext_Asset_snapshotId(ctx, field) case "name": return ec.fieldContext_Asset_name(ctx, field) case "amount": @@ -59641,6 +59719,33 @@ func (ec *executionContext) unmarshalInputAssessVendorInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputAssetFilter(ctx context.Context, obj any) (types.AssetFilter, error) { + var it types.AssetFilter + 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) unmarshalInputAssetOrder(ctx context.Context, obj any) (types.AssetOrderBy, error) { var it types.AssetOrderBy asMap := map[string]any{} @@ -66297,6 +66402,8 @@ func (ec *executionContext) _Asset(ctx context.Context, sel ast.SelectionSet, ob if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "snapshotId": + out.Values[i] = ec._Asset_snapshotId(ctx, field, obj) case "name": out.Values[i] = ec._Asset_name(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -87081,6 +87188,14 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) unmarshalOAssetFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssetFilter(ctx context.Context, v any) (*types.AssetFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputAssetFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOAssetOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssetOrderBy(ctx context.Context, v any) (*types.AssetOrderBy, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/asset.go b/pkg/server/api/console/v1/types/asset.go index dba3295d1..32e35d197 100644 --- a/pkg/server/api/console/v1/types/asset.go +++ b/pkg/server/api/console/v1/types/asset.go @@ -16,6 +16,7 @@ type ( Resolver any ParentID gid.GID + Filter *AssetFilter } ) @@ -23,6 +24,7 @@ func NewAssetConnection( p *page.Page[*coredata.Asset, coredata.AssetOrderField], resolver any, parentID gid.GID, + filter *AssetFilter, ) *AssetConnection { edges := make([]*AssetEdge, len(p.Data)) for i, asset := range p.Data { @@ -35,12 +37,14 @@ func NewAssetConnection( Resolver: resolver, ParentID: parentID, + Filter: filter, } } func NewAsset(asset *coredata.Asset) *Asset { return &Asset{ ID: asset.ID, + SnapshotID: asset.SnapshotID, Name: asset.Name, Amount: asset.Amount, Criticity: asset.Criticity, diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index c8af3e47c..cdfb01083 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -27,6 +27,7 @@ type AssessVendorPayload struct { type Asset struct { ID gid.GID `json:"id"` + SnapshotID *gid.GID `json:"snapshotId,omitempty"` Name string `json:"name"` Amount int `json:"amount"` Owner *People `json:"owner"` @@ -47,6 +48,10 @@ type AssetEdge struct { Node *Asset `json:"node"` } +type AssetFilter struct { + SnapshotID *gid.GID `json:"snapshotId,omitempty"` +} + type AssignTaskInput struct { TaskID gid.GID `json:"taskId"` AssignedToID gid.GID `json:"assignedToId"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index cb623811d..9671ce9cb 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -98,7 +98,12 @@ func (r *assetConnectionResolver) TotalCount(ctx context.Context, obj *types.Ass switch obj.Resolver.(type) { case *organizationResolver: - count, err := prb.Assets.CountForOrganizationID(ctx, obj.ParentID) + assetFilter := coredata.NewAssetFilter(nil) + if obj.Filter != nil { + assetFilter = coredata.NewAssetFilter(&obj.Filter.SnapshotID) + } + + count, err := prb.Assets.CountForOrganizationID(ctx, obj.ParentID, assetFilter) if err != nil { return 0, fmt.Errorf("cannot count assets: %w", err) } @@ -3626,7 +3631,7 @@ func (r *organizationResolver) Tasks(ctx context.Context, obj *types.Organizatio } // Assets is the resolver for the assets field. -func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy) (*types.AssetConnection, error) { +func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy, filter *types.AssetFilter) (*types.AssetConnection, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.AssetOrderField]{ @@ -3642,12 +3647,17 @@ func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organizati cursor := types.NewCursor(first, after, last, before, pageOrderBy) - page, err := prb.Assets.ListForOrganizationID(ctx, obj.ID, cursor) + assetFilter := coredata.NewAssetFilter(nil) + if filter != nil { + assetFilter = coredata.NewAssetFilter(&filter.SnapshotID) + } + + page, err := prb.Assets.ListForOrganizationID(ctx, obj.ID, cursor, assetFilter) if err != nil { panic(fmt.Errorf("cannot list organization assets: %w", err)) } - return types.NewAssetConnection(page, r, obj.ID), nil + return types.NewAssetConnection(page, r, obj.ID, filter), nil } // Assets is the resolver for the assets field. diff --git a/pkg/trust/vendor_service.go b/pkg/trust/vendor_service.go index d4274c2ff..5569fe9d5 100644 --- a/pkg/trust/vendor_service.go +++ b/pkg/trust/vendor_service.go @@ -64,7 +64,9 @@ func (s VendorService) ListForOrganizationId( ctx, func(conn pg.Conn) error { showOnTrustCenter := true - filter := coredata.NewVendorFilter(nil, &showOnTrustCenter) + var nilSnapshotID *gid.GID = nil + filter := coredata.NewVendorFilter(&nilSnapshotID, &showOnTrustCenter) + err := vendors.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) if err != nil { return fmt.Errorf("cannot load vendors: %w", err)