Add create control and framework

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-27 14:22:08 +01:00
parent 63b596464a
commit c54febadb8
21 changed files with 2141 additions and 290 deletions

View File

@@ -44,6 +44,8 @@ const RegisterPage = lazy(() => import("./pages/RegisterPage"));
const CreateOrganizationPage = lazy( const CreateOrganizationPage = lazy(
() => import("./pages/CreateOrganizationPage") () => import("./pages/CreateOrganizationPage")
); );
const CreateFrameworkPage = lazy(() => import("./pages/CreateFrameworkPage"));
const CreateControlPage = lazy(() => import("./pages/CreateControlPage"));
function App() { function App() {
return ( return (
@@ -208,6 +210,16 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path="frameworks/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreateFrameworkPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route <Route
path="frameworks/:frameworkId" path="frameworks/:frameworkId"
element={ element={
@@ -218,6 +230,16 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path="frameworks/:frameworkId/controls/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreateControlPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route <Route
path="frameworks/:frameworkId/controls/:controlId" path="frameworks/:frameworkId/controls/:controlId"
element={ element={

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };

View File

@@ -16,13 +16,13 @@ import {
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { graphql } from "react-relay"; import { graphql, useLazyLoadQuery } from "react-relay";
import { useLazyLoadQuery } from "react-relay";
import { ConsoleLayoutBreadcrumbFrameworkOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbFrameworkOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbFrameworkOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbFrameworkOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbPeopleOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbPeopleOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbPeopleOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbPeopleOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql";
import { ConsoleLayoutOrganizationQuery } from "./__generated__/ConsoleLayoutOrganizationQuery.graphql"; import { ConsoleLayoutOrganizationQuery } from "./__generated__/ConsoleLayoutOrganizationQuery.graphql";
import { ConsoleLayoutBreadcrumbCreateControlQuery } from "./__generated__/ConsoleLayoutBreadcrumbCreateControlQuery.graphql";
function BreadcrumbHome({ children }: { children: React.ReactNode }) { function BreadcrumbHome({ children }: { children: React.ReactNode }) {
const { organizationId } = useParams(); const { organizationId } = useParams();
@@ -57,7 +57,7 @@ function BreadcrumbHome({ children }: { children: React.ReactNode }) {
} }
`, `,
{ organizationId: organizationId! }, { organizationId: organizationId! },
{ fetchPolicy: "store-or-network" }, { fetchPolicy: "store-or-network" }
); );
return ( return (
@@ -93,6 +93,17 @@ function BreadcrumbFrameworkList() {
); );
} }
function BreadcrumbCreateFramework() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create</BreadcrumbPage>
</BreadcrumbItem>
</>
);
}
function BreadcrumbFrameworkOverview() { function BreadcrumbFrameworkOverview() {
const { organizationId, frameworkId } = useParams(); const { organizationId, frameworkId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>( const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>(
@@ -106,8 +117,7 @@ function BreadcrumbFrameworkOverview() {
} }
} }
`, `,
{ frameworkId: frameworkId! }, { frameworkId: frameworkId! }
{ fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -116,9 +126,9 @@ function BreadcrumbFrameworkOverview() {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`} to={`/organizations/${organizationId}/frameworks/${frameworkId}`}
> >
{data.framework.name} {data.framework?.name}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
@@ -156,7 +166,7 @@ function BreadcrumbVendorOverview() {
} }
`, `,
{ vendorId: vendorId! }, { vendorId: vendorId! },
{ fetchPolicy: "store-or-network" }, { fetchPolicy: "store-or-network" }
); );
return ( return (
@@ -216,7 +226,7 @@ function BreadcrumbPeopleOverview() {
} }
`, `,
{ peopleId: peopleId! }, { peopleId: peopleId! },
{ fetchPolicy: "store-or-network" }, { fetchPolicy: "store-or-network" }
); );
return ( return (
@@ -258,8 +268,7 @@ function BreadcrumbControlOverview() {
} }
} }
`, `,
{ frameworkId: frameworkId!, controlId: controlId! }, { frameworkId: frameworkId!, controlId: controlId! }
{ fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -268,9 +277,9 @@ function BreadcrumbControlOverview() {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`} to={`/organizations/${organizationId}/frameworks/${frameworkId}`}
> >
{data.framework.name} {data.framework?.name}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
@@ -278,13 +287,48 @@ function BreadcrumbControlOverview() {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}/controls/${data.control.id}`} to={`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`}
> >
{data.control.name} {data.control?.name}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<Outlet /> </>
);
}
function BreadcrumbCreateControl() {
const { organizationId, frameworkId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbCreateControlQuery>(
graphql`
query ConsoleLayoutBreadcrumbCreateControlQuery($frameworkId: ID!) {
framework: node(id: $frameworkId) {
id
... on Framework {
name
}
}
}
`,
{ frameworkId: frameworkId! }
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
to={`/organizations/${organizationId}/frameworks/${frameworkId}`}
>
{data.framework?.name}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create Control</BreadcrumbPage>
</BreadcrumbItem>
</> </>
); );
} }
@@ -311,10 +355,19 @@ export default function ConsoleLayout() {
<Route <Route
path=":frameworkId" path=":frameworkId"
element={<BreadcrumbFrameworkOverview />} element={<BreadcrumbFrameworkOverview />}
/> >
<Route
path="controls/create"
element={<BreadcrumbCreateControl />}
/>
<Route
path="controls/:controlId"
element={<BreadcrumbControlOverview />}
/>
</Route>
<Route <Route
path=":frameworkId/controls/:controlId" path="create"
element={<BreadcrumbControlOverview />} element={<BreadcrumbCreateFramework />}
/> />
</Route> </Route>
<Route path="peoples" element={<BreadcrumbPeopleList />}> <Route path="peoples" element={<BreadcrumbPeopleList />}>

View File

@@ -0,0 +1,127 @@
/**
* @generated SignedSource<<72acdb0b83faf29c9d1a4f403a40ac5e>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ConsoleLayoutBreadcrumbCreateControlQuery$variables = {
frameworkId: string;
};
export type ConsoleLayoutBreadcrumbCreateControlQuery$data = {
readonly framework: {
readonly id: string;
readonly name?: string;
};
};
export type ConsoleLayoutBreadcrumbCreateControlQuery = {
response: ConsoleLayoutBreadcrumbCreateControlQuery$data;
variables: ConsoleLayoutBreadcrumbCreateControlQuery$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 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"type": "Framework",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ConsoleLayoutBreadcrumbCreateControlQuery",
"selections": [
{
"alias": "framework",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ConsoleLayoutBreadcrumbCreateControlQuery",
"selections": [
{
"alias": "framework",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "96453f593f21e812afc5b888cc68da3e",
"id": null,
"metadata": {},
"name": "ConsoleLayoutBreadcrumbCreateControlQuery",
"operationKind": "query",
"text": "query ConsoleLayoutBreadcrumbCreateControlQuery(\n $frameworkId: ID!\n) {\n framework: node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n }\n }\n}\n"
}
};
})();
(node as any).hash = "f249019075c60c4449f6f232229a6965";
export default node;

View File

@@ -0,0 +1,241 @@
import { Suspense, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { graphql, useMutation, ConnectionHandler } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { CreateControlPageCreateControlMutation } from "./__generated__/CreateControlPageCreateControlMutation.graphql";
const createControlMutation = graphql`
mutation CreateControlPageCreateControlMutation(
$input: CreateControlInput!
$connections: [ID!]!
) {
createControl(input: $input) {
controlEdge @prependEdge(connections: $connections) {
node {
id
name
description
category
state
}
}
}
}
`;
function EditableField({
label,
value,
onChange,
type = "text",
helpText,
required,
multiline = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
helpText?: string;
required?: boolean;
multiline?: boolean;
}) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor={label} className="text-sm font-medium">
{label}
{required && <span className="text-red-500">*</span>}
</Label>
{helpText && (
<div className="relative flex items-center">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">{helpText}</span>
</div>
)}
</div>
{multiline ? (
<Textarea
id={label}
value={value}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(e.target.value)
}
className={cn(
"w-full resize-none",
required && !value && "border-red-500"
)}
placeholder={`Enter ${label.toLowerCase()}`}
rows={4}
/>
) : (
<Input
id={label}
type={type}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
}
className={cn("w-full", required && !value && "border-red-500")}
placeholder={`Enter ${label.toLowerCase()}`}
/>
)}
</div>
);
}
function CreateControlPageContent() {
const { organizationId, frameworkId } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
const [formData, setFormData] = useState({
name: "",
description: "",
category: "",
});
const [commit, isInFlight] =
useMutation<CreateControlPageCreateControlMutation>(createControlMutation);
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.description || !formData.category) {
toast({
title: "Validation Error",
description: "Please fill in all required fields.",
variant: "destructive",
});
return;
}
const connectionId = ConnectionHandler.getConnectionID(
frameworkId!,
"FrameworkOverviewPage_controls"
);
commit({
variables: {
input: {
frameworkId: frameworkId!,
name: formData.name,
description: formData.description,
category: formData.category,
},
connections: [connectionId],
},
onCompleted(data, errors) {
if (errors) {
toast({
title: "Error",
description: errors[0]?.message || "Failed to create control",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Control created successfully",
});
navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${data.createControl.controlEdge.node.id}`
);
},
onError(error) {
toast({
title: "Error",
description: error.message || "Failed to create control",
variant: "destructive",
});
},
});
};
return (
<>
<Helmet>
<title>Create Control - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Create Control</h1>
<p className="text-muted-foreground">
Create a new control for your framework
</p>
</div>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField
label="Name"
value={formData.name}
onChange={(value) => handleFieldChange("name", value)}
required
/>
<EditableField
label="Category"
value={formData.category}
onChange={(value) => handleFieldChange("category", value)}
required
helpText="The category this control belongs to"
/>
<EditableField
label="Description"
value={formData.description}
onChange={(value) => handleFieldChange("description", value)}
required
multiline
helpText="Provide a detailed description of the control"
/>
<div className="flex justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={() =>
navigate(
`/organizations/${
frameworkId?.split(":")[1]
}/frameworks/${frameworkId}`
)
}
>
Cancel
</Button>
<Button type="submit" disabled={isInFlight}>
{isInFlight ? "Creating..." : "Create Control"}
</Button>
</div>
</form>
</Card>
</div>
</>
);
}
export default function CreateControlPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<CreateControlPageContent />
</Suspense>
);
}

View File

@@ -0,0 +1,227 @@
import { Suspense, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { graphql, useMutation, ConnectionHandler } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql";
const createFrameworkMutation = graphql`
mutation CreateFrameworkPageCreateFrameworkMutation(
$input: CreateFrameworkInput!
$connections: [ID!]!
) {
createFramework(input: $input) {
frameworkEdge @prependEdge(connections: $connections) {
node {
id
name
description
}
}
}
}
`;
function EditableField({
label,
value,
onChange,
type = "text",
helpText,
required,
multiline = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
helpText?: string;
required?: boolean;
multiline?: boolean;
}) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor={label} className="text-sm font-medium">
{label}
{required && <span className="text-red-500">*</span>}
</Label>
{helpText && (
<div className="relative flex items-center">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">{helpText}</span>
</div>
)}
</div>
{multiline ? (
<Textarea
id={label}
value={value}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(e.target.value)
}
className={cn(
"w-full resize-none",
required && !value && "border-red-500"
)}
placeholder={`Enter ${label.toLowerCase()}`}
rows={4}
/>
) : (
<Input
id={label}
type={type}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
}
className={cn("w-full", required && !value && "border-red-500")}
placeholder={`Enter ${label.toLowerCase()}`}
/>
)}
</div>
);
}
function CreateFrameworkPageContent() {
const { organizationId } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
const [formData, setFormData] = useState({
name: "",
description: "",
});
const [commit, isInFlight] =
useMutation<CreateFrameworkPageCreateFrameworkMutation>(
createFrameworkMutation
);
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.description) {
toast({
title: "Validation Error",
description: "Please fill in all required fields.",
variant: "destructive",
});
return;
}
const connectionId = ConnectionHandler.getConnectionID(
organizationId!,
"FrameworkListPage_frameworks"
);
commit({
variables: {
input: {
organizationId: organizationId!,
name: formData.name,
description: formData.description,
},
connections: [connectionId],
},
onCompleted(data, errors) {
if (errors) {
toast({
title: "Error",
description: errors[0]?.message || "Failed to create framework",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Framework created successfully",
});
navigate(
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`
);
},
onError(error) {
toast({
title: "Error",
description: error.message || "Failed to create framework",
variant: "destructive",
});
},
});
};
return (
<>
<Helmet>
<title>Create Framework - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Create Framework</h1>
<p className="text-muted-foreground">
Create a new framework to organize your controls
</p>
</div>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField
label="Name"
value={formData.name}
onChange={(value) => handleFieldChange("name", value)}
required
/>
<EditableField
label="Description"
value={formData.description}
onChange={(value) => handleFieldChange("description", value)}
required
multiline
helpText="Provide a detailed description of the framework"
/>
<div className="flex justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={() =>
navigate(`/organizations/${organizationId}/frameworks`)
}
>
Cancel
</Button>
<Button type="submit" disabled={isInFlight}>
{isInFlight ? "Creating..." : "Create Framework"}
</Button>
</div>
</form>
</Card>
</div>
</>
);
}
export default function CreateFrameworkPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<CreateFrameworkPageContent />
</Suspense>
);
}

View File

@@ -9,12 +9,14 @@ 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 { Plus } from "lucide-react";
const FrameworkListPageQuery = graphql` const FrameworkListPageQuery = graphql`
query FrameworkListPageQuery($organizationId: ID!) { query FrameworkListPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
... on Organization { ... on Organization {
frameworks { frameworks(first: 25) @connection(key: "FrameworkListPage_frameworks") {
edges { edges {
node { node {
id id
@@ -84,56 +86,75 @@ function FrameworkListPageContent({
}: { }: {
queryRef: PreloadedQuery<FrameworkListPageQueryType>; queryRef: PreloadedQuery<FrameworkListPageQueryType>;
}) { }) {
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef); const data = usePreloadedQuery<FrameworkListPageQueryType>(
FrameworkListPageQuery,
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) ?? [];
return ( return (
<div className="space-y-6"> <>
<div> <Helmet>
<h2 className="text-2xl font-semibold mb-1">Framework</h2> <title>Frameworks - Probo</title>
<p className="text-muted-foreground"> </Helmet>
Track and manage your compliance frameworks and their controls. <div className="container mx-auto py-6">
</p> <div className="flex justify-between items-center mb-6">
</div> <div>
<h1 className="text-2xl font-bold">Frameworks</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2"> <p className="text-muted-foreground">
{frameworks.map((framework) => { Manage your compliance frameworks
const validatedControls = framework.controls.edges.filter( </p>
(edge) => edge?.node?.state === "IMPLEMENTED", </div>
).length; <Button asChild>
const totalControls = framework.controls.edges.length; <Link to={`/organizations/${organizationId}/frameworks/create`}>
<Plus className="mr-2 h-4 w-4" />
return ( Create Framework
<Link
key={framework.id}
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
>
<FrameworkCard
title={framework.name}
description={framework.description}
icon={
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
<span className="text-lg font-semibold text-blue-900">
{framework.name.split(" ")[0]}
</span>
</div>
}
status={
validatedControls === totalControls ? "Compliant" : undefined
}
progress={
validatedControls === totalControls
? "All controls validated"
: `${validatedControls}/${totalControls} Controls validated`
}
/>
</Link> </Link>
); </Button>
})} </div>
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
{frameworks.map((framework) => {
const validatedControls = framework.controls.edges.filter(
(edge) => edge?.node?.state === "IMPLEMENTED"
).length;
const totalControls = framework.controls.edges.length;
return (
<Link
key={framework.id}
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
>
<FrameworkCard
title={framework.name}
description={framework.description}
icon={
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
<span className="text-lg font-semibold text-blue-900">
{framework.name.split(" ")[0]}
</span>
</div>
}
status={
validatedControls === totalControls
? "Compliant"
: undefined
}
progress={
validatedControls === totalControls
? "All controls validated"
: `${validatedControls}/${totalControls} Controls validated`
}
/>
</Link>
);
})}
</div>
</div>
</div> </div>
</div> </>
); );
} }
@@ -164,7 +185,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();

View File

@@ -1,14 +1,15 @@
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router"; import { useParams, useNavigate, Link } from "react-router";
import { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { Shield, MoveUpRight, Clock } from "lucide-react"; import { Shield, MoveUpRight, Clock, Plus } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql"; import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
@@ -20,7 +21,7 @@ const FrameworkOverviewPageQuery = graphql`
... on Framework { ... on Framework {
name name
description description
controls { controls(first: 90) @connection(key: "FrameworkOverviewPage_controls") {
edges { edges {
node { node {
id id
@@ -56,17 +57,14 @@ function FrameworkOverviewPageContent({
const { organizationId } = useParams(); const { organizationId } = useParams();
// Group controls by their category // Group controls by their category
const controlsByCategory = controls.reduce( const controlsByCategory = controls.reduce((acc, control) => {
(acc, control) => { if (!control?.category) return acc;
if (!control?.category) return acc; if (!acc[control.category]) {
if (!acc[control.category]) { acc[control.category] = [];
acc[control.category] = []; }
} acc[control.category].push(control);
acc[control.category].push(control); return acc;
return acc; }, {} as Record<string, typeof controls>);
},
{} as Record<string, typeof controls>,
);
const controlCards = Object.entries(controlsByCategory).map( const controlCards = Object.entries(controlsByCategory).map(
([category, controls]) => ({ ([category, controls]) => ({
@@ -74,20 +72,30 @@ function FrameworkOverviewPageContent({
controls, controls,
completed: controls.filter((c) => c?.state === "IMPLEMENTED").length, completed: controls.filter((c) => c?.state === "IMPLEMENTED").length,
total: controls.length, total: controls.length,
}), })
); );
const totalImplemented = controls.filter( const totalImplemented = controls.filter(
(c) => c?.state === "IMPLEMENTED", (c) => c?.state === "IMPLEMENTED"
).length; ).length;
return ( return (
<div className="min-h-screen bg-background p-6 space-y-6"> <div className="min-h-screen bg-background p-6 space-y-6">
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<h1 className="text-2xl font-semibold">{framework.name}</h1> <div className="flex justify-between items-center">
<p className="text-muted-foreground max-w-3xl"> <div>
{framework.description} <h2 className="text-2xl font-semibold mb-1">{framework.name}</h2>
</p> <p className="text-muted-foreground">{framework.description}</p>
</div>
<Button asChild>
<Link
to={`/organizations/${organizationId}/frameworks/${framework.id}/controls/create`}
>
<Plus className="mr-2 h-4 w-4" />
Create Control
</Link>
</Button>
</div>
</div> </div>
<div> <div>
@@ -190,7 +198,7 @@ function FrameworkOverviewPageContent({
onClick={() => { onClick={() => {
if (control?.id) { if (control?.id) {
navigate( navigate(
`/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}`, `/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}`
); );
} }
}} }}
@@ -274,7 +282,7 @@ function FrameworkOverviewPageContent({
</div> </div>
</div> </div>
</div>, </div>,
document.body, document.body
)} )}
</div> </div>
); );
@@ -308,7 +316,7 @@ function FrameworkOverviewPageFallback() {
export default function FrameworkOverviewPage() { export default function FrameworkOverviewPage() {
const { frameworkId } = useParams(); const { frameworkId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
FrameworkOverviewPageQuery, FrameworkOverviewPageQuery
); );
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,194 @@
/**
* @generated SignedSource<<0e78396d5cf08670ae21d8a415c97948>>
* @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 CreateControlInput = {
category: string;
description: string;
frameworkId: string;
name: string;
};
export type CreateControlPageCreateControlMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateControlInput;
};
export type CreateControlPageCreateControlMutation$data = {
readonly createControl: {
readonly controlEdge: {
readonly node: {
readonly category: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly state: ControlState;
};
};
};
};
export type CreateControlPageCreateControlMutation = {
response: CreateControlPageCreateControlMutation$data;
variables: CreateControlPageCreateControlMutation$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": "ControlEdge",
"kind": "LinkedField",
"name": "controlEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Control",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "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
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "CreateControlPageCreateControlMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateControlPayload",
"kind": "LinkedField",
"name": "createControl",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "CreateControlPageCreateControlMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateControlPayload",
"kind": "LinkedField",
"name": "createControl",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "controlEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "c6b6a979bccf19dd57123c49d5ce738b",
"id": null,
"metadata": {},
"name": "CreateControlPageCreateControlMutation",
"operationKind": "mutation",
"text": "mutation CreateControlPageCreateControlMutation(\n $input: CreateControlInput!\n) {\n createControl(input: $input) {\n controlEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "4f8f339056a2f83c78ce3a642fa6ecc6";
export default node;

View File

@@ -0,0 +1,176 @@
/**
* @generated SignedSource<<9c88cb0bf2c3491784bdea7e46c918a3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateFrameworkInput = {
description: string;
name: string;
organizationId: string;
};
export type CreateFrameworkPageCreateFrameworkMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateFrameworkInput;
};
export type CreateFrameworkPageCreateFrameworkMutation$data = {
readonly createFramework: {
readonly frameworkEdge: {
readonly node: {
readonly description: string;
readonly id: string;
readonly name: string;
};
};
};
};
export type CreateFrameworkPageCreateFrameworkMutation = {
response: CreateFrameworkPageCreateFrameworkMutation$data;
variables: CreateFrameworkPageCreateFrameworkMutation$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": "FrameworkEdge",
"kind": "LinkedField",
"name": "frameworkEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Framework",
"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
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "CreateFrameworkPageCreateFrameworkMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateFrameworkPayload",
"kind": "LinkedField",
"name": "createFramework",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "CreateFrameworkPageCreateFrameworkMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateFrameworkPayload",
"kind": "LinkedField",
"name": "createFramework",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "frameworkEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "142ffd990da914bddaa2752dfff0faaf",
"id": null,
"metadata": {},
"name": "CreateFrameworkPageCreateFrameworkMutation",
"operationKind": "mutation",
"text": "mutation CreateFrameworkPageCreateFrameworkMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "60b83b4b5302f15a8e6f6b711c905963";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<ea02ef2ebc2c52a72d443c130a44087c>> * @generated SignedSource<<fd058cd12c357b25b11a3806c359e33b>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -64,115 +64,146 @@ v2 = {
"storageKey": null "storageKey": null
}, },
v3 = { v3 = {
"kind": "InlineFragment", "alias": null,
"selections": [ "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "__typename",
"args": null, "storageKey": null
"concreteType": "FrameworkConnection", },
"kind": "LinkedField", v4 = [
"name": "frameworks", {
"plural": false, "alias": null,
"selections": [ "args": null,
{ "concreteType": "FrameworkEdge",
"alias": null, "kind": "LinkedField",
"args": null, "name": "edges",
"concreteType": "FrameworkEdge", "plural": true,
"kind": "LinkedField", "selections": [
"name": "edges", {
"plural": true, "alias": null,
"selections": [ "args": null,
{ "concreteType": "Framework",
"alias": null, "kind": "LinkedField",
"args": null, "name": "node",
"concreteType": "Framework", "plural": false,
"kind": "LinkedField", "selections": [
"name": "node", (v2/*: any*/),
"plural": false, {
"selections": [ "alias": null,
(v2/*: any*/), "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "name",
"args": null, "storageKey": null
"kind": "ScalarField", },
"name": "name", {
"storageKey": null "alias": null,
}, "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "description",
"args": null, "storageKey": null
"kind": "ScalarField", },
"name": "description", {
"storageKey": null "alias": null,
}, "args": null,
{ "concreteType": "ControlConnection",
"alias": null, "kind": "LinkedField",
"args": null, "name": "controls",
"concreteType": "ControlConnection", "plural": false,
"kind": "LinkedField", "selections": [
"name": "controls", {
"plural": false, "alias": null,
"selections": [ "args": null,
{ "concreteType": "ControlEdge",
"alias": null, "kind": "LinkedField",
"args": null, "name": "edges",
"concreteType": "ControlEdge", "plural": true,
"kind": "LinkedField", "selections": [
"name": "edges", {
"plural": true, "alias": null,
"selections": [ "args": null,
{ "concreteType": "Control",
"alias": null, "kind": "LinkedField",
"args": null, "name": "node",
"concreteType": "Control", "plural": false,
"kind": "LinkedField", "selections": [
"name": "node", (v2/*: any*/),
"plural": false, {
"selections": [ "alias": null,
(v2/*: any*/), "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "state",
"args": null, "storageKey": null
"kind": "ScalarField", }
"name": "state", ],
"storageKey": null "storageKey": null
} }
], ],
"storageKey": null "storageKey": null
} }
], ],
"storageKey": null "storageKey": null
} },
], {
"storageKey": null "alias": null,
}, "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "createdAt",
"args": null, "storageKey": null
"kind": "ScalarField", },
"name": "createdAt", {
"storageKey": null "alias": null,
}, "args": null,
{ "kind": "ScalarField",
"alias": null, "name": "updatedAt",
"args": null, "storageKey": null
"kind": "ScalarField", },
"name": "updatedAt", (v3/*: any*/)
"storageKey": null ],
} "storageKey": null
], },
"storageKey": null {
} "alias": null,
], "args": null,
"storageKey": null "kind": "ScalarField",
} "name": "cursor",
], "storageKey": null
"storageKey": null }
} ],
], "storageKey": null
"type": "Organization", },
"abstractKey": 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
}
],
v5 = [
{
"kind": "Literal",
"name": "first",
"value": 25
}
];
return { return {
"fragment": { "fragment": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
@@ -188,7 +219,23 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/) {
"kind": "InlineFragment",
"selections": [
{
"alias": "frameworks",
"args": null,
"concreteType": "FrameworkConnection",
"kind": "LinkedField",
"name": "__FrameworkListPage_frameworks_connection",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
], ],
"storageKey": null "storageKey": null
} }
@@ -210,14 +257,33 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v3/*: any*/), (v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v5/*: any*/),
"concreteType": "FrameworkConnection",
"kind": "LinkedField",
"name": "frameworks",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "frameworks(first:25)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "FrameworkListPage_frameworks",
"kind": "LinkedHandle",
"name": "frameworks"
}
],
"type": "Organization",
"abstractKey": null
},
(v2/*: any*/) (v2/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -225,16 +291,28 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "e45d4e64a00b3e27cd6dc52179f47d52", "cacheID": "0922c219bcd69d7b2fd6b85399de7029",
"id": null, "id": null,
"metadata": {}, "metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"organization",
"frameworks"
]
}
]
},
"name": "FrameworkListPageQuery", "name": "FrameworkListPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query FrameworkListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n frameworks {\n edges {\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 id\n }\n}\n" "text": "query FrameworkListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n frameworks(first: 25) {\n edges {\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 __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "5cd314faffe31e716de2336e70cca4ee"; (node as any).hash = "11e5b067132c7a20da3f6d0d7b6ff450";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<27e925c9f55d555878de5cbe0c131d44>> * @generated SignedSource<<6f707328d181f9b362855a7b6023acc0>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -73,64 +73,93 @@ v4 = {
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"kind": "InlineFragment", "alias": null,
"selections": [ "args": null,
(v3/*: any*/), "kind": "ScalarField",
(v4/*: any*/), "name": "__typename",
{ "storageKey": null
"alias": null, },
"args": null, v6 = [
"concreteType": "ControlConnection", {
"kind": "LinkedField", "alias": null,
"name": "controls", "args": null,
"plural": false, "concreteType": "ControlEdge",
"selections": [ "kind": "LinkedField",
{ "name": "edges",
"alias": null, "plural": true,
"args": null, "selections": [
"concreteType": "ControlEdge", {
"kind": "LinkedField", "alias": null,
"name": "edges", "args": null,
"plural": true, "concreteType": "Control",
"selections": [ "kind": "LinkedField",
{ "name": "node",
"alias": null, "plural": false,
"args": null, "selections": [
"concreteType": "Control", (v2/*: any*/),
"kind": "LinkedField", (v3/*: any*/),
"name": "node", (v4/*: any*/),
"plural": false, {
"selections": [ "alias": null,
(v2/*: any*/), "args": null,
(v3/*: any*/), "kind": "ScalarField",
(v4/*: any*/), "name": "state",
{ "storageKey": null
"alias": null, },
"args": null, {
"kind": "ScalarField", "alias": null,
"name": "state", "args": null,
"storageKey": null "kind": "ScalarField",
}, "name": "category",
{ "storageKey": null
"alias": null, },
"args": null, (v5/*: any*/)
"kind": "ScalarField", ],
"name": "category", "storageKey": null
"storageKey": null },
} {
], "alias": null,
"storageKey": null "args": null,
} "kind": "ScalarField",
], "name": "cursor",
"storageKey": null "storageKey": null
} }
], ],
"storageKey": null "storageKey": null
} },
], {
"type": "Framework", "alias": null,
"abstractKey": 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
}
],
v7 = [
{
"kind": "Literal",
"name": "first",
"value": 90
}
];
return { return {
"fragment": { "fragment": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
@@ -147,7 +176,25 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
(v5/*: any*/) {
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": "controls",
"args": null,
"concreteType": "ControlConnection",
"kind": "LinkedField",
"name": "__FrameworkOverviewPage_controls_connection",
"plural": false,
"selections": (v6/*: any*/),
"storageKey": null
}
],
"type": "Framework",
"abstractKey": null
}
], ],
"storageKey": null "storageKey": null
} }
@@ -169,31 +216,64 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
{ (v5/*: any*/),
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/), (v2/*: any*/),
(v5/*: any*/) {
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": (v7/*: any*/),
"concreteType": "ControlConnection",
"kind": "LinkedField",
"name": "controls",
"plural": false,
"selections": (v6/*: any*/),
"storageKey": "controls(first:90)"
},
{
"alias": null,
"args": (v7/*: any*/),
"filters": null,
"handle": "connection",
"key": "FrameworkOverviewPage_controls",
"kind": "LinkedHandle",
"name": "controls"
}
],
"type": "Framework",
"abstractKey": null
}
], ],
"storageKey": null "storageKey": null
} }
] ]
}, },
"params": { "params": {
"cacheID": "c02ad61a9b6ce13a83576ad58aa0a7f4", "cacheID": "77f347f74c922dbf3c1dd0baf0e37efd",
"id": null, "id": null,
"metadata": {}, "metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"node",
"controls"
]
}
]
},
"name": "FrameworkOverviewPageQuery", "name": "FrameworkOverviewPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query FrameworkOverviewPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls {\n edges {\n node {\n id\n name\n description\n state\n category\n }\n }\n }\n }\n }\n}\n" "text": "query FrameworkOverviewPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 90) {\n edges {\n node {\n id\n name\n description\n state\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "3103fb6eba6ddf2625aa5b50bcdb43f6"; (node as any).hash = "de913888fcb5a475baf829a5773326a3";
export default node; export default node;

View File

@@ -394,6 +394,8 @@ type Mutation {
updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload! updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload!
createTask(input: CreateTaskInput!): CreateTaskPayload! createTask(input: CreateTaskInput!): CreateTaskPayload!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -540,3 +542,24 @@ input DeleteTaskInput {
type DeleteTaskPayload { type DeleteTaskPayload {
deletedTaskId: ID! deletedTaskId: ID!
} }
input CreateFrameworkInput {
organizationId: ID!
name: String!
description: String!
}
type CreateFrameworkPayload {
frameworkEdge: FrameworkEdge!
}
input CreateControlInput {
frameworkId: ID!
name: String!
description: String!
category: String!
}
type CreateControlPayload {
controlEdge: ControlEdge!
}

View File

@@ -97,6 +97,14 @@ type ComplexityRoot struct {
Node func(childComplexity int) int Node func(childComplexity int) int
} }
CreateControlPayload struct {
ControlEdge func(childComplexity int) int
}
CreateFrameworkPayload struct {
FrameworkEdge func(childComplexity int) int
}
CreateOrganizationPayload struct { CreateOrganizationPayload struct {
OrganizationEdge func(childComplexity int) int OrganizationEdge func(childComplexity int) int
} }
@@ -189,6 +197,8 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
CreateControl func(childComplexity int, input types.CreateControlInput) int
CreateFramework func(childComplexity int, input types.CreateFrameworkInput) int
CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int
CreatePeople func(childComplexity int, input types.CreatePeopleInput) int CreatePeople func(childComplexity int, input types.CreatePeopleInput) int
CreateTask func(childComplexity int, input types.CreateTaskInput) int CreateTask func(childComplexity int, input types.CreateTaskInput) int
@@ -363,6 +373,8 @@ type MutationResolver interface {
UpdateTaskState(ctx context.Context, input types.UpdateTaskStateInput) (*types.UpdateTaskStatePayload, error) UpdateTaskState(ctx context.Context, input types.UpdateTaskStateInput) (*types.UpdateTaskStatePayload, error)
CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error)
DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error)
CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error)
CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error)
@@ -571,6 +583,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ControlStateTransitionEdge.Node(childComplexity), true return e.complexity.ControlStateTransitionEdge.Node(childComplexity), true
case "CreateControlPayload.controlEdge":
if e.complexity.CreateControlPayload.ControlEdge == nil {
break
}
return e.complexity.CreateControlPayload.ControlEdge(childComplexity), true
case "CreateFrameworkPayload.frameworkEdge":
if e.complexity.CreateFrameworkPayload.FrameworkEdge == nil {
break
}
return e.complexity.CreateFrameworkPayload.FrameworkEdge(childComplexity), true
case "CreateOrganizationPayload.organizationEdge": case "CreateOrganizationPayload.organizationEdge":
if e.complexity.CreateOrganizationPayload.OrganizationEdge == nil { if e.complexity.CreateOrganizationPayload.OrganizationEdge == nil {
break break
@@ -861,6 +887,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.FrameworkEdge.Node(childComplexity), true return e.complexity.FrameworkEdge.Node(childComplexity), true
case "Mutation.createControl":
if e.complexity.Mutation.CreateControl == nil {
break
}
args, err := ec.field_Mutation_createControl_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CreateControl(childComplexity, args["input"].(types.CreateControlInput)), true
case "Mutation.createFramework":
if e.complexity.Mutation.CreateFramework == nil {
break
}
args, err := ec.field_Mutation_createFramework_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CreateFramework(childComplexity, args["input"].(types.CreateFrameworkInput)), true
case "Mutation.createOrganization": case "Mutation.createOrganization":
if e.complexity.Mutation.CreateOrganization == nil { if e.complexity.Mutation.CreateOrganization == nil {
break break
@@ -1582,6 +1632,8 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
opCtx := graphql.GetOperationContext(ctx) opCtx := graphql.GetOperationContext(ctx)
ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)}
inputUnmarshalMap := graphql.BuildUnmarshalerMap( inputUnmarshalMap := graphql.BuildUnmarshalerMap(
ec.unmarshalInputCreateControlInput,
ec.unmarshalInputCreateFrameworkInput,
ec.unmarshalInputCreateOrganizationInput, ec.unmarshalInputCreateOrganizationInput,
ec.unmarshalInputCreatePeopleInput, ec.unmarshalInputCreatePeopleInput,
ec.unmarshalInputCreateTaskInput, ec.unmarshalInputCreateTaskInput,
@@ -2086,6 +2138,8 @@ type Mutation {
updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload! updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload!
createTask(input: CreateTaskInput!): CreateTaskPayload! createTask(input: CreateTaskInput!): CreateTaskPayload!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -2232,6 +2286,27 @@ input DeleteTaskInput {
type DeleteTaskPayload { type DeleteTaskPayload {
deletedTaskId: ID! deletedTaskId: ID!
} }
input CreateFrameworkInput {
organizationId: ID!
name: String!
description: String!
}
type CreateFrameworkPayload {
frameworkEdge: FrameworkEdge!
}
input CreateControlInput {
frameworkId: ID!
name: String!
description: String!
category: String!
}
type CreateControlPayload {
controlEdge: ControlEdge!
}
`, BuiltIn: false}, `, BuiltIn: false},
} }
var parsedSchema = gqlparser.MustLoadSchema(sources...) var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -2548,6 +2623,52 @@ func (ec *executionContext) field_Framework_controls_argsBefore(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Mutation_createControl_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_createControl_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_createControl_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.CreateControlInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNCreateControlInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlInput(ctx, tmp)
}
var zeroVal types.CreateControlInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_createFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_createFramework_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_createFramework_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.CreateFrameworkInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNCreateFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateFrameworkInput(ctx, tmp)
}
var zeroVal types.CreateFrameworkInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_createOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_createOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -4319,6 +4440,94 @@ func (ec *executionContext) fieldContext_ControlStateTransitionEdge_node(_ conte
return fc, nil return fc, nil
} }
func (ec *executionContext) _CreateControlPayload_controlEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateControlPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_CreateControlPayload_controlEdge(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.ControlEdge, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.ControlEdge)
fc.Result = res
return ec.marshalNControlEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlEdge(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_CreateControlPayload_controlEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "CreateControlPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "cursor":
return ec.fieldContext_ControlEdge_cursor(ctx, field)
case "node":
return ec.fieldContext_ControlEdge_node(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type ControlEdge", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _CreateFrameworkPayload_frameworkEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateFrameworkPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_CreateFrameworkPayload_frameworkEdge(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.FrameworkEdge, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.FrameworkEdge)
fc.Result = res
return ec.marshalNFrameworkEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐFrameworkEdge(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_CreateFrameworkPayload_frameworkEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "CreateFrameworkPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "cursor":
return ec.fieldContext_FrameworkEdge_cursor(ctx, field)
case "node":
return ec.fieldContext_FrameworkEdge_node(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type FrameworkEdge", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _CreateOrganizationPayload_organizationEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateOrganizationPayload) (ret graphql.Marshaler) { func (ec *executionContext) _CreateOrganizationPayload_organizationEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateOrganizationPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_CreateOrganizationPayload_organizationEdge(ctx, field) fc, err := ec.fieldContext_CreateOrganizationPayload_organizationEdge(ctx, field)
if err != nil { if err != nil {
@@ -6528,6 +6737,100 @@ func (ec *executionContext) fieldContext_Mutation_deleteTask(ctx context.Context
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_createFramework(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createFramework(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreateFramework(rctx, fc.Args["input"].(types.CreateFrameworkInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.CreateFrameworkPayload)
fc.Result = res
return ec.marshalNCreateFrameworkPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateFrameworkPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_createFramework(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "frameworkEdge":
return ec.fieldContext_CreateFrameworkPayload_frameworkEdge(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type CreateFrameworkPayload", field.Name)
},
}
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_createFramework_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_createControl(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createControl(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreateControl(rctx, fc.Args["input"].(types.CreateControlInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.CreateControlPayload)
fc.Result = res
return ec.marshalNCreateControlPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_createControl(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "controlEdge":
return ec.fieldContext_CreateControlPayload_controlEdge(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type CreateControlPayload", field.Name)
},
}
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_createControl_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Organization_id(ctx, field) fc, err := ec.fieldContext_Organization_id(ctx, field)
if err != nil { if err != nil {
@@ -11409,6 +11712,95 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context
// region **************************** input.gotpl ***************************** // region **************************** input.gotpl *****************************
func (ec *executionContext) unmarshalInputCreateControlInput(ctx context.Context, obj any) (types.CreateControlInput, error) {
var it types.CreateControlInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"frameworkId", "name", "description", "category"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "frameworkId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("frameworkId"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.FrameworkID = data
case "name":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Name = data
case "description":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Description = data
case "category":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("category"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Category = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputCreateFrameworkInput(ctx context.Context, obj any) (types.CreateFrameworkInput, error) {
var it types.CreateFrameworkInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId", "name", "description"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "organizationId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OrganizationID = data
case "name":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Name = data
case "description":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Description = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputCreateOrganizationInput(ctx context.Context, obj any) (types.CreateOrganizationInput, error) { func (ec *executionContext) unmarshalInputCreateOrganizationInput(ctx context.Context, obj any) (types.CreateOrganizationInput, error) {
var it types.CreateOrganizationInput var it types.CreateOrganizationInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -12361,6 +12753,84 @@ func (ec *executionContext) _ControlStateTransitionEdge(ctx context.Context, sel
return out return out
} }
var createControlPayloadImplementors = []string{"CreateControlPayload"}
func (ec *executionContext) _CreateControlPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateControlPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, createControlPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CreateControlPayload")
case "controlEdge":
out.Values[i] = ec._CreateControlPayload_controlEdge(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var createFrameworkPayloadImplementors = []string{"CreateFrameworkPayload"}
func (ec *executionContext) _CreateFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateFrameworkPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, createFrameworkPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CreateFrameworkPayload")
case "frameworkEdge":
out.Values[i] = ec._CreateFrameworkPayload_frameworkEdge(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var createOrganizationPayloadImplementors = []string{"CreateOrganizationPayload"} var createOrganizationPayloadImplementors = []string{"CreateOrganizationPayload"}
func (ec *executionContext) _CreateOrganizationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateOrganizationPayload) graphql.Marshaler { func (ec *executionContext) _CreateOrganizationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateOrganizationPayload) graphql.Marshaler {
@@ -13281,6 +13751,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "createFramework":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createFramework(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createControl":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createControl(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -15062,6 +15546,44 @@ func (ec *executionContext) marshalNControlStateTransitionEdge2ᚖgithubᚗcom
return ec._ControlStateTransitionEdge(ctx, sel, v) return ec._ControlStateTransitionEdge(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNCreateControlInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlInput(ctx context.Context, v any) (types.CreateControlInput, error) {
res, err := ec.unmarshalInputCreateControlInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNCreateControlPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlPayload(ctx context.Context, sel ast.SelectionSet, v types.CreateControlPayload) graphql.Marshaler {
return ec._CreateControlPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNCreateControlPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateControlPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._CreateControlPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNCreateFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateFrameworkInput(ctx context.Context, v any) (types.CreateFrameworkInput, error) {
res, err := ec.unmarshalInputCreateFrameworkInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNCreateFrameworkPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateFrameworkPayload(ctx context.Context, sel ast.SelectionSet, v types.CreateFrameworkPayload) graphql.Marshaler {
return ec._CreateFrameworkPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNCreateFrameworkPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateFrameworkPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateFrameworkPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._CreateFrameworkPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNCreateOrganizationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateOrganizationInput(ctx context.Context, v any) (types.CreateOrganizationInput, error) { func (ec *executionContext) unmarshalNCreateOrganizationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateOrganizationInput(ctx context.Context, v any) (types.CreateOrganizationInput, error) {
res, err := ec.unmarshalInputCreateOrganizationInput(ctx, v) res, err := ec.unmarshalInputCreateOrganizationInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -59,6 +59,27 @@ type ControlStateTransitionEdge struct {
Node *ControlStateTransition `json:"node"` Node *ControlStateTransition `json:"node"`
} }
type CreateControlInput struct {
FrameworkID gid.GID `json:"frameworkId"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
}
type CreateControlPayload struct {
ControlEdge *ControlEdge `json:"controlEdge"`
}
type CreateFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
}
type CreateFrameworkPayload struct {
FrameworkEdge *FrameworkEdge `json:"frameworkEdge"`
}
type CreateOrganizationInput struct { type CreateOrganizationInput struct {
Name string `json:"name"` Name string `json:"name"`
} }

View File

@@ -237,6 +237,39 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTas
}, nil }, nil
} }
// CreateFramework is the resolver for the createFramework field.
func (r *mutationResolver) CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error) {
framework, err := r.proboSvc.CreateFramework(ctx, probo.CreateFrameworkRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
})
if err != nil {
return nil, fmt.Errorf("cannot create framework: %w", err)
}
return &types.CreateFrameworkPayload{
FrameworkEdge: types.NewFrameworkEdge(framework),
}, nil
}
// CreateControl is the resolver for the createControl field.
func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) {
control, err := r.proboSvc.CreateControl(ctx, probo.CreateControlRequest{
FrameworkID: input.FrameworkID,
Name: input.Name,
Description: input.Description,
Category: input.Category,
})
if err != nil {
return nil, fmt.Errorf("cannot create control: %w", err)
}
return &types.CreateControlPayload{
ControlEdge: types.NewControlEdge(control),
}, nil
}
// Frameworks is the resolver for the frameworks field. // Frameworks is the resolver for the frameworks field.
func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) {
cursor := types.NewCursor(first, after, last, before) cursor := types.NewCursor(first, after, last, before)

View File

@@ -127,22 +127,21 @@ func (c Control) Insert(
INSERT INTO INSERT INTO
controls ( controls (
id, id,
control_id, framework_id,
category, category,
name, name,
description, description,
content_ref, content_ref,
state,
created_at, created_at,
updated_at updated_at
) )
VALUES ( VALUES (
@control_id, @control_id,
@framework_id, @framework_id,
@category,
@name, @name,
@description, @description,
@content_ref, @content_ref,
@state,
@created_at, @created_at,
@updated_at @updated_at
); );
@@ -151,6 +150,7 @@ VALUES (
args := pgx.NamedArgs{ args := pgx.NamedArgs{
"control_id": c.ID, "control_id": c.ID,
"framework_id": c.FrameworkID, "framework_id": c.FrameworkID,
"category": c.Category,
"name": c.Name, "name": c.Name,
"description": c.Description, "description": c.Description,
"content_ref": c.ContentRef, "content_ref": c.ContentRef,

View File

@@ -60,7 +60,7 @@ INSERT INTO
control_state_transitions ( control_state_transitions (
id, id,
control_id, control_id,
from_state from_state,
to_state, to_state,
reason, reason,
created_at, created_at,

View File

@@ -165,7 +165,7 @@ INSERT INTO
updated_at updated_at
) )
VALUES ( VALUES (
@frameworkd_id, @framework_id,
@organization_id, @organization_id,
@name, @name,
@description, @description,

View File

@@ -31,6 +31,7 @@ type (
Name string Name string
Description string Description string
ContentRef string ContentRef string
Category string
} }
) )
@@ -54,6 +55,7 @@ func (s Service) CreateControl(
FrameworkID: req.FrameworkID, FrameworkID: req.FrameworkID,
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
Category: req.Category,
State: coredata.ControlStateNotStarted, State: coredata.ControlStateNotStarted,
ContentRef: req.ContentRef, ContentRef: req.ContentRef,
CreatedAt: now, CreatedAt: now,

View File

@@ -26,9 +26,10 @@ import (
type ( type (
CreateFrameworkRequest struct { CreateFrameworkRequest struct {
Name string OrganizationID gid.GID
Description string Name string
ContentRef string Description string
ContentRef string
} }
) )
@@ -44,7 +45,7 @@ func (s Service) CreateFramework(
framework := &coredata.Framework{ framework := &coredata.Framework{
ID: frameworkID, ID: frameworkID,
OrganizationID: gid.Nil, OrganizationID: req.OrganizationID,
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
ContentRef: req.ContentRef, ContentRef: req.ContentRef,