@@ -145,9 +145,11 @@ function getNavItems(organizationId?: string): NavItem[] {
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
title: "Risk",
|
||||
title: "Risks",
|
||||
icon: Shield,
|
||||
url: organizationId ? `/organizations/${organizationId}/risk` : undefined,
|
||||
url: organizationId
|
||||
? `/organizations/${organizationId}/risks`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
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 { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
||||
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
||||
import { CreateMitigationPage } from "./frameworks/mitigations/CreateMitigationPage";
|
||||
import { MitigationPage } from "./frameworks/mitigations/MitigationPage";
|
||||
import { UpdateMitigationPage } from "./frameworks/mitigations/UpdateMitigationPage";
|
||||
import { MitigationPage } from "./mitigations/MitigationPage";
|
||||
import { VendorPage } from "./vendors/VendorPage";
|
||||
import { PolicyListPage } from "./policies/PolicyListPage";
|
||||
import { CreatePolicyPage } from "./policies/CreatePolicyPage";
|
||||
@@ -19,6 +17,8 @@ import { PolicyPage } from "./policies/PolicyPage";
|
||||
import { UpdatePolicyPage } from "./policies/UpdatePolicyPage";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
import { CreateOrganizationPage } from "./CreateOrganizationPage";
|
||||
import { MitigationListPage } from "./mitigations/MitigationListPage";
|
||||
|
||||
import HomePage from "./HomePage";
|
||||
import NotFoundPage from "../NotFoundPage";
|
||||
|
||||
@@ -38,18 +38,13 @@ export function OrganizationsRoutes() {
|
||||
path="frameworks/:frameworkId/update"
|
||||
element={<UpdateFrameworkPage />}
|
||||
/>
|
||||
<Route path="mitigations" element={<MitigationListPage />} />
|
||||
<Route path="mitigations/:mitigationId" element={<MitigationPage />} />
|
||||
{/* <Route path="mitigations/create" element={<CreateMitigationPage />} />
|
||||
<Route
|
||||
path="frameworks/:frameworkId/mitigations/create"
|
||||
element={<CreateMitigationPage />}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId/mitigations/:mitigationId"
|
||||
element={<MitigationPage />}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId/mitigations/:mitigationId/update"
|
||||
path="mitigations/:mitigationId/update"
|
||||
element={<UpdateMitigationPage />}
|
||||
/>
|
||||
/> */}
|
||||
<Route path="vendors/:vendorId" element={<VendorPage />} />
|
||||
<Route path="policies" element={<PolicyListPage />} />
|
||||
<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 { lazy } from "@probo/react-lazy";
|
||||
import { useLocation } from "react-router";
|
||||
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
||||
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||
|
||||
const MitigationView = lazy(() => import("./MitigationView"));
|
||||
|
||||
@@ -71,18 +71,18 @@ import {
|
||||
SheetClose,
|
||||
} 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 { 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
|
||||
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
|
||||
|
||||
UpdateMitigationParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
Category *string
|
||||
State *MitigationState
|
||||
Importance *MitigationImportance
|
||||
}
|
||||
)
|
||||
|
||||
func (c Mitigation) CursorKey(orderBy MitigationOrderField) page.CursorKey {
|
||||
@@ -223,60 +214,33 @@ func (c *Mitigation) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateMitigationParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE mitigations SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
category = COALESCE(@category, category),
|
||||
state = COALESCE(@state, state),
|
||||
importance = COALESCE(@importance, importance),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
UPDATE mitigations
|
||||
SET
|
||||
name = @name,
|
||||
description = @description,
|
||||
category = @category,
|
||||
state = @state,
|
||||
importance = @importance,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
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())
|
||||
|
||||
args := pgx.NamedArgs{
|
||||
"mitigation_id": c.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"name": params.Name,
|
||||
"description": params.Description,
|
||||
"category": params.Category,
|
||||
"state": params.State,
|
||||
"importance": params.Importance,
|
||||
"updated_at": time.Now(),
|
||||
"mitigation_id": c.ID,
|
||||
"name": c.Name,
|
||||
"description": c.Description,
|
||||
"category": c.Category,
|
||||
"state": c.State,
|
||||
"importance": c.Importance,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
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
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -42,14 +42,6 @@ type (
|
||||
}
|
||||
|
||||
Tasks []*Task
|
||||
|
||||
UpdateTaskParams struct {
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
State *TaskState
|
||||
TimeEstimate *time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey {
|
||||
@@ -210,58 +202,33 @@ func (c *Task) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
params UpdateTaskParams,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE tasks SET
|
||||
name = COALESCE(@name, name),
|
||||
description = COALESCE(@description, description),
|
||||
state = COALESCE(@state, state),
|
||||
time_estimate = COALESCE(@time_estimate, time_estimate),
|
||||
updated_at = @updated_at,
|
||||
version = version + 1
|
||||
UPDATE tasks
|
||||
SET
|
||||
name = @name,
|
||||
description = @description,
|
||||
state = @state,
|
||||
time_estimate = @time_estimate,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
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())
|
||||
|
||||
args := pgx.NamedArgs{
|
||||
"task_id": c.ID,
|
||||
"expected_version": params.ExpectedVersion,
|
||||
"name": params.Name,
|
||||
"description": params.Description,
|
||||
"state": params.State,
|
||||
"time_estimate": params.TimeEstimate,
|
||||
"updated_at": time.Now(),
|
||||
"task_id": c.ID,
|
||||
"name": c.Name,
|
||||
"description": c.Description,
|
||||
"state": c.State,
|
||||
"time_estimate": c.TimeEstimate,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
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
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Task) AssignTo(
|
||||
@@ -387,94 +354,3 @@ WHERE %s
|
||||
|
||||
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 {
|
||||
ID gid.GID
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
Category *string
|
||||
State *coredata.MitigationState
|
||||
Importance *coredata.MitigationImportance
|
||||
ID gid.GID
|
||||
Name *string
|
||||
Description *string
|
||||
Category *string
|
||||
State *coredata.MitigationState
|
||||
Importance *coredata.MitigationImportance
|
||||
}
|
||||
)
|
||||
|
||||
@@ -73,22 +72,44 @@ func (s MitigationService) Update(
|
||||
ctx context.Context,
|
||||
req UpdateMitigationRequest,
|
||||
) (*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}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ package probo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -40,12 +39,11 @@ type (
|
||||
}
|
||||
|
||||
UpdateTaskRequest struct {
|
||||
TaskID gid.GID
|
||||
ExpectedVersion int
|
||||
Name *string
|
||||
Description *string
|
||||
State *coredata.TaskState
|
||||
TimeEstimate *time.Duration
|
||||
TaskID gid.GID
|
||||
Name *string
|
||||
Description *string
|
||||
State *coredata.TaskState
|
||||
TimeEstimate *time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,20 +112,15 @@ func (s TaskService) Assign(
|
||||
taskID gid.GID,
|
||||
assignedToID gid.GID,
|
||||
) (*coredata.Task, error) {
|
||||
task := &coredata.Task{}
|
||||
task := &coredata.Task{ID: taskID}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
var assignErr error
|
||||
task, assignErr = coredata.AssignTask(ctx, conn, s.svc.scope, taskID, assignedToID)
|
||||
return assignErr
|
||||
return task.AssignTo(ctx, conn, s.svc.scope, assignedToID)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrAssignTaskFailed) {
|
||||
return nil, errors.New("failed to assign task, please try again")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -138,20 +131,15 @@ func (s TaskService) Unassign(
|
||||
ctx context.Context,
|
||||
taskID gid.GID,
|
||||
) (*coredata.Task, error) {
|
||||
task := &coredata.Task{}
|
||||
task := &coredata.Task{ID: taskID}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
var unassignErr error
|
||||
task, unassignErr = coredata.UnassignTask(ctx, conn, s.svc.scope, taskID)
|
||||
return unassignErr
|
||||
return task.Unassign(ctx, conn, s.svc.scope)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrUnassignTaskFailed) {
|
||||
return nil, errors.New("failed to unassign task, please try again")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -162,32 +150,41 @@ func (s TaskService) Update(
|
||||
ctx context.Context,
|
||||
req UpdateTaskRequest,
|
||||
) (*coredata.Task, error) {
|
||||
task := &coredata.Task{}
|
||||
task := &coredata.Task{ID: req.TaskID}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
var updateErr error
|
||||
task, updateErr = coredata.UpdateTask(
|
||||
ctx,
|
||||
conn,
|
||||
s.svc.scope,
|
||||
req.TaskID,
|
||||
req.ExpectedVersion,
|
||||
&coredata.TaskUpdate{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
State: req.State,
|
||||
TimeEstimate: req.TimeEstimate,
|
||||
},
|
||||
)
|
||||
return updateErr
|
||||
if err := task.LoadByID(ctx, conn, s.svc.scope, req.TaskID); err != nil {
|
||||
return fmt.Errorf("cannot load task %q: %w", req.TaskID, err)
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
task.Name = *req.Name
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
task.Description = *req.Description
|
||||
}
|
||||
|
||||
if req.State != nil {
|
||||
task.State = *req.State
|
||||
}
|
||||
|
||||
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 errors.Is(err, coredata.ErrUpdateTaskFailed) {
|
||||
return nil, errors.New("failed to update task, please try again")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -198,16 +195,15 @@ func (s TaskService) Delete(
|
||||
ctx context.Context,
|
||||
taskID gid.GID,
|
||||
) error {
|
||||
task := &coredata.Task{ID: taskID}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
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 errors.Is(err, coredata.ErrDeleteTaskFailed) {
|
||||
return errors.New("failed to delete task, please try again")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,10 @@ enum MitigationOrderField
|
||||
@goModel(
|
||||
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
||||
) {
|
||||
NAME
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.MitigationOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
enum TaskOrderField
|
||||
|
||||
@@ -2409,7 +2409,10 @@ enum MitigationOrderField
|
||||
@goModel(
|
||||
model: "github.com/getprobo/probo/pkg/coredata.MitigationOrderField"
|
||||
) {
|
||||
NAME
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.MitigationOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
tmp, err := graphql.UnmarshalString(v)
|
||||
res := coredata.MitigationOrderField(tmp)
|
||||
res := unmarshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField[tmp]
|
||||
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 {
|
||||
res := graphql.MarshalString(string(v))
|
||||
res := graphql.MarshalString(marshalNMitigationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationOrderField[v])
|
||||
if res == graphql.Null {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
tmp, err := graphql.UnmarshalString(v)
|
||||
res := unmarshalNMitigationState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMitigationState[tmp]
|
||||
|
||||
@@ -372,20 +372,6 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
|
||||
}, 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.
|
||||
func (r *mutationResolver) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) {
|
||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
||||
@@ -405,6 +391,20 @@ func (r *mutationResolver) ImportFramework(ctx context.Context, input types.Impo
|
||||
}, 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.
|
||||
func (r *mutationResolver) CreateMitigation(ctx context.Context, input types.CreateMitigationInput) (*types.CreateMitigationPayload, error) {
|
||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
|
||||
|
||||
Reference in New Issue
Block a user