@@ -145,9 +145,11 @@ function getNavItems(organizationId?: string): NavItem[] {
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Risk",
|
title: "Risks",
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
url: organizationId ? `/organizations/${organizationId}/risk` : undefined,
|
url: organizationId
|
||||||
|
? `/organizations/${organizationId}/risks`
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Frameworks",
|
title: "Frameworks",
|
||||||
|
|||||||
114
apps/console/src/components/ui/table.tsx
Normal file
114
apps/console/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
@@ -9,9 +9,7 @@ import { CreatePeoplePage } from "./people/CreatePeoplePage";
|
|||||||
import { PeoplePage } from "./people/PeoplePage";
|
import { PeoplePage } from "./people/PeoplePage";
|
||||||
import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
||||||
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
||||||
import { CreateMitigationPage } from "./frameworks/mitigations/CreateMitigationPage";
|
import { MitigationPage } from "./mitigations/MitigationPage";
|
||||||
import { MitigationPage } from "./frameworks/mitigations/MitigationPage";
|
|
||||||
import { UpdateMitigationPage } from "./frameworks/mitigations/UpdateMitigationPage";
|
|
||||||
import { VendorPage } from "./vendors/VendorPage";
|
import { VendorPage } from "./vendors/VendorPage";
|
||||||
import { PolicyListPage } from "./policies/PolicyListPage";
|
import { PolicyListPage } from "./policies/PolicyListPage";
|
||||||
import { CreatePolicyPage } from "./policies/CreatePolicyPage";
|
import { CreatePolicyPage } from "./policies/CreatePolicyPage";
|
||||||
@@ -19,6 +17,8 @@ import { PolicyPage } from "./policies/PolicyPage";
|
|||||||
import { UpdatePolicyPage } from "./policies/UpdatePolicyPage";
|
import { UpdatePolicyPage } from "./policies/UpdatePolicyPage";
|
||||||
import { SettingsPage } from "./SettingsPage";
|
import { SettingsPage } from "./SettingsPage";
|
||||||
import { CreateOrganizationPage } from "./CreateOrganizationPage";
|
import { CreateOrganizationPage } from "./CreateOrganizationPage";
|
||||||
|
import { MitigationListPage } from "./mitigations/MitigationListPage";
|
||||||
|
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import NotFoundPage from "../NotFoundPage";
|
import NotFoundPage from "../NotFoundPage";
|
||||||
|
|
||||||
@@ -38,18 +38,13 @@ export function OrganizationsRoutes() {
|
|||||||
path="frameworks/:frameworkId/update"
|
path="frameworks/:frameworkId/update"
|
||||||
element={<UpdateFrameworkPage />}
|
element={<UpdateFrameworkPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="mitigations" element={<MitigationListPage />} />
|
||||||
|
<Route path="mitigations/:mitigationId" element={<MitigationPage />} />
|
||||||
|
{/* <Route path="mitigations/create" element={<CreateMitigationPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/mitigations/create"
|
path="mitigations/:mitigationId/update"
|
||||||
element={<CreateMitigationPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="frameworks/:frameworkId/mitigations/:mitigationId"
|
|
||||||
element={<MitigationPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="frameworks/:frameworkId/mitigations/:mitigationId/update"
|
|
||||||
element={<UpdateMitigationPage />}
|
element={<UpdateMitigationPage />}
|
||||||
/>
|
/> */}
|
||||||
<Route path="vendors/:vendorId" element={<VendorPage />} />
|
<Route path="vendors/:vendorId" element={<VendorPage />} />
|
||||||
<Route path="policies" element={<PolicyListPage />} />
|
<Route path="policies" element={<PolicyListPage />} />
|
||||||
<Route path="policies/create" element={<CreatePolicyPage />} />
|
<Route path="policies/create" element={<CreatePolicyPage />} />
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { useLocation } from "react-router";
|
|
||||||
import { lazy } from "@probo/react-lazy";
|
|
||||||
|
|
||||||
const CreateMitigationView = lazy(() => import("./CreateMitigationView"));
|
|
||||||
|
|
||||||
export function CreateMitigationViewSkeleton() {
|
|
||||||
return (
|
|
||||||
<PageTemplateSkeleton
|
|
||||||
title="Create Mitigation"
|
|
||||||
description="Create a new mitigation for your framework"
|
|
||||||
>
|
|
||||||
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateMitigationPage() {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
key={location.pathname}
|
|
||||||
fallback={<CreateMitigationViewSkeleton />}
|
|
||||||
>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreateMitigationView />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
import { Suspense, useState } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router";
|
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
|
||||||
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 {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
CreateMitigationViewCreateMitigationMutation,
|
|
||||||
MitigationImportance,
|
|
||||||
} from "./__generated__/CreateMitigationViewCreateMitigationMutation.graphql";
|
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
|
||||||
import { CreateMitigationViewSkeleton } from "./CreateMitigationPage";
|
|
||||||
|
|
||||||
const createMitigationMutation = graphql`
|
|
||||||
mutation CreateMitigationViewCreateMitigationMutation(
|
|
||||||
$input: CreateMitigationInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createMitigation(input: $input) {
|
|
||||||
mitigationEdge @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 CreateMitigationViewContent() {
|
|
||||||
const { organizationId, frameworkId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
category: "",
|
|
||||||
importance: "MANDATORY" as MitigationImportance,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<CreateMitigationViewCreateMitigationMutation>(
|
|
||||||
createMitigationMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
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_mitigations"
|
|
||||||
);
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organizationId!,
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
category: formData.category,
|
|
||||||
importance: formData.importance,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to create mitigation",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Mitigation created successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/mitigations/${data.createMitigation.mitigationEdge.node.id}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to create mitigation",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Create Mitigation"
|
|
||||||
description="Create a new mitigation for your framework"
|
|
||||||
>
|
|
||||||
<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 mitigation belongs to"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
|
||||||
required
|
|
||||||
multiline
|
|
||||||
helpText="Provide a detailed description of the mitigation"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
|
||||||
Importance
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.importance}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleFieldChange("importance", value as MitigationImportance)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select importance" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Creating..." : "Create Mitigation"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateControlView() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<CreateMitigationViewSkeleton />}>
|
|
||||||
<CreateMitigationViewContent />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { lazy } from "@probo/react-lazy";
|
|
||||||
import { useLocation } from "react-router";
|
|
||||||
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
|
||||||
|
|
||||||
const UpdateMitigationView = lazy(() => import("./UpdateMitigationView"));
|
|
||||||
|
|
||||||
export function UpdateMitigationViewSkeleton() {
|
|
||||||
return (
|
|
||||||
<PageTemplateSkeleton
|
|
||||||
title="Update Mitigation"
|
|
||||||
description="Update the mitigation details"
|
|
||||||
>
|
|
||||||
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdateMitigationPage() {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
key={location.pathname}
|
|
||||||
fallback={<UpdateMitigationViewSkeleton />}
|
|
||||||
>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<UpdateMitigationView />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
import { Suspense, useState, useEffect } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router";
|
|
||||||
import {
|
|
||||||
graphql,
|
|
||||||
useMutation,
|
|
||||||
usePreloadedQuery,
|
|
||||||
PreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
} from "react-relay";
|
|
||||||
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 {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import type {
|
|
||||||
MitigationState,
|
|
||||||
MitigationImportance,
|
|
||||||
UpdateMitigationViewUpdateMitigationMutation,
|
|
||||||
} from "./__generated__/UpdateMitigationViewUpdateMitigationMutation.graphql";
|
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
|
||||||
import { MitigationViewSkeleton } from "./MitigationPage";
|
|
||||||
|
|
||||||
const updateMitigationMutation = graphql`
|
|
||||||
mutation UpdateMitigationViewUpdateMitigationMutation(
|
|
||||||
$input: UpdateMitigationInput!
|
|
||||||
) {
|
|
||||||
updateMitigation(input: $input) {
|
|
||||||
mitigation {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const updateMitigationQuery = graphql`
|
|
||||||
query UpdateMitigationViewQuery($mitigationId: ID!) {
|
|
||||||
node(id: $mitigationId) {
|
|
||||||
... on Mitigation {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
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 UpdateMitigationViewContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
queryRef: PreloadedQuery<any>;
|
|
||||||
}) {
|
|
||||||
const { organizationId, frameworkId, mitigationId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const data = usePreloadedQuery(updateMitigationQuery, queryRef);
|
|
||||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
category: "",
|
|
||||||
state: "",
|
|
||||||
importance: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data.node) {
|
|
||||||
setFormData({
|
|
||||||
name: data.node.name || "",
|
|
||||||
description: data.node.description || "",
|
|
||||||
category: data.node.category || "",
|
|
||||||
state: data.node.state || "",
|
|
||||||
importance: data.node.importance || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data.node]);
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<UpdateMitigationViewUpdateMitigationMutation>(
|
|
||||||
updateMitigationMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: string) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
setEditedFields((prev) => new Set(prev).add(field));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/mitigations/${mitigationId}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
|
||||||
|
|
||||||
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 input: {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
state?: MitigationState;
|
|
||||||
importance?: MitigationImportance;
|
|
||||||
} = {
|
|
||||||
id: mitigationId!,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (editedFields.has("name")) {
|
|
||||||
input.name = formData.name;
|
|
||||||
}
|
|
||||||
if (editedFields.has("description")) {
|
|
||||||
input.description = formData.description;
|
|
||||||
}
|
|
||||||
if (editedFields.has("category")) {
|
|
||||||
input.category = formData.category;
|
|
||||||
}
|
|
||||||
if (editedFields.has("state")) {
|
|
||||||
input.state = formData.state as MitigationState;
|
|
||||||
}
|
|
||||||
if (editedFields.has("importance")) {
|
|
||||||
input.importance = formData.importance as MitigationImportance;
|
|
||||||
}
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input,
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to update mitigation",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Mitigation updated successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/mitigations/${mitigationId}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to update mitigation",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Update Mitigation"
|
|
||||||
description="Update the mitigation details"
|
|
||||||
>
|
|
||||||
<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 mitigation"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Category"
|
|
||||||
value={formData.category}
|
|
||||||
onChange={(value) => handleFieldChange("category", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
|
||||||
Importance
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.importance}
|
|
||||||
onValueChange={(value) => handleFieldChange("importance", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select importance" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="state" className="text-sm font-medium">
|
|
||||||
State
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.state}
|
|
||||||
onValueChange={(value) => handleFieldChange("state", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select state" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
|
|
||||||
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
|
||||||
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
|
|
||||||
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight || !hasChanges}>
|
|
||||||
{isInFlight ? "Updating..." : "Update Mitigation"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdateControlView() {
|
|
||||||
const { mitigationId } = useParams();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [queryRef, loadQuery] = useQueryLoader<any>(updateMitigationQuery);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mitigationId) {
|
|
||||||
loadQuery({ mitigationId });
|
|
||||||
}
|
|
||||||
}, [mitigationId, loadQuery]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <MitigationViewSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<MitigationViewSkeleton />}>
|
|
||||||
<UpdateMitigationViewContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
/**
|
|
||||||
* @generated SignedSource<<07454812ea72dd51d0b615737e75a2a1>>
|
|
||||||
* @lightSyntaxTransform
|
|
||||||
* @nogrep
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
|
||||||
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
|
||||||
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
|
||||||
export type CreateMitigationInput = {
|
|
||||||
category: string;
|
|
||||||
description: string;
|
|
||||||
importance: MitigationImportance;
|
|
||||||
name: string;
|
|
||||||
organizationId: string;
|
|
||||||
};
|
|
||||||
export type CreateMitigationViewCreateMitigationMutation$variables = {
|
|
||||||
connections: ReadonlyArray<string>;
|
|
||||||
input: CreateMitigationInput;
|
|
||||||
};
|
|
||||||
export type CreateMitigationViewCreateMitigationMutation$data = {
|
|
||||||
readonly createMitigation: {
|
|
||||||
readonly mitigationEdge: {
|
|
||||||
readonly node: {
|
|
||||||
readonly category: string;
|
|
||||||
readonly description: string;
|
|
||||||
readonly id: string;
|
|
||||||
readonly name: string;
|
|
||||||
readonly state: MitigationState;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type CreateMitigationViewCreateMitigationMutation = {
|
|
||||||
response: CreateMitigationViewCreateMitigationMutation$data;
|
|
||||||
variables: CreateMitigationViewCreateMitigationMutation$variables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
|
||||||
var v0 = {
|
|
||||||
"defaultValue": null,
|
|
||||||
"kind": "LocalArgument",
|
|
||||||
"name": "connections"
|
|
||||||
},
|
|
||||||
v1 = {
|
|
||||||
"defaultValue": null,
|
|
||||||
"kind": "LocalArgument",
|
|
||||||
"name": "input"
|
|
||||||
},
|
|
||||||
v2 = [
|
|
||||||
{
|
|
||||||
"kind": "Variable",
|
|
||||||
"name": "input",
|
|
||||||
"variableName": "input"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
v3 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "MitigationEdge",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "mitigationEdge",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "Mitigation",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "node",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "id",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "name",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "description",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "category",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "state",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
"fragment": {
|
|
||||||
"argumentDefinitions": [
|
|
||||||
(v0/*: any*/),
|
|
||||||
(v1/*: any*/)
|
|
||||||
],
|
|
||||||
"kind": "Fragment",
|
|
||||||
"metadata": null,
|
|
||||||
"name": "CreateMitigationViewCreateMitigationMutation",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v2/*: any*/),
|
|
||||||
"concreteType": "CreateMitigationPayload",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "createMitigation",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
(v3/*: any*/)
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Mutation",
|
|
||||||
"abstractKey": null
|
|
||||||
},
|
|
||||||
"kind": "Request",
|
|
||||||
"operation": {
|
|
||||||
"argumentDefinitions": [
|
|
||||||
(v1/*: any*/),
|
|
||||||
(v0/*: any*/)
|
|
||||||
],
|
|
||||||
"kind": "Operation",
|
|
||||||
"name": "CreateMitigationViewCreateMitigationMutation",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v2/*: any*/),
|
|
||||||
"concreteType": "CreateMitigationPayload",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "createMitigation",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
(v3/*: any*/),
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"filters": null,
|
|
||||||
"handle": "prependEdge",
|
|
||||||
"key": "",
|
|
||||||
"kind": "LinkedHandle",
|
|
||||||
"name": "mitigationEdge",
|
|
||||||
"handleArgs": [
|
|
||||||
{
|
|
||||||
"kind": "Variable",
|
|
||||||
"name": "connections",
|
|
||||||
"variableName": "connections"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"cacheID": "09c15768681bc81e7035ff20e90e81a0",
|
|
||||||
"id": null,
|
|
||||||
"metadata": {},
|
|
||||||
"name": "CreateMitigationViewCreateMitigationMutation",
|
|
||||||
"operationKind": "mutation",
|
|
||||||
"text": "mutation CreateMitigationViewCreateMitigationMutation(\n $input: CreateMitigationInput!\n) {\n createMitigation(input: $input) {\n mitigationEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
(node as any).hash = "42db1acd8c6e92678c9e6355a77e1eb6";
|
|
||||||
|
|
||||||
export default node;
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
/**
|
|
||||||
* @generated SignedSource<<c4f9f399e7a3d6c0e0482af3eccc8492>>
|
|
||||||
* @lightSyntaxTransform
|
|
||||||
* @nogrep
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
|
||||||
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
|
||||||
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
|
||||||
export type UpdateMitigationViewQuery$variables = {
|
|
||||||
mitigationId: string;
|
|
||||||
};
|
|
||||||
export type UpdateMitigationViewQuery$data = {
|
|
||||||
readonly node: {
|
|
||||||
readonly category?: string;
|
|
||||||
readonly description?: string;
|
|
||||||
readonly id?: string;
|
|
||||||
readonly importance?: MitigationImportance;
|
|
||||||
readonly name?: string;
|
|
||||||
readonly state?: MitigationState;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type UpdateMitigationViewQuery = {
|
|
||||||
response: UpdateMitigationViewQuery$data;
|
|
||||||
variables: UpdateMitigationViewQuery$variables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
|
||||||
var v0 = [
|
|
||||||
{
|
|
||||||
"defaultValue": null,
|
|
||||||
"kind": "LocalArgument",
|
|
||||||
"name": "mitigationId"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
v1 = [
|
|
||||||
{
|
|
||||||
"kind": "Variable",
|
|
||||||
"name": "id",
|
|
||||||
"variableName": "mitigationId"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
v2 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "id",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
v3 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "name",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
v4 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "description",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
v5 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "category",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
v6 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "importance",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
v7 = {
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "state",
|
|
||||||
"storageKey": null
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
"fragment": {
|
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
|
||||||
"kind": "Fragment",
|
|
||||||
"metadata": null,
|
|
||||||
"name": "UpdateMitigationViewQuery",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v1/*: any*/),
|
|
||||||
"concreteType": null,
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "node",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"kind": "InlineFragment",
|
|
||||||
"selections": [
|
|
||||||
(v2/*: any*/),
|
|
||||||
(v3/*: any*/),
|
|
||||||
(v4/*: any*/),
|
|
||||||
(v5/*: any*/),
|
|
||||||
(v6/*: any*/),
|
|
||||||
(v7/*: any*/)
|
|
||||||
],
|
|
||||||
"type": "Mitigation",
|
|
||||||
"abstractKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Query",
|
|
||||||
"abstractKey": null
|
|
||||||
},
|
|
||||||
"kind": "Request",
|
|
||||||
"operation": {
|
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
|
||||||
"kind": "Operation",
|
|
||||||
"name": "UpdateMitigationViewQuery",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": (v1/*: any*/),
|
|
||||||
"concreteType": null,
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "node",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "__typename",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
(v2/*: any*/),
|
|
||||||
{
|
|
||||||
"kind": "InlineFragment",
|
|
||||||
"selections": [
|
|
||||||
(v3/*: any*/),
|
|
||||||
(v4/*: any*/),
|
|
||||||
(v5/*: any*/),
|
|
||||||
(v6/*: any*/),
|
|
||||||
(v7/*: any*/)
|
|
||||||
],
|
|
||||||
"type": "Mitigation",
|
|
||||||
"abstractKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"cacheID": "81b47701802e110078ee303a83c19d88",
|
|
||||||
"id": null,
|
|
||||||
"metadata": {},
|
|
||||||
"name": "UpdateMitigationViewQuery",
|
|
||||||
"operationKind": "query",
|
|
||||||
"text": "query UpdateMitigationViewQuery(\n $mitigationId: ID!\n) {\n node(id: $mitigationId) {\n __typename\n ... on Mitigation {\n id\n name\n description\n category\n importance\n state\n }\n id\n }\n}\n"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
(node as any).hash = "126fe89ef3da933cfb7847ff05179832";
|
|
||||||
|
|
||||||
export default node;
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
/**
|
|
||||||
* @generated SignedSource<<d6d22035bb11c4fff1db97bac26aa7d1>>
|
|
||||||
* @lightSyntaxTransform
|
|
||||||
* @nogrep
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
|
||||||
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
|
||||||
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
|
||||||
export type UpdateMitigationInput = {
|
|
||||||
category?: string | null | undefined;
|
|
||||||
description?: string | null | undefined;
|
|
||||||
id: string;
|
|
||||||
importance?: MitigationImportance | null | undefined;
|
|
||||||
name?: string | null | undefined;
|
|
||||||
state?: MitigationState | null | undefined;
|
|
||||||
};
|
|
||||||
export type UpdateMitigationViewUpdateMitigationMutation$variables = {
|
|
||||||
input: UpdateMitigationInput;
|
|
||||||
};
|
|
||||||
export type UpdateMitigationViewUpdateMitigationMutation$data = {
|
|
||||||
readonly updateMitigation: {
|
|
||||||
readonly mitigation: {
|
|
||||||
readonly category: string;
|
|
||||||
readonly description: string;
|
|
||||||
readonly id: string;
|
|
||||||
readonly importance: MitigationImportance;
|
|
||||||
readonly name: string;
|
|
||||||
readonly state: MitigationState;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type UpdateMitigationViewUpdateMitigationMutation = {
|
|
||||||
response: UpdateMitigationViewUpdateMitigationMutation$data;
|
|
||||||
variables: UpdateMitigationViewUpdateMitigationMutation$variables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
|
||||||
var v0 = [
|
|
||||||
{
|
|
||||||
"defaultValue": null,
|
|
||||||
"kind": "LocalArgument",
|
|
||||||
"name": "input"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
v1 = [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"kind": "Variable",
|
|
||||||
"name": "input",
|
|
||||||
"variableName": "input"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"concreteType": "UpdateMitigationPayload",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "updateMitigation",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "Mitigation",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "mitigation",
|
|
||||||
"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": "importance",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "state",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
"fragment": {
|
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
|
||||||
"kind": "Fragment",
|
|
||||||
"metadata": null,
|
|
||||||
"name": "UpdateMitigationViewUpdateMitigationMutation",
|
|
||||||
"selections": (v1/*: any*/),
|
|
||||||
"type": "Mutation",
|
|
||||||
"abstractKey": null
|
|
||||||
},
|
|
||||||
"kind": "Request",
|
|
||||||
"operation": {
|
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
|
||||||
"kind": "Operation",
|
|
||||||
"name": "UpdateMitigationViewUpdateMitigationMutation",
|
|
||||||
"selections": (v1/*: any*/)
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"cacheID": "4a58a30780331c2d00bab2c3b613635b",
|
|
||||||
"id": null,
|
|
||||||
"metadata": {},
|
|
||||||
"name": "UpdateMitigationViewUpdateMitigationMutation",
|
|
||||||
"operationKind": "mutation",
|
|
||||||
"text": "mutation UpdateMitigationViewUpdateMitigationMutation(\n $input: UpdateMitigationInput!\n) {\n updateMitigation(input: $input) {\n mitigation {\n id\n name\n description\n category\n importance\n state\n }\n }\n}\n"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
(node as any).hash = "b8cf81c9342fe42160ee39f572b01b24";
|
|
||||||
|
|
||||||
export default node;
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { lazy } from "@probo/react-lazy";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
|
const MitigationListView = lazy(() => import("./MitigationListView"));
|
||||||
|
|
||||||
|
export function MitigationListViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
title="Mitigations"
|
||||||
|
description="Mitigations are actions taken to reduce the risk of a risk."
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="rounded-xl border bg-card p-4 space-y-4">
|
||||||
|
<div className="h-5 w-32 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-10 flex-1 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-10 w-32 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-[72px] bg-muted animate-pulse rounded-xl"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MitigationListPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<MitigationListViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<MitigationListView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
|
import { useParams, useNavigate } from "react-router";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
ChevronDown,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { MitigationListViewQuery as MitigationListViewQueryType } from "./__generated__/MitigationListViewQuery.graphql";
|
||||||
|
import { MitigationListViewSkeleton } from "./MitigationListPage";
|
||||||
|
|
||||||
|
const mitigationListViewQuery = graphql`
|
||||||
|
query MitigationListViewQuery($organizationId: ID!, $first: Int) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
mitigations(
|
||||||
|
first: $first
|
||||||
|
orderBy: { direction: ASC, field: CREATED_AT }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
state
|
||||||
|
importance
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Mitigation {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
state?: string;
|
||||||
|
category?: string;
|
||||||
|
importance?: string;
|
||||||
|
status?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
progress: number;
|
||||||
|
mitigations: Mitigation[];
|
||||||
|
doneCount: number;
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationData {
|
||||||
|
organization: {
|
||||||
|
id: string;
|
||||||
|
mitigations?: {
|
||||||
|
edges: Array<{
|
||||||
|
node: Mitigation;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function MitigationListContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<MitigationListViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery<MitigationListViewQueryType>(
|
||||||
|
mitigationListViewQuery,
|
||||||
|
queryRef
|
||||||
|
) as unknown as OrganizationData;
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
// Monitor URL hash for changes and update state accordingly
|
||||||
|
const [hashValue, setHashValue] = useState(window.location.hash);
|
||||||
|
|
||||||
|
// Get the active category from the hash
|
||||||
|
const hashCategory = hashValue.substring(1)
|
||||||
|
? decodeURIComponent(hashValue.substring(1))
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Keep track of manually expanded categories
|
||||||
|
const [expandedCategories, setExpandedCategories] = useState<string[]>(() => {
|
||||||
|
return hashCategory ? [hashCategory] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// When hash changes, update expanded categories to include the hash category
|
||||||
|
useEffect(() => {
|
||||||
|
if (hashCategory && !expandedCategories.includes(hashCategory)) {
|
||||||
|
setExpandedCategories((prev) => [...prev, hashCategory]);
|
||||||
|
}
|
||||||
|
}, [hashCategory, expandedCategories]);
|
||||||
|
|
||||||
|
// Listen for hash changes (like when using back button)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHashChange = () => {
|
||||||
|
setHashValue(window.location.hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("hashchange", handleHashChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mitigations =
|
||||||
|
data.organization.mitigations?.edges.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
// Map mitigation state to status for the new design
|
||||||
|
const mapStateToStatus = (state?: string): string => {
|
||||||
|
if (!state) return "incomplete";
|
||||||
|
switch (state) {
|
||||||
|
case "IMPLEMENTED":
|
||||||
|
return "complete";
|
||||||
|
case "NOT_APPLICABLE":
|
||||||
|
return "not-applicable";
|
||||||
|
case "NOT_STARTED":
|
||||||
|
return "not-started";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "in-progress";
|
||||||
|
default:
|
||||||
|
return "incomplete";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedMitigations = mitigations.map((mitigation: Mitigation) => ({
|
||||||
|
...mitigation,
|
||||||
|
status: mapStateToStatus(mitigation.state),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Calculate global progress
|
||||||
|
const implementedCount = processedMitigations.filter(
|
||||||
|
(mitigation: Mitigation) => mitigation.status === "complete"
|
||||||
|
).length;
|
||||||
|
const notApplicableCount = processedMitigations.filter(
|
||||||
|
(mitigation: Mitigation) => mitigation.status === "not-applicable"
|
||||||
|
).length;
|
||||||
|
const totalMitigations = processedMitigations.length;
|
||||||
|
|
||||||
|
// Include not-applicable as effectively "complete" for progress percentage
|
||||||
|
const effectiveCompletedCount = implementedCount + notApplicableCount;
|
||||||
|
const globalProgress = totalMitigations
|
||||||
|
? Math.round((effectiveCompletedCount / totalMitigations) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Get global status counts
|
||||||
|
const globalStatusCounts = processedMitigations.reduce(
|
||||||
|
(acc: Record<string, number>, mitigation: Mitigation) => {
|
||||||
|
if (mitigation.status) {
|
||||||
|
acc[mitigation.status] = (acc[mitigation.status] || 0) + 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group mitigations by category
|
||||||
|
const mitigationsByCategory = processedMitigations.reduce(
|
||||||
|
(acc: Record<string, Mitigation[]>, mitigation: Mitigation) => {
|
||||||
|
if (!mitigation?.category) return acc;
|
||||||
|
if (!acc[mitigation.category]) {
|
||||||
|
acc[mitigation.category] = [];
|
||||||
|
}
|
||||||
|
acc[mitigation.category].push(mitigation);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Mitigation[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function to toggle a category's expanded state
|
||||||
|
const toggleCategory = (categoryId: string) => {
|
||||||
|
setExpandedCategories((prev) => {
|
||||||
|
if (prev.includes(categoryId)) {
|
||||||
|
return prev.filter((id) => id !== categoryId);
|
||||||
|
} else {
|
||||||
|
return [...prev, categoryId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories: Category[] = Object.entries(mitigationsByCategory)
|
||||||
|
.map(([categoryName, categoryMitigations]: [string, Mitigation[]]) => {
|
||||||
|
const catImplementedCount = categoryMitigations.filter(
|
||||||
|
(mitigation) => mitigation.status === "complete"
|
||||||
|
).length;
|
||||||
|
const catNotApplicableCount = categoryMitigations.filter(
|
||||||
|
(mitigation) => mitigation.status === "not-applicable"
|
||||||
|
).length;
|
||||||
|
// Consider both "complete" and "not-applicable" as done for category progress
|
||||||
|
const catDoneCount = catImplementedCount + catNotApplicableCount;
|
||||||
|
const catTotalCount = categoryMitigations.length;
|
||||||
|
const progress = catTotalCount
|
||||||
|
? Math.round((catDoneCount / catTotalCount) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: categoryName,
|
||||||
|
name: categoryName,
|
||||||
|
description: `Mitigations related to ${categoryName.toLowerCase()}`,
|
||||||
|
progress: progress,
|
||||||
|
mitigations: categoryMitigations.sort((a, b) =>
|
||||||
|
(a.name || "").localeCompare(b.name || "")
|
||||||
|
),
|
||||||
|
doneCount: catDoneCount,
|
||||||
|
totalCount: catTotalCount,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((category) => category.mitigations.length > 0)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "complete":
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||||
|
case "in-progress":
|
||||||
|
return <Clock className="h-5 w-5 text-blue-500" />;
|
||||||
|
case "not-started":
|
||||||
|
return <AlertCircle className="h-5 w-5 text-gray-200" />;
|
||||||
|
case "incomplete":
|
||||||
|
return <AlertCircle className="h-5 w-5 text-red-500" />;
|
||||||
|
case "not-applicable":
|
||||||
|
return <X className="h-5 w-5 text-gray-300" />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Mitigations"
|
||||||
|
description="Mitigations are actions taken to reduce the risk. Add them to track their implementation status."
|
||||||
|
>
|
||||||
|
{/* Global Progress Summary */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<h3 className="text-lg font-medium">Mitigation Implementation</h3>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{globalProgress}% complete
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar container */}
|
||||||
|
<div className="w-full h-5 rounded-full overflow-hidden bg-muted mb-2">
|
||||||
|
{/* Segmented progress bar */}
|
||||||
|
<div className="flex h-full">
|
||||||
|
{/* Complete segment */}
|
||||||
|
{globalStatusCounts.complete > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-green-500 h-full"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
(globalStatusCounts.complete / totalMitigations) * 100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* In-progress segment */}
|
||||||
|
{globalStatusCounts["in-progress"] > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-full"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
(globalStatusCounts["in-progress"] / totalMitigations) * 100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Incomplete segment */}
|
||||||
|
{globalStatusCounts.incomplete > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-red-500 h-full"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
(globalStatusCounts.incomplete / totalMitigations) * 100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Not applicable segment */}
|
||||||
|
{globalStatusCounts["not-applicable"] > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-gray-600 h-full"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
(globalStatusCounts["not-applicable"] / totalMitigations) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Not started segment */}
|
||||||
|
{globalStatusCounts["not-started"] > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-gray-200 h-full"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
(globalStatusCounts["not-started"] / totalMitigations) * 100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status legend */}
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||||
|
{globalStatusCounts.complete > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
<span>Complete ({globalStatusCounts.complete})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{globalStatusCounts["in-progress"] > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||||
|
<span>In Progress ({globalStatusCounts["in-progress"]})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{globalStatusCounts.incomplete > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||||
|
<span>Incomplete ({globalStatusCounts.incomplete})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{globalStatusCounts["not-applicable"] > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-gray-600"></div>
|
||||||
|
<span>
|
||||||
|
Not Applicable ({globalStatusCounts["not-applicable"]})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{globalStatusCounts["not-started"] > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-gray-200"></div>
|
||||||
|
<span>Not Started ({globalStatusCounts["not-started"]})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category groups */}
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{categories.map((category) => {
|
||||||
|
const isExpanded = expandedCategories.includes(category.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className="border rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<Card className="border-0 shadow-none">
|
||||||
|
<CardHeader
|
||||||
|
className="bg-muted/50 cursor-pointer"
|
||||||
|
onClick={() => toggleCategory(category.id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CardTitle>{category.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{category.doneCount} / {category.totalCount}
|
||||||
|
</span>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{category.mitigations.length > 0 ? (
|
||||||
|
<div className="w-full">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/30 text-sm font-medium text-muted-foreground">
|
||||||
|
<th className="w-24 px-4 py-2 text-left">
|
||||||
|
Importance
|
||||||
|
</th>
|
||||||
|
<th className="w-24 px-4 py-2 text-left">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left">
|
||||||
|
Mitigation
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{category.mitigations.map((mitigation) => (
|
||||||
|
<tr
|
||||||
|
key={mitigation.id || Math.random().toString()}
|
||||||
|
className="hover:bg-muted/50 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
if (mitigation?.id) {
|
||||||
|
// Store this category in the hash
|
||||||
|
const encoded = encodeURIComponent(
|
||||||
|
category.id
|
||||||
|
);
|
||||||
|
window.location.hash = encoded;
|
||||||
|
setHashValue("#" + encoded);
|
||||||
|
|
||||||
|
// Make sure this category is expanded in the local state
|
||||||
|
if (
|
||||||
|
!expandedCategories.includes(category.id)
|
||||||
|
) {
|
||||||
|
setExpandedCategories((prev) => [
|
||||||
|
...prev,
|
||||||
|
category.id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a small timeout to ensure the hash change is processed
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/mitigations/${mitigation.id}`
|
||||||
|
);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="w-24 px-4 py-3 align-middle">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{mitigation.importance}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="w-24 px-4 py-3 align-middle">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{mitigation.status
|
||||||
|
? getStatusIcon(mitigation.status)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 align-middle">
|
||||||
|
<div className="font-medium">
|
||||||
|
{mitigation.name}
|
||||||
|
</div>
|
||||||
|
{mitigation.description && (
|
||||||
|
<div className="text-sm text-muted-foreground line-clamp-1">
|
||||||
|
{mitigation.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center p-6 text-center text-muted-foreground">
|
||||||
|
No mitigations in this category
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MitigationListView() {
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<MitigationListViewQueryType>(
|
||||||
|
mitigationListViewQuery
|
||||||
|
);
|
||||||
|
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({
|
||||||
|
organizationId: organizationId!,
|
||||||
|
first: 250,
|
||||||
|
});
|
||||||
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <MitigationListViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<MitigationListViewSkeleton />}>
|
||||||
|
<MitigationListContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { lazy } from "@probo/react-lazy";
|
import { lazy } from "@probo/react-lazy";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
const MitigationView = lazy(() => import("./MitigationView"));
|
const MitigationView = lazy(() => import("./MitigationView"));
|
||||||
|
|
||||||
@@ -71,18 +71,18 @@ import {
|
|||||||
SheetClose,
|
SheetClose,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
|
|
||||||
import type { MitigationViewQuery as MitigationViewQueryType } from "./__generated__/MitigationViewQuery.graphql";
|
|
||||||
import type { MitigationViewUpdateTaskStateMutation as MitigationViewUpdateTaskStateMutationType } from "./__generated__/MitigationViewUpdateTaskStateMutation.graphql";
|
|
||||||
import type { MitigationViewCreateTaskMutation as MitigationViewCreateTaskMutationType } from "./__generated__/MitigationViewCreateTaskMutation.graphql";
|
|
||||||
import type { MitigationViewDeleteTaskMutation as MitigationViewDeleteTaskMutationType } from "./__generated__/MitigationViewDeleteTaskMutation.graphql";
|
|
||||||
import type { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql";
|
|
||||||
import type { MitigationViewDeleteEvidenceMutation as MitigationViewDeleteEvidenceMutationType } from "./__generated__/MitigationViewDeleteEvidenceMutation.graphql";
|
|
||||||
import type { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationType } from "./__generated__/MitigationViewAssignTaskMutation.graphql";
|
|
||||||
import type { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql";
|
|
||||||
import type { MitigationViewOrganizationQuery$data } from "./__generated__/MitigationViewOrganizationQuery.graphql";
|
|
||||||
import type { MitigationViewUpdateMitigationStateMutation as MitigationViewUpdateMitigationStateMutationType } from "./__generated__/MitigationViewUpdateMitigationStateMutation.graphql";
|
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { MitigationViewSkeleton } from "./MitigationPage";
|
import { MitigationViewSkeleton } from "./MitigationPage";
|
||||||
|
import { MitigationViewUpdateTaskStateMutation as MitigationViewUpdateTaskStateMutationType } from "./__generated__/MitigationViewUpdateTaskStateMutation.graphql";
|
||||||
|
import { MitigationViewCreateTaskMutation as MitigationViewCreateTaskMutationType } from "./__generated__/MitigationViewCreateTaskMutation.graphql";
|
||||||
|
import { MitigationViewDeleteTaskMutation as MitigationViewDeleteTaskMutationType } from "./__generated__/MitigationViewDeleteTaskMutation.graphql";
|
||||||
|
import { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql";
|
||||||
|
import { MitigationViewDeleteEvidenceMutation as MitigationViewDeleteEvidenceMutationType } from "./__generated__/MitigationViewDeleteEvidenceMutation.graphql";
|
||||||
|
import { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationType } from "./__generated__/MitigationViewAssignTaskMutation.graphql";
|
||||||
|
import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql";
|
||||||
|
import { MitigationViewUpdateMitigationStateMutation as MitigationViewUpdateMitigationStateMutationType } from "./__generated__/MitigationViewUpdateMitigationStateMutation.graphql";
|
||||||
|
import { MitigationViewQuery as MitigationViewQueryType } from "./__generated__/MitigationViewQuery.graphql";
|
||||||
|
import { MitigationViewOrganizationQuery$data } from "./__generated__/MitigationViewOrganizationQuery.graphql";
|
||||||
|
|
||||||
// Function to format ISO8601 duration to human-readable format
|
// Function to format ISO8601 duration to human-readable format
|
||||||
const formatDuration = (isoDuration: string): string => {
|
const formatDuration = (isoDuration: string): string => {
|
||||||
242
apps/console/src/pages/organizations/mitigations/__generated__/MitigationListViewQuery.graphql.ts
generated
Normal file
242
apps/console/src/pages/organizations/mitigations/__generated__/MitigationListViewQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<f60cb96210dc7db1b8617d7d4414a3d0>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type MitigationListViewQuery$variables = {
|
||||||
|
first?: number | null | undefined;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type MitigationListViewQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly mitigations?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly category: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly importance: MitigationImportance;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: MitigationState;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MitigationListViewQuery = {
|
||||||
|
response: MitigationListViewQuery$data;
|
||||||
|
variables: MitigationListViewQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "first"
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "organizationId"
|
||||||
|
},
|
||||||
|
v2 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = {
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "first",
|
||||||
|
"variableName": "first"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "orderBy",
|
||||||
|
"value": {
|
||||||
|
"direction": "ASC",
|
||||||
|
"field": "CREATED_AT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mitigations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Mitigation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: 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,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "category",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "importance",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MitigationListViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MitigationListViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "112f47aec28111c799333f3cd144d3c1",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "MitigationListViewQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query MitigationListViewQuery(\n $organizationId: ID!\n $first: Int\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: $first, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "80e8c9cffc7d63bd3ce97ea90f696e82";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -44,15 +44,6 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mitigations []*Mitigation
|
Mitigations []*Mitigation
|
||||||
|
|
||||||
UpdateMitigationParams struct {
|
|
||||||
ExpectedVersion int
|
|
||||||
Name *string
|
|
||||||
Description *string
|
|
||||||
Category *string
|
|
||||||
State *MitigationState
|
|
||||||
Importance *MitigationImportance
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c Mitigation) CursorKey(orderBy MitigationOrderField) page.CursorKey {
|
func (c Mitigation) CursorKey(orderBy MitigationOrderField) page.CursorKey {
|
||||||
@@ -223,60 +214,33 @@ func (c *Mitigation) Update(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
params UpdateMitigationParams,
|
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
UPDATE mitigations SET
|
UPDATE mitigations
|
||||||
name = COALESCE(@name, name),
|
SET
|
||||||
description = COALESCE(@description, description),
|
name = @name,
|
||||||
category = COALESCE(@category, category),
|
description = @description,
|
||||||
state = COALESCE(@state, state),
|
category = @category,
|
||||||
importance = COALESCE(@importance, importance),
|
state = @state,
|
||||||
updated_at = @updated_at,
|
importance = @importance,
|
||||||
version = version + 1
|
updated_at = @updated_at
|
||||||
WHERE %s
|
WHERE %s
|
||||||
AND id = @mitigation_id
|
AND id = @mitigation_id
|
||||||
AND version = @expected_version
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
category,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
importance,
|
|
||||||
state,
|
|
||||||
content_ref,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
standards,
|
|
||||||
version
|
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.NamedArgs{
|
args := pgx.NamedArgs{
|
||||||
"mitigation_id": c.ID,
|
"mitigation_id": c.ID,
|
||||||
"expected_version": params.ExpectedVersion,
|
"name": c.Name,
|
||||||
"name": params.Name,
|
"description": c.Description,
|
||||||
"description": params.Description,
|
"category": c.Category,
|
||||||
"category": params.Category,
|
"state": c.State,
|
||||||
"state": params.State,
|
"importance": c.Importance,
|
||||||
"importance": params.Importance,
|
"updated_at": c.UpdatedAt,
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("cannot query mitigations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mitigation, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Mitigation])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect mitigations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = mitigation
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,14 +42,6 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Tasks []*Task
|
Tasks []*Task
|
||||||
|
|
||||||
UpdateTaskParams struct {
|
|
||||||
ExpectedVersion int
|
|
||||||
Name *string
|
|
||||||
Description *string
|
|
||||||
State *TaskState
|
|
||||||
TimeEstimate *time.Duration
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey {
|
func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey {
|
||||||
@@ -210,58 +202,33 @@ func (c *Task) Update(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
params UpdateTaskParams,
|
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
UPDATE tasks SET
|
UPDATE tasks
|
||||||
name = COALESCE(@name, name),
|
SET
|
||||||
description = COALESCE(@description, description),
|
name = @name,
|
||||||
state = COALESCE(@state, state),
|
description = @description,
|
||||||
time_estimate = COALESCE(@time_estimate, time_estimate),
|
state = @state,
|
||||||
updated_at = @updated_at,
|
time_estimate = @time_estimate,
|
||||||
version = version + 1
|
updated_at = @updated_at
|
||||||
WHERE %s
|
WHERE %s
|
||||||
AND id = @task_id
|
AND id = @task_id
|
||||||
AND version = @expected_version
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
mitigation_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
state,
|
|
||||||
time_estimate,
|
|
||||||
assigned_to,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
version
|
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.NamedArgs{
|
args := pgx.NamedArgs{
|
||||||
"task_id": c.ID,
|
"task_id": c.ID,
|
||||||
"expected_version": params.ExpectedVersion,
|
"name": c.Name,
|
||||||
"name": params.Name,
|
"description": c.Description,
|
||||||
"description": params.Description,
|
"state": c.State,
|
||||||
"state": params.State,
|
"time_estimate": c.TimeEstimate,
|
||||||
"time_estimate": params.TimeEstimate,
|
"updated_at": c.UpdatedAt,
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("cannot query tasks: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
task, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Task])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect tasks: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = task
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Task) AssignTo(
|
func (c *Task) AssignTo(
|
||||||
@@ -387,94 +354,3 @@ WHERE %s
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for task management
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAssignTaskFailed = fmt.Errorf("failed to assign task")
|
|
||||||
ErrUnassignTaskFailed = fmt.Errorf("failed to unassign task")
|
|
||||||
ErrUpdateTaskFailed = fmt.Errorf("failed to update task")
|
|
||||||
ErrDeleteTaskFailed = fmt.Errorf("failed to delete task")
|
|
||||||
)
|
|
||||||
|
|
||||||
type TaskUpdate struct {
|
|
||||||
Name *string
|
|
||||||
Description *string
|
|
||||||
State *TaskState
|
|
||||||
TimeEstimate *time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssignTask(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
taskID gid.GID,
|
|
||||||
assignedToID gid.GID,
|
|
||||||
) (*Task, error) {
|
|
||||||
task := &Task{ID: taskID}
|
|
||||||
if err := task.LoadByID(ctx, conn, scope, taskID); err != nil {
|
|
||||||
return nil, ErrAssignTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := task.AssignTo(ctx, conn, scope, assignedToID); err != nil {
|
|
||||||
return nil, ErrAssignTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnassignTask(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
taskID gid.GID,
|
|
||||||
) (*Task, error) {
|
|
||||||
task := &Task{ID: taskID}
|
|
||||||
if err := task.LoadByID(ctx, conn, scope, taskID); err != nil {
|
|
||||||
return nil, ErrUnassignTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := task.Unassign(ctx, conn, scope); err != nil {
|
|
||||||
return nil, ErrUnassignTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateTask(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
taskID gid.GID,
|
|
||||||
expectedVersion int,
|
|
||||||
updates *TaskUpdate,
|
|
||||||
) (*Task, error) {
|
|
||||||
task := &Task{ID: taskID}
|
|
||||||
|
|
||||||
if err := task.Update(ctx, conn, scope, UpdateTaskParams{
|
|
||||||
ExpectedVersion: expectedVersion,
|
|
||||||
Name: updates.Name,
|
|
||||||
Description: updates.Description,
|
|
||||||
State: updates.State,
|
|
||||||
TimeEstimate: updates.TimeEstimate,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, ErrUpdateTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteTask(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
taskID gid.GID,
|
|
||||||
) error {
|
|
||||||
task := &Task{ID: taskID}
|
|
||||||
|
|
||||||
if err := task.Delete(ctx, conn, scope); err != nil {
|
|
||||||
return ErrDeleteTaskFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,13 +39,12 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateMitigationRequest struct {
|
UpdateMitigationRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
ExpectedVersion int
|
Name *string
|
||||||
Name *string
|
Description *string
|
||||||
Description *string
|
Category *string
|
||||||
Category *string
|
State *coredata.MitigationState
|
||||||
State *coredata.MitigationState
|
Importance *coredata.MitigationImportance
|
||||||
Importance *coredata.MitigationImportance
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,22 +72,44 @@ func (s MitigationService) Update(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req UpdateMitigationRequest,
|
req UpdateMitigationRequest,
|
||||||
) (*coredata.Mitigation, error) {
|
) (*coredata.Mitigation, error) {
|
||||||
params := coredata.UpdateMitigationParams{
|
|
||||||
ExpectedVersion: req.ExpectedVersion,
|
|
||||||
Name: req.Name,
|
|
||||||
Description: req.Description,
|
|
||||||
Category: req.Category,
|
|
||||||
State: req.State,
|
|
||||||
Importance: req.Importance,
|
|
||||||
}
|
|
||||||
|
|
||||||
mitigation := &coredata.Mitigation{ID: req.ID}
|
mitigation := &coredata.Mitigation{ID: req.ID}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
return mitigation.Update(ctx, conn, s.svc.scope, params)
|
if err := mitigation.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil {
|
||||||
})
|
return fmt.Errorf("cannot load mitigation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
mitigation.Name = *req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != nil {
|
||||||
|
mitigation.Description = *req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Category != nil {
|
||||||
|
mitigation.Category = *req.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.State != nil {
|
||||||
|
mitigation.State = *req.State
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Importance != nil {
|
||||||
|
mitigation.Importance = *req.Importance
|
||||||
|
}
|
||||||
|
|
||||||
|
mitigation.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := mitigation.Update(ctx, conn, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update mitigation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ package probo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -40,12 +39,11 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateTaskRequest struct {
|
UpdateTaskRequest struct {
|
||||||
TaskID gid.GID
|
TaskID gid.GID
|
||||||
ExpectedVersion int
|
Name *string
|
||||||
Name *string
|
Description *string
|
||||||
Description *string
|
State *coredata.TaskState
|
||||||
State *coredata.TaskState
|
TimeEstimate *time.Duration
|
||||||
TimeEstimate *time.Duration
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,20 +112,15 @@ func (s TaskService) Assign(
|
|||||||
taskID gid.GID,
|
taskID gid.GID,
|
||||||
assignedToID gid.GID,
|
assignedToID gid.GID,
|
||||||
) (*coredata.Task, error) {
|
) (*coredata.Task, error) {
|
||||||
task := &coredata.Task{}
|
task := &coredata.Task{ID: taskID}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
var assignErr error
|
return task.AssignTo(ctx, conn, s.svc.scope, assignedToID)
|
||||||
task, assignErr = coredata.AssignTask(ctx, conn, s.svc.scope, taskID, assignedToID)
|
|
||||||
return assignErr
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrAssignTaskFailed) {
|
|
||||||
return nil, errors.New("failed to assign task, please try again")
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,20 +131,15 @@ func (s TaskService) Unassign(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
taskID gid.GID,
|
taskID gid.GID,
|
||||||
) (*coredata.Task, error) {
|
) (*coredata.Task, error) {
|
||||||
task := &coredata.Task{}
|
task := &coredata.Task{ID: taskID}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
var unassignErr error
|
return task.Unassign(ctx, conn, s.svc.scope)
|
||||||
task, unassignErr = coredata.UnassignTask(ctx, conn, s.svc.scope, taskID)
|
|
||||||
return unassignErr
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrUnassignTaskFailed) {
|
|
||||||
return nil, errors.New("failed to unassign task, please try again")
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,32 +150,41 @@ func (s TaskService) Update(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req UpdateTaskRequest,
|
req UpdateTaskRequest,
|
||||||
) (*coredata.Task, error) {
|
) (*coredata.Task, error) {
|
||||||
task := &coredata.Task{}
|
task := &coredata.Task{ID: req.TaskID}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
var updateErr error
|
if err := task.LoadByID(ctx, conn, s.svc.scope, req.TaskID); err != nil {
|
||||||
task, updateErr = coredata.UpdateTask(
|
return fmt.Errorf("cannot load task %q: %w", req.TaskID, err)
|
||||||
ctx,
|
}
|
||||||
conn,
|
|
||||||
s.svc.scope,
|
if req.Name != nil {
|
||||||
req.TaskID,
|
task.Name = *req.Name
|
||||||
req.ExpectedVersion,
|
}
|
||||||
&coredata.TaskUpdate{
|
|
||||||
Name: req.Name,
|
if req.Description != nil {
|
||||||
Description: req.Description,
|
task.Description = *req.Description
|
||||||
State: req.State,
|
}
|
||||||
TimeEstimate: req.TimeEstimate,
|
|
||||||
},
|
if req.State != nil {
|
||||||
)
|
task.State = *req.State
|
||||||
return updateErr
|
}
|
||||||
|
|
||||||
|
if req.TimeEstimate != nil {
|
||||||
|
task.TimeEstimate = req.TimeEstimate
|
||||||
|
}
|
||||||
|
|
||||||
|
task.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := task.Update(ctx, conn, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update task: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrUpdateTaskFailed) {
|
|
||||||
return nil, errors.New("failed to update task, please try again")
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,16 +195,15 @@ func (s TaskService) Delete(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
taskID gid.GID,
|
taskID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
|
task := &coredata.Task{ID: taskID}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
return coredata.DeleteTask(ctx, conn, s.svc.scope, taskID)
|
return task.Delete(ctx, conn, s.svc.scope)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrDeleteTaskFailed) {
|
|
||||||
return errors.New("failed to delete task, please try again")
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,10 @@ enum MitigationOrderField
|
|||||||
@goModel(
|
@goModel(
|
||||||
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
||||||
) {
|
) {
|
||||||
NAME
|
CREATED_AT
|
||||||
|
@goEnum(
|
||||||
|
value: "github.com/getprobo/probo/pkg/coredata.MitigationOrderFieldCreatedAt"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TaskOrderField
|
enum TaskOrderField
|
||||||
|
|||||||
@@ -2409,7 +2409,10 @@ enum MitigationOrderField
|
|||||||
@goModel(
|
@goModel(
|
||||||
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
||||||
) {
|
) {
|
||||||
NAME
|
CREATED_AT
|
||||||
|
@goEnum(
|
||||||
|
value: "github.com/getprobo/probo/pkg/coredata.MitigationOrderFieldCreatedAt"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TaskOrderField
|
enum TaskOrderField
|
||||||
@@ -23057,12 +23060,12 @@ var (
|
|||||||
|
|
||||||
func (ec *executionContext) unmarshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField(ctx context.Context, v any) (coredata.MitigationOrderField, error) {
|
func (ec *executionContext) unmarshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField(ctx context.Context, v any) (coredata.MitigationOrderField, error) {
|
||||||
tmp, err := graphql.UnmarshalString(v)
|
tmp, err := graphql.UnmarshalString(v)
|
||||||
res := coredata.MitigationOrderField(tmp)
|
res := unmarshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField[tmp]
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.MitigationOrderField) graphql.Marshaler {
|
func (ec *executionContext) marshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.MitigationOrderField) graphql.Marshaler {
|
||||||
res := graphql.MarshalString(string(v))
|
res := graphql.MarshalString(marshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField[v])
|
||||||
if res == graphql.Null {
|
if res == graphql.Null {
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
|
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
|
||||||
@@ -23071,6 +23074,15 @@ func (ec *executionContext) marshalNMitigationOrderField2githubᚗcomᚋgetprobo
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
unmarshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField = map[string]coredata.MitigationOrderField{
|
||||||
|
"CREATED_AT": coredata.MitigationOrderFieldCreatedAt,
|
||||||
|
}
|
||||||
|
marshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField = map[coredata.MitigationOrderField]string{
|
||||||
|
coredata.MitigationOrderFieldCreatedAt: "CREATED_AT",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNMitigationState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationState(ctx context.Context, v any) (coredata.MitigationState, error) {
|
func (ec *executionContext) unmarshalNMitigationState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationState(ctx context.Context, v any) (coredata.MitigationState, error) {
|
||||||
tmp, err := graphql.UnmarshalString(v)
|
tmp, err := graphql.UnmarshalString(v)
|
||||||
res := unmarshalNMitigationState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationState[tmp]
|
res := unmarshalNMitigationState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationState[tmp]
|
||||||
|
|||||||
@@ -372,20 +372,6 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFramework is the resolver for the deleteFramework field.
|
|
||||||
func (r *mutationResolver) DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, error) {
|
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID())
|
|
||||||
|
|
||||||
err := svc.Frameworks.Delete(ctx, input.FrameworkID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot delete framework: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.DeleteFrameworkPayload{
|
|
||||||
DeletedFrameworkID: input.FrameworkID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
||||||
@@ -405,6 +391,20 @@ func (r *mutationResolver) ImportFramework(ctx context.Context, input types.Impo
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteFramework is the resolver for the deleteFramework field.
|
||||||
|
func (r *mutationResolver) DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, error) {
|
||||||
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID())
|
||||||
|
|
||||||
|
err := svc.Frameworks.Delete(ctx, input.FrameworkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot delete framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DeleteFrameworkPayload{
|
||||||
|
DeletedFrameworkID: input.FrameworkID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// // CreateMitigation is the resolver for the createMitigation field.
|
// // CreateMitigation is the resolver for the createMitigation field.
|
||||||
func (r *mutationResolver) CreateMitigation(ctx context.Context, input types.CreateMitigationInput) (*types.CreateMitigationPayload, error) {
|
func (r *mutationResolver) CreateMitigation(ctx context.Context, input types.CreateMitigationInput) (*types.CreateMitigationPayload, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
||||||
|
|||||||
Reference in New Issue
Block a user