Fix framework page displaying first control
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -388,6 +388,7 @@ function BreadcrumbControl() {
|
|||||||
{ controlId: controlId! },
|
{ controlId: controlId! },
|
||||||
{ fetchPolicy: "store-or-network" }
|
{ fetchPolicy: "store-or-network" }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import NoOrganizationLayout from "./NoOrganizationLayout";
|
|||||||
import OrganizationLayout from "./OrganizationLayout";
|
import OrganizationLayout from "./OrganizationLayout";
|
||||||
import { SettingsPage } from "./SettingsPage";
|
import { SettingsPage } from "./SettingsPage";
|
||||||
import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
||||||
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
|
||||||
import { FrameworkLayout } from "./frameworks/FrameworkLayout";
|
import { FrameworkLayout } from "./frameworks/FrameworkLayout";
|
||||||
|
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
||||||
|
import { FrameworkPage } from "./frameworks/FrameworkPage";
|
||||||
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
||||||
import { ControlPage } from "./frameworks/controls/ControlPage";
|
import { ControlPage } from "./frameworks/controls/ControlPage";
|
||||||
import { EditMitigationPage } from "./mitigations/EditMitigationPage";
|
import { EditMitigationPage } from "./mitigations/EditMitigationPage";
|
||||||
@@ -40,7 +41,7 @@ export function OrganizationsRoutes() {
|
|||||||
<Route path="frameworks/create" element={<CreateFrameworkPage />} />
|
<Route path="frameworks/create" element={<CreateFrameworkPage />} />
|
||||||
<Route path="frameworks/:frameworkId/*">
|
<Route path="frameworks/:frameworkId/*">
|
||||||
<Route element={<FrameworkLayout />}>
|
<Route element={<FrameworkLayout />}>
|
||||||
<Route index />
|
<Route index element={<FrameworkPage />} />
|
||||||
<Route path="controls/:controlId" element={<ControlPage />} />
|
<Route path="controls/:controlId" element={<ControlPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="update" element={<UpdateFrameworkPage />} />
|
<Route path="update" element={<UpdateFrameworkPage />} />
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { FrameworkViewQuery as FrameworkViewQueryType } from "./__generated__/FrameworkViewQuery.graphql";
|
import type { FrameworkLayoutViewQuery as FrameworkLayoutViewQueryType } from "./__generated__/FrameworkLayoutViewQuery.graphql";
|
||||||
import type { FrameworkViewDeleteMutation } from "./__generated__/FrameworkViewDeleteMutation.graphql";
|
import type { FrameworkLayoutViewDeleteMutation } from "./__generated__/FrameworkLayoutViewDeleteMutation.graphql";
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { FrameworkLayoutViewSkeleton } from "./FrameworkLayout";
|
import { FrameworkLayoutViewSkeleton } from "./FrameworkLayout";
|
||||||
import { ControlList } from "./FrameworkLayoutView/ControlList";
|
import { ControlList } from "./FrameworkLayoutView/ControlList";
|
||||||
|
|
||||||
const FrameworkViewQuery = graphql`
|
const FrameworkLayoutViewQuery = graphql`
|
||||||
query FrameworkViewQuery($frameworkId: ID!) {
|
query FrameworkLayoutViewQuery($frameworkId: ID!) {
|
||||||
node(id: $frameworkId) {
|
node(id: $frameworkId) {
|
||||||
id
|
id
|
||||||
... on Framework {
|
... on Framework {
|
||||||
@@ -35,7 +35,7 @@ const FrameworkViewQuery = graphql`
|
|||||||
firstControl: controls(
|
firstControl: controls(
|
||||||
first: 1
|
first: 1
|
||||||
orderBy: { field: CREATED_AT, direction: ASC }
|
orderBy: { field: CREATED_AT, direction: ASC }
|
||||||
) @connection(key: "FrameworkView_firstControl") {
|
) @connection(key: "FrameworkLayoutView_firstControl") {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@@ -50,7 +50,7 @@ const FrameworkViewQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const DeleteFrameworkMutation = graphql`
|
const DeleteFrameworkMutation = graphql`
|
||||||
mutation FrameworkViewDeleteMutation(
|
mutation FrameworkLayoutViewDeleteMutation(
|
||||||
$input: DeleteFrameworkInput!
|
$input: DeleteFrameworkInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -60,12 +60,12 @@ const DeleteFrameworkMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function FrameworkViewContent({
|
function FrameworkLayoutViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<FrameworkViewQueryType>;
|
queryRef: PreloadedQuery<FrameworkLayoutViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(FrameworkViewQuery, queryRef);
|
const data = usePreloadedQuery(FrameworkLayoutViewQuery, queryRef);
|
||||||
const framework = data.node;
|
const framework = data.node;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
@@ -74,7 +74,7 @@ function FrameworkViewContent({
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
// Setup delete mutation
|
// Setup delete mutation
|
||||||
const [commitDeleteMutation] = useMutation<FrameworkViewDeleteMutation>(
|
const [commitDeleteMutation] = useMutation<FrameworkLayoutViewDeleteMutation>(
|
||||||
DeleteFrameworkMutation
|
DeleteFrameworkMutation
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -186,10 +186,11 @@ function FrameworkViewContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FrameworkView() {
|
export default function FrameworkLayoutView() {
|
||||||
const { frameworkId } = useParams();
|
const { frameworkId } = useParams();
|
||||||
const [queryRef, loadQuery] =
|
const [queryRef, loadQuery] = useQueryLoader<FrameworkLayoutViewQueryType>(
|
||||||
useQueryLoader<FrameworkViewQueryType>(FrameworkViewQuery);
|
FrameworkLayoutViewQuery
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuery({ frameworkId: frameworkId! });
|
loadQuery({ frameworkId: frameworkId! });
|
||||||
@@ -201,7 +202,7 @@ export default function FrameworkView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FrameworkLayoutViewSkeleton />}>
|
<Suspense fallback={<FrameworkLayoutViewSkeleton />}>
|
||||||
<FrameworkViewContent queryRef={queryRef} />
|
<FrameworkLayoutViewContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 FrameworkView = lazy(() => import("./FrameworkView"));
|
||||||
|
|
||||||
|
export function FrameworkViewSkeleton() {
|
||||||
|
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 FrameworkPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<FrameworkViewSkeleton />}>
|
||||||
|
<ErrorBoundary key={location.pathname}>
|
||||||
|
<FrameworkView />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { FrameworkViewQuery } from "./__generated__/FrameworkViewQuery.graphql";
|
||||||
|
import { Suspense, useEffect } from "react";
|
||||||
|
import { Control } from "./controls/Control";
|
||||||
|
import { FrameworkViewSkeleton } from "./FrameworkPage";
|
||||||
|
|
||||||
|
const frameworkViewQuery = graphql`
|
||||||
|
query FrameworkViewQuery($frameworkId: ID!) {
|
||||||
|
node(id: $frameworkId) {
|
||||||
|
id
|
||||||
|
... on Framework {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
firstControl: controls(
|
||||||
|
first: 1
|
||||||
|
orderBy: { field: CREATED_AT, direction: ASC }
|
||||||
|
) @connection(key: "FrameworkView_firstControl") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ControlFragment_Control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function FrameworkViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<FrameworkViewQuery>;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery<FrameworkViewQuery>(
|
||||||
|
frameworkViewQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data.node.firstControl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Control controlKey={data.node.firstControl.edges[0].node} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FrameworkView() {
|
||||||
|
const { frameworkId } = useParams();
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<FrameworkViewQuery>(frameworkViewQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({ frameworkId: frameworkId! });
|
||||||
|
}, [loadQuery, frameworkId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <FrameworkViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FrameworkViewSkeleton />}>
|
||||||
|
<FrameworkViewContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<9f35422e7d027485b106e6fb8879d460>>
|
* @generated SignedSource<<615df626961da33fb1e48249870eefe1>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type DeleteFrameworkInput = {
|
export type DeleteFrameworkInput = {
|
||||||
frameworkId: string;
|
frameworkId: string;
|
||||||
};
|
};
|
||||||
export type FrameworkViewDeleteMutation$variables = {
|
export type FrameworkLayoutViewDeleteMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: DeleteFrameworkInput;
|
input: DeleteFrameworkInput;
|
||||||
};
|
};
|
||||||
export type FrameworkViewDeleteMutation$data = {
|
export type FrameworkLayoutViewDeleteMutation$data = {
|
||||||
readonly deleteFramework: {
|
readonly deleteFramework: {
|
||||||
readonly deletedFrameworkId: string;
|
readonly deletedFrameworkId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkViewDeleteMutation = {
|
export type FrameworkLayoutViewDeleteMutation = {
|
||||||
response: FrameworkViewDeleteMutation$data;
|
response: FrameworkLayoutViewDeleteMutation$data;
|
||||||
variables: FrameworkViewDeleteMutation$variables;
|
variables: FrameworkLayoutViewDeleteMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -59,7 +59,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "FrameworkViewDeleteMutation",
|
"name": "FrameworkLayoutViewDeleteMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -84,7 +84,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "FrameworkViewDeleteMutation",
|
"name": "FrameworkLayoutViewDeleteMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -117,16 +117,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b10bac4b295c5d2a9a564cbba4569633",
|
"cacheID": "bf9a90fe1038778a5bc29ea20e8b07b9",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "FrameworkViewDeleteMutation",
|
"name": "FrameworkLayoutViewDeleteMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation FrameworkViewDeleteMutation(\n $input: DeleteFrameworkInput!\n) {\n deleteFramework(input: $input) {\n deletedFrameworkId\n }\n}\n"
|
"text": "mutation FrameworkLayoutViewDeleteMutation(\n $input: DeleteFrameworkInput!\n) {\n deleteFramework(input: $input) {\n deletedFrameworkId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "8b20886821651ffb7e335df58fb46fc9";
|
(node as any).hash = "52564100dcf31f83cabd0683a616982c";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
316
apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewQuery.graphql.ts
generated
Normal file
316
apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<4f0c7f7d3fb0272b838162d615ba905b>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type FrameworkLayoutViewQuery$variables = {
|
||||||
|
frameworkId: string;
|
||||||
|
};
|
||||||
|
export type FrameworkLayoutViewQuery$data = {
|
||||||
|
readonly node: {
|
||||||
|
readonly description?: string;
|
||||||
|
readonly firstControl?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly referenceId: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id: string;
|
||||||
|
readonly name?: string;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"ControlList_List">;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type FrameworkLayoutViewQuery = {
|
||||||
|
response: FrameworkLayoutViewQuery$data;
|
||||||
|
variables: FrameworkLayoutViewQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "frameworkId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "frameworkId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v5 = {
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "orderBy",
|
||||||
|
"value": {
|
||||||
|
"direction": "ASC",
|
||||||
|
"field": "CREATED_AT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
v6 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v7 = [
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "referenceId",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v8 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
},
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
v9 = [
|
||||||
|
"orderBy"
|
||||||
|
],
|
||||||
|
v10 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
(v5/*: any*/)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworkLayoutViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "ControlList_List"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "firstControl",
|
||||||
|
"args": [
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__FrameworkLayoutView_firstControl_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v7/*: any*/),
|
||||||
|
"storageKey": "__FrameworkLayoutView_firstControl_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Framework",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "FrameworkLayoutViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v6/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "controls",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v7/*: any*/),
|
||||||
|
"storageKey": "controls(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"filters": (v9/*: any*/),
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "FrameworkView_controls",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "controls"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "firstControl",
|
||||||
|
"args": (v10/*: any*/),
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "controls",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v7/*: any*/),
|
||||||
|
"storageKey": "controls(first:1,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "firstControl",
|
||||||
|
"args": (v10/*: any*/),
|
||||||
|
"filters": (v9/*: any*/),
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "FrameworkLayoutView_firstControl",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "controls"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Framework",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "98ca0e0c503bb7c5e3e538e8831eeefa",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"node",
|
||||||
|
"firstControl"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "FrameworkLayoutViewQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query FrameworkLayoutViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n ...ControlList_List\n firstControl: controls(first: 1, 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 = "1935177e374d21015e102745c4d51e94";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<0476147756ad426abb8651c6a62821f3>>
|
* @generated SignedSource<<2f29bc26fdb324cad6eb8d7bdd48447f>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -19,15 +19,12 @@ export type FrameworkViewQuery$data = {
|
|||||||
readonly firstControl?: {
|
readonly firstControl?: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly id: string;
|
readonly " $fragmentSpreads": FragmentRefs<"ControlFragment_Control">;
|
||||||
readonly name: string;
|
|
||||||
readonly referenceId: string;
|
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"ControlList_List">;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkViewQuery = {
|
export type FrameworkViewQuery = {
|
||||||
@@ -86,47 +83,14 @@ v6 = {
|
|||||||
"name": "__typename",
|
"name": "__typename",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v7 = [
|
v7 = {
|
||||||
{
|
|
||||||
"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": [
|
|
||||||
(v2/*: any*/),
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "referenceId",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
(v3/*: any*/),
|
|
||||||
(v6/*: any*/)
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "cursor",
|
"name": "cursor",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
},
|
||||||
],
|
v8 = {
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "PageInfo",
|
"concreteType": "PageInfo",
|
||||||
@@ -150,20 +114,8 @@ v7 = [
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
},
|
||||||
],
|
|
||||||
v8 = [
|
|
||||||
{
|
|
||||||
"kind": "Literal",
|
|
||||||
"name": "first",
|
|
||||||
"value": 100
|
|
||||||
},
|
|
||||||
(v5/*: any*/)
|
|
||||||
],
|
|
||||||
v9 = [
|
v9 = [
|
||||||
"orderBy"
|
|
||||||
],
|
|
||||||
v10 = [
|
|
||||||
{
|
{
|
||||||
"kind": "Literal",
|
"kind": "Literal",
|
||||||
"name": "first",
|
"name": "first",
|
||||||
@@ -192,11 +144,6 @@ return {
|
|||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v3/*: any*/),
|
||||||
(v4/*: any*/),
|
(v4/*: any*/),
|
||||||
{
|
|
||||||
"args": null,
|
|
||||||
"kind": "FragmentSpread",
|
|
||||||
"name": "ControlList_List"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"alias": "firstControl",
|
"alias": "firstControl",
|
||||||
"args": [
|
"args": [
|
||||||
@@ -206,7 +153,38 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__FrameworkView_firstControl_connection",
|
"name": "__FrameworkView_firstControl_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v7/*: any*/),
|
"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": [
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "ControlFragment_Control"
|
||||||
|
},
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v7/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v8/*: any*/)
|
||||||
|
],
|
||||||
"storageKey": "__FrameworkView_firstControl_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
"storageKey": "__FrameworkView_firstControl_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -241,39 +219,58 @@ return {
|
|||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v3/*: any*/),
|
||||||
(v4/*: any*/),
|
(v4/*: any*/),
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v8/*: any*/),
|
|
||||||
"concreteType": "ControlConnection",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "controls",
|
|
||||||
"plural": false,
|
|
||||||
"selections": (v7/*: any*/),
|
|
||||||
"storageKey": "controls(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v8/*: any*/),
|
|
||||||
"filters": (v9/*: any*/),
|
|
||||||
"handle": "connection",
|
|
||||||
"key": "FrameworkView_controls",
|
|
||||||
"kind": "LinkedHandle",
|
|
||||||
"name": "controls"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"alias": "firstControl",
|
"alias": "firstControl",
|
||||||
"args": (v10/*: any*/),
|
"args": (v9/*: any*/),
|
||||||
"concreteType": "ControlConnection",
|
"concreteType": "ControlConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "controls",
|
"name": "controls",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v7/*: any*/),
|
"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": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v4/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "referenceId",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v7/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v8/*: any*/)
|
||||||
|
],
|
||||||
"storageKey": "controls(first:1,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
"storageKey": "controls(first:1,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "firstControl",
|
"alias": "firstControl",
|
||||||
"args": (v10/*: any*/),
|
"args": (v9/*: any*/),
|
||||||
"filters": (v9/*: any*/),
|
"filters": [
|
||||||
|
"orderBy"
|
||||||
|
],
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "FrameworkView_firstControl",
|
"key": "FrameworkView_firstControl",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
@@ -289,7 +286,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "17fd996ebe2e664652323e290a027cfa",
|
"cacheID": "c7d856630ed8706055e1ff572d0e7293",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -306,11 +303,11 @@ return {
|
|||||||
},
|
},
|
||||||
"name": "FrameworkViewQuery",
|
"name": "FrameworkViewQuery",
|
||||||
"operationKind": "query",
|
"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 ...ControlList_List\n firstControl: controls(first: 1, 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"
|
"text": "query FrameworkViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n firstControl: controls(first: 1, orderBy: {field: CREATED_AT, direction: ASC}) {\n edges {\n node {\n ...ControlFragment_Control\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n\nfragment ControlFragment_Control on Control {\n id\n description\n name\n referenceId\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "2567d3e2996074a21656830be7f807f1";
|
(node as any).hash = "44f29d19bae9e1a0426f44f5d0edca45";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -0,0 +1,779 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DialogHeader, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@radix-ui/react-dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from "@radix-ui/react-select";
|
||||||
|
import { Search, Loader2, X, LinkIcon } from "lucide-react";
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
fetchQuery,
|
||||||
|
graphql,
|
||||||
|
useFragment,
|
||||||
|
useMutation,
|
||||||
|
useRelayEnvironment,
|
||||||
|
} from "react-relay";
|
||||||
|
import { useParams, Link } from "react-router";
|
||||||
|
import {
|
||||||
|
ControlLinkedMitigationsQuery$data,
|
||||||
|
ControlLinkedMitigationsQuery,
|
||||||
|
} from "./__generated__/ControlLinkedMitigationsQuery.graphql";
|
||||||
|
import {
|
||||||
|
ControlOrganizationMitigationsQuery$data,
|
||||||
|
ControlOrganizationMitigationsQuery,
|
||||||
|
} from "./__generated__/ControlOrganizationMitigationsQuery.graphql";
|
||||||
|
import { ControlFragment_Control$key } from "./__generated__/ControlFragment_Control.graphql";
|
||||||
|
|
||||||
|
const controlFragment = graphql`
|
||||||
|
fragment ControlFragment_Control on Control {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
name
|
||||||
|
referenceId
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// New query to fetch linked mitigations
|
||||||
|
const linkedMitigationsQuery = graphql`
|
||||||
|
query ControlLinkedMitigationsQuery($controlId: ID!) {
|
||||||
|
control: node(id: $controlId) {
|
||||||
|
id
|
||||||
|
... on Control {
|
||||||
|
mitigations(first: 100) @connection(key: "Control__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Query to fetch all mitigations for the organization
|
||||||
|
const organizationMitigationsQuery = graphql`
|
||||||
|
query ControlOrganizationMitigationsQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
mitigations(first: 100) @connection(key: "Organization__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to create a mapping between a control and a mitigation
|
||||||
|
const createMitigationMappingMutation = graphql`
|
||||||
|
mutation ControlCreateMitigationMappingMutation(
|
||||||
|
$input: CreateControlMappingInput!
|
||||||
|
) {
|
||||||
|
createControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to delete a mapping between a control and a mitigation
|
||||||
|
const deleteMitigationMappingMutation = graphql`
|
||||||
|
mutation ControlDeleteMitigationMappingMutation(
|
||||||
|
$input: DeleteControlMappingInput!
|
||||||
|
) {
|
||||||
|
deleteControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function Control({
|
||||||
|
controlKey,
|
||||||
|
}: {
|
||||||
|
controlKey: ControlFragment_Control$key;
|
||||||
|
}) {
|
||||||
|
const { organizationId /* frameworkId */ } = useParams<{
|
||||||
|
organizationId: string;
|
||||||
|
frameworkId: string;
|
||||||
|
}>();
|
||||||
|
const control = useFragment(controlFragment, controlKey);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const environment = useRelayEnvironment();
|
||||||
|
|
||||||
|
// State for mitigation mapping
|
||||||
|
const [isMitigationMappingDialogOpen, setIsMitigationMappingDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [linkedMitigationsData, setLinkedMitigationsData] =
|
||||||
|
useState<ControlLinkedMitigationsQuery$data | null>(null);
|
||||||
|
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
||||||
|
useState<ControlOrganizationMitigationsQuery$data | null>(null);
|
||||||
|
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
||||||
|
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
||||||
|
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
||||||
|
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Create mutation hooks
|
||||||
|
const [commitCreateMitigationMapping] = useMutation(
|
||||||
|
createMitigationMappingMutation
|
||||||
|
);
|
||||||
|
const [commitDeleteMitigationMapping] = useMutation(
|
||||||
|
deleteMitigationMappingMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load initial linked mitigations data
|
||||||
|
useEffect(() => {
|
||||||
|
if (control.id) {
|
||||||
|
setIsLoadingMitigations(true);
|
||||||
|
fetchQuery<ControlLinkedMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error loading initial mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [control.id, environment]);
|
||||||
|
|
||||||
|
// Load mitigations data
|
||||||
|
const loadMitigationsData = useCallback(() => {
|
||||||
|
if (!organizationId || !control.id) return;
|
||||||
|
|
||||||
|
setIsLoadingMitigations(true);
|
||||||
|
|
||||||
|
// Fetch all mitigations for the organization
|
||||||
|
fetchQuery<ControlOrganizationMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
organizationMitigationsQuery,
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setOrganizationMitigationsData(data);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// Fetch linked mitigations for this control
|
||||||
|
fetchQuery<ControlLinkedMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching linked mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load linked mitigations.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching organization mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load mitigations.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [control.id, environment, organizationId, toast]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getMitigations = useCallback(() => {
|
||||||
|
if (!organizationMitigationsData?.organization?.mitigations?.edges)
|
||||||
|
return [];
|
||||||
|
return organizationMitigationsData.organization.mitigations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [organizationMitigationsData]);
|
||||||
|
|
||||||
|
const getLinkedMitigations = useCallback(() => {
|
||||||
|
if (!linkedMitigationsData?.control?.mitigations?.edges) return [];
|
||||||
|
return linkedMitigationsData.control.mitigations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [linkedMitigationsData]);
|
||||||
|
|
||||||
|
const isMitigationLinked = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
const linkedMitigations = getLinkedMitigations();
|
||||||
|
return linkedMitigations.some(
|
||||||
|
(mitigation) => mitigation.id === mitigationId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[getLinkedMitigations]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMitigationCategories = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
const categories = new Set<string>();
|
||||||
|
|
||||||
|
mitigations.forEach((mitigation) => {
|
||||||
|
if (mitigation.category) {
|
||||||
|
categories.add(mitigation.category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(categories).sort();
|
||||||
|
}, [getMitigations]);
|
||||||
|
|
||||||
|
const filteredMitigations = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
if (!mitigationSearchQuery && !categoryFilter) return mitigations;
|
||||||
|
|
||||||
|
return mitigations.filter((mitigation) => {
|
||||||
|
// Filter by search query
|
||||||
|
const matchesSearch =
|
||||||
|
!mitigationSearchQuery ||
|
||||||
|
mitigation.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()) ||
|
||||||
|
(mitigation.description &&
|
||||||
|
mitigation.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
const matchesCategory =
|
||||||
|
!categoryFilter ||
|
||||||
|
categoryFilter === "all" ||
|
||||||
|
mitigation.category === categoryFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
}, [categoryFilter, getMitigations, mitigationSearchQuery]);
|
||||||
|
|
||||||
|
// Handle link/unlink functions
|
||||||
|
const handleLinkMitigation = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsLinkingMitigation(true);
|
||||||
|
|
||||||
|
commitCreateMitigationMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
mitigationId: mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error linking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked mitigations data
|
||||||
|
fetchQuery<ControlLinkedMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Mitigation successfully linked to control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
console.error("Error linking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitCreateMitigationMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnlinkMitigation = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsUnlinkingMitigation(true);
|
||||||
|
|
||||||
|
commitDeleteMitigationMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
mitigationId: mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error unlinking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked mitigations data
|
||||||
|
fetchQuery<ControlLinkedMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Mitigation successfully unlinked from control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
console.error("Error unlinking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitDeleteMitigationMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenMitigationMappingDialog = useCallback(() => {
|
||||||
|
loadMitigationsData();
|
||||||
|
setIsMitigationMappingDialogOpen(true);
|
||||||
|
}, [loadMitigationsData]);
|
||||||
|
|
||||||
|
// UI helper functions
|
||||||
|
const formatImportance = (importance: string | undefined): string => {
|
||||||
|
if (!importance) return "Unknown";
|
||||||
|
|
||||||
|
switch (importance) {
|
||||||
|
case "LOW":
|
||||||
|
return "Low";
|
||||||
|
case "MEDIUM":
|
||||||
|
return "Medium";
|
||||||
|
case "HIGH":
|
||||||
|
return "High";
|
||||||
|
case "CRITICAL":
|
||||||
|
return "Critical";
|
||||||
|
default:
|
||||||
|
return importance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatState = (state: string | undefined): string => {
|
||||||
|
if (!state) return "Unknown";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "NOT_STARTED":
|
||||||
|
return "Not Started";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "In Progress";
|
||||||
|
case "IMPLEMENTED":
|
||||||
|
return "Implemented";
|
||||||
|
case "NOT_APPLICABLE":
|
||||||
|
return "Not Applicable";
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImportanceColor = (importance: string | undefined): string => {
|
||||||
|
if (!importance) return "bg-gray-100 text-gray-800";
|
||||||
|
|
||||||
|
switch (importance) {
|
||||||
|
case "LOW":
|
||||||
|
return "bg-blue-100 text-blue-800";
|
||||||
|
case "MEDIUM":
|
||||||
|
return "bg-yellow-100 text-yellow-800";
|
||||||
|
case "HIGH":
|
||||||
|
return "bg-orange-100 text-orange-800";
|
||||||
|
case "CRITICAL":
|
||||||
|
return "bg-red-100 text-red-800";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStateColor = (state: string | undefined): string => {
|
||||||
|
if (!state) return "bg-gray-100 text-gray-800";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "NOT_STARTED":
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "bg-blue-100 text-blue-800";
|
||||||
|
case "IMPLEMENTED":
|
||||||
|
return "bg-green-100 text-green-800";
|
||||||
|
case "NOT_APPLICABLE":
|
||||||
|
return "bg-purple-100 text-purple-800";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Control Description */}
|
||||||
|
{control.description && (
|
||||||
|
<div className="mt-4 text-gray-600">{control.description}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Security Measures Section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
{/* Mitigation Mapping Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isMitigationMappingDialogOpen}
|
||||||
|
onOpenChange={setIsMitigationMappingDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Link Security Measures to Control</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Search and select security measures to link to this control.
|
||||||
|
This helps track which security measures address this control.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search security measures by name or description..."
|
||||||
|
value={mitigationSearchQuery}
|
||||||
|
onChange={(e) => setMitigationSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[200px]">
|
||||||
|
<Select
|
||||||
|
value={categoryFilter || "all"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCategoryFilter(value === "all" ? null : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="All categories" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All categories</SelectItem>
|
||||||
|
{getMitigationCategories().map((category) => (
|
||||||
|
<SelectItem key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{isLoadingMitigations ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||||
|
<span className="ml-2">Loading security measures...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-[50vh] overflow-y-auto pr-2">
|
||||||
|
{filteredMitigations().length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No security measures found. Try adjusting your search or
|
||||||
|
select a different category.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="sticky top-0 bg-white">
|
||||||
|
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">
|
||||||
|
Importance
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 font-medium">State</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredMitigations().map((mitigation) => {
|
||||||
|
const isLinked = isMitigationLinked(mitigation.id);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={mitigation.id}
|
||||||
|
className="border-b hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">
|
||||||
|
{mitigation.name}
|
||||||
|
</div>
|
||||||
|
{mitigation.description && (
|
||||||
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
|
{mitigation.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
|
mitigation.importance
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatImportance(mitigation.importance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{isUnlinkingMitigation ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Unlink</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleLinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isLinkingMitigation}
|
||||||
|
className="text-xs h-7 text-blue-500 border-blue-200 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
{isLinkingMitigation ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Link</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button onClick={() => setIsMitigationMappingDialogOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Linked Mitigations List */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-medium text-gray-600">
|
||||||
|
Security measures
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={handleOpenMitigationMappingDialog}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>Link Security Measures</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingMitigations ? (
|
||||||
|
<div className="flex items-center justify-center h-24">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
||||||
|
<span className="ml-2">Loading security measures...</span>
|
||||||
|
</div>
|
||||||
|
) : linkedMitigationsData?.control?.mitigations?.edges &&
|
||||||
|
linkedMitigationsData.control.mitigations.edges.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto border rounded-md">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">Importance</th>
|
||||||
|
<th className="py-3 px-4 font-medium">State</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getLinkedMitigations().map((mitigation) => (
|
||||||
|
<tr
|
||||||
|
key={mitigation.id}
|
||||||
|
className="border-b hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">{mitigation.name}</div>
|
||||||
|
{mitigation.description && (
|
||||||
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
|
{mitigation.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
|
mitigation.importance
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatImportance(mitigation.importance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Unlink
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500 border rounded-md">
|
||||||
|
No security measures linked to this control yet. Click
|
||||||
|
"Link Security Measures" to connect some.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,802 +3,33 @@ import {
|
|||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
useMutation,
|
|
||||||
fetchQuery,
|
|
||||||
useRelayEnvironment,
|
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { ControlViewSkeleton } from "./ControlPage";
|
import { ControlViewSkeleton } from "./ControlPage";
|
||||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
import { Suspense, useEffect } from "react";
|
||||||
import {
|
import { ControlViewQuery } from "./__generated__/ControlViewQuery.graphql";
|
||||||
ControlViewQuery,
|
import { useParams } from "react-router";
|
||||||
ControlViewQuery$data,
|
import { Control } from "./Control";
|
||||||
} from "./__generated__/ControlViewQuery.graphql";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Link, useParams } from "react-router";
|
|
||||||
import { LinkIcon, X, Loader2, Search } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
ControlViewLinkedMitigationsQuery,
|
|
||||||
ControlViewLinkedMitigationsQuery$data,
|
|
||||||
} from "./__generated__/ControlViewLinkedMitigationsQuery.graphql";
|
|
||||||
import {
|
|
||||||
ControlViewOrganizationMitigationsQuery,
|
|
||||||
ControlViewOrganizationMitigationsQuery$data,
|
|
||||||
} from "./__generated__/ControlViewOrganizationMitigationsQuery.graphql";
|
|
||||||
|
|
||||||
const controlViewQuery = graphql`
|
const controlViewQuery = graphql`
|
||||||
query ControlViewQuery($controlId: ID!) {
|
query ControlViewQuery($controlId: ID!) {
|
||||||
node(id: $controlId) {
|
node(id: $controlId) {
|
||||||
id
|
id
|
||||||
... on Control {
|
...ControlFragment_Control
|
||||||
description
|
|
||||||
name
|
|
||||||
referenceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// New query to fetch linked mitigations
|
|
||||||
const linkedMitigationsQuery = graphql`
|
|
||||||
query ControlViewLinkedMitigationsQuery($controlId: ID!) {
|
|
||||||
control: node(id: $controlId) {
|
|
||||||
id
|
|
||||||
... on Control {
|
|
||||||
mitigations(first: 100) @connection(key: "Control__mitigations") {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Query to fetch all mitigations for the organization
|
|
||||||
const organizationMitigationsQuery = graphql`
|
|
||||||
query ControlViewOrganizationMitigationsQuery($organizationId: ID!) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
id
|
|
||||||
... on Organization {
|
|
||||||
mitigations(first: 100) @connection(key: "Organization__mitigations") {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Mutation to create a mapping between a control and a mitigation
|
|
||||||
const createMitigationMappingMutation = graphql`
|
|
||||||
mutation ControlViewCreateMitigationMappingMutation(
|
|
||||||
$input: CreateControlMappingInput!
|
|
||||||
) {
|
|
||||||
createControlMapping(input: $input) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Mutation to delete a mapping between a control and a mitigation
|
|
||||||
const deleteMitigationMappingMutation = graphql`
|
|
||||||
mutation ControlViewDeleteMitigationMappingMutation(
|
|
||||||
$input: DeleteControlMappingInput!
|
|
||||||
) {
|
|
||||||
deleteControlMapping(input: $input) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function Control({
|
|
||||||
control,
|
|
||||||
}: {
|
|
||||||
control: ControlViewQuery$data["node"];
|
|
||||||
}) {
|
|
||||||
const { organizationId /* frameworkId */ } = useParams<{
|
|
||||||
organizationId: string;
|
|
||||||
frameworkId: string;
|
|
||||||
}>();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const environment = useRelayEnvironment();
|
|
||||||
|
|
||||||
// State for mitigation mapping
|
|
||||||
const [isMitigationMappingDialogOpen, setIsMitigationMappingDialogOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [linkedMitigationsData, setLinkedMitigationsData] =
|
|
||||||
useState<ControlViewLinkedMitigationsQuery$data | null>(null);
|
|
||||||
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
|
||||||
useState<ControlViewOrganizationMitigationsQuery$data | null>(null);
|
|
||||||
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
|
||||||
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
|
||||||
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
|
||||||
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Create mutation hooks
|
|
||||||
const [commitCreateMitigationMapping] = useMutation(
|
|
||||||
createMitigationMappingMutation
|
|
||||||
);
|
|
||||||
const [commitDeleteMitigationMapping] = useMutation(
|
|
||||||
deleteMitigationMappingMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load initial linked mitigations data
|
|
||||||
useEffect(() => {
|
|
||||||
if (control.id) {
|
|
||||||
setIsLoadingMitigations(true);
|
|
||||||
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
|
||||||
environment,
|
|
||||||
linkedMitigationsQuery,
|
|
||||||
{
|
|
||||||
controlId: control.id,
|
|
||||||
}
|
|
||||||
).subscribe({
|
|
||||||
next: (data) => {
|
|
||||||
setLinkedMitigationsData(data);
|
|
||||||
setIsLoadingMitigations(false);
|
|
||||||
},
|
|
||||||
error: (error: Error) => {
|
|
||||||
console.error("Error loading initial mitigations:", error);
|
|
||||||
setIsLoadingMitigations(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [control.id, environment]);
|
|
||||||
|
|
||||||
// Load mitigations data
|
|
||||||
const loadMitigationsData = useCallback(() => {
|
|
||||||
if (!organizationId || !control.id) return;
|
|
||||||
|
|
||||||
setIsLoadingMitigations(true);
|
|
||||||
|
|
||||||
// Fetch all mitigations for the organization
|
|
||||||
fetchQuery<ControlViewOrganizationMitigationsQuery>(
|
|
||||||
environment,
|
|
||||||
organizationMitigationsQuery,
|
|
||||||
{
|
|
||||||
organizationId,
|
|
||||||
}
|
|
||||||
).subscribe({
|
|
||||||
next: (data) => {
|
|
||||||
setOrganizationMitigationsData(data);
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
// Fetch linked mitigations for this control
|
|
||||||
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
|
||||||
environment,
|
|
||||||
linkedMitigationsQuery,
|
|
||||||
{
|
|
||||||
controlId: control.id,
|
|
||||||
}
|
|
||||||
).subscribe({
|
|
||||||
next: (data) => {
|
|
||||||
setLinkedMitigationsData(data);
|
|
||||||
setIsLoadingMitigations(false);
|
|
||||||
},
|
|
||||||
error: (error: Error) => {
|
|
||||||
console.error("Error fetching linked mitigations:", error);
|
|
||||||
setIsLoadingMitigations(false);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to load linked mitigations.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: (error: Error) => {
|
|
||||||
console.error("Error fetching organization mitigations:", error);
|
|
||||||
setIsLoadingMitigations(false);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to load mitigations.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [control.id, environment, organizationId, toast]);
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
const getMitigations = useCallback(() => {
|
|
||||||
if (!organizationMitigationsData?.organization?.mitigations?.edges)
|
|
||||||
return [];
|
|
||||||
return organizationMitigationsData.organization.mitigations.edges.map(
|
|
||||||
(edge) => edge.node
|
|
||||||
);
|
|
||||||
}, [organizationMitigationsData]);
|
|
||||||
|
|
||||||
const getLinkedMitigations = useCallback(() => {
|
|
||||||
if (!linkedMitigationsData?.control?.mitigations?.edges) return [];
|
|
||||||
return linkedMitigationsData.control.mitigations.edges.map(
|
|
||||||
(edge) => edge.node
|
|
||||||
);
|
|
||||||
}, [linkedMitigationsData]);
|
|
||||||
|
|
||||||
const isMitigationLinked = useCallback(
|
|
||||||
(mitigationId: string) => {
|
|
||||||
const linkedMitigations = getLinkedMitigations();
|
|
||||||
return linkedMitigations.some(
|
|
||||||
(mitigation) => mitigation.id === mitigationId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[getLinkedMitigations]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMitigationCategories = useCallback(() => {
|
|
||||||
const mitigations = getMitigations();
|
|
||||||
const categories = new Set<string>();
|
|
||||||
|
|
||||||
mitigations.forEach((mitigation) => {
|
|
||||||
if (mitigation.category) {
|
|
||||||
categories.add(mitigation.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(categories).sort();
|
|
||||||
}, [getMitigations]);
|
|
||||||
|
|
||||||
const filteredMitigations = useCallback(() => {
|
|
||||||
const mitigations = getMitigations();
|
|
||||||
if (!mitigationSearchQuery && !categoryFilter) return mitigations;
|
|
||||||
|
|
||||||
return mitigations.filter((mitigation) => {
|
|
||||||
// Filter by search query
|
|
||||||
const matchesSearch =
|
|
||||||
!mitigationSearchQuery ||
|
|
||||||
mitigation.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(mitigationSearchQuery.toLowerCase()) ||
|
|
||||||
(mitigation.description &&
|
|
||||||
mitigation.description
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(mitigationSearchQuery.toLowerCase()));
|
|
||||||
|
|
||||||
// Filter by category
|
|
||||||
const matchesCategory =
|
|
||||||
!categoryFilter ||
|
|
||||||
categoryFilter === "all" ||
|
|
||||||
mitigation.category === categoryFilter;
|
|
||||||
|
|
||||||
return matchesSearch && matchesCategory;
|
|
||||||
});
|
|
||||||
}, [categoryFilter, getMitigations, mitigationSearchQuery]);
|
|
||||||
|
|
||||||
// Handle link/unlink functions
|
|
||||||
const handleLinkMitigation = useCallback(
|
|
||||||
(mitigationId: string) => {
|
|
||||||
if (!control.id) return;
|
|
||||||
|
|
||||||
setIsLinkingMitigation(true);
|
|
||||||
|
|
||||||
commitCreateMitigationMapping({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
controlId: control.id,
|
|
||||||
mitigationId: mitigationId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: (_, errors) => {
|
|
||||||
setIsLinkingMitigation(false);
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
console.error("Error linking mitigation:", errors);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to link mitigation. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh linked mitigations data
|
|
||||||
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
|
||||||
environment,
|
|
||||||
linkedMitigationsQuery,
|
|
||||||
{
|
|
||||||
controlId: control.id,
|
|
||||||
}
|
|
||||||
).subscribe({
|
|
||||||
next: (data) => {
|
|
||||||
setLinkedMitigationsData(data);
|
|
||||||
},
|
|
||||||
error: (error: Error) => {
|
|
||||||
console.error("Error refreshing linked mitigations:", error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Mitigation successfully linked to control.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsLinkingMitigation(false);
|
|
||||||
console.error("Error linking mitigation:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to link mitigation. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[commitCreateMitigationMapping, control.id, environment, toast]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUnlinkMitigation = useCallback(
|
|
||||||
(mitigationId: string) => {
|
|
||||||
if (!control.id) return;
|
|
||||||
|
|
||||||
setIsUnlinkingMitigation(true);
|
|
||||||
|
|
||||||
commitDeleteMitigationMapping({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
controlId: control.id,
|
|
||||||
mitigationId: mitigationId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: (_, errors) => {
|
|
||||||
setIsUnlinkingMitigation(false);
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
console.error("Error unlinking mitigation:", errors);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to unlink mitigation. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh linked mitigations data
|
|
||||||
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
|
||||||
environment,
|
|
||||||
linkedMitigationsQuery,
|
|
||||||
{
|
|
||||||
controlId: control.id,
|
|
||||||
}
|
|
||||||
).subscribe({
|
|
||||||
next: (data) => {
|
|
||||||
setLinkedMitigationsData(data);
|
|
||||||
},
|
|
||||||
error: (error: Error) => {
|
|
||||||
console.error("Error refreshing linked mitigations:", error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Mitigation successfully unlinked from control.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsUnlinkingMitigation(false);
|
|
||||||
console.error("Error unlinking mitigation:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to unlink mitigation. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[commitDeleteMitigationMapping, control.id, environment, toast]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenMitigationMappingDialog = useCallback(() => {
|
|
||||||
loadMitigationsData();
|
|
||||||
setIsMitigationMappingDialogOpen(true);
|
|
||||||
}, [loadMitigationsData]);
|
|
||||||
|
|
||||||
// UI helper functions
|
|
||||||
const formatImportance = (importance: string | undefined): string => {
|
|
||||||
if (!importance) return "Unknown";
|
|
||||||
|
|
||||||
switch (importance) {
|
|
||||||
case "LOW":
|
|
||||||
return "Low";
|
|
||||||
case "MEDIUM":
|
|
||||||
return "Medium";
|
|
||||||
case "HIGH":
|
|
||||||
return "High";
|
|
||||||
case "CRITICAL":
|
|
||||||
return "Critical";
|
|
||||||
default:
|
|
||||||
return importance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatState = (state: string | undefined): string => {
|
|
||||||
if (!state) return "Unknown";
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "NOT_STARTED":
|
|
||||||
return "Not Started";
|
|
||||||
case "IN_PROGRESS":
|
|
||||||
return "In Progress";
|
|
||||||
case "IMPLEMENTED":
|
|
||||||
return "Implemented";
|
|
||||||
case "NOT_APPLICABLE":
|
|
||||||
return "Not Applicable";
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getImportanceColor = (importance: string | undefined): string => {
|
|
||||||
if (!importance) return "bg-gray-100 text-gray-800";
|
|
||||||
|
|
||||||
switch (importance) {
|
|
||||||
case "LOW":
|
|
||||||
return "bg-blue-100 text-blue-800";
|
|
||||||
case "MEDIUM":
|
|
||||||
return "bg-yellow-100 text-yellow-800";
|
|
||||||
case "HIGH":
|
|
||||||
return "bg-orange-100 text-orange-800";
|
|
||||||
case "CRITICAL":
|
|
||||||
return "bg-red-100 text-red-800";
|
|
||||||
default:
|
|
||||||
return "bg-gray-100 text-gray-800";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStateColor = (state: string | undefined): string => {
|
|
||||||
if (!state) return "bg-gray-100 text-gray-800";
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "NOT_STARTED":
|
|
||||||
return "bg-gray-100 text-gray-800";
|
|
||||||
case "IN_PROGRESS":
|
|
||||||
return "bg-blue-100 text-blue-800";
|
|
||||||
case "IMPLEMENTED":
|
|
||||||
return "bg-green-100 text-green-800";
|
|
||||||
case "NOT_APPLICABLE":
|
|
||||||
return "bg-purple-100 text-purple-800";
|
|
||||||
default:
|
|
||||||
return "bg-gray-100 text-gray-800";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Control Description */}
|
|
||||||
{control.description && (
|
|
||||||
<div className="mt-4 text-gray-600">{control.description}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Security Measures Section */}
|
|
||||||
<div className="mt-8">
|
|
||||||
{/* Mitigation Mapping Dialog */}
|
|
||||||
<Dialog
|
|
||||||
open={isMitigationMappingDialogOpen}
|
|
||||||
onOpenChange={setIsMitigationMappingDialogOpen}
|
|
||||||
>
|
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Link Security Measures to Control</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Search and select security measures to link to this control.
|
|
||||||
This helps track which security measures address this control.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 mb-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search security measures by name or description..."
|
|
||||||
value={mitigationSearchQuery}
|
|
||||||
onChange={(e) => setMitigationSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<Select
|
|
||||||
value={categoryFilter || "all"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setCategoryFilter(value === "all" ? null : value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="All categories" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All categories</SelectItem>
|
|
||||||
{getMitigationCategories().map((category) => (
|
|
||||||
<SelectItem key={category} value={category}>
|
|
||||||
{category}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
{isLoadingMitigations ? (
|
|
||||||
<div className="flex items-center justify-center h-full">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
|
||||||
<span className="ml-2">Loading security measures...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="max-h-[50vh] overflow-y-auto pr-2">
|
|
||||||
{filteredMitigations().length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
No security measures found. Try adjusting your search or
|
|
||||||
select a different category.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="sticky top-0 bg-white">
|
|
||||||
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
|
||||||
<th className="py-3 px-4 font-medium">Name</th>
|
|
||||||
<th className="py-3 px-4 font-medium">
|
|
||||||
Importance
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-4 font-medium">State</th>
|
|
||||||
<th className="py-3 px-4 font-medium text-right">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredMitigations().map((mitigation) => {
|
|
||||||
const isLinked = isMitigationLinked(mitigation.id);
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={mitigation.id}
|
|
||||||
className="border-b hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div className="font-medium">
|
|
||||||
{mitigation.name}
|
|
||||||
</div>
|
|
||||||
{mitigation.description && (
|
|
||||||
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
|
||||||
{mitigation.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div
|
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
|
||||||
mitigation.importance
|
|
||||||
)} inline-block`}
|
|
||||||
>
|
|
||||||
{formatImportance(mitigation.importance)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div
|
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
|
||||||
mitigation.state
|
|
||||||
)} inline-block`}
|
|
||||||
>
|
|
||||||
{formatState(mitigation.state)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 text-right whitespace-nowrap">
|
|
||||||
{isLinked ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleUnlinkMitigation(mitigation.id)
|
|
||||||
}
|
|
||||||
disabled={isUnlinkingMitigation}
|
|
||||||
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
{isUnlinkingMitigation ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">Unlink</span>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleLinkMitigation(mitigation.id)
|
|
||||||
}
|
|
||||||
disabled={isLinkingMitigation}
|
|
||||||
className="text-xs h-7 text-blue-500 border-blue-200 hover:bg-blue-50"
|
|
||||||
>
|
|
||||||
{isLinkingMitigation ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<LinkIcon className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">Link</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="mt-4">
|
|
||||||
<Button onClick={() => setIsMitigationMappingDialogOpen(false)}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Linked Mitigations List */}
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="text-xl font-medium text-gray-600">
|
|
||||||
Security measures
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
onClick={handleOpenMitigationMappingDialog}
|
|
||||||
>
|
|
||||||
<LinkIcon className="w-4 h-4" />
|
|
||||||
<span>Link Security Measures</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoadingMitigations ? (
|
|
||||||
<div className="flex items-center justify-center h-24">
|
|
||||||
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
|
||||||
<span className="ml-2">Loading security measures...</span>
|
|
||||||
</div>
|
|
||||||
) : linkedMitigationsData?.control?.mitigations?.edges &&
|
|
||||||
linkedMitigationsData.control.mitigations.edges.length > 0 ? (
|
|
||||||
<div className="overflow-x-auto border rounded-md">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
|
||||||
<th className="py-3 px-4 font-medium">Name</th>
|
|
||||||
<th className="py-3 px-4 font-medium">Importance</th>
|
|
||||||
<th className="py-3 px-4 font-medium">State</th>
|
|
||||||
<th className="py-3 px-4 font-medium text-right">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{getLinkedMitigations().map((mitigation) => (
|
|
||||||
<tr
|
|
||||||
key={mitigation.id}
|
|
||||||
className="border-b hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div className="font-medium">{mitigation.name}</div>
|
|
||||||
{mitigation.description && (
|
|
||||||
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
|
||||||
{mitigation.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div
|
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
|
||||||
mitigation.importance
|
|
||||||
)} inline-block`}
|
|
||||||
>
|
|
||||||
{formatImportance(mitigation.importance)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4">
|
|
||||||
<div
|
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
|
||||||
mitigation.state
|
|
||||||
)} inline-block`}
|
|
||||||
>
|
|
||||||
{formatState(mitigation.state)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 text-right whitespace-nowrap">
|
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
asChild
|
|
||||||
className="text-xs h-7"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleUnlinkMitigation(mitigation.id)
|
|
||||||
}
|
|
||||||
disabled={isUnlinkingMitigation}
|
|
||||||
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
Unlink
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-gray-500 border rounded-md">
|
|
||||||
No security measures linked to this control yet. Click
|
|
||||||
"Link Security Measures" to connect some.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ControlViewContent({
|
function ControlViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<ControlViewQuery>;
|
queryRef: PreloadedQuery<ControlViewQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { node: control } = usePreloadedQuery<ControlViewQuery>(
|
const { node } = usePreloadedQuery<ControlViewQuery>(
|
||||||
controlViewQuery,
|
controlViewQuery,
|
||||||
queryRef
|
queryRef
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Control control={control} />;
|
return <Control controlKey={node} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ControlView({ controlId }: { controlId?: string }) {
|
export default function ControlView({ controlId }: { controlId?: string }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<02b5ad33ae08fb81731cf27c34f31ccf>>
|
* @generated SignedSource<<7598a6cf0314f71bab809d65acc60237>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -13,17 +13,17 @@ export type CreateControlMappingInput = {
|
|||||||
controlId: string;
|
controlId: string;
|
||||||
mitigationId: string;
|
mitigationId: string;
|
||||||
};
|
};
|
||||||
export type ControlViewCreateMitigationMappingMutation$variables = {
|
export type ControlCreateMitigationMappingMutation$variables = {
|
||||||
input: CreateControlMappingInput;
|
input: CreateControlMappingInput;
|
||||||
};
|
};
|
||||||
export type ControlViewCreateMitigationMappingMutation$data = {
|
export type ControlCreateMitigationMappingMutation$data = {
|
||||||
readonly createControlMapping: {
|
readonly createControlMapping: {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlViewCreateMitigationMappingMutation = {
|
export type ControlCreateMitigationMappingMutation = {
|
||||||
response: ControlViewCreateMitigationMappingMutation$data;
|
response: ControlCreateMitigationMappingMutation$data;
|
||||||
variables: ControlViewCreateMitigationMappingMutation$variables;
|
variables: ControlCreateMitigationMappingMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -65,7 +65,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlViewCreateMitigationMappingMutation",
|
"name": "ControlCreateMitigationMappingMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -74,20 +74,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlViewCreateMitigationMappingMutation",
|
"name": "ControlCreateMitigationMappingMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "1d95423714868543cd1ae0867f7274f5",
|
"cacheID": "57ab0bf99dd6ed3d2e980c51f185b9c0",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlViewCreateMitigationMappingMutation",
|
"name": "ControlCreateMitigationMappingMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlViewCreateMitigationMappingMutation(\n $input: CreateControlMappingInput!\n) {\n createControlMapping(input: $input) {\n success\n }\n}\n"
|
"text": "mutation ControlCreateMitigationMappingMutation(\n $input: CreateControlMappingInput!\n) {\n createControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "caa36b4928fc747295d0b3cb8c90f786";
|
(node as any).hash = "17ef8f09acabf19168dad896e4225e3d";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<c4c2d951d675bc0a763fcf90b6a9a959>>
|
* @generated SignedSource<<c1cd34e6543447b414b10f63225883c2>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -13,17 +13,17 @@ export type DeleteControlMappingInput = {
|
|||||||
controlId: string;
|
controlId: string;
|
||||||
mitigationId: string;
|
mitigationId: string;
|
||||||
};
|
};
|
||||||
export type ControlViewDeleteMitigationMappingMutation$variables = {
|
export type ControlDeleteMitigationMappingMutation$variables = {
|
||||||
input: DeleteControlMappingInput;
|
input: DeleteControlMappingInput;
|
||||||
};
|
};
|
||||||
export type ControlViewDeleteMitigationMappingMutation$data = {
|
export type ControlDeleteMitigationMappingMutation$data = {
|
||||||
readonly deleteControlMapping: {
|
readonly deleteControlMapping: {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlViewDeleteMitigationMappingMutation = {
|
export type ControlDeleteMitigationMappingMutation = {
|
||||||
response: ControlViewDeleteMitigationMappingMutation$data;
|
response: ControlDeleteMitigationMappingMutation$data;
|
||||||
variables: ControlViewDeleteMitigationMappingMutation$variables;
|
variables: ControlDeleteMitigationMappingMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -65,7 +65,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlViewDeleteMitigationMappingMutation",
|
"name": "ControlDeleteMitigationMappingMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -74,20 +74,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlViewDeleteMitigationMappingMutation",
|
"name": "ControlDeleteMitigationMappingMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "49d66f6dddce3b6c7a82334e36d9b464",
|
"cacheID": "31dcbb70c0b32e78dcb3fd2a2dc1a58b",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlViewDeleteMitigationMappingMutation",
|
"name": "ControlDeleteMitigationMappingMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlViewDeleteMitigationMappingMutation(\n $input: DeleteControlMappingInput!\n) {\n deleteControlMapping(input: $input) {\n success\n }\n}\n"
|
"text": "mutation ControlDeleteMitigationMappingMutation(\n $input: DeleteControlMappingInput!\n) {\n deleteControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "6f954d3c3c2b38b68a0be5f51de3b935";
|
(node as any).hash = "6070b58009682dfa712e445363f0ebf5";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<8c59833cd4588accdf1d316d9e494d26>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type ControlFragment_Control$data = {
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly referenceId: string;
|
||||||
|
readonly " $fragmentType": "ControlFragment_Control";
|
||||||
|
};
|
||||||
|
export type ControlFragment_Control$key = {
|
||||||
|
readonly " $data"?: ControlFragment_Control$data;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"ControlFragment_Control">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ReaderFragment = {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlFragment_Control",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
};
|
||||||
|
|
||||||
|
(node as any).hash = "ccbc9d6743d45b9a4049b661a1f0ad57";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<5a15ae55cd27e88ee96715ba785e412d>>
|
* @generated SignedSource<<d03d81a6969d83d42491f33bcbd1fc00>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type ControlViewLinkedMitigationsQuery$variables = {
|
export type ControlLinkedMitigationsQuery$variables = {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
};
|
};
|
||||||
export type ControlViewLinkedMitigationsQuery$data = {
|
export type ControlLinkedMitigationsQuery$data = {
|
||||||
readonly control: {
|
readonly control: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly mitigations?: {
|
readonly mitigations?: {
|
||||||
@@ -31,9 +31,9 @@ export type ControlViewLinkedMitigationsQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlViewLinkedMitigationsQuery = {
|
export type ControlLinkedMitigationsQuery = {
|
||||||
response: ControlViewLinkedMitigationsQuery$data;
|
response: ControlLinkedMitigationsQuery$data;
|
||||||
variables: ControlViewLinkedMitigationsQuery$variables;
|
variables: ControlLinkedMitigationsQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -170,7 +170,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlViewLinkedMitigationsQuery",
|
"name": "ControlLinkedMitigationsQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "control",
|
"alias": "control",
|
||||||
@@ -209,7 +209,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlViewLinkedMitigationsQuery",
|
"name": "ControlLinkedMitigationsQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "control",
|
"alias": "control",
|
||||||
@@ -253,7 +253,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "909a089ca2452cc35928267d86d1bb7b",
|
"cacheID": "082617925f3db2937654823bd7dc8833",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -268,13 +268,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "ControlViewLinkedMitigationsQuery",
|
"name": "ControlLinkedMitigationsQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlViewLinkedMitigationsQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlLinkedMitigationsQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "c89fb8a01e18c89d2ae310f4c7a2b522";
|
(node as any).hash = "3d61e207c8a46a72f19e3414152438b8";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<a41b64671a286465fbaa69551d5ec1c8>>
|
* @generated SignedSource<<3c11f3c5fa5a298493ba19d950baa7d2>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type ControlViewOrganizationMitigationsQuery$variables = {
|
export type ControlOrganizationMitigationsQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type ControlViewOrganizationMitigationsQuery$data = {
|
export type ControlOrganizationMitigationsQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly mitigations?: {
|
readonly mitigations?: {
|
||||||
@@ -31,9 +31,9 @@ export type ControlViewOrganizationMitigationsQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlViewOrganizationMitigationsQuery = {
|
export type ControlOrganizationMitigationsQuery = {
|
||||||
response: ControlViewOrganizationMitigationsQuery$data;
|
response: ControlOrganizationMitigationsQuery$data;
|
||||||
variables: ControlViewOrganizationMitigationsQuery$variables;
|
variables: ControlOrganizationMitigationsQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -170,7 +170,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlViewOrganizationMitigationsQuery",
|
"name": "ControlOrganizationMitigationsQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -209,7 +209,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlViewOrganizationMitigationsQuery",
|
"name": "ControlOrganizationMitigationsQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -253,7 +253,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "f5c0ad1c64f05306ed6c23a5ad4d649e",
|
"cacheID": "21e0815f3ed8b62621aa0d7824a93036",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -268,13 +268,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "ControlViewOrganizationMitigationsQuery",
|
"name": "ControlOrganizationMitigationsQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlViewOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "5c14bf02367f0b6e5ccc633b7a77d1aa";
|
(node as any).hash = "54b1b5a6966cf761d49739da3a988db8";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<08f7a2f65fb9ae3f6970e76485fa59e4>>
|
* @generated SignedSource<<2b4d05f2a954b567c9278d604e32db9e>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,15 +9,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type ControlViewQuery$variables = {
|
export type ControlViewQuery$variables = {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
};
|
};
|
||||||
export type ControlViewQuery$data = {
|
export type ControlViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly description?: string;
|
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name?: string;
|
readonly " $fragmentSpreads": FragmentRefs<"ControlFragment_Control">;
|
||||||
readonly referenceId?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlViewQuery = {
|
export type ControlViewQuery = {
|
||||||
@@ -46,34 +45,6 @@ v2 = {
|
|||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"storageKey": null
|
"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 {
|
return {
|
||||||
"fragment": {
|
"fragment": {
|
||||||
@@ -91,7 +62,11 @@ return {
|
|||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v2/*: any*/),
|
(v2/*: any*/),
|
||||||
(v3/*: any*/)
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "ControlFragment_Control"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
@@ -121,23 +96,50 @@ return {
|
|||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v2/*: any*/),
|
(v2/*: any*/),
|
||||||
(v3/*: any*/)
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "be2e7ad630198d4c5476be3767a45d97",
|
"cacheID": "a65968fe47c5fb75f72a01d9785ae176",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlViewQuery",
|
"name": "ControlViewQuery",
|
||||||
"operationKind": "query",
|
"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"
|
"text": "query ControlViewQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n id\n ...ControlFragment_Control\n }\n}\n\nfragment ControlFragment_Control on Control {\n id\n description\n name\n referenceId\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "d692873ef9c85a8f56b8a35e61e5a5bc";
|
(node as any).hash = "ea6b1e9919c960be158d2f48aa3b4254";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
Reference in New Issue
Block a user