Add import mitigations

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-30 15:02:46 +02:00
parent 2baa2eb0d1
commit 4e4e2369fc
8 changed files with 2028 additions and 1335 deletions

View File

@@ -1,9 +1,10 @@
import { Suspense, useEffect, useState } from "react";
import { Suspense, useEffect, useState, useRef } from "react";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
useMutation,
} from "react-relay";
import { useParams, useNavigate, Link } from "react-router";
import { Badge } from "@/components/ui/badge";
@@ -20,16 +21,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { MitigationListViewQuery as MitigationListViewQueryType } from "./__generated__/MitigationListViewQuery.graphql";
import { MitigationListViewSkeleton } from "./MitigationListPage";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { MitigationListViewImportMitigationMutation as MitigationListViewImportMitigationMutationType } from "./__generated__/MitigationListViewImportMitigationMutation.graphql";
const mitigationListViewQuery = graphql`
query MitigationListViewQuery($organizationId: ID!, $first: Int) {
organization: node(id: $organizationId) {
id
... on Organization {
mitigations(
first: $first
orderBy: { direction: ASC, field: CREATED_AT }
) @connection(key: "MitigationListView_mitigations") {
mitigations(first: $first)
@connection(key: "MitigationListView_mitigations") {
__id
edges {
node {
id
@@ -48,6 +50,28 @@ const mitigationListViewQuery = graphql`
}
`;
const importMitigationMutation = graphql`
mutation MitigationListViewImportMitigationMutation(
$input: ImportMitigationInput!
$connections: [ID!]!
) {
importMitigation(input: $input) {
mitigationEdges @appendEdge(connections: $connections) {
node {
id
name
description
category
state
importance
createdAt
updatedAt
}
}
}
}
`;
interface Mitigation {
id?: string;
name?: string;
@@ -93,6 +117,14 @@ function MitigationListContent({
const navigate = useNavigate();
const { organizationId } = useParams();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isImporting, setIsImporting] = useState(false);
const { toast } = useToast();
const [importMitigation] =
useMutation<MitigationListViewImportMitigationMutationType>(
importMitigationMutation
);
// Monitor URL hash for changes and update state accordingly
const [hashValue, setHashValue] = useState(window.location.hash);
@@ -249,18 +281,75 @@ function MitigationListContent({
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsImporting(true);
importMitigation({
variables: {
connections: [data.organization.mitigations.__id],
input: {
organizationId: organizationId!,
file: null,
},
},
uploadables: {
"input.file": file,
},
onCompleted: () => {
setIsImporting(false);
toast({
title: "Mitigations imported",
description: "Mitigations have been imported successfully.",
variant: "default",
});
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
},
onError: (error) => {
setIsImporting(false);
toast({
title: "Error importing mitigations",
description: error.message,
variant: "destructive",
});
},
});
};
return (
<PageTemplate
title="Mitigations"
description="Mitigations are actions taken to reduce the risk. Add them to track their implementation status."
actions={
<Button asChild>
<Link to={`/organizations/${organizationId}/mitigations/new`}>
New Mitigation
</Link>
</Button>
<div className="flex gap-4">
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isImporting}
>
{isImporting ? "Importing..." : "Import Mitigations"}
</Button>
<Button asChild>
<Link to={`/organizations/${organizationId}/mitigations/new`}>
New Mitigation
</Link>
</Button>
</div>
}
>
{/* Hidden file input for mitigation import */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
accept=".json"
/>
{/* Global Progress Summary */}
<div className="mb-8">
<div className="flex items-center justify-between mb-1">

View File

@@ -0,0 +1,217 @@
/**
* @generated SignedSource<<d4590a13a15cf248104340854bc46aa7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type ImportMitigationInput = {
file: any;
organizationId: string;
};
export type MitigationListViewImportMitigationMutation$variables = {
connections: ReadonlyArray<string>;
input: ImportMitigationInput;
};
export type MitigationListViewImportMitigationMutation$data = {
readonly importMitigation: {
readonly mitigationEdges: ReadonlyArray<{
readonly node: {
readonly category: string;
readonly createdAt: string;
readonly description: string;
readonly id: string;
readonly importance: MitigationImportance;
readonly name: string;
readonly state: MitigationState;
readonly updatedAt: string;
};
}>;
};
};
export type MitigationListViewImportMitigationMutation = {
response: MitigationListViewImportMitigationMutation$data;
variables: MitigationListViewImportMitigationMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "MitigationEdge",
"kind": "LinkedField",
"name": "mitigationEdges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Mitigation",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationListViewImportMitigationMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "ImportMitigationPayload",
"kind": "LinkedField",
"name": "importMitigation",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationListViewImportMitigationMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "ImportMitigationPayload",
"kind": "LinkedField",
"name": "importMitigation",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "mitigationEdges",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "84c038c572378999bfaacb87e8172b1e",
"id": null,
"metadata": {},
"name": "MitigationListViewImportMitigationMutation",
"operationKind": "mutation",
"text": "mutation MitigationListViewImportMitigationMutation(\n $input: ImportMitigationInput!\n) {\n importMitigation(input: $input) {\n mitigationEdges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "59e3d4a8e71cee0dcee606b93d706dad";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<c919ae742bdab52e5e68fb5c7fe92812>>
* @generated SignedSource<<a98cd84786da5d6229cb66bc24ad92d0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,6 +19,7 @@ export type MitigationListViewQuery$data = {
readonly organization: {
readonly id: string;
readonly mitigations?: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly category: string;
@@ -65,21 +66,13 @@ v3 = {
"storageKey": null
},
v4 = {
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "CREATED_AT"
}
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v6 = [
v5 = [
{
"alias": null,
"args": null,
@@ -146,7 +139,7 @@ v6 = [
"name": "updatedAt",
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
@@ -184,15 +177,26 @@ v6 = [
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
v7 = [
v6 = [
{
"kind": "Variable",
"name": "first",
"variableName": "first"
},
(v4/*: any*/)
}
];
return {
"fragment": {
@@ -218,15 +222,13 @@ return {
"selections": [
{
"alias": "mitigations",
"args": [
(v4/*: any*/)
],
"args": null,
"concreteType": "MitigationConnection",
"kind": "LinkedField",
"name": "__MitigationListView_mitigations_connection",
"plural": false,
"selections": (v6/*: any*/),
"storageKey": "__MitigationListView_mitigations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
"selections": (v5/*: any*/),
"storageKey": null
}
],
"type": "Organization",
@@ -256,27 +258,25 @@ return {
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
(v4/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v7/*: any*/),
"args": (v6/*: any*/),
"concreteType": "MitigationConnection",
"kind": "LinkedField",
"name": "mitigations",
"plural": false,
"selections": (v6/*: any*/),
"selections": (v5/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": (v7/*: any*/),
"filters": [
"orderBy"
],
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "MitigationListView_mitigations",
"kind": "LinkedHandle",
@@ -292,7 +292,7 @@ return {
]
},
"params": {
"cacheID": "cb49e5e7740ec71ce0b8e1abbdbcf24d",
"cacheID": "9951f5e4be50df7a883af63426fe5fde",
"id": null,
"metadata": {
"connection": [
@@ -309,11 +309,11 @@ return {
},
"name": "MitigationListViewQuery",
"operationKind": "query",
"text": "query MitigationListViewQuery(\n $organizationId: ID!\n $first: Int\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: $first, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query MitigationListViewQuery(\n $organizationId: ID!\n $first: Int\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: $first) {\n edges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ee7fdf984d5cb376068ba53d0a56c59b";
(node as any).hash = "c2da6b42a986dd18cd18603c5f5809ad";
export default node;