@@ -1,16 +1,28 @@
|
|||||||
import { Suspense, useEffect } from "react";
|
import { Suspense, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
graphql,
|
graphql,
|
||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
|
useMutation,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus, Upload } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { FrameworkListPageImportFrameworkMutation as FrameworkListPageImportFrameworkMutationType } from "./__generated__/FrameworkListPageImportFrameworkMutation.graphql";
|
||||||
|
|
||||||
const FrameworkListPageQuery = graphql`
|
const FrameworkListPageQuery = graphql`
|
||||||
query FrameworkListPageQuery($organizationId: ID!) {
|
query FrameworkListPageQuery($organizationId: ID!) {
|
||||||
@@ -40,6 +52,32 @@ const FrameworkListPageQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FrameworkListPageImportFrameworkMutation = graphql`
|
||||||
|
mutation FrameworkListPageImportFrameworkMutation(
|
||||||
|
$input: ImportFrameworkInput!
|
||||||
|
) {
|
||||||
|
importFramework(input: $input) {
|
||||||
|
frameworkEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
controls {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function FrameworkCard({
|
function FrameworkCard({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -88,12 +126,60 @@ function FrameworkListPageContent({
|
|||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery<FrameworkListPageQueryType>(
|
const data = usePreloadedQuery<FrameworkListPageQueryType>(
|
||||||
FrameworkListPageQuery,
|
FrameworkListPageQuery,
|
||||||
queryRef,
|
queryRef
|
||||||
);
|
);
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
const frameworks =
|
const frameworks =
|
||||||
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
||||||
|
|
||||||
|
const [importFramework] =
|
||||||
|
useMutation<FrameworkListPageImportFrameworkMutationType>(
|
||||||
|
FrameworkListPageImportFrameworkMutation
|
||||||
|
);
|
||||||
|
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
importFramework({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId!,
|
||||||
|
file: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uploadables: {
|
||||||
|
"input.file": file,
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
setIsUploading(false);
|
||||||
|
setIsImportDialogOpen(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",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -107,6 +193,41 @@ function FrameworkListPageContent({
|
|||||||
Manage your compliance frameworks
|
Manage your compliance frameworks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Dialog
|
||||||
|
open={isImportDialogOpen}
|
||||||
|
onOpenChange={setIsImportDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Import Framework
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Import Framework</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="framework-file">
|
||||||
|
Upload Framework File
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="framework-file"
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
disabled={isUploading}
|
||||||
|
accept=".json"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Upload a JSON file containing your framework definition.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
@@ -114,11 +235,12 @@ function FrameworkListPageContent({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||||
{frameworks.map((framework) => {
|
{frameworks.map((framework) => {
|
||||||
const validatedControls = framework.controls.edges.filter(
|
const validatedControls = framework.controls.edges.filter(
|
||||||
(edge) => edge?.node?.state === "IMPLEMENTED",
|
(edge) => edge?.node?.state === "IMPLEMENTED"
|
||||||
).length;
|
).length;
|
||||||
const totalControls = framework.controls.edges.length;
|
const totalControls = framework.controls.edges.length;
|
||||||
|
|
||||||
@@ -185,7 +307,7 @@ function FrameworkListPageFallback() {
|
|||||||
|
|
||||||
export default function FrameworkListPage() {
|
export default function FrameworkListPage() {
|
||||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
|
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
|
||||||
FrameworkListPageQuery,
|
FrameworkListPageQuery
|
||||||
);
|
);
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
|
|||||||
202
apps/console/src/pages/__generated__/FrameworkListPageImportFrameworkMutation.graphql.ts
generated
Normal file
202
apps/console/src/pages/__generated__/FrameworkListPageImportFrameworkMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<886db9744d1d937174d0138601e644a5>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type ImportFrameworkInput = {
|
||||||
|
file: any;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type FrameworkListPageImportFrameworkMutation$variables = {
|
||||||
|
input: ImportFrameworkInput;
|
||||||
|
};
|
||||||
|
export type FrameworkListPageImportFrameworkMutation$data = {
|
||||||
|
readonly importFramework: {
|
||||||
|
readonly frameworkEdge: {
|
||||||
|
readonly node: {
|
||||||
|
readonly controls: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly state: ControlState;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type FrameworkListPageImportFrameworkMutation = {
|
||||||
|
response: FrameworkListPageImportFrameworkMutation$data;
|
||||||
|
variables: FrameworkListPageImportFrameworkMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v2 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ImportFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "importFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "controls",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Control",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworkListPageImportFrameworkMutation",
|
||||||
|
"selections": (v2/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "FrameworkListPageImportFrameworkMutation",
|
||||||
|
"selections": (v2/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "2ea6f840625bd25b0469103c8f98d3f2",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "FrameworkListPageImportFrameworkMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation FrameworkListPageImportFrameworkMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "cb68b53c21e128e59354c51048f347cc";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -40,6 +40,7 @@ type (
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
Version int `db:"version"`
|
Version int `db:"version"`
|
||||||
|
Standards []string `db:"standards"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Controls []*Control
|
Controls []*Control
|
||||||
@@ -76,6 +77,7 @@ SELECT
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
standards,
|
||||||
version
|
version
|
||||||
FROM
|
FROM
|
||||||
controls
|
controls
|
||||||
@@ -124,6 +126,7 @@ INSERT INTO
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
standards,
|
||||||
version
|
version
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@@ -138,6 +141,7 @@ VALUES (
|
|||||||
@content_ref,
|
@content_ref,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at,
|
@updated_at,
|
||||||
|
@standards,
|
||||||
@version
|
@version
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
@@ -155,6 +159,7 @@ VALUES (
|
|||||||
"updated_at": c.UpdatedAt,
|
"updated_at": c.UpdatedAt,
|
||||||
"state": c.State,
|
"state": c.State,
|
||||||
"importance": c.Importance,
|
"importance": c.Importance,
|
||||||
|
"standards": c.Standards,
|
||||||
}
|
}
|
||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
@@ -179,6 +184,7 @@ SELECT
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
standards,
|
||||||
version
|
version
|
||||||
FROM
|
FROM
|
||||||
controls
|
controls
|
||||||
@@ -237,7 +243,8 @@ RETURNING
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
version
|
version,
|
||||||
|
standards
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
|||||||
2
pkg/coredata/migrations/20250313T160700Z.sql
Normal file
2
pkg/coredata/migrations/20250313T160700Z.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE controls ADD COLUMN standards TEXT[] DEFAULT '{}' NOT NULL;
|
||||||
|
ALTER TABLE controls ALTER COLUMN standards DROP DEFAULT;
|
||||||
@@ -43,6 +43,25 @@ type (
|
|||||||
Name *string
|
Name *string
|
||||||
Description *string
|
Description *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImportFrameworkRequest struct {
|
||||||
|
Data struct {
|
||||||
|
Framework struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ContentRef string `json:"content-ref"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Controls []struct {
|
||||||
|
ContentRef string `json:"content-ref"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Importance coredata.ControlImportance `json:"importance"`
|
||||||
|
Standards []string `json:"standards"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
} `json:"controls"`
|
||||||
|
} `json:"framework"`
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s FrameworkService) Create(
|
func (s FrameworkService) Create(
|
||||||
@@ -163,3 +182,76 @@ func (s FrameworkService) Delete(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s FrameworkService) Import(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
req ImportFrameworkRequest,
|
||||||
|
) (*coredata.Framework, error) {
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
frameworkID, err := gid.NewGID(organizationID.TenantID(), coredata.FrameworkEntityType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create global id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
framework := &coredata.Framework{
|
||||||
|
ID: frameworkID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
Name: req.Data.Framework.Name,
|
||||||
|
Description: req.Data.Framework.Description,
|
||||||
|
ContentRef: req.Data.Framework.ContentRef,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
importedControls := coredata.Controls{}
|
||||||
|
for _, control := range req.Data.Framework.Controls {
|
||||||
|
controlID, err := gid.NewGID(organizationID.TenantID(), coredata.ControlEntityType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create global id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
importedControl := &coredata.Control{
|
||||||
|
ID: controlID,
|
||||||
|
FrameworkID: frameworkID,
|
||||||
|
Category: control.Category,
|
||||||
|
Importance: coredata.ControlImportance(control.Importance),
|
||||||
|
Name: control.Name,
|
||||||
|
Description: control.Description,
|
||||||
|
State: coredata.ControlStateNotStarted,
|
||||||
|
ContentRef: control.ContentRef,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
Standards: control.Standards,
|
||||||
|
}
|
||||||
|
|
||||||
|
importedControls = append(importedControls, importedControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
|
||||||
|
err := framework.Insert(ctx, tx, s.svc.scope)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, importedControl := range importedControls {
|
||||||
|
if err := importedControl.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert control: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return framework, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package console_v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -348,7 +349,21 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
|
|||||||
|
|
||||||
// ImportFramework is the resolver for the importFramework field.
|
// ImportFramework is the resolver for the importFramework field.
|
||||||
func (r *mutationResolver) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) {
|
func (r *mutationResolver) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) {
|
||||||
panic(fmt.Errorf("not implemented: ImportFramework - importFramework"))
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
req := probo.ImportFrameworkRequest{}
|
||||||
|
if err := json.NewDecoder(input.File.File).Decode(&req.Data); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
framework, err := svc.Frameworks.Import(ctx, input.OrganizationID, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot import framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ImportFrameworkPayload{
|
||||||
|
FrameworkEdge: types.NewFrameworkEdge(framework),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateControl is the resolver for the createControl field.
|
// CreateControl is the resolver for the createControl field.
|
||||||
|
|||||||
Reference in New Issue
Block a user