Add pre-configured framework templates
Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -1,22 +1,16 @@
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
ConnectionHandler,
|
||||
} from "react-relay";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Link, useParams } from "react-router";
|
||||
import type { FrameworkListViewQuery as FrameworkListViewQueryType } from "./__generated__/FrameworkListViewQuery.graphql";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FrameworkListViewImportFrameworkMutation as FrameworkListViewImportFrameworkMutationType } from "./__generated__/FrameworkListViewImportFrameworkMutation.graphql";
|
||||
import { PageTemplate } from "@/components/PageTemplate";
|
||||
import { FrameworkListViewSkeleton } from "./FrameworkListPage";
|
||||
import { FrameworkImportDropdown } from "./ImportFrameworkDialog";
|
||||
|
||||
const FrameworkListViewQuery = graphql`
|
||||
query FrameworkListViewQuery($organizationId: ID!) {
|
||||
@@ -36,124 +30,21 @@ const FrameworkListViewQuery = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const FrameworkListViewImportFrameworkMutation = graphql`
|
||||
mutation FrameworkListViewImportFrameworkMutation(
|
||||
$input: ImportFrameworkInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
importFramework(input: $input) {
|
||||
frameworkEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function FrameworkListViewContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<FrameworkListViewQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery<FrameworkListViewQueryType>(
|
||||
FrameworkListViewQuery,
|
||||
queryRef
|
||||
);
|
||||
const data = usePreloadedQuery(FrameworkListViewQuery, queryRef);
|
||||
const { organizationId } = useParams();
|
||||
const frameworks =
|
||||
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
||||
|
||||
const [importFramework] =
|
||||
useMutation<FrameworkListViewImportFrameworkMutationType>(
|
||||
FrameworkListViewImportFrameworkMutation
|
||||
);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleImportClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
importFramework({
|
||||
variables: {
|
||||
connections: [
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId!,
|
||||
"FrameworkListView_frameworks"
|
||||
),
|
||||
],
|
||||
input: {
|
||||
organizationId: organizationId!,
|
||||
file: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.file": file,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsUploading(false);
|
||||
toast({
|
||||
title: "Framework imported",
|
||||
description: "Framework has been imported successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsUploading(false);
|
||||
toast({
|
||||
title: "Error importing framework",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
data.organization?.frameworks?.edges?.map((edge) => edge?.node) ?? [];
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
id="framework-file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleImportClick}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Import
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organizationId}/frameworks/new`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Framework
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
actions={<FrameworkImportDropdown />}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
@@ -196,17 +87,13 @@ function FrameworkListViewContent({
|
||||
}
|
||||
|
||||
export default function FrameworkListView() {
|
||||
const { organizationId } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListViewQueryType>(
|
||||
FrameworkListViewQuery
|
||||
);
|
||||
|
||||
const { organizationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId: organizationId! });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
loadQuery({ organizationId: organizationId! });
|
||||
return <FrameworkListViewSkeleton />;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Plus, Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { ConnectionHandler, graphql, useMutation } from "react-relay";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { ImportFrameworkDialogImportFrameworkMutation } from "./__generated__/ImportFrameworkDialogImportFrameworkMutation.graphql";
|
||||
import { useState } from "react";
|
||||
|
||||
const AVAILABLE_FRAMEWORKS = [
|
||||
{ id: "ISO27001-2022", name: "ISO/IEC 27001:2022" },
|
||||
{ id: "SOC2", name: "SOC 2" },
|
||||
];
|
||||
|
||||
const importFrameworkMutation = graphql`
|
||||
mutation ImportFrameworkDialogImportFrameworkMutation(
|
||||
$input: ImportFrameworkInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
importFramework(input: $input) {
|
||||
frameworkEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function FrameworkImportDropdown() {
|
||||
const { organizationId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [loadingFramework, setLoadingFramework] = useState<string | null>(null);
|
||||
const [commit, isInFlight] =
|
||||
useMutation<ImportFrameworkDialogImportFrameworkMutation>(
|
||||
importFrameworkMutation
|
||||
);
|
||||
|
||||
const handleImport = async (frameworkId: string) => {
|
||||
setLoadingFramework(frameworkId);
|
||||
try {
|
||||
const frameworkTemplate = await import(
|
||||
`./../../../../../data/frameworks/${frameworkId}.json`,
|
||||
{ with: { type: "json" } }
|
||||
);
|
||||
|
||||
const file = new File(
|
||||
[JSON.stringify(frameworkTemplate.default)],
|
||||
`${frameworkId}.json`,
|
||||
{
|
||||
type: "application/json",
|
||||
}
|
||||
);
|
||||
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId!,
|
||||
"FrameworkListView_frameworks"
|
||||
);
|
||||
|
||||
commit({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organizationId!,
|
||||
file: null,
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
uploadables: {
|
||||
"input.file": file,
|
||||
},
|
||||
onCompleted(data, errors) {
|
||||
if (errors) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: errors[0]?.message || "Failed to import framework",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Framework imported successfully",
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to import framework",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to load framework template",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoadingFramework(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button disabled={isInFlight || loadingFramework !== null}>
|
||||
{isInFlight || loadingFramework !== null ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{isInFlight ? "Importing..." : "Loading..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigate(`/organizations/${organizationId}/frameworks/new`)
|
||||
}
|
||||
>
|
||||
Custom Framework
|
||||
</DropdownMenuItem>
|
||||
{AVAILABLE_FRAMEWORKS.map((framework) => (
|
||||
<DropdownMenuItem
|
||||
key={framework.id}
|
||||
onClick={() => handleImport(framework.id)}
|
||||
disabled={loadingFramework === framework.id}
|
||||
>
|
||||
{loadingFramework === framework.id ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
framework.name
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<c9bf877d405180bb5fe0211022bd846f>>
|
||||
* @generated SignedSource<<2b831625a3ae5e59ef58f138f56c9bda>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -13,11 +13,11 @@ export type ImportFrameworkInput = {
|
||||
file: any;
|
||||
organizationId: string;
|
||||
};
|
||||
export type FrameworkListViewImportFrameworkMutation$variables = {
|
||||
export type ImportFrameworkDialogImportFrameworkMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: ImportFrameworkInput;
|
||||
};
|
||||
export type FrameworkListViewImportFrameworkMutation$data = {
|
||||
export type ImportFrameworkDialogImportFrameworkMutation$data = {
|
||||
readonly importFramework: {
|
||||
readonly frameworkEdge: {
|
||||
readonly node: {
|
||||
@@ -27,9 +27,9 @@ export type FrameworkListViewImportFrameworkMutation$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FrameworkListViewImportFrameworkMutation = {
|
||||
response: FrameworkListViewImportFrameworkMutation$data;
|
||||
variables: FrameworkListViewImportFrameworkMutation$variables;
|
||||
export type ImportFrameworkDialogImportFrameworkMutation = {
|
||||
response: ImportFrameworkDialogImportFrameworkMutation$data;
|
||||
variables: ImportFrameworkDialogImportFrameworkMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -94,7 +94,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"name": "ImportFrameworkDialogImportFrameworkMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -119,7 +119,7 @@ return {
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"name": "ImportFrameworkDialogImportFrameworkMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -152,16 +152,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "04a42349410d12247fcbf3bd99b0b625",
|
||||
"cacheID": "3ff0540d8fa361dfc521ef8267c3d853",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"name": "ImportFrameworkDialogImportFrameworkMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation FrameworkListViewImportFrameworkMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n }\n }\n }\n}\n"
|
||||
"text": "mutation ImportFrameworkDialogImportFrameworkMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "9fb0921f16ca755b8130fcd8603e8933";
|
||||
(node as any).hash = "14ca8dd8ef307731893baaf35eaaba65";
|
||||
|
||||
export default node;
|
||||
Reference in New Issue
Block a user