From 553901e4ad8734d51e656fbe3f468042a2b49488 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 29 Apr 2026 18:11:19 +0200 Subject: [PATCH] Add risk publish to document system Replace the old snapshot-based system for risks with the publish document system, mirroring the prior vendor / processing activity / DPIA / TIA migration. Includes the GraphQL mutation, MCP tool, CLI command, n8n operation, frontend publish dialog, e2e tests, and a prosemirror register template covering name, description, category, treatment, owner, inherent and residual scoring, and notes. The risk register lives as a generated DocumentTypeRegister document on the organization, reused across publishes (the major version bumps on every republish). Approvers can be passed in to create a draft pending approval; otherwise the version is published immediately. The frontend Risks page exposes a Publish button and a Document link button when the document exists, and pre-fills the previous default approvers. Risks was the last remaining snapshot type, so this commit also removes the entire snapshot system: drop snapshotId from the Risk GraphQL type and RiskFilter; remove RiskSnapshotter, Risks.Snapshot, InsertRiskSnapshots, and the SnapshotID/SourceID fields on Risk; delete Snapshot, ControlSnapshot, SnapshotsType, SnapshotOrderField, Snapshottable, the SnapshotService, the Snapshot console resolvers and GraphQL schema, the Snapshot MCP types and operations (list/get/take/listControlSnapshots), the snapshot CLI (prb snapshot), the snapshot frontend pages, routes, banner, LinkedSnapshotsCard, SnapshotGraph, snapshot helpers, and the snapshot n8n resource and control link/unlink snapshot operations. The snapshot_id columns remain in the database but are now filtered out with snapshot_id IS NULL. Add Get/Upsert/Clear GeneratedDocumentID methods on Risk backed by a new risks_document_id column on generated_documents, matching the ProcessingActivity/Finding/Vendor pattern. The migration command migrate-risk-snapshots-to-documents uses raw SQL queries instead of the Go snapshot types, since those are gone. Signed-off-by: Sacha Al Himdani --- .../console/src/components/SnapshotBanner.tsx | 92 --- .../components/form/SnapshotTypeOptions.tsx | 31 - .../snapshots/LinkedSnapshotsCard.tsx | 232 ------ .../snapshots/LinkedSnapshotsDialog.tsx | 212 ----- .../console/src/hooks/graph/FrameworkGraph.ts | 16 - apps/console/src/hooks/graph/RiskGraph.ts | 19 +- apps/console/src/hooks/graph/SnapshotGraph.ts | 148 ---- .../iam/organizations/_components/Sidebar.tsx | 9 - .../frameworks/FrameworkControlPage.tsx | 61 -- .../measures/tabs/MeasureEvidencesTab.tsx | 57 +- .../organizations/risks/RiskDetailPage.tsx | 115 +-- .../pages/organizations/risks/RisksPage.tsx | 82 +- .../risks/dialogs/PublishRiskListDialog.tsx | 160 ++++ .../snapshots/SnapshotDetailPage.tsx | 57 -- .../organizations/snapshots/SnapshotsPage.tsx | 196 ----- .../snapshots/dialog/SnapshotFormDialog.tsx | 153 ---- .../vendors/tabs/VendorCertificationsTab.tsx | 10 +- .../vendors/tabs/VendorComplianceTab.tsx | 62 +- .../vendors/tabs/VendorContactsTab.tsx | 14 +- .../vendors/tabs/VendorOverviewTab.tsx | 38 +- .../vendors/tabs/VendorRiskAssessmentTab.tsx | 8 +- .../vendors/tabs/VendorServicesTab.tsx | 14 +- apps/console/src/routes.tsx | 2 - apps/console/src/routes/riskRoutes.ts | 43 - apps/console/src/routes/snapshotsRoutes.ts | 54 -- .../main.go | 4 +- .../main.go | 4 +- .../main.go | 4 +- .../main.go | 4 +- .../main.go | 525 ++++++++++++ e2e/console/mapping_test.go | 138 ---- e2e/console/risk_publish_test.go | 336 ++++++++ e2e/console/snapshot_test.go | 261 ------ e2e/mcp/snapshot_test.go | 69 -- migrate-risk-snapshots-to-documents | Bin 0 -> 33578866 bytes packages/helpers/src/index.ts | 6 - packages/helpers/src/snapshots.ts | 62 -- packages/n8n-node/nodes/Probo/Probo.node.ts | 5 - .../nodes/Probo/actions/control/index.ts | 18 - .../control/unlinkSnapshot.operation.ts | 71 -- .../n8n-node/nodes/Probo/actions/index.ts | 2 - .../nodes/Probo/actions/risk/index.ts | 9 + .../publish.operation.ts} | 62 +- .../actions/snapshot/create.operation.ts | 146 ---- .../actions/snapshot/delete.operation.ts | 55 -- .../Probo/actions/snapshot/get.operation.ts | 65 -- .../actions/snapshot/getAll.operation.ts | 114 --- .../nodes/Probo/actions/snapshot/index.ts | 66 -- pkg/cmd/risk/publish/publish.go | 148 ++++ pkg/cmd/risk/risk.go | 2 + pkg/cmd/root/root.go | 2 - pkg/cmd/snapshot/create/create.go | 175 ---- pkg/cmd/snapshot/delete/delete.go | 103 --- pkg/cmd/snapshot/list/list.go | 193 ----- pkg/cmd/snapshot/snapshot.go | 38 - pkg/cmd/snapshot/view/view.go | 133 --- pkg/coredata/audit_log_resource_type.go | 2 - pkg/coredata/control.go | 71 -- pkg/coredata/control_snapshot.go | 100 --- pkg/coredata/entity_type_reg.go | 4 +- pkg/coredata/migrations/20260429T150423Z.sql | 90 +++ pkg/coredata/risk.go | 263 +++--- pkg/coredata/risk_filter.go | 33 +- pkg/coredata/snapshot.go | 316 -------- pkg/coredata/snapshot_filter.go | 65 -- pkg/coredata/snapshot_order_field.go | 51 -- pkg/coredata/snapshots_type.go | 80 -- pkg/coredata/snapshottable.go | 36 - pkg/docgen/generator.go | 29 + pkg/probo/actions.go | 9 +- pkg/probo/control_service.go | 106 --- pkg/probo/generated_document_service.go | 763 +++++++++--------- pkg/probo/policies.go | 2 - pkg/probo/service.go | 2 - pkg/probo/snapshot_service.go | 221 ----- pkg/probo/templates/risk_list.json.tmpl | 178 ++++ pkg/server/api/console/v1/base_resolvers.go | 9 - .../api/console/v1/control_resolvers.go | 71 -- .../api/console/v1/graphql/control.graphql | 34 - .../console/v1/graphql/organization.graphql | 10 +- .../api/console/v1/graphql/risk.graphql | 15 +- .../api/console/v1/graphql/snapshot.graphql | 83 -- .../api/console/v1/measure_resolvers.go | 4 +- .../api/console/v1/organization_resolvers.go | 41 +- pkg/server/api/console/v1/risk_resolvers.go | 23 + .../api/console/v1/snapshot_resolvers.go | 165 ---- pkg/server/api/console/v1/types/risk.go | 1 - pkg/server/api/console/v1/types/snapshot.go | 73 -- pkg/server/api/mcp/v1/schema.resolvers.go | 121 +-- pkg/server/api/mcp/v1/specification.yaml | 281 +------ pkg/server/api/mcp/v1/types/risk.go | 1 - pkg/server/api/mcp/v1/types/snapshot.go | 66 -- .../v1/types/statement_of_applicability.go | 1 - 93 files changed, 2384 insertions(+), 5741 deletions(-) delete mode 100644 apps/console/src/components/SnapshotBanner.tsx delete mode 100644 apps/console/src/components/form/SnapshotTypeOptions.tsx delete mode 100644 apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx delete mode 100644 apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx delete mode 100644 apps/console/src/hooks/graph/SnapshotGraph.ts create mode 100644 apps/console/src/pages/organizations/risks/dialogs/PublishRiskListDialog.tsx delete mode 100644 apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx delete mode 100644 apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx delete mode 100644 apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx delete mode 100644 apps/console/src/routes/snapshotsRoutes.ts create mode 100644 cmd/migrate-risk-snapshots-to-documents/main.go create mode 100644 e2e/console/risk_publish_test.go delete mode 100644 e2e/console/snapshot_test.go delete mode 100644 e2e/mcp/snapshot_test.go create mode 100755 migrate-risk-snapshots-to-documents delete mode 100644 packages/helpers/src/snapshots.ts delete mode 100644 packages/n8n-node/nodes/Probo/actions/control/unlinkSnapshot.operation.ts rename packages/n8n-node/nodes/Probo/actions/{control/linkSnapshot.operation.ts => risk/publish.operation.ts} (56%) delete mode 100644 packages/n8n-node/nodes/Probo/actions/snapshot/create.operation.ts delete mode 100644 packages/n8n-node/nodes/Probo/actions/snapshot/delete.operation.ts delete mode 100644 packages/n8n-node/nodes/Probo/actions/snapshot/get.operation.ts delete mode 100644 packages/n8n-node/nodes/Probo/actions/snapshot/getAll.operation.ts delete mode 100644 packages/n8n-node/nodes/Probo/actions/snapshot/index.ts create mode 100644 pkg/cmd/risk/publish/publish.go delete mode 100644 pkg/cmd/snapshot/create/create.go delete mode 100644 pkg/cmd/snapshot/delete/delete.go delete mode 100644 pkg/cmd/snapshot/list/list.go delete mode 100644 pkg/cmd/snapshot/snapshot.go delete mode 100644 pkg/cmd/snapshot/view/view.go delete mode 100644 pkg/coredata/control_snapshot.go create mode 100644 pkg/coredata/migrations/20260429T150423Z.sql delete mode 100644 pkg/coredata/snapshot.go delete mode 100644 pkg/coredata/snapshot_filter.go delete mode 100644 pkg/coredata/snapshot_order_field.go delete mode 100644 pkg/coredata/snapshots_type.go delete mode 100644 pkg/coredata/snapshottable.go delete mode 100644 pkg/probo/snapshot_service.go create mode 100644 pkg/probo/templates/risk_list.json.tmpl delete mode 100644 pkg/server/api/console/v1/graphql/snapshot.graphql delete mode 100644 pkg/server/api/console/v1/snapshot_resolvers.go delete mode 100644 pkg/server/api/console/v1/types/snapshot.go delete mode 100644 pkg/server/api/mcp/v1/types/snapshot.go diff --git a/apps/console/src/components/SnapshotBanner.tsx b/apps/console/src/components/SnapshotBanner.tsx deleted file mode 100644 index 05850914c..000000000 --- a/apps/console/src/components/SnapshotBanner.tsx +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { - formatDate, - getSnapshotTypeLabel, - getSnapshotTypeUrlPath, - sprintf, -} from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { IconClock } from "@probo/ui"; -import { graphql, useLazyLoadQuery } from "react-relay"; -import { useLocation } from "react-router"; - -import type { SnapshotBannerQuery } from "#/__generated__/core/SnapshotBannerQuery.graphql"; - -const snapshotQuery = graphql` - query SnapshotBannerQuery($snapshotId: ID!) { - node(id: $snapshotId) { - ... on Snapshot { - # eslint-disable-next-line relay/unused-fields - id - name - type - createdAt - } - } - } -`; - -const isSnapshotTypeValidForUrl = (type: string, pathname: string) => { - const urlPath = getSnapshotTypeUrlPath(type); - return pathname.includes(urlPath); -}; - -type Props = { - snapshotId: string; -}; - -export function SnapshotBanner({ snapshotId }: Props) { - const { __ } = useTranslate(); - const location = useLocation(); - - const data = useLazyLoadQuery(snapshotQuery, { - snapshotId, - }); - const snapshot = data.node; - - if (!snapshot) { - return null; - } - - if ( - snapshot.type - && !isSnapshotTypeValidForUrl(snapshot.type, location.pathname) - ) { - throw new Error("PAGE_NOT_FOUND"); - } - - return ( -
- -
-
- - {__("Snapshot")} - {" "} - {snapshot.name} - -
-

- {sprintf( - __("You are viewing a %s snapshot from %s"), - getSnapshotTypeLabel(__, snapshot.type).toLocaleLowerCase(), - formatDate(snapshot.createdAt), - )} -

-
-
- ); -} diff --git a/apps/console/src/components/form/SnapshotTypeOptions.tsx b/apps/console/src/components/form/SnapshotTypeOptions.tsx deleted file mode 100644 index d371281bb..000000000 --- a/apps/console/src/components/form/SnapshotTypeOptions.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { getSnapshotTypeLabel, snapshotTypes } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { Option } from "@probo/ui"; - -export function SnapshotTypeOptions() { - const { __ } = useTranslate(); - - return ( - <> - {snapshotTypes.map(type => ( - - ))} - - ); -} diff --git a/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx b/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx deleted file mode 100644 index 841147aa7..000000000 --- a/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { - formatDate, - getSnapshotTypeLabel, - getSnapshotTypeUrlPath, - sprintf, -} from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { - Badge, - Button, - Card, - IconChevronDown, - IconPlusLarge, - IconTrashCan, - Table, - Tbody, - Td, - Th, - Thead, - Tr, - TrButton, -} from "@probo/ui"; -import { clsx } from "clsx"; -import { useMemo, useState } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import type { LinkedSnapshotsCardFragment$key } from "#/__generated__/core/LinkedSnapshotsCardFragment.graphql"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; - -import { LinkedSnapshotsDialog } from "./LinkedSnapshotsDialog"; - -const linkedSnapshotFragment = graphql` - fragment LinkedSnapshotsCardFragment on Snapshot { - id - name - description - type - createdAt - } -`; - -type Mutation = (p: { - variables: { - input: { - snapshotId: string; - } & Params; - connections: string[]; - }; -}) => void; - -type Props = { - snapshots: (LinkedSnapshotsCardFragment$key & { id: string })[]; - params: Params; - disabled?: boolean; - connectionId: string; - onAttach: Mutation; - onDetach: Mutation; - variant?: "card" | "table"; - readOnly?: boolean; -}; - -export function LinkedSnapshotsCard(props: Props) { - const { __ } = useTranslate(); - const [limit, setLimit] = useState(4); - const snapshots = useMemo(() => { - return limit ? props.snapshots.slice(0, limit) : props.snapshots; - }, [props.snapshots, limit]); - const showMoreButton = limit !== null && props.snapshots.length > limit; - const variant = props.variant ?? "table"; - - const onAttach = (snapshotId: string) => { - props.onAttach({ - variables: { - input: { - snapshotId, - ...props.params, - }, - connections: [props.connectionId], - }, - }); - }; - - const onDetach = (snapshotId: string) => { - props.onDetach({ - variables: { - input: { - snapshotId, - ...props.params, - }, - connections: [props.connectionId], - }, - }); - }; - - const Wrapper = variant === "card" ? Card : "div"; - - const colSpanTable = props.readOnly ? 4 : 5; - const colSpanCard = props.readOnly ? 3 : 4; - - return ( - - {variant === "card" && ( -
-
{__("Snapshots")}
- {!props.readOnly && ( - - - - )} -
- )} - - - - - - {variant === "table" && } - - {!props.readOnly && } - - - - {snapshots.length === 0 && ( - - - - )} - {snapshots.map(snapshot => ( - - ))} - {variant === "table" && !props.readOnly && ( - - - {__("Link snapshot")} - - - )} - -
{__("Name")}{__("Type")}{__("Description")}{__("Created")}
- {__("No snapshots linked")} -
- {showMoreButton && ( - - )} -
- ); -} - -function SnapshotRow(props: { - snapshot: LinkedSnapshotsCardFragment$key & { id: string }; - onClick: (snapshotId: string) => void; - variant: "card" | "table"; - readOnly?: boolean; -}) { - const snapshot = useFragment(linkedSnapshotFragment, props.snapshot); - const organizationId = useOrganizationId(); - const { __ } = useTranslate(); - - const urlPath = getSnapshotTypeUrlPath(snapshot.type); - const snapshotUrl = `/organizations/${organizationId}/snapshots/${snapshot.id}${urlPath}`; - - return ( - - {snapshot.name} - - - {getSnapshotTypeLabel(__, snapshot.type)} - - - {props.variant === "table" && ( - - {snapshot.description || __("No description")} - - )} - {formatDate(snapshot.createdAt)} - {!props.readOnly && ( - - - - )} - - ); -} diff --git a/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx b/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx deleted file mode 100644 index 52a77243e..000000000 --- a/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { formatDate, getSnapshotTypeLabel } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { - Badge, - Button, - Dialog, - DialogContent, - DialogFooter, - IconMagnifyingGlass, - IconPlusLarge, - IconTrashCan, - InfiniteScrollTrigger, - Input, - Spinner, -} from "@probo/ui"; -import { type ReactNode, Suspense, useMemo, useState } from "react"; -import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import type { - LinkedSnapshotsDialogFragment$data, - LinkedSnapshotsDialogFragment$key, -} from "#/__generated__/core/LinkedSnapshotsDialogFragment.graphql"; -import type { LinkedSnapshotsDialogQuery } from "#/__generated__/core/LinkedSnapshotsDialogQuery.graphql"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; -import type { NodeOf } from "#/types"; - -const snapshotsQuery = graphql` - query LinkedSnapshotsDialogQuery($organizationId: ID!) { - organization: node(id: $organizationId) { - id - ... on Organization { - ...LinkedSnapshotsDialogFragment - } - } - } -`; - -const snapshotsFragment = graphql` - fragment LinkedSnapshotsDialogFragment on Organization - @refetchable(queryName: "LinkedSnapshotsDialogQuery_fragment") - @argumentDefinitions( - first: { type: "Int", defaultValue: 20 } - order: { type: "SnapshotOrder", defaultValue: null } - after: { type: "CursorKey", defaultValue: null } - before: { type: "CursorKey", defaultValue: null } - last: { type: "Int", defaultValue: null } - ) { - snapshots( - first: $first - after: $after - last: $last - before: $before - orderBy: $order - ) @connection(key: "LinkedSnapshotsDialogQuery_snapshots") { - edges { - node { - id - name - description - type - createdAt - } - } - } - } -`; - -type Props = { - children: ReactNode; - disabled?: boolean; - linkedSnapshots?: { id: string }[]; - onLink: (snapshotId: string) => void; - onUnlink: (snapshotId: string) => void; -}; - -export function LinkedSnapshotsDialog({ children, ...props }: Props) { - const { __ } = useTranslate(); - - return ( - - - }> - - - - - - ); -} - -function LinkedSnapshotsDialogContent(props: Omit) { - const organizationId = useOrganizationId(); - const query = useLazyLoadQuery(snapshotsQuery, { - organizationId, - }); - const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< - LinkedSnapshotsDialogQuery, - LinkedSnapshotsDialogFragment$key - >(snapshotsFragment, query.organization); - - const { __ } = useTranslate(); - const [search, setSearch] = useState(""); - const snapshots = useMemo( - () => data.snapshots?.edges?.map(edge => edge.node) ?? [], - [data.snapshots], - ); - const linkedIds = useMemo(() => { - return new Set(props.linkedSnapshots?.map(s => s.id) ?? []); - }, [props.linkedSnapshots]); - - const filteredSnapshots = useMemo(() => { - return snapshots.filter(snapshot => - snapshot.name.toLowerCase().includes(search.toLowerCase()), - ); - }, [snapshots, search]); - - return ( - <> -
- -
-
- {filteredSnapshots.map(snapshot => ( - - ))} - {hasNext && ( - loadNext(20)} - /> - )} -
- - ); -} - -type Snapshot = NodeOf; - -type RowProps = { - snapshot: Snapshot; - linkedSnapshots: Set; - disabled?: boolean; - onLink: (snapshotId: string) => void; - onUnlink: (snapshotId: string) => void; -}; - -function SnapshotRow(props: RowProps) { - const { __ } = useTranslate(); - - const isLinked = props.linkedSnapshots.has(props.snapshot.id); - const onClick = isLinked ? props.onUnlink : props.onLink; - const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; - - return ( - - - ); -} diff --git a/apps/console/src/hooks/graph/FrameworkGraph.ts b/apps/console/src/hooks/graph/FrameworkGraph.ts index bd0fbaf44..45d3c7981 100644 --- a/apps/console/src/hooks/graph/FrameworkGraph.ts +++ b/apps/console/src/hooks/graph/FrameworkGraph.ts @@ -140,12 +140,6 @@ export const frameworkControlNodeQuery = graphql` canDeleteAuditMapping: permission( action: "core:control:delete-audit-mapping" ) - canCreateSnapshotMapping: permission( - action: "core:control:create-snapshot-mapping" - ) - canDeleteSnapshotMapping: permission( - action: "core:control:delete-snapshot-mapping" - ) canCreateObligationMapping: permission( action: "core:control:create-obligation-mapping" ) @@ -192,16 +186,6 @@ export const frameworkControlNodeQuery = graphql` } } } - snapshots(first: 100) - @connection(key: "FrameworkGraphControl_snapshots") { - __id - edges { - node { - id - ...LinkedSnapshotsCardFragment - } - } - } } } } diff --git a/apps/console/src/hooks/graph/RiskGraph.ts b/apps/console/src/hooks/graph/RiskGraph.ts index cd08a3154..0fbae1444 100644 --- a/apps/console/src/hooks/graph/RiskGraph.ts +++ b/apps/console/src/hooks/graph/RiskGraph.ts @@ -50,10 +50,10 @@ export function useDeleteRiskMutation() { } export const risksQuery = graphql` - query RiskGraphListQuery($organizationId: ID!, $snapshotId: ID) { + query RiskGraphListQuery($organizationId: ID!) { organization: node(id: $organizationId) { id - ...RiskGraphFragment @arguments(snapshotId: $snapshotId) + ...RiskGraphFragment } } `; @@ -70,22 +70,28 @@ const risksFragment = graphql` after: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null } last: { type: "Int", defaultValue: null } - snapshotId: { type: "ID", defaultValue: null } ) { canCreateRisk: permission(action: "core:risk:create") + canPublishRisk: permission(action: "core:risk:publish") + risksDocument { + id + currentPublishedMajor + currentPublishedMinor + defaultApprovers { + id + } + } risks( first: $first after: $after last: $last before: $before orderBy: $order - filter: { snapshotId: $snapshotId } - ) @connection(key: "RisksListQuery_risks", filters: ["filter"]) { + ) @connection(key: "RisksListQuery_risks", filters: []) { __id edges { node { id - snapshotId name category treatment @@ -130,7 +136,6 @@ export const riskNodeQuery = graphql` node(id: $riskId) { ... on Risk { id - snapshotId name description treatment diff --git a/apps/console/src/hooks/graph/SnapshotGraph.ts b/apps/console/src/hooks/graph/SnapshotGraph.ts deleted file mode 100644 index cf97d7043..000000000 --- a/apps/console/src/hooks/graph/SnapshotGraph.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { promisifyMutation, sprintf } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { useConfirm } from "@probo/ui"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { useMutationWithToasts } from "../useMutationWithToasts"; - -/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */ - -export const SnapshotsConnectionKey = "SnapshotsPage_snapshots"; - -export const snapshotsQuery = graphql` - query SnapshotGraphListQuery($organizationId: ID!) { - organization: node(id: $organizationId) { - ... on Organization { - canCreateSnapshot: permission(action: "core:snapshot:create") - ...SnapshotsPageFragment - } - } - } -`; - -export const snapshotNodeQuery = graphql` - query SnapshotGraphNodeQuery($snapshotId: ID!) { - node(id: $snapshotId) { - ... on Snapshot { - id - name - description - type - organization { - id - name - } - createdAt - } - } - } -`; - -export const createSnapshotMutation = graphql` - mutation SnapshotGraphCreateMutation( - $input: CreateSnapshotInput! - $connections: [ID!]! - ) { - createSnapshot(input: $input) { - snapshotEdge @prependEdge(connections: $connections) { - node { - id - name - description - type - createdAt - } - } - } - } -`; - -export const deleteSnapshotMutation = graphql` - mutation SnapshotGraphDeleteMutation( - $input: DeleteSnapshotInput! - $connections: [ID!]! - ) { - deleteSnapshot(input: $input) { - deletedSnapshotId @deleteEdge(connections: $connections) - } - } -`; - -export const useDeleteSnapshot = ( - snapshot: { id: string; name: string }, - connectionId: string, -) => { - const { __ } = useTranslate(); - const [mutate] = useMutationWithToasts(deleteSnapshotMutation, { - successMessage: __("Snapshot deleted successfully"), - errorMessage: __("Failed to delete snapshot"), - }); - const confirm = useConfirm(); - - return () => { - confirm( - () => - mutate({ - variables: { - input: { - snapshotId: snapshot.id, - }, - connections: [connectionId], - }, - }), - { - message: sprintf( - __( - "This will permanently delete the snapshot %s. This action cannot be undone.", - ), - snapshot.name, - ), - }, - ); - }; -}; - -export const useCreateSnapshot = (connectionId: string) => { - // eslint-disable-next-line relay/generated-typescript-types - const [mutate] = useMutation(createSnapshotMutation); - const { __ } = useTranslate(); - - return (input: { - organizationId: string; - name: string; - description?: string; - }) => { - if (!input.organizationId) { - return alert(__("Failed to create snapshot: organization is required")); - } - if (!input.name) { - return alert(__("Failed to create snapshot: name is required")); - } - - return promisifyMutation(mutate)({ - variables: { - input: { - organizationId: input.organizationId, - name: input.name, - description: input.description, - }, - connections: [connectionId], - }, - }); - }; -}; diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx index f09d0d70b..384a32e8f 100644 --- a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -19,7 +19,6 @@ import { IconBook, IconBox, IconCircleProgress, - IconClock, IconFire3, IconGroup1, IconInboxEmpty, @@ -62,7 +61,6 @@ const fragment = graphql` action: "core:processing-activity:list" ) canListRightsRequests: permission(action: "core:rights-request:list") - canListSnapshots: permission(action: "core:snapshot:list") canGetTrustCenter: permission(action: "core:trust-center:get") canListCookieBanners: permission(action: "core:cookie-banner:list") canUpdateOrganization: permission(action: "iam:organization:update") @@ -199,13 +197,6 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) { to={`${prefix}/rights-requests`} /> )} - {organization.canListSnapshots && ( - - )} {organization.canListAccessReviewCampaigns && ( -
- edge.node) - ?? [] - } - params={{ controlId: control.id }} - connectionId={control.snapshots?.__id ?? ""} - onAttach={withErrorHandling( - attachSnapshot, - __("Failed to link snapshot"), - )} - onDetach={withErrorHandling( - detachSnapshot, - __("Failed to unlink snapshot"), - )} - disabled={isAttachingSnapshot || isDetachingSnapshot} - readOnly={snapshotsReadOnly} - /> -
); diff --git a/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx b/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx index 6ae298118..5f1e83a36 100644 --- a/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx +++ b/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx @@ -114,10 +114,9 @@ export default function MeasureEvidencesTab() { const { measure } = useOutletContext<{ measure: MeasureEvidencesTabFragment$key; }>(); - const { measureId, evidenceId, snapshotId } = useParams<{ + const { measureId, evidenceId } = useParams<{ measureId: string; evidenceId: string; - snapshotId?: string; }>(); if (!measureId) { throw new Error("Missing :measureId param in route"); @@ -132,7 +131,6 @@ export default function MeasureEvidencesTab() { const evidence = evidences.find(e => e.id === evidenceId); const organizationId = useOrganizationId(); const dialogRef = useDialogRef(); - const isSnapshotMode = Boolean(snapshotId); usePageTitle(pagination.data.name + " - " + __("Evidences")); @@ -156,11 +154,9 @@ export default function MeasureEvidencesTab() { measureId={measureId} organizationId={organizationId} connectionId={connectionId} - hideActions={isSnapshotMode} - snapshotId={snapshotId} /> ))} - {!isSnapshotMode && pagination.data.canUploadEvidence && ( + {pagination.data.canUploadEvidence && ( dialogRef.current?.open()} @@ -175,16 +171,13 @@ export default function MeasureEvidencesTab() { { - const baseUrl = isSnapshotMode - ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/measures/${measureId}/evidences` - : `/organizations/${organizationId}/measures/${measureId}/evidences`; - void navigate(baseUrl); + void navigate(`/organizations/${organizationId}/measures/${measureId}/evidences`); }} evidenceId={evidence.id} filename={evidence.file?.fileName || ""} /> )} - {!isSnapshotMode && pagination.data.canUploadEvidence && ( + {pagination.data.canUploadEvidence && ( @@ -275,26 +264,24 @@ function EvidenceRow(props: { {fileSize(__, evidence.file?.size || 0)} {formatDate(evidence.createdAt)} - {!props.hideActions && ( -
- - setIsDownloading(true)}> - - {__("Download")} +
+ + setIsDownloading(true)}> + + {__("Download")} + + {evidence.canDelete && ( + + {__("Delete")} - {evidence.canDelete && ( - - {__("Delete")} - - )} - -
- )} + )} +
+
diff --git a/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx b/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx index ae4bc7aea..df3bcd0ac 100644 --- a/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx +++ b/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx @@ -12,11 +12,7 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { - getTreatment, - sprintf, - validateSnapshotConsistency, -} from "@probo/helpers"; +import { getTreatment, sprintf } from "@probo/helpers"; import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { @@ -41,7 +37,6 @@ import { Outlet, useNavigate, useParams } from "react-router"; import { ConnectionHandler } from "relay-runtime"; import type { RiskGraphNodeQuery } from "#/__generated__/core/RiskGraphNodeQuery.graphql"; -import { SnapshotBanner } from "#/components/SnapshotBanner"; import { riskNodeQuery, RisksConnectionKey, @@ -56,13 +51,11 @@ type Props = { }; export default function RiskDetailPage(props: Props) { - const { riskId, snapshotId } = useParams<{ + const { riskId } = useParams<{ riskId: string; - snapshotId?: string; }>(); const organizationId = useOrganizationId(); const navigate = useNavigate(); - const isSnapshotMode = Boolean(snapshotId); if (!riskId) { throw new Error("Cannot load risk detail page without riskId parameter"); @@ -74,7 +67,6 @@ export default function RiskDetailPage(props: Props) { props.queryRef, ); - validateSnapshotConsistency(risk, snapshotId); const [deleteRisk] = useDeleteRiskMutation(); usePageTitle(risk.name ?? "Risk detail"); @@ -84,7 +76,6 @@ export default function RiskDetailPage(props: Props) { const connectionId = ConnectionHandler.getConnectionID( organizationId, RisksConnectionKey, - { filter: { snapshotId: snapshotId || null } }, ); confirm( () => @@ -95,11 +86,7 @@ export default function RiskDetailPage(props: Props) { connections: [connectionId], }, onSuccess() { - const risksUrl - = isSnapshotMode && snapshotId - ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks` - : `/organizations/${organizationId}/risks`; - void navigate(risksUrl); + void navigate(`/organizations/${organizationId}/risks`); resolve(); }, }); @@ -120,19 +107,11 @@ export default function RiskDetailPage(props: Props) { const controlsCount = risk.controlsInfo?.totalCount ?? 0; const obligationsCount = risk.obligationsInfo?.totalCount ?? 0; - const risksUrl - = isSnapshotMode && snapshotId - ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks` - : `/organizations/${organizationId}/risks`; - - const baseTabUrl - = isSnapshotMode && snapshotId - ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${riskId}` - : `/organizations/${organizationId}/risks/${riskId}`; + const risksUrl = `/organizations/${organizationId}/risks`; + const baseTabUrl = `/organizations/${organizationId}/risks/${riskId}`; return (
- {snapshotId && } {/* Header */}
- {!isSnapshotMode && ( -
- {risk.canUpdate && ( - - {__("Edit")} - - )} - risk={{ id: riskId, ...risk }} - /> - )} - {risk.canDelete && ( - - - {__("Delete")} - - - )} -
- )} +
+ {risk.canUpdate && ( + + {__("Edit")} + + )} + risk={{ id: riskId, ...risk }} + /> + )} + {risk.canDelete && ( + + + {__("Delete")} + + + )} +
{__("Overview")} - {!isSnapshotMode && ( - <> - - {__("Measures")} - {measuresCount} - - - {__("Documents")} - {documentsCount} - - - {__("Controls")} - {controlsCount} - - - {__("Obligations")} - {obligationsCount} - - - )} + + {__("Measures")} + {measuresCount} + + + {__("Documents")} + {documentsCount} + + + {__("Controls")} + {controlsCount} + + + {__("Obligations")} + {obligationsCount} + diff --git a/apps/console/src/pages/organizations/risks/RisksPage.tsx b/apps/console/src/pages/organizations/risks/RisksPage.tsx index 451b8ba95..474acf5b9 100644 --- a/apps/console/src/pages/organizations/risks/RisksPage.tsx +++ b/apps/console/src/pages/organizations/risks/RisksPage.tsx @@ -19,9 +19,11 @@ import { ActionDropdown, Button, DropdownItem, + IconPageTextLine, IconPencil, IconPlusLarge, IconTrashCan, + IconUpload, PageHeader, RisksChart, SeverityBadge, @@ -34,16 +36,16 @@ import { useDialogRef, } from "@probo/ui"; import type { PreloadedQuery } from "react-relay"; -import { useParams } from "react-router"; +import { useNavigate } from "react-router"; import type { RiskGraphFragment$data } from "#/__generated__/core/RiskGraphFragment.graphql"; import type { RiskGraphListQuery } from "#/__generated__/core/RiskGraphListQuery.graphql"; -import { SnapshotBanner } from "#/components/SnapshotBanner"; import { SortableTable, SortableTh } from "#/components/SortableTable"; import { useDeleteRiskMutation, useRisksQuery } from "#/hooks/graph/RiskGraph"; import { useOrganizationId } from "#/hooks/useOrganizationId"; import type { NodeOf } from "#/types"; +import { PublishRiskListDialog } from "./dialogs/PublishRiskListDialog"; import FormRiskDialog from "./FormRiskDialog"; type Props = { @@ -53,11 +55,10 @@ type Props = { export default function RisksPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); - const { snapshotId } = useParams<{ snapshotId?: string }>(); - const isSnapshotMode = Boolean(snapshotId); + const navigate = useNavigate(); const { - data: { canCreateRisk }, + data: { canCreateRisk, canPublishRisk, risksDocument }, connectionId, risks, ...pagination @@ -70,7 +71,6 @@ export default function RisksPage(props: Props) { }) => { pagination.refetch( { - snapshotId, order: { direction: order.direction as "ASC" | "DESC", field: order.field as @@ -90,27 +90,54 @@ export default function RisksPage(props: Props) { usePageTitle(__("Risks")); const hasAnyAction - = !isSnapshotMode - && risks.some(({ canDelete, canUpdate }) => canUpdate || canDelete); + = risks.some(({ canDelete, canUpdate }) => canUpdate || canDelete); + + const defaultApproverIds + = risksDocument?.defaultApprovers?.map(a => a.id) ?? []; return (
- {snapshotId && } - {!isSnapshotMode && canCreateRisk && ( - { - pagination.refetch({ snapshotId }); - }} - trigger={} - /> - )} +
+ {risksDocument && ( + + )} + {canPublishRisk && ( + void navigate( + `/organizations/${organizationId}/documents/${documentId}`, + )} + > + + + )} + {canCreateRisk && ( + { + pagination.refetch({}); + }} + trigger={} + /> + )} +
@@ -167,8 +194,6 @@ type RowProps = { function RiskRow(props: RowProps) { const { __ } = useTranslate(); const { risk, connectionId, organizationId } = props; - const { snapshotId } = useParams<{ snapshotId?: string }>(); - const isSnapshotMode = Boolean(snapshotId); const [deleteRisk] = useDeleteRiskMutation(); const confirm = useConfirm(); const onDelete = () => { @@ -195,20 +220,15 @@ function RiskRow(props: RowProps) { }; const formDialogRef = useDialogRef(); - const riskUrl - = isSnapshotMode && snapshotId - ? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${risk.id}/overview` - : `/organizations/${organizationId}/risks/${risk.id}/overview`; + const riskUrl = `/organizations/${organizationId}/risks/${risk.id}/overview`; return ( <> - {!isSnapshotMode && ( - - )} + {risk.name} {risk.category} diff --git a/apps/console/src/pages/organizations/risks/dialogs/PublishRiskListDialog.tsx b/apps/console/src/pages/organizations/risks/dialogs/PublishRiskListDialog.tsx new file mode 100644 index 000000000..7831babc4 --- /dev/null +++ b/apps/console/src/pages/organizations/risks/dialogs/PublishRiskListDialog.tsx @@ -0,0 +1,160 @@ +// Copyright (c) 2026 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. + +import { formatError, type GraphQLError } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + IconSend, + IconUpload, + useDialogRef, + useToast, +} from "@probo/ui"; +import type { ReactNode } from "react"; +import { useMemo } from "react"; +import { useMutation } from "react-relay"; +import { graphql } from "relay-runtime"; +import { z } from "zod"; + +import type { PublishRiskListDialogMutation } from "#/__generated__/core/PublishRiskListDialogMutation.graphql"; +import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField"; +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; + +const publishMutation = graphql` + mutation PublishRiskListDialogMutation( + $input: PublishRiskListInput! + ) { + publishRiskList(input: $input) { + documentEdge { + node { + id + } + } + } + } +`; + +type Props = { + children: ReactNode; + organizationId: string; + defaultApproverIds?: string[]; + onPublished?: (documentId: string) => void; +}; + +export function PublishRiskListDialog({ + children, + organizationId, + defaultApproverIds, + onPublished, +}: Props) { + const { __ } = useTranslate(); + const { toast } = useToast(); + const dialogRef = useDialogRef(); + + const schema = useMemo(() => + z.object({ + approverIds: z.array(z.string()), + }), []); + + const { + control, + handleSubmit, + reset, + watch, + } = useFormWithSchema(schema, { + defaultValues: { + approverIds: defaultApproverIds ?? [], + }, + }); + + const [publish, isPublishing] + = useMutation(publishMutation); + + const approverIds = watch("approverIds"); + const hasApprovers = approverIds.length > 0; + + const onSubmit = (data: z.infer) => { + publish({ + variables: { + input: { + organizationId, + approverIds: data.approverIds.length > 0 ? data.approverIds : undefined, + }, + }, + onCompleted(response) { + const documentId = response.publishRiskList?.documentEdge?.node?.id; + if (documentId) { + toast({ + title: __("Success"), + description: hasApprovers + ? __("Approval requested successfully.") + : __("Risks published successfully."), + variant: "success", + }); + dialogRef.current?.close(); + reset(); + onPublished?.(documentId); + } + }, + onError(error) { + toast({ + title: __("Error"), + description: formatError( + __("Failed to publish risks"), + error as GraphQLError, + ), + variant: "error", + }); + }, + }); + }; + + return ( + +
void handleSubmit(onSubmit)(e)}> + +
+

+ {__("Select approvers to request approval before publishing, or publish directly without approvers.")} +

+ +
+
+ + + +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx b/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx deleted file mode 100644 index 9b4d0f8ea..000000000 --- a/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { getSnapshotTypeUrlPath } from "@probo/helpers"; -import { useEffect } from "react"; -import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; -import { useNavigate, useParams } from "react-router"; - -import type { SnapshotGraphNodeQuery } from "#/__generated__/core/SnapshotGraphNodeQuery.graphql"; -import { PageError } from "#/components/PageError"; -import { snapshotNodeQuery } from "#/hooks/graph/SnapshotGraph"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; - -type Props = { - queryRef: PreloadedQuery; -}; - -export default function SnapshotDetailPage({ queryRef }: Props) { - const navigate = useNavigate(); - const organizationId = useOrganizationId(); - const { snapshotId } = useParams(); - const data = usePreloadedQuery(snapshotNodeQuery, queryRef); - - useEffect(() => { - if (!data.node || !data.node.type) { - return; - } - - const snapshot = data.node; - const snapshotType = snapshot.type; - const urlPath = getSnapshotTypeUrlPath(snapshotType); - - void navigate( - `/organizations/${organizationId}/snapshots/${snapshotId}${urlPath}`, - { - replace: true, - }, - ); - }, [data.node, navigate, organizationId, snapshotId]); - - if (!data.node || !data.node.type) { - return ; - } - - return null; -} diff --git a/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx b/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx deleted file mode 100644 index 7ef387148..000000000 --- a/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { - formatDate, - getSnapshotTypeLabel, - getSnapshotTypeUrlPath, -} from "@probo/helpers"; -import { usePageTitle } from "@probo/hooks"; -import { useTranslate } from "@probo/i18n"; -import { - ActionDropdown, - Badge, - Button, - DropdownItem, - IconPlusLarge, - IconTrashCan, - PageHeader, - Table, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@probo/ui"; -import { - type PreloadedQuery, - useFragment, - usePreloadedQuery, -} from "react-relay"; -import { graphql } from "relay-runtime"; - -import type { SnapshotGraphListQuery } from "#/__generated__/core/SnapshotGraphListQuery.graphql"; -import type { - SnapshotsPageFragment$data, - SnapshotsPageFragment$key, -} from "#/__generated__/core/SnapshotsPageFragment.graphql"; -import { snapshotsQuery, useDeleteSnapshot } from "#/hooks/graph/SnapshotGraph"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; -import type { NodeOf } from "#/types"; - -import SnapshotFormDialog from "./dialog/SnapshotFormDialog"; - -type Props = { - queryRef: PreloadedQuery; -}; - -const snapshotsFragment = graphql` - fragment SnapshotsPageFragment on Organization { - snapshots(first: 100) - @connection(key: "SnapshotsGraphListQuery__snapshots") { - __id - edges { - node { - id - name - description - type - createdAt - canDelete: permission(action: "core:snapshot:delete") - } - } - } - } -`; - -export default function SnapshotsPage(props: Props) { - const { __ } = useTranslate(); - const organizationId = useOrganizationId(); - const organization = usePreloadedQuery( - snapshotsQuery, - props.queryRef, - ).organization; - const data = useFragment( - snapshotsFragment, - organization, - ); - const connectionId = data.snapshots.__id; - const snapshots = data.snapshots.edges.map(edge => edge.node); - usePageTitle(__("Snapshots")); - - const hasAnyAction = snapshots.some(({ canDelete }) => canDelete); - - return ( -
- - {organization.canCreateSnapshot && ( - - - - )} - - - {snapshots.length > 0 - ? ( - - - - - - - - {hasAnyAction && } - - - - {snapshots.map(snapshot => ( - - ))} - -
{__("Name")}{__("Type")}{__("Description")}{__("Created")}
- ) - : ( -
-

- {__("No snapshots yet")} -

-

- {__("Create your first snapshot to get started")} -

-
- )} -
- ); -} - -type SnapshotRowProps = { - snapshot: NodeOf; - connectionId: string; - organizationId: string; - hasAnyAction: boolean; -}; - -function SnapshotRow(props: SnapshotRowProps) { - const { __ } = useTranslate(); - const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId); - const typePath = getSnapshotTypeUrlPath(props.snapshot.type); - - return ( - - {props.snapshot.name} - - - {getSnapshotTypeLabel(__, props.snapshot.type)} - - - - {props.snapshot.description || __("No description")} - - - {formatDate(props.snapshot.createdAt)} - - {props.hasAnyAction && ( - - - {props.snapshot.canDelete && ( - - {__("Delete")} - - )} - - - )} - - ); -} diff --git a/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx b/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx deleted file mode 100644 index b8a064fa1..000000000 --- a/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2025-2026 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. - -import { snapshotTypes } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { Breadcrumb, - Button, - Dialog, - DialogContent, - DialogFooter, - Input, - Label, - PropertyRow, - Textarea, - useDialogRef, -} from "@probo/ui"; -import type { ReactNode } from "react"; -import { graphql } from "relay-runtime"; -import { z } from "zod"; - -import { ControlledField } from "#/components/form/ControlledField"; -import { SnapshotTypeOptions } from "#/components/form/SnapshotTypeOptions"; -import { useFormWithSchema } from "#/hooks/useFormWithSchema"; -import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; - -const snapshotCreateMutation = graphql` - mutation SnapshotFormDialogCreateMutation( - $input: CreateSnapshotInput! - $connections: [ID!]! - ) { - createSnapshot(input: $input) { - snapshotEdge @prependEdge(connections: $connections) { - node { - id - name - description - type - createdAt - canDelete: permission(action: "core:snapshot:delete") - } - } - } - } -`; - -const snapshotSchema = z.object({ - name: z.string().min(2, { message: "Name is required" }), - description: z.string().optional(), - type: z.enum(snapshotTypes), -}); - -type Props = { - children?: ReactNode; - connection?: string; -}; - -export default function SnapshotFormDialog(props: Props) { - const { __ } = useTranslate(); - const dialogRef = useDialogRef(); - const organizationId = useOrganizationId(); - const [mutate] = useMutationWithToasts(snapshotCreateMutation, { - successMessage: __("Snapshot created successfully."), - errorMessage: __("Failed to create snapshot"), - }); - - const { - handleSubmit, - register, - reset, - control, - formState: { errors }, - } = useFormWithSchema(snapshotSchema, { - defaultValues: { - name: "", - description: "", - type: "RISKS", - }, - }); - - const onSubmit = async (data: z.infer) => { - await mutate({ - variables: { - input: { - organizationId, - name: data.name, - description: data.description || undefined, - type: data.type, - }, - connections: props.connection ? [props.connection] : [], - }, - }); - reset(); - dialogRef.current?.close(); - }; - - return ( - } - > -
void handleSubmit(onSubmit)(e)}> - -
- -