Framework view with control list and view
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -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() {
|
||||
<SidebarInset>
|
||||
<div
|
||||
className={cn(
|
||||
"p-8",
|
||||
match
|
||||
"p-8 h-screen overflow-auto",
|
||||
controlMatch || frameworkMatch
|
||||
? "w-full"
|
||||
: "mx-auto w-lg md:w-xl lg:w-3xl xl:w-4xl 2xl:w-5xl"
|
||||
)}
|
||||
>
|
||||
<header className="flex shrink-0 items-center gap-2">
|
||||
<header className="flex shrink-0 items-center gap-2 mb-7">
|
||||
<BreadCrumb />
|
||||
</header>
|
||||
<div className="mt-7 w-full">
|
||||
<div className="w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<Route path="vendors" element={<VendorListPage />} />
|
||||
<Route path="frameworks" element={<FrameworkListPage />} />
|
||||
<Route path="frameworks/create" element={<CreateFrameworkPage />} />
|
||||
<Route path="frameworks/:frameworkId" element={<FrameworkPage />} />
|
||||
<Route
|
||||
path="frameworks/:frameworkId/update"
|
||||
element={<UpdateFrameworkPage />}
|
||||
/>
|
||||
<Route path="frameworks/:frameworkId/*">
|
||||
<Route element={<FrameworkPage />}>
|
||||
<Route index />
|
||||
<Route path="controls/:controlId" element={<ControlPage />} />
|
||||
</Route>
|
||||
<Route path="update" element={<UpdateFrameworkPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
<Route path="mitigations" element={<MitigationListPage />} />
|
||||
<Route path="mitigations/:mitigationId" element={<MitigationPage />} />
|
||||
{/* <Route path="mitigations/create" element={<CreateMitigationPage />} />
|
||||
<Route
|
||||
path="mitigations/:mitigationId/update"
|
||||
element={<UpdateMitigationPage />}
|
||||
/> */}
|
||||
<Route path="vendors/:vendorId" element={<VendorPage />} />
|
||||
<Route path="policies" element={<PolicyListPage />} />
|
||||
<Route path="policies/create" element={<CreatePolicyPage />} />
|
||||
|
||||
@@ -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 (
|
||||
<Suspense key={location.pathname} fallback={<FrameworkViewSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<Suspense key={match?.pathnameBase} fallback={<FrameworkViewSkeleton />}>
|
||||
<ErrorBoundary key={match?.pathnameBase}>
|
||||
<FrameworkView />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<FrameworkViewDeleteMutation>(
|
||||
DeleteFrameworkMutation
|
||||
@@ -151,54 +149,9 @@ function FrameworkViewContent({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-6">
|
||||
{controls.length > 0 ? (
|
||||
controls.map((control) => (
|
||||
<Card key={control.id} className="overflow-hidden">
|
||||
<CardHeader className="bg-muted/20">
|
||||
<div
|
||||
className="font-medium cursor-pointer flex items-center"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-sm mr-3">
|
||||
{control.referenceId}
|
||||
</span>
|
||||
<CardTitle>{control.name}</CardTitle>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1 whitespace-pre-wrap">
|
||||
{control.description}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<h4 className="text-sm font-semibold mb-3">Mitigations</h4>
|
||||
<div className="text-sm text-muted-foreground text-center py-4 border rounded-md">
|
||||
<p>
|
||||
Mitigations will be displayed here once connected to this
|
||||
control
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/frameworks/${framework.id}/mitigations/create`}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Link Mitigation
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-6 text-center text-muted-foreground">
|
||||
No controls available for this framework
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-start justify-start w-full h-[calc(100vh-216px)] overflow-hidden -ml-8">
|
||||
<ControlList fragmentKey={framework} />
|
||||
{controlId ? <Outlet /> : <ControlView controlId={control?.node.id} />}
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
@@ -248,7 +201,7 @@ export default function FrameworkView() {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FrameworkViewSkeleton />}>
|
||||
{queryRef && <FrameworkViewContent queryRef={queryRef} />}
|
||||
<FrameworkViewContent queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ControlList_List$key>(
|
||||
controlListFragment,
|
||||
fragmentKey
|
||||
);
|
||||
|
||||
if (controls.edges.length === 0) {
|
||||
return "No controls available for this framework";
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="shrink-0 w-70 border-r border-gray-100 h-full overflow-x-hidden overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-xs">
|
||||
{controls.edges.map(({ node: control }, i) => (
|
||||
<NavLink
|
||||
key={control.id}
|
||||
to={`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${control.id}`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block pl-8 pr-4 py-4 hover:bg-gray-50 border-b border-gray-100",
|
||||
(isActive || (i === 0 && !controlId)) && "bg-gray-50"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-block font-mono text-sm px-1 py-0.25 rounded-sm bg-lime-2 border border-lime-4 text-lime-9 font-semibold",
|
||||
isActive && "font-bold bg-lime-3 border-lime-6 text-lime-11"
|
||||
)}
|
||||
>
|
||||
{control.referenceId}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm leading-none break-words mt-2",
|
||||
isActive && "font-medium"
|
||||
)}
|
||||
>
|
||||
{control.name}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
))}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
156
apps/console/src/pages/organizations/frameworks/FrameworkView/__generated__/ControlList_List.graphql.ts
generated
Normal file
156
apps/console/src/pages/organizations/frameworks/FrameworkView/__generated__/ControlList_List.graphql.ts
generated
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @generated SignedSource<<a3e361b3b8152318476292098dd890f4>>
|
||||
* @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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<div className="bg-muted w-24 h-24 rounded-full animate-pulse mb-4" />
|
||||
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
||||
<div className="h-20 w-full bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ControlPage() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Suspense key={location.pathname} fallback={<ControlViewSkeleton />}>
|
||||
<ErrorBoundary key={location.pathname}>
|
||||
<ControlView />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="w-auto p-5 flex items-start gap-5">
|
||||
<div className="font-mono text-lg px-1 py-0.25 rounded-sm bg-lime-3 border border-lime-6 text-lime-11 font-bold">
|
||||
{control.referenceId}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-medium">{control.name}</h2>
|
||||
<h3 className="text-xl font-medium text-gray-600 mt-8">
|
||||
Security measures
|
||||
</h3>
|
||||
<p className="mt-4">
|
||||
Security measures will be displayed here once connected to this
|
||||
control
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<Button asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/frameworks/${frameworkId}/mitigations/create`}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Link Security Measures
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ControlViewContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<ControlViewQuery>;
|
||||
}) {
|
||||
const { node: control } = usePreloadedQuery<ControlViewQuery>(
|
||||
controlViewQuery,
|
||||
queryRef
|
||||
);
|
||||
|
||||
return <Control control={control} />;
|
||||
}
|
||||
|
||||
export default function ControlView({ controlId }: { controlId?: string }) {
|
||||
const { controlId: controlIdParam } = useParams();
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<ControlViewQuery>(controlViewQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ controlId: (controlId ?? controlIdParam)! });
|
||||
}, [loadQuery, controlId, controlIdParam]);
|
||||
|
||||
if (!queryRef) return <ControlViewSkeleton />;
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<ControlViewContent queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
143
apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewQuery.graphql.ts
generated
Normal file
143
apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewQuery.graphql.ts
generated
Normal file
@@ -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;
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user