diff --git a/apps/console/src/pages/organizations/OrganizationLayout.tsx b/apps/console/src/pages/organizations/OrganizationLayout.tsx index 4ba5f4d94..30d3e65f2 100644 --- a/apps/console/src/pages/organizations/OrganizationLayout.tsx +++ b/apps/console/src/pages/organizations/OrganizationLayout.tsx @@ -7,7 +7,10 @@ import { BreadCrumb } from "./OrganizationBreadcrumb"; import { cn } from "@/lib/utils"; export default function OrganizationLayout() { - const match = useMatch( + const controlMatch = useMatch( + "organizations/:organizationId/frameworks/:frameworkId/controls/*" + ); + const frameworkMatch = useMatch( "organizations/:organizationId/frameworks/:frameworkId" ); @@ -19,16 +22,16 @@ export default function OrganizationLayout() {
-
+
-
+
diff --git a/apps/console/src/pages/organizations/Routes.tsx b/apps/console/src/pages/organizations/Routes.tsx index 7f43c0185..eae7f37c0 100644 --- a/apps/console/src/pages/organizations/Routes.tsx +++ b/apps/console/src/pages/organizations/Routes.tsx @@ -1,26 +1,26 @@ import { Route, Routes } from "react-router"; -import { VendorListPage } from "./vendors/VendorListPage"; -import OrganizationLayout from "./OrganizationLayout"; +import NotFoundPage from "../NotFoundPage"; +import { CreateOrganizationPage } from "./CreateOrganizationPage"; +import HomePage from "./HomePage"; import NoOrganizationLayout from "./NoOrganizationLayout"; +import OrganizationLayout from "./OrganizationLayout"; +import { SettingsPage } from "./SettingsPage"; +import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage"; import { FrameworkListPage } from "./frameworks/FrameworkListPage"; import { FrameworkPage } from "./frameworks/FrameworkPage"; -import { PeopleListPage } from "./people/PeopleListPage"; -import { CreatePeoplePage } from "./people/CreatePeoplePage"; -import { PeoplePage } from "./people/PeoplePage"; -import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage"; import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage"; +import { ControlPage } from "./frameworks/controls/ControlPage"; import { MitigationPage } from "./mitigations/MitigationPage"; -import { VendorPage } from "./vendors/VendorPage"; -import { PolicyListPage } from "./policies/PolicyListPage"; +import { MitigationListPage } from "./mitigations/MitigationListPage"; +import { CreatePeoplePage } from "./people/CreatePeoplePage"; +import { PeopleListPage } from "./people/PeopleListPage"; +import { PeoplePage } from "./people/PeoplePage"; import { CreatePolicyPage } from "./policies/CreatePolicyPage"; +import { PolicyListPage } from "./policies/PolicyListPage"; import { PolicyPage } from "./policies/PolicyPage"; import { UpdatePolicyPage } from "./policies/UpdatePolicyPage"; -import { SettingsPage } from "./SettingsPage"; -import { CreateOrganizationPage } from "./CreateOrganizationPage"; -import { MitigationListPage } from "./mitigations/MitigationListPage"; - -import HomePage from "./HomePage"; -import NotFoundPage from "../NotFoundPage"; +import { VendorListPage } from "./vendors/VendorListPage"; +import { VendorPage } from "./vendors/VendorPage"; export function OrganizationsRoutes() { return ( @@ -33,18 +33,16 @@ export function OrganizationsRoutes() { } /> } /> } /> - } /> - } - /> + + }> + + } /> + + } /> + } /> + } /> } /> - {/* } /> - } - /> */} } /> } /> } /> diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx index df350a23c..a0d39239e 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx @@ -1,9 +1,9 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent } from "@/components/ui/card"; import { Suspense } from "react"; -import { useLocation } from "react-router"; -import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { useMatch } from "react-router"; import { lazy } from "@probo/react-lazy"; +import ErrorBoundary from "@/components/ErrorBoundary"; const FrameworkView = lazy(() => import("./FrameworkView")); @@ -36,13 +36,15 @@ export function FrameworkViewSkeleton() { } export function FrameworkPage() { - const location = useLocation(); + const match = useMatch( + "organizations/:organizationId/frameworks/:frameworkId/*" + ); return ( - }> - + }> + - + ); } diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkView.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkView.tsx index 7786d8e89..55d2becbf 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkView.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkView.tsx @@ -1,5 +1,5 @@ import { Suspense, useEffect, useState, useCallback } from "react"; -import { useParams, useNavigate, Link } from "react-router"; +import { useParams, useNavigate, Link, Outlet } from "react-router"; import { graphql, PreloadedQuery, @@ -8,8 +8,6 @@ import { useMutation, ConnectionHandler, } from "react-relay"; -import { Plus } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -24,6 +22,8 @@ import type { FrameworkViewQuery as FrameworkViewQueryType } from "./__generated import type { FrameworkViewDeleteMutation } from "./__generated__/FrameworkViewDeleteMutation.graphql"; import { PageTemplate } from "@/components/PageTemplate"; import { FrameworkViewSkeleton } from "./FrameworkPage"; +import { ControlList } from "./FrameworkView/ControlList"; +import ControlView from "./controls/ControlView"; const FrameworkViewQuery = graphql` query FrameworkViewQuery($frameworkId: ID!) { @@ -32,14 +32,14 @@ const FrameworkViewQuery = graphql` ... on Framework { name description - controls(first: 100, orderBy: { field: CREATED_AT, direction: ASC }) + ...ControlList_List + controls(first: 1, orderBy: { field: CREATED_AT, direction: ASC }) @connection(key: "FrameworkView_controls") { edges { node { id referenceId name - description } } } @@ -66,15 +66,13 @@ function FrameworkViewContent({ }) { const data = usePreloadedQuery(FrameworkViewQuery, queryRef); const framework = data.node; + const control = data.node.controls?.edges[0]; const navigate = useNavigate(); - const { organizationId } = useParams(); + const { organizationId, controlId } = useParams(); const { toast } = useToast(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - // Extract controls from the GraphQL response - const controls = framework?.controls?.edges?.map((edge) => edge.node) || []; - // Setup delete mutation const [commitDeleteMutation] = useMutation( DeleteFrameworkMutation @@ -151,54 +149,9 @@ function FrameworkViewContent({
} > -
- {controls.length > 0 ? ( - controls.map((control) => ( - - -
{ - navigate( - `/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}` - ); - }} - > - - {control.referenceId} - - {control.name} -
-
- {control.description} -
-
- -

Mitigations

-
-

- Mitigations will be displayed here once connected to this - control -

-
- -
-
-
-
- )) - ) : ( -
- No controls available for this framework -
- )} +
+ + {controlId ? : }
{/* Delete Confirmation Dialog */} @@ -248,7 +201,7 @@ export default function FrameworkView() { return ( }> - {queryRef && } + ); } diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkView/ControlList.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkView/ControlList.tsx new file mode 100644 index 000000000..68b849c96 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/FrameworkView/ControlList.tsx @@ -0,0 +1,80 @@ +import { graphql, useFragment } from "react-relay"; +import { ControlList_List$key } from "./__generated__/ControlList_List.graphql"; +import { NavLink, useParams } from "react-router"; +import { cn } from "@/lib/utils"; + +export const controlListFragment = graphql` + fragment ControlList_List on Framework { + controls(first: 100, orderBy: { field: CREATED_AT, direction: ASC }) + @connection(key: "FrameworkView_controls") { + edges { + node { + id + referenceId + name + } + } + } + } +`; + +interface ControlListProps { + fragmentKey: ControlList_List$key; +} + +export function ControlList(props: ControlListProps) { + const { organizationId, frameworkId, controlId } = useParams<{ + organizationId: string; + frameworkId: string; + controlId?: string; + }>(); + const { fragmentKey } = props; + const { controls } = useFragment( + controlListFragment, + fragmentKey + ); + + if (controls.edges.length === 0) { + return "No controls available for this framework"; + } + + return ( + + ); +} diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkView/__generated__/ControlList_List.graphql.ts b/apps/console/src/pages/organizations/frameworks/FrameworkView/__generated__/ControlList_List.graphql.ts new file mode 100644 index 000000000..ed637d92a --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/FrameworkView/__generated__/ControlList_List.graphql.ts @@ -0,0 +1,156 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ControlList_List$data = { + readonly controls: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly referenceId: string; + }; + }>; + }; + readonly " $fragmentType": "ControlList_List"; +}; +export type ControlList_List$key = { + readonly " $data"?: ControlList_List$data; + readonly " $fragmentSpreads": FragmentRefs<"ControlList_List">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "controls" + ] + } + ] + }, + "name": "ControlList_List", + "selections": [ + { + "alias": "controls", + "args": [ + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "CREATED_AT" + } + } + ], + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "__FrameworkView_controls_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "__FrameworkView_controls_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + } + ], + "type": "Framework", + "abstractKey": null +}; + +(node as any).hash = "aa95219172d60909d7a503d246e0f4f5"; + +export default node; diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkViewQuery.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkViewQuery.graphql.ts index b9d022100..04d7771c9 100644 --- a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<0185ea6dc887f2bbb5ca70ec28e916b2>> + * @generated SignedSource<<11d451bf710d818645f55fe5f0ddb114>> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type FrameworkViewQuery$variables = { frameworkId: string; }; @@ -17,7 +18,6 @@ export type FrameworkViewQuery$data = { readonly controls?: { readonly edges: ReadonlyArray<{ readonly node: { - readonly description: string; readonly id: string; readonly name: string; readonly referenceId: string; @@ -27,6 +27,7 @@ export type FrameworkViewQuery$data = { readonly description?: string; readonly id: string; readonly name?: string; + readonly " $fragmentSpreads": FragmentRefs<"ControlList_List">; }; }; export type FrameworkViewQuery = { @@ -111,7 +112,6 @@ v7 = [ "storageKey": null }, (v3/*: any*/), - (v4/*: any*/), (v6/*: any*/) ], "storageKey": null @@ -181,6 +181,11 @@ return { "selections": [ (v3/*: any*/), (v4/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "ControlList_List" + }, { "alias": "controls", "args": [ @@ -256,7 +261,7 @@ return { ] }, "params": { - "cacheID": "a1fa5a0efc291c92478c061d32b90e43", + "cacheID": "5bf64e09bf7f44c56b75bbb63b9b272f", "id": null, "metadata": { "connection": [ @@ -273,11 +278,11 @@ return { }, "name": "FrameworkViewQuery", "operationKind": "query", - "text": "query FrameworkViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 100, orderBy: {field: CREATED_AT, direction: ASC}) {\n edges {\n node {\n id\n referenceId\n name\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + "text": "query FrameworkViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n ...ControlList_List\n controls(first: 100, orderBy: {field: CREATED_AT, direction: ASC}) {\n edges {\n node {\n id\n referenceId\n name\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n\nfragment ControlList_List on Framework {\n controls(first: 100, orderBy: {field: CREATED_AT, direction: ASC}) {\n edges {\n node {\n id\n referenceId\n name\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); -(node as any).hash = "ff9593fc321c840ae9ef1da48a13c3e5"; +(node as any).hash = "d4837d95c7bd721bf1851c1b977fd018"; export default node; diff --git a/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx b/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx new file mode 100644 index 000000000..1c5a949ff --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx @@ -0,0 +1,38 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Suspense } from "react"; +import { useLocation } from "react-router"; +import { lazy } from "@probo/react-lazy"; +import ErrorBoundary from "@/components/ErrorBoundary"; + +const ControlView = lazy(() => import("./ControlView")); + +export function ControlViewSkeleton() { + return ( +
+ {[1, 2, 3].map((i) => ( + + +
+
+
+
+
+
+ + + ))} +
+ ); +} + +export function ControlPage() { + const location = useLocation(); + + return ( + }> + + + + + ); +} diff --git a/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx new file mode 100644 index 000000000..025ae2a92 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx @@ -0,0 +1,98 @@ +import { + graphql, + PreloadedQuery, + usePreloadedQuery, + useQueryLoader, +} from "react-relay"; +import { ControlViewSkeleton } from "./ControlPage"; +import { Suspense, useEffect } from "react"; +import { + ControlViewQuery, + ControlViewQuery$data, +} from "./__generated__/ControlViewQuery.graphql"; +import { Button } from "@/components/ui/button"; +import { Link, useParams } from "react-router"; +import { Plus } from "lucide-react"; + +const controlViewQuery = graphql` + query ControlViewQuery($controlId: ID!) { + node(id: $controlId) { + id + ... on Control { + description + name + referenceId + } + } + } +`; + +export function Control({ + control, +}: { + control: ControlViewQuery$data["node"]; +}) { + const { organizationId, frameworkId } = useParams<{ + organizationId: string; + frameworkId: string; + }>(); + + return ( +
+
+ {control.referenceId} +
+
+

{control.name}

+

+ Security measures +

+

+ Security measures will be displayed here once connected to this + control +

+
+ +
+
+
+ ); +} + +function ControlViewContent({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { + const { node: control } = usePreloadedQuery( + controlViewQuery, + queryRef + ); + + return ; +} + +export default function ControlView({ controlId }: { controlId?: string }) { + const { controlId: controlIdParam } = useParams(); + const [queryRef, loadQuery] = + useQueryLoader(controlViewQuery); + + useEffect(() => { + loadQuery({ controlId: (controlId ?? controlIdParam)! }); + }, [loadQuery, controlId, controlIdParam]); + + if (!queryRef) return ; + + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewQuery.graphql.ts b/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewQuery.graphql.ts new file mode 100644 index 000000000..13f163e3d --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewQuery.graphql.ts @@ -0,0 +1,143 @@ +/** + * @generated SignedSource<<08f7a2f65fb9ae3f6970e76485fa59e4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ControlViewQuery$variables = { + controlId: string; +}; +export type ControlViewQuery$data = { + readonly node: { + readonly description?: string; + readonly id: string; + readonly name?: string; + readonly referenceId?: string; + }; +}; +export type ControlViewQuery = { + response: ControlViewQuery$data; + variables: ControlViewQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "controlId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "controlId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + } + ], + "type": "Control", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "ControlViewQuery", + "selections": [ + { + "alias": null, + "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": "ControlViewQuery", + "selections": [ + { + "alias": null, + "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": "be2e7ad630198d4c5476be3767a45d97", + "id": null, + "metadata": {}, + "name": "ControlViewQuery", + "operationKind": "query", + "text": "query ControlViewQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n id\n ... on Control {\n description\n name\n referenceId\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "d692873ef9c85a8f56b8a35e61e5a5bc"; + +export default node; diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index c64f6b762..62517d126 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -844,6 +844,13 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return nil, err } return types.NewPolicy(policy), nil + case coredata.ControlEntityType: + control, err := svc.Controls.Get(ctx, id) + if err != nil { + return nil, err + } + + return types.NewControl(control), nil default: }