From 312474354ca800b6d63b2952596129819a8ea5e5 Mon Sep 17 00:00:00 2001 From: gearnode Date: Tue, 4 Mar 2025 12:09:22 +0100 Subject: [PATCH] Add policies Signed-off-by: gearnode --- apps/console/src/App.tsx | 52 +- apps/console/src/components/AppSidebar.tsx | 8 + apps/console/src/layouts/ConsoleLayout.tsx | 125 +- ...utBreadcrumbPolicyOverviewQuery.graphql.ts | 127 ++ ...youtBreadcrumbUpdatePolicyQuery.graphql.ts | 127 ++ .../console/src/pages/ControlOverviewPage.tsx | 24 +- apps/console/src/pages/CreatePolicyPage.tsx | 194 ++ apps/console/src/pages/PolicyListPage.tsx | 179 ++ apps/console/src/pages/PolicyOverviewPage.tsx | 230 +++ apps/console/src/pages/UpdatePolicyPage.tsx | 270 +++ .../CreatePolicyPageMutation.graphql.ts | 186 ++ .../PolicyListPageQuery.graphql.ts | 269 +++ .../PolicyOverviewPageQuery.graphql.ts | 160 ++ .../UpdatePolicyPageMutation.graphql.ts | 134 ++ .../UpdatePolicyPageQuery.graphql.ts | 152 ++ package-lock.json | 79 +- pkg/api/console/v1/schema.graphql | 73 + pkg/api/console/v1/schema/schema.go | 1833 +++++++++++++++++ pkg/api/console/v1/types/policy.go | 51 + pkg/api/console/v1/types/types.go | 55 + pkg/api/console/v1/v1_resolver.go | 65 + pkg/probo/coredata/entity_type_reg.go | 1 + .../coredata/migrations/20250228T000000Z.sql | 12 + pkg/probo/coredata/policy.go | 250 +++ pkg/probo/coredata/policy_status.go | 74 + pkg/probo/policy_service.go | 162 ++ pkg/probo/{probo.go => service.go} | 17 +- 27 files changed, 4882 insertions(+), 27 deletions(-) create mode 100644 apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbPolicyOverviewQuery.graphql.ts create mode 100644 apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbUpdatePolicyQuery.graphql.ts create mode 100644 apps/console/src/pages/CreatePolicyPage.tsx create mode 100644 apps/console/src/pages/PolicyListPage.tsx create mode 100644 apps/console/src/pages/PolicyOverviewPage.tsx create mode 100644 apps/console/src/pages/UpdatePolicyPage.tsx create mode 100644 apps/console/src/pages/__generated__/CreatePolicyPageMutation.graphql.ts create mode 100644 apps/console/src/pages/__generated__/PolicyListPageQuery.graphql.ts create mode 100644 apps/console/src/pages/__generated__/PolicyOverviewPageQuery.graphql.ts create mode 100644 apps/console/src/pages/__generated__/UpdatePolicyPageMutation.graphql.ts create mode 100644 apps/console/src/pages/__generated__/UpdatePolicyPageQuery.graphql.ts create mode 100644 pkg/api/console/v1/types/policy.go create mode 100644 pkg/probo/coredata/migrations/20250228T000000Z.sql create mode 100644 pkg/probo/coredata/policy.go create mode 100644 pkg/probo/coredata/policy_status.go create mode 100644 pkg/probo/policy_service.go rename pkg/probo/{probo.go => service.go} (86%) diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 6e76d8cf6..eafe1f673 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -24,7 +24,7 @@ posthog.init(process.env.POSTHOG_KEY!, { }); const OrganizationSelectionPage = lazy( - () => import("./pages/OrganizationSelectionPage"), + () => import("./pages/OrganizationSelectionPage") ); const HomePage = lazy(() => import("./pages/HomePage")); const NotFoundPage = lazy(() => import("./pages/NotFoundPage")); @@ -35,19 +35,24 @@ const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage")); const SettingsPage = lazy(() => import("./pages/SettingsPage")); const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage")); const FrameworkOverviewPage = lazy( - () => import("./pages/FrameworkOverviewPage"), + () => import("./pages/FrameworkOverviewPage") ); const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage")); const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage")); const LoginPage = lazy(() => import("./pages/LoginPage")); const RegisterPage = lazy(() => import("./pages/RegisterPage")); const CreateOrganizationPage = lazy( - () => import("./pages/CreateOrganizationPage"), + () => import("./pages/CreateOrganizationPage") ); const CreateFrameworkPage = lazy(() => import("./pages/CreateFrameworkPage")); const CreateControlPage = lazy(() => import("./pages/CreateControlPage")); const UpdateFrameworkPage = lazy(() => import("./pages/UpdateFrameworkPage")); const UpdateControlPage = lazy(() => import("./pages/UpdateControlPage")); +// Policy pages +const PolicyListPage = lazy(() => import("./pages/PolicyListPage")); +const PolicyOverviewPage = lazy(() => import("./pages/PolicyOverviewPage")); +const CreatePolicyPage = lazy(() => import("./pages/CreatePolicyPage")); +const UpdatePolicyPage = lazy(() => import("./pages/UpdatePolicyPage")); function App() { return ( @@ -282,6 +287,47 @@ function App() { } /> + {/* Policy Routes */} + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + Policies + + + + + ); +} + +function BreadcrumbCreatePolicy() { + return ( + <> + + + Create Policy + + + ); +} + +function BreadcrumbPolicyOverview() { + const { organizationId, policyId } = useParams(); + const data = useLazyLoadQuery( + graphql` + query ConsoleLayoutBreadcrumbPolicyOverviewQuery($policyId: ID!) { + policy: node(id: $policyId) { + id + ... on Policy { + name + } + } + } + `, + { policyId: policyId! }, + { fetchPolicy: "store-or-network" } + ); + + return ( + <> + + + + + {data.policy?.name} + + + + + + ); +} + +function BreadcrumbUpdatePolicy() { + const { organizationId, policyId } = useParams(); + const data = useLazyLoadQuery( + graphql` + query ConsoleLayoutBreadcrumbUpdatePolicyQuery($policyId: ID!) { + policy: node(id: $policyId) { + id + ... on Policy { + name + } + } + } + `, + { policyId: policyId! }, + { fetchPolicy: "store-or-network" } + ); + + return ( + <> + + + + + {data.policy?.name} + + + + + + Update + + + + ); +} + export default function ConsoleLayout() { const { organizationId } = useParams(); const showBreadcrumb = !!organizationId; @@ -410,6 +510,17 @@ export default function ConsoleLayout() { element={} /> + }> + } + /> + } + /> + } /> + )} diff --git a/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbPolicyOverviewQuery.graphql.ts b/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbPolicyOverviewQuery.graphql.ts new file mode 100644 index 000000000..2412c717a --- /dev/null +++ b/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbPolicyOverviewQuery.graphql.ts @@ -0,0 +1,127 @@ +/** + * @generated SignedSource<<03812f2d66d34ef3636bc7890059f4cf>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ConsoleLayoutBreadcrumbPolicyOverviewQuery$variables = { + policyId: string; +}; +export type ConsoleLayoutBreadcrumbPolicyOverviewQuery$data = { + readonly policy: { + readonly id: string; + readonly name?: string; + }; +}; +export type ConsoleLayoutBreadcrumbPolicyOverviewQuery = { + response: ConsoleLayoutBreadcrumbPolicyOverviewQuery$data; + variables: ConsoleLayoutBreadcrumbPolicyOverviewQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "policyId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "policyId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Policy", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "ConsoleLayoutBreadcrumbPolicyOverviewQuery", + "selections": [ + { + "alias": "policy", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "ConsoleLayoutBreadcrumbPolicyOverviewQuery", + "selections": [ + { + "alias": "policy", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "2c38760f04efaf9b41d6b2abb17c2074", + "id": null, + "metadata": {}, + "name": "ConsoleLayoutBreadcrumbPolicyOverviewQuery", + "operationKind": "query", + "text": "query ConsoleLayoutBreadcrumbPolicyOverviewQuery(\n $policyId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f2a356a6aa9b98ca13dde3b82e2e868a"; + +export default node; diff --git a/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbUpdatePolicyQuery.graphql.ts b/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbUpdatePolicyQuery.graphql.ts new file mode 100644 index 000000000..c3ad0009a --- /dev/null +++ b/apps/console/src/layouts/__generated__/ConsoleLayoutBreadcrumbUpdatePolicyQuery.graphql.ts @@ -0,0 +1,127 @@ +/** + * @generated SignedSource<<7fd05943b12b7927bddcb4ee8a1cdd76>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ConsoleLayoutBreadcrumbUpdatePolicyQuery$variables = { + policyId: string; +}; +export type ConsoleLayoutBreadcrumbUpdatePolicyQuery$data = { + readonly policy: { + readonly id: string; + readonly name?: string; + }; +}; +export type ConsoleLayoutBreadcrumbUpdatePolicyQuery = { + response: ConsoleLayoutBreadcrumbUpdatePolicyQuery$data; + variables: ConsoleLayoutBreadcrumbUpdatePolicyQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "policyId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "policyId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Policy", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "ConsoleLayoutBreadcrumbUpdatePolicyQuery", + "selections": [ + { + "alias": "policy", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "ConsoleLayoutBreadcrumbUpdatePolicyQuery", + "selections": [ + { + "alias": "policy", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "6b895165c1104f2caf67bf4ded9a795d", + "id": null, + "metadata": {}, + "name": "ConsoleLayoutBreadcrumbUpdatePolicyQuery", + "operationKind": "query", + "text": "query ConsoleLayoutBreadcrumbUpdatePolicyQuery(\n $policyId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "ee8e83ba02f1545ad73fe5d9742136ff"; + +export default node; diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index 8c78a55bf..1cf978ea0 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -186,7 +186,7 @@ function ControlOverviewPageContent({ }) { const data = usePreloadedQuery( controlOverviewPageQuery, - queryRef, + queryRef ); const { toast } = useToast(); const { organizationId, frameworkId, controlId } = useParams(); @@ -194,7 +194,7 @@ function ControlOverviewPageContent({ const environment = useRelayEnvironment(); const [updateTaskState] = useMutation( - updateTaskStateMutation, + updateTaskStateMutation ); const [createTask] = useMutation(createTaskMutation); @@ -202,11 +202,11 @@ function ControlOverviewPageContent({ useMutation(deleteTaskMutation); const [uploadEvidence] = useMutation( - uploadEvidenceMutation, + uploadEvidenceMutation ); const [deleteEvidence] = useMutation( - deleteEvidenceMutation, + deleteEvidenceMutation ); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); @@ -228,7 +228,7 @@ function ControlOverviewPageContent({ const fileInputRef = useRef(null); const [draggedOverTaskId, setDraggedOverTaskId] = useState( - null, + null ); const [uploadingTaskId, setUploadingTaskId] = useState(null); const [isDraggingFile, setIsDraggingFile] = useState(false); @@ -265,7 +265,7 @@ function ControlOverviewPageContent({ } return null; }, - [tasks], + [tasks] ); useEffect(() => { @@ -424,7 +424,7 @@ function ControlOverviewPageContent({ const handleEditControl = () => { navigate( - `/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}/update`, + `/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}/update` ); }; @@ -630,7 +630,7 @@ function ControlOverviewPageContent({ const handleDeleteEvidence = ( evidenceId: string, filename: string, - taskId: string, + taskId: string ) => { setEvidenceToDelete({ id: evidenceId, filename, taskId }); setIsDeleteEvidenceOpen(true); @@ -640,7 +640,7 @@ function ControlOverviewPageContent({ if (!evidenceToDelete) return; const evidenceConnectionId = getEvidenceConnectionId( - evidenceToDelete.taskId, + evidenceToDelete.taskId ); deleteEvidence({ @@ -998,10 +998,10 @@ function ControlOverviewPageContent({ handleDeleteEvidence( evidence.id, evidence.filename, - task.id, + task.id ); }} - className="p-1 rounded-full hover:bg-gray-100 hover:bg-red-50 hover:text-red-600" + className="p-1 rounded-full hover:bg-gray-10 hover:text-red-600" title="Delete Evidence" > @@ -1238,7 +1238,7 @@ function ControlOverviewPageFallback() { export default function ControlOverviewPage() { const { controlId } = useParams(); const [queryRef, loadQuery] = useQueryLoader( - controlOverviewPageQuery, + controlOverviewPageQuery ); useEffect(() => { diff --git a/apps/console/src/pages/CreatePolicyPage.tsx b/apps/console/src/pages/CreatePolicyPage.tsx new file mode 100644 index 000000000..14ed93c8c --- /dev/null +++ b/apps/console/src/pages/CreatePolicyPage.tsx @@ -0,0 +1,194 @@ +import { useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import { ConnectionHandler, graphql, useMutation } from "react-relay"; +import { Helmet } from "react-helmet-async"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useToast } from "@/hooks/use-toast"; +import { FileText } from "lucide-react"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql"; + +const CreatePolicyMutation = graphql` + mutation CreatePolicyPageMutation( + $input: CreatePolicyInput! + $connections: [ID!]! + ) { + createPolicy(input: $input) { + policyEdge @prependEdge(connections: $connections) { + node { + id + name + content + status + } + } + } + } +`; + +export default function CreatePolicyPage() { + const navigate = useNavigate(); + const { organizationId } = useParams(); + const [name, setName] = useState(""); + const [content, setContent] = useState(""); + const [status, setStatus] = useState<"DRAFT" | "ACTIVE">("DRAFT"); + const [isSubmitting, setIsSubmitting] = useState(false); + const { toast } = useToast(); + + const [commitMutation] = + useMutation(CreatePolicyMutation); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + const input = { + organizationId: organizationId!, + name, + content, + status, + }; + + commitMutation({ + variables: { + input, + connections: [ + ConnectionHandler.getConnectionID( + organizationId!, + "PolicyListPage_policies" + ), + ], + }, + onCompleted: (response, errors) => { + setIsSubmitting(false); + if (errors) { + console.error("Error creating policy:", errors); + toast({ + title: "Error", + description: "Failed to create policy. Please try again.", + variant: "destructive", + }); + return; + } + + toast({ + title: "Success", + description: "Policy created successfully!", + }); + + navigate( + `/organizations/${organizationId}/policies/${response.createPolicy.policyEdge.node.id}` + ); + }, + onError: (error) => { + setIsSubmitting(false); + console.error("Error creating policy:", error); + toast({ + title: "Error", + description: "Failed to create policy. Please try again.", + variant: "destructive", + }); + }, + }); + }; + + return ( + <> + + Create Policy - Probo Console + +
+
+
+
+ +
+
+
+

Create Policy

+

+ Create a new policy for your organization +

+
+
+ +
+
+ + + Policy Information + + +
+ + setName(e.target.value)} + required + /> +
+ +
+ +