From b8bbeaba11c6b8ffcacb9fdabc5cde3f6fbfa1a2 Mon Sep 17 00:00:00 2001 From: gearnode Date: Thu, 20 Mar 2025 17:25:29 +0100 Subject: [PATCH] Add evidence type link close #44 Signed-off-by: gearnode --- .../console/src/pages/ControlOverviewPage.tsx | 322 +++++++++++++++--- ...viewPageGetEvidenceFileUrlQuery.graphql.ts | 4 +- .../ControlOverviewPageQuery.graphql.ts | 25 +- ...rviewPageUploadEvidenceMutation.graphql.ts | 32 +- pkg/coredata/evidence.go | 36 +- pkg/coredata/evidence_type.go | 74 ++++ pkg/coredata/migrations/20250320T162619Z.sql | 15 + pkg/probo/evidence_service.go | 100 +++--- pkg/server/api/console/v1/schema.graphql | 16 +- pkg/server/api/console/v1/schema/schema.go | 251 ++++++++++++-- pkg/server/api/console/v1/types/evidence.go | 27 +- pkg/server/api/console/v1/types/types.go | 28 +- pkg/server/api/console/v1/v1_resolver.go | 35 +- 13 files changed, 798 insertions(+), 167 deletions(-) create mode 100644 pkg/coredata/evidence_type.go create mode 100644 pkg/coredata/migrations/20250320T162619Z.sql diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index 896d215ca..39ebab779 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -20,7 +20,6 @@ import { CheckCircle2, Plus, Trash2, - Upload, FileIcon, Loader2, ChevronDown, @@ -34,6 +33,7 @@ import { UserPlus, UserMinus, User, + Link2, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; @@ -60,6 +60,9 @@ import { SelectTrigger, } from "@/components/ui/select"; import ReactMarkdown from "react-markdown"; +import { Textarea } from "@/components/ui/textarea"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Label } from "@/components/ui/label"; import { Helmet } from "react-helmet-async"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; @@ -72,7 +75,6 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql"; import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql"; import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql"; -import { Textarea } from "@/components/ui/textarea"; // Function to format ISO8601 duration to human-readable format const formatDuration = (isoDuration: string): string => { @@ -147,6 +149,8 @@ const controlOverviewPageQuery = graphql` filename size state + type + url createdAt } } @@ -221,6 +225,8 @@ const uploadEvidenceMutation = graphql` filename fileUrl mimeType + type + url size state createdAt @@ -496,6 +502,13 @@ function ControlOverviewPageContent({ [key: string]: string; }>({}); + // Add state variables for the evidence dialog and link evidence + const [evidenceDialogOpen, setEvidenceDialogOpen] = useState(false); + const [linkEvidenceName, setLinkEvidenceName] = useState(""); + const [linkEvidenceUrl, setLinkEvidenceUrl] = useState(""); + const [linkEvidenceDescription, setLinkEvidenceDescription] = useState(""); + const [activeTab, setActiveTab] = useState<"file" | "link">("file"); + const tasks = data.control.tasks?.edges.map((edge) => edge.node) || []; const getEvidenceConnectionId = useCallback( @@ -701,10 +714,12 @@ function ControlOverviewPageContent({ const handleUploadEvidence = (taskId: string, taskName: string) => { setTaskForEvidence({ id: taskId, name: taskName }); - // Instead of opening a modal, directly trigger the file input click - if (hiddenFileInputRef.current) { - hiddenFileInputRef.current.click(); - } + setEvidenceDialogOpen(true); + // Reset form fields + setLinkEvidenceName(""); + setLinkEvidenceUrl(""); + setLinkEvidenceDescription(""); + setActiveTab("file"); }; const handleFileSelected = (e: React.ChangeEvent) => { @@ -713,10 +728,10 @@ function ControlOverviewPageContent({ const file = e.target.files[0]; - // Show toast for upload started + // Show toast for add started toast({ - title: "Upload started", - description: `Uploading ${file.name}...`, + title: "Adding document", + description: `Adding ${file.name}...`, variant: "default", }); @@ -728,7 +743,9 @@ function ControlOverviewPageContent({ input: { taskId: taskForEvidence.id, name: file.name, + type: "FILE", file: null, + description: "Document evidence", }, connections: evidenceConnectionId ? [evidenceConnectionId] : [], }, @@ -737,8 +754,8 @@ function ControlOverviewPageContent({ }, onCompleted: () => { toast({ - title: "Evidence uploaded", - description: "Evidence has been uploaded successfully.", + title: "Document added", + description: "Document evidence has been added successfully.", variant: "default", }); setTaskForEvidence(null); @@ -749,7 +766,81 @@ function ControlOverviewPageContent({ }, onError: (error) => { toast({ - title: "Error uploading evidence", + title: "Error adding document", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + + const handleLinkEvidenceSubmit = () => { + if (!taskForEvidence) return; + + // Validate form + if (!linkEvidenceName.trim()) { + toast({ + title: "Error", + description: "Please provide a name for the evidence", + variant: "destructive", + }); + return; + } + + if (!linkEvidenceUrl.trim()) { + toast({ + title: "Error", + description: "Please provide a URL for the evidence", + variant: "destructive", + }); + return; + } + + // Description is now optional for link evidence + // Remove the validation check for empty description + + // Show toast for add started + toast({ + title: "Adding link evidence", + description: `Adding ${linkEvidenceName}...`, + variant: "default", + }); + + // Get the evidence connection ID for this task + const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id); + + // Use a default description if none is provided + const description = + linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`; + + uploadEvidence({ + variables: { + input: { + taskId: taskForEvidence.id, + name: linkEvidenceName, + type: "LINK", + url: linkEvidenceUrl, + description: description, + file: null, + }, + connections: evidenceConnectionId ? [evidenceConnectionId] : [], + }, + onCompleted: () => { + toast({ + title: "Link evidence added", + description: "Link evidence has been added successfully.", + variant: "default", + }); + setTaskForEvidence(null); + setEvidenceDialogOpen(false); + // Reset form fields + setLinkEvidenceName(""); + setLinkEvidenceUrl(""); + setLinkEvidenceDescription(""); + }, + onError: (error) => { + toast({ + title: "Error adding link evidence", description: error.message, variant: "destructive", }); @@ -783,10 +874,10 @@ function ControlOverviewPageContent({ const file = files[0]; setUploadingTaskId(taskId); - // Show toast for upload started + // Show toast for add started toast({ - title: "Upload started", - description: `Uploading ${file.name}...`, + title: "Adding document", + description: `Adding ${file.name}...`, variant: "default", }); @@ -798,7 +889,9 @@ function ControlOverviewPageContent({ input: { taskId: taskId, name: file.name, + type: "FILE", file: null, + description: "Document evidence", }, connections: evidenceConnectionId ? [evidenceConnectionId] : [], }, @@ -808,15 +901,15 @@ function ControlOverviewPageContent({ onCompleted: () => { setUploadingTaskId(null); toast({ - title: "Evidence uploaded", - description: "Evidence has been uploaded successfully.", + title: "Document added", + description: "Document evidence has been added successfully.", variant: "default", }); }, onError: (error) => { setUploadingTaskId(null); toast({ - title: "Error uploading evidence", + title: "Error adding document", description: error.message, variant: "destructive", }); @@ -850,9 +943,11 @@ function ControlOverviewPageContent({ } }; - // Function to get file icon based on mime type - const getFileIcon = (mimeType: string) => { - if (mimeType.startsWith("image/")) { + // Function to get file icon based on mime type and evidence type + const getFileIcon = (mimeType: string, evidenceType: string) => { + if (evidenceType === "LINK") { + return ; + } else if (mimeType.startsWith("image/")) { return ; } else if (mimeType.includes("pdf")) { return ; @@ -1088,7 +1183,7 @@ function ControlOverviewPageContent({
- Drag & drop files onto tasks to upload evidence + Drag & drop files onto tasks to add evidence
- +

- Drop file to upload evidence + Drop file to add as evidence

@@ -1258,7 +1353,7 @@ function ControlOverviewPageContent({
-

Uploading evidence...

+

Adding document...

Please wait

@@ -1491,8 +1586,9 @@ function ControlOverviewPageContent({ handleUploadEvidence(task.id, task.name); } }} + title="Add Evidence" > - + - ) : ( + {evidence.type === "FILE" ? ( + <> + {evidence.mimeType.startsWith("image/") ? ( + + ) : ( + + )} + + ) : evidence.url ? ( - )} - + ) : null} + + + + +
+
+ + setLinkEvidenceName(e.target.value)} + placeholder="Name for this evidence" + /> +
+ +
+ + setLinkEvidenceUrl(e.target.value)} + placeholder="https://example.com" + type="url" + /> +
+ +
+ +