From 0453618cd12f846a42bae0b982d93d9662930141 Mon Sep 17 00:00:00 2001 From: "[[alvarobernal2412]]" Date: Tue, 25 Mar 2025 11:34:37 +0100 Subject: [PATCH 01/37] feat: added draft catalog step --- .../catalog/CatalogControlsStep.jsx | 475 ++++++++---------- src/components/catalog/CatalogInfoStep.jsx | 13 +- src/forms/control/new/form.jsx | 41 +- src/forms/control/new/schemas.js | 5 +- src/pages/app/catalog/CatalogWizard.jsx | 12 +- src/services/catalogs.js | 26 + src/services/controls.js | 35 ++ src/styles/index.css | 3 +- 8 files changed, 309 insertions(+), 301 deletions(-) diff --git a/src/components/catalog/CatalogControlsStep.jsx b/src/components/catalog/CatalogControlsStep.jsx index 4e06fbdf..e9c01b90 100644 --- a/src/components/catalog/CatalogControlsStep.jsx +++ b/src/components/catalog/CatalogControlsStep.jsx @@ -1,26 +1,17 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Loader2, ArrowRight, Plus, Trash, Edit, AlertCircle } from 'lucide-react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { NewControlForm } from '@/forms/control/new/form'; +import { getDraftControlsByCatalogId, deleteControl, createDraftControl } from '@/services/controls'; +import { createScopeSet } from '@/services/scopes'; +import { toast } from 'sonner'; import { Dialog, DialogContent, @@ -28,60 +19,26 @@ import { DialogTitle, DialogFooter, } from '@/components/ui/dialog'; -import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; - -// Schema for control validation -const controlSchema = z.object({ - name: z.string().min(1, { message: 'Control name is required' }), - description: z.string().min(1, { message: 'Description is required' }), - type: z.string().min(1, { message: 'Control type is required' }), - severity: z.string().min(1, { message: 'Severity is required' }), - implementation: z.string().optional(), -}); - -// Control types and severity options -const controlTypes = [ - { value: 'preventive', label: 'Preventive' }, - { value: 'detective', label: 'Detective' }, - { value: 'corrective', label: 'Corrective' }, - { value: 'deterrent', label: 'Deterrent' }, - { value: 'recovery', label: 'Recovery' }, -]; - -const severityLevels = [ - { value: 'critical', label: 'Critical', color: 'bg-red-500' }, - { value: 'high', label: 'High', color: 'bg-orange-500' }, - { value: 'medium', label: 'Medium', color: 'bg-yellow-500' }, - { value: 'low', label: 'Low', color: 'bg-green-500' }, - { value: 'informational', label: 'Informational', color: 'bg-blue-500' }, -]; export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, isSubmitting, apiError = null }) { const [controls, setControls] = useState([]); - const [isEditing, setIsEditing] = useState(false); - const [currentControl, setCurrentControl] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [openDialog, setOpenDialog] = useState(false); + const [openNewControlForm, setOpenNewControlForm] = useState(false); const [submitError, setSubmitError] = useState(null); + const [currentControl, setCurrentControl] = useState(null); + const [confirmingDelete, setConfirmingDelete] = useState(false); - // Setup form with zod resolver - const form = useForm({ - resolver: zodResolver(controlSchema), - defaultValues: { - name: '', - description: '', - type: '', - severity: '', - implementation: '', - }, - }); - + // Load draft controls when component mounts or catalogId changes useEffect(() => { - if (initialControls && initialControls.length > 0) { - setControls([...initialControls]); + if (catalogId) { + fetchDraftControls(); + } else if (initialControls.length > 0) { + // If no catalogId but initialControls exist, use them + setControls(initialControls); } - }, [initialControls]); + }, [catalogId, initialControls]); // Update error message when API error changes useEffect(() => { @@ -90,62 +47,108 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, } }, [apiError]); + const fetchDraftControls = async () => { + if (!catalogId) return; + + setIsLoading(true); + try { + const response = await getDraftControlsByCatalogId(catalogId); + setControls(response || []); + } catch (error) { + console.error('Error fetching draft controls:', error); + toast.error('Failed to load draft controls'); + } finally { + setIsLoading(false); + } + }; + const handleAddControl = () => { - setIsEditing(false); - setCurrentControl(null); - form.reset({ - name: '', - description: '', - type: '', - severity: '', - implementation: '', - }); - setOpenDialog(true); + setOpenNewControlForm(true); }; - const handleEditControl = (control, index) => { - setIsEditing(true); - setCurrentControl({ ...control, index }); - form.reset({ - name: control.name, - description: control.description, - type: control.type, - severity: control.severity, - implementation: control.implementation || '', - }); - setOpenDialog(true); + const handleCloseNewControlForm = () => { + setOpenNewControlForm(false); }; - const handleDeleteControl = (index) => { - const updatedControls = [...controls]; - updatedControls.splice(index, 1); - setControls(updatedControls); + const handleCustomSubmit = async (data) => { + try { + // Use createDraftControl specifically in the catalog context + const createdControl = await createDraftControl(data); + + // Create the scope set if needed + if (Object.keys(data.scopes).length > 0) { + const scopeSetData = { + controlId: createdControl.id, + scopes: data.scopes + }; + + await createScopeSet(scopeSetData); + } + + return createdControl; + } catch (error) { + console.error('Error creating draft control:', error); + throw error; + } }; - const handleControlSubmit = (data) => { - if (isEditing && currentControl) { - // Update existing control - const updatedControls = [...controls]; - updatedControls[currentControl.index] = { - id: currentControl.id || `temp-${Date.now()}`, - ...data - }; - setControls(updatedControls); - } else { - // Add new control - setControls([ - ...controls, - { - id: `temp-${Date.now()}`, - ...data + const handleControlSuccess = async (newControl) => { + setOpenNewControlForm(false); + + if (newControl && newControl.id) { + try { + // Fetch the updated list of controls + if (catalogId) { + const response = await getDraftControlsByCatalogId(catalogId); + setControls(response || []); + } else { + // If no catalogId, manually add the new control to the current list + setControls(prevControls => [...prevControls, newControl]); } - ]); + toast.success(`Draft control "${newControl.name}" added successfully`); + } catch (error) { + console.error('Error updating controls list:', error); + toast.error('Control was created but the list could not be updated'); + + // As a fallback, manually add the control to the list + setControls(prevControls => [...prevControls, newControl]); + } } - setOpenDialog(false); }; - const closeDialog = () => { + const handleEditControl = (control) => { + // Placeholder for edit functionality + toast.info('Edit functionality will be available in a future update'); + }; + + const handleDeleteConfirm = (control) => { + setCurrentControl(control); + setConfirmingDelete(true); + setOpenDialog(true); + }; + + const handleDeleteCancel = () => { setOpenDialog(false); + setConfirmingDelete(false); + setCurrentControl(null); + }; + + const handleDeleteControl = async () => { + if (!currentControl || !currentControl.id) return; + + try { + await deleteControl(currentControl.id); + // After deletion, refresh the draft controls list + await fetchDraftControls(); + toast.success('Draft control deleted successfully'); + } catch (error) { + console.error('Error deleting draft control:', error); + toast.error('Failed to delete draft control'); + } finally { + setOpenDialog(false); + setConfirmingDelete(false); + setCurrentControl(null); + } }; const handleSubmit = () => { @@ -159,24 +162,16 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, onSubmit(controls); }; - const getSeverityBadge = (severity) => { - const severityInfo = severityLevels.find(level => level.value === severity); - return ( - - {severityInfo ? severityInfo.label : severity} - - ); - }; - return (
-

Security Controls

+

Draft Controls

@@ -192,32 +187,43 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, )} - {/* Controls list */} -
- {controls.length === 0 ? ( -
-

No controls added yet. Click "Add Control" to start.

+ {/* Empty state with helpful message */} + {!isLoading && controls.length === 0 && ( +
+
+
- ) : ( - controls.map((control, index) => ( - +

No draft controls yet

+

Add draft controls to define what to monitor in your catalog

+
+ )} + + {/* Controls list with shimmer effect when loading */} +
+ {isLoading ? ( + // Shimmer loading effect for controls + Array(3).fill(0).map((_, index) => ( +
+
+
+ )) + ) : controls.length > 0 ? ( + controls.map((control) => ( + -
+
{control.name} - - {getSeverityBadge(control.severity)} -

- Type: {controlTypes.find(type => type.value === control.type)?.label || control.type} + Period: {control.period}

@@ -225,7 +231,7 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, size="sm" variant="ghost" className="text-red-500 hover:text-red-700 hover:bg-red-50" - onClick={() => handleDeleteControl(index)} + onClick={() => handleDeleteConfirm(control)} > @@ -233,159 +239,92 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit,

{control.description}

- {control.implementation && ( -
- Implementation: -

{control.implementation}

+ + {/* Añadimos la visualización de scopes con badges */} + {control.scopes && Object.keys(control.scopes).length > 0 && ( +
+
+ {Object.entries(control.scopes).map(([key, value]) => ( + + {key}: {value} + + ))} +
)} + +
+
Start Date: {new Date(control.startDate).toLocaleDateString()}
+ {control.endDate &&
End Date: {new Date(control.endDate).toLocaleDateString()}
} + {control.mashupId &&
API Flow ID: {control.mashupId}
} + {control.params && Object.keys(control.params).length > 0 && ( +
+ Parameters: + {Object.entries(control.params).map(([key, value]) => ( + + {key}: {typeof value === 'object' ? JSON.stringify(value) : value} + + ))} +
+ )} +
)) - )} + ) : null}
- {/* Control form dialog */} - - + {/* Draft Control Form - Pass customSubmit for draft control creation */} + {openNewControlForm && ( + + )} + + {/* Delete Confirmation Dialog */} + + - {isEditing ? 'Edit Control' : 'Add New Control'} + Delete Draft Control - -
- - ( - - Control Name * - - - - - - )} - /> - -
- ( - - Type * - - - - )} - /> - - ( - - Severity * - - - - )} - /> -
- - ( - - Description * - -