Add evidence type link
close #44 Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
Upload,
|
|
||||||
FileIcon,
|
FileIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
UserPlus,
|
UserPlus,
|
||||||
UserMinus,
|
UserMinus,
|
||||||
User,
|
User,
|
||||||
|
Link2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -60,6 +60,9 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import ReactMarkdown from "react-markdown";
|
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 { Helmet } from "react-helmet-async";
|
||||||
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
|
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 { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
||||||
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.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
|
// Function to format ISO8601 duration to human-readable format
|
||||||
const formatDuration = (isoDuration: string): string => {
|
const formatDuration = (isoDuration: string): string => {
|
||||||
@@ -147,6 +149,8 @@ const controlOverviewPageQuery = graphql`
|
|||||||
filename
|
filename
|
||||||
size
|
size
|
||||||
state
|
state
|
||||||
|
type
|
||||||
|
url
|
||||||
createdAt
|
createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +225,8 @@ const uploadEvidenceMutation = graphql`
|
|||||||
filename
|
filename
|
||||||
fileUrl
|
fileUrl
|
||||||
mimeType
|
mimeType
|
||||||
|
type
|
||||||
|
url
|
||||||
size
|
size
|
||||||
state
|
state
|
||||||
createdAt
|
createdAt
|
||||||
@@ -496,6 +502,13 @@ function ControlOverviewPageContent({
|
|||||||
[key: string]: string;
|
[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 tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
|
||||||
|
|
||||||
const getEvidenceConnectionId = useCallback(
|
const getEvidenceConnectionId = useCallback(
|
||||||
@@ -701,10 +714,12 @@ function ControlOverviewPageContent({
|
|||||||
|
|
||||||
const handleUploadEvidence = (taskId: string, taskName: string) => {
|
const handleUploadEvidence = (taskId: string, taskName: string) => {
|
||||||
setTaskForEvidence({ id: taskId, name: taskName });
|
setTaskForEvidence({ id: taskId, name: taskName });
|
||||||
// Instead of opening a modal, directly trigger the file input click
|
setEvidenceDialogOpen(true);
|
||||||
if (hiddenFileInputRef.current) {
|
// Reset form fields
|
||||||
hiddenFileInputRef.current.click();
|
setLinkEvidenceName("");
|
||||||
}
|
setLinkEvidenceUrl("");
|
||||||
|
setLinkEvidenceDescription("");
|
||||||
|
setActiveTab("file");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -713,10 +728,10 @@ function ControlOverviewPageContent({
|
|||||||
|
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
|
|
||||||
// Show toast for upload started
|
// Show toast for add started
|
||||||
toast({
|
toast({
|
||||||
title: "Upload started",
|
title: "Adding document",
|
||||||
description: `Uploading ${file.name}...`,
|
description: `Adding ${file.name}...`,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -728,7 +743,9 @@ function ControlOverviewPageContent({
|
|||||||
input: {
|
input: {
|
||||||
taskId: taskForEvidence.id,
|
taskId: taskForEvidence.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
type: "FILE",
|
||||||
file: null,
|
file: null,
|
||||||
|
description: "Document evidence",
|
||||||
},
|
},
|
||||||
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
||||||
},
|
},
|
||||||
@@ -737,8 +754,8 @@ function ControlOverviewPageContent({
|
|||||||
},
|
},
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast({
|
toast({
|
||||||
title: "Evidence uploaded",
|
title: "Document added",
|
||||||
description: "Evidence has been uploaded successfully.",
|
description: "Document evidence has been added successfully.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
setTaskForEvidence(null);
|
setTaskForEvidence(null);
|
||||||
@@ -749,7 +766,81 @@ function ControlOverviewPageContent({
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast({
|
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,
|
description: error.message,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@@ -783,10 +874,10 @@ function ControlOverviewPageContent({
|
|||||||
const file = files[0];
|
const file = files[0];
|
||||||
setUploadingTaskId(taskId);
|
setUploadingTaskId(taskId);
|
||||||
|
|
||||||
// Show toast for upload started
|
// Show toast for add started
|
||||||
toast({
|
toast({
|
||||||
title: "Upload started",
|
title: "Adding document",
|
||||||
description: `Uploading ${file.name}...`,
|
description: `Adding ${file.name}...`,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -798,7 +889,9 @@ function ControlOverviewPageContent({
|
|||||||
input: {
|
input: {
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
type: "FILE",
|
||||||
file: null,
|
file: null,
|
||||||
|
description: "Document evidence",
|
||||||
},
|
},
|
||||||
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
||||||
},
|
},
|
||||||
@@ -808,15 +901,15 @@ function ControlOverviewPageContent({
|
|||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
setUploadingTaskId(null);
|
setUploadingTaskId(null);
|
||||||
toast({
|
toast({
|
||||||
title: "Evidence uploaded",
|
title: "Document added",
|
||||||
description: "Evidence has been uploaded successfully.",
|
description: "Document evidence has been added successfully.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setUploadingTaskId(null);
|
setUploadingTaskId(null);
|
||||||
toast({
|
toast({
|
||||||
title: "Error uploading evidence",
|
title: "Error adding document",
|
||||||
description: error.message,
|
description: error.message,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@@ -850,9 +943,11 @@ function ControlOverviewPageContent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to get file icon based on mime type
|
// Function to get file icon based on mime type and evidence type
|
||||||
const getFileIcon = (mimeType: string) => {
|
const getFileIcon = (mimeType: string, evidenceType: string) => {
|
||||||
if (mimeType.startsWith("image/")) {
|
if (evidenceType === "LINK") {
|
||||||
|
return <Link2 className="w-4 h-4 text-blue-600" />;
|
||||||
|
} else if (mimeType.startsWith("image/")) {
|
||||||
return <Image className="w-4 h-4 text-blue-500" />;
|
return <Image className="w-4 h-4 text-blue-500" />;
|
||||||
} else if (mimeType.includes("pdf")) {
|
} else if (mimeType.includes("pdf")) {
|
||||||
return <FileText className="w-4 h-4 text-red-500" />;
|
return <FileText className="w-4 h-4 text-red-500" />;
|
||||||
@@ -1088,7 +1183,7 @@ function ControlOverviewPageContent({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-sm text-gray-500 flex items-center bg-gray-50 px-3 py-1.5 rounded-md border border-gray-200">
|
<div className="text-sm text-gray-500 flex items-center bg-gray-50 px-3 py-1.5 rounded-md border border-gray-200">
|
||||||
<FileIcon className="w-4 h-4 mr-2 text-blue-500" />
|
<FileIcon className="w-4 h-4 mr-2 text-blue-500" />
|
||||||
<span>Drag & drop files onto tasks to upload evidence</span>
|
<span>Drag & drop files onto tasks to add evidence</span>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isCreateTaskOpen}
|
open={isCreateTaskOpen}
|
||||||
@@ -1246,9 +1341,9 @@ function ControlOverviewPageContent({
|
|||||||
{draggedOverTaskId === task?.id && (
|
{draggedOverTaskId === task?.id && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center rounded-md z-10 bg-blue-50 bg-opacity-90 backdrop-blur-sm border-2 border-dashed border-blue-400">
|
<div className="absolute inset-0 flex items-center justify-center rounded-md z-10 bg-blue-50 bg-opacity-90 backdrop-blur-sm border-2 border-dashed border-blue-400">
|
||||||
<div className="flex items-center gap-2 text-blue-600 bg-white p-5 rounded-lg shadow-md">
|
<div className="flex items-center gap-2 text-blue-600 bg-white p-5 rounded-lg shadow-md">
|
||||||
<Upload className="w-4 h-4 text-blue-500" />
|
<FileText className="w-4 h-4 text-blue-500" />
|
||||||
<p className="text-sm font-medium text-center">
|
<p className="text-sm font-medium text-center">
|
||||||
Drop file to upload evidence
|
Drop file to add as evidence
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1258,7 +1353,7 @@ function ControlOverviewPageContent({
|
|||||||
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-95 rounded-md z-10 backdrop-blur-sm">
|
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-95 rounded-md z-10 backdrop-blur-sm">
|
||||||
<div className="flex flex-col items-center gap-3 text-blue-600">
|
<div className="flex flex-col items-center gap-3 text-blue-600">
|
||||||
<Loader2 className="w-10 h-10 animate-spin text-blue-500" />
|
<Loader2 className="w-10 h-10 animate-spin text-blue-500" />
|
||||||
<p className="font-medium">Uploading evidence...</p>
|
<p className="font-medium">Adding document...</p>
|
||||||
<p className="text-sm text-gray-500">Please wait</p>
|
<p className="text-sm text-gray-500">Please wait</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1491,8 +1586,9 @@ function ControlOverviewPageContent({
|
|||||||
handleUploadEvidence(task.id, task.name);
|
handleUploadEvidence(task.id, task.name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
title="Add Evidence"
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1547,17 +1643,31 @@ function ControlOverviewPageContent({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="bg-white p-2 rounded-md border border-gray-200">
|
<div className="bg-white p-2 rounded-md border border-gray-200">
|
||||||
{getFileIcon(evidence.mimeType)}
|
{getFileIcon(
|
||||||
|
evidence.mimeType,
|
||||||
|
evidence.type
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-800">
|
<div className="text-sm font-medium text-gray-800">
|
||||||
{evidence.filename}
|
{evidence.filename}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
|
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
|
||||||
<span className="font-medium text-gray-600">
|
{evidence.type === "FILE" ? (
|
||||||
{formatFileSize(evidence.size)}
|
<>
|
||||||
</span>
|
<span className="font-medium text-gray-600">
|
||||||
<span>•</span>
|
{formatFileSize(evidence.size)}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
</>
|
||||||
|
) : evidence.url ? (
|
||||||
|
<>
|
||||||
|
<span className="font-medium text-blue-600 truncate max-w-[200px]">
|
||||||
|
{evidence.url}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<span>
|
<span>
|
||||||
{formatDate(evidence.createdAt)}
|
{formatDate(evidence.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
@@ -1565,38 +1675,45 @@ function ControlOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{evidence.mimeType.startsWith("image/") ? (
|
{evidence.type === "FILE" ? (
|
||||||
<button
|
<>
|
||||||
onClick={() =>
|
{evidence.mimeType.startsWith("image/") ? (
|
||||||
handlePreviewEvidence(evidence)
|
<button
|
||||||
}
|
onClick={() =>
|
||||||
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
handlePreviewEvidence(evidence)
|
||||||
title="Preview Image"
|
}
|
||||||
>
|
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
||||||
<Eye className="w-4 h-4 text-blue-600" />
|
title="Preview Image"
|
||||||
</button>
|
>
|
||||||
) : (
|
<Eye className="w-4 h-4 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePreviewEvidence(evidence);
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : evidence.url ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handlePreviewEvidence(evidence);
|
if (evidence.url) {
|
||||||
|
window.open(evidence.url, "_blank");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
||||||
title="Download"
|
title="Open Link"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 text-blue-600" />
|
<Link2 className="w-4 h-4 text-blue-600" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : null}
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handlePreviewEvidence(evidence);
|
|
||||||
}}
|
|
||||||
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
|
||||||
title="Download"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 text-blue-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1632,6 +1749,99 @@ function ControlOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Evidence Add Dialog */}
|
||||||
|
<Dialog open={evidenceDialogOpen} onOpenChange={setEvidenceDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[525px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Evidence</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Choose the type of evidence you want to add to this task.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(value: string) =>
|
||||||
|
setActiveTab(value as "file" | "link")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="file">Document</TabsTrigger>
|
||||||
|
<TabsTrigger value="link">Link</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="file" className="space-y-4">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<p>Select a document to add as evidence.</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (hiddenFileInputRef.current) {
|
||||||
|
hiddenFileInputRef.current.click();
|
||||||
|
setEvidenceDialogOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Select Document
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="link" className="space-y-4 pt-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="evidence-name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="evidence-name"
|
||||||
|
value={linkEvidenceName}
|
||||||
|
onChange={(e) => setLinkEvidenceName(e.target.value)}
|
||||||
|
placeholder="Name for this evidence"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="evidence-url">URL</Label>
|
||||||
|
<Input
|
||||||
|
id="evidence-url"
|
||||||
|
value={linkEvidenceUrl}
|
||||||
|
onChange={(e) => setLinkEvidenceUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="evidence-description">
|
||||||
|
Description (optional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="evidence-description"
|
||||||
|
value={linkEvidenceDescription}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLinkEvidenceDescription(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Describe this evidence (optional)"
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setEvidenceDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{activeTab === "link" && (
|
||||||
|
<Button onClick={handleLinkEvidenceSubmit}>Add Link</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Hidden file input for direct uploads */}
|
{/* Hidden file input for direct uploads */}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<4cf4f895bdcd936c74cb70f562210df7>>
|
* @generated SignedSource<<5fff8009247229346c3cda94359f2129>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,7 +14,7 @@ export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
|
|||||||
};
|
};
|
||||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly fileUrl?: string;
|
readonly fileUrl?: string | null | undefined;
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<084485ae03f7089bfdee310d631b01eb>>
|
* @generated SignedSource<<e273ee54aacd3db274341d21b33809b2>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
||||||
|
export type EvidenceType = "FILE" | "LINK";
|
||||||
export type TaskState = "DONE" | "TODO";
|
export type TaskState = "DONE" | "TODO";
|
||||||
export type ControlOverviewPageQuery$variables = {
|
export type ControlOverviewPageQuery$variables = {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
@@ -44,6 +45,8 @@ export type ControlOverviewPageQuery$data = {
|
|||||||
readonly mimeType: string;
|
readonly mimeType: string;
|
||||||
readonly size: number;
|
readonly size: number;
|
||||||
readonly state: EvidenceState;
|
readonly state: EvidenceState;
|
||||||
|
readonly type: EvidenceType;
|
||||||
|
readonly url: string | null | undefined;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -251,6 +254,20 @@ v15 = [
|
|||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v5/*: any*/),
|
(v5/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "type",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "url",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -480,7 +497,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "4bcea497959829f998cdab71797c8675",
|
"cacheID": "6f419912ad357b36c51113ef7672ad51",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -503,11 +520,11 @@ return {
|
|||||||
},
|
},
|
||||||
"name": "ControlOverviewPageQuery",
|
"name": "ControlOverviewPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "4710a9799b3daf286afdd430f026e6c2";
|
(node as any).hash = "59a0c6e5a414410be244c7a62bba7696";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<313084c3c2422d51df42fe4d6b848d49>>
|
* @generated SignedSource<<6726ce2446381674b1992ece913fd680>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,10 +10,14 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
||||||
|
export type EvidenceType = "FILE" | "LINK";
|
||||||
export type UploadEvidenceInput = {
|
export type UploadEvidenceInput = {
|
||||||
file: any;
|
description: string;
|
||||||
|
file?: any | null | undefined;
|
||||||
name: string;
|
name: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
type: EvidenceType;
|
||||||
|
url?: string | null | undefined;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUploadEvidenceMutation$variables = {
|
export type ControlOverviewPageUploadEvidenceMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
@@ -24,12 +28,14 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
|
|||||||
readonly evidenceEdge: {
|
readonly evidenceEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly createdAt: string;
|
readonly createdAt: string;
|
||||||
readonly fileUrl: string;
|
readonly fileUrl: string | null | undefined;
|
||||||
readonly filename: string;
|
readonly filename: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly mimeType: string;
|
readonly mimeType: string;
|
||||||
readonly size: number;
|
readonly size: number;
|
||||||
readonly state: EvidenceState;
|
readonly state: EvidenceState;
|
||||||
|
readonly type: EvidenceType;
|
||||||
|
readonly url: string | null | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -101,6 +107,20 @@ v3 = {
|
|||||||
"name": "mimeType",
|
"name": "mimeType",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "type",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "url",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -194,16 +214,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "5edaf64bde176530d9952e3e8aeb15af",
|
"cacheID": "de3681706266cd453ddccb5fe2900ea8",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageUploadEvidenceMutation",
|
"name": "ControlOverviewPageUploadEvidenceMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n size\n state\n createdAt\n }\n }\n }\n}\n"
|
"text": "mutation ControlOverviewPageUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "51f35fe735a747ddaeb0f0cfa298f7e2";
|
(node as any).hash = "396146017c07e06dc709199a0b5ad012";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -28,15 +28,18 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Evidence struct {
|
Evidence struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
TaskID gid.GID `db:"task_id"`
|
TaskID gid.GID `db:"task_id"`
|
||||||
State EvidenceState `db:"state"`
|
State EvidenceState `db:"state"`
|
||||||
ObjectKey string `db:"object_key"`
|
Type EvidenceType `db:"type"`
|
||||||
MimeType string `db:"mime_type"`
|
ObjectKey string `db:"object_key"`
|
||||||
Size uint64 `db:"size"`
|
MimeType string `db:"mime_type"`
|
||||||
Filename string `db:"filename"`
|
Size uint64 `db:"size"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
Filename string `db:"filename"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
URL string `db:"url"`
|
||||||
|
Description string `db:"description"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Evidences []*Evidence
|
Evidences []*Evidence
|
||||||
@@ -66,7 +69,10 @@ INSERT INTO
|
|||||||
mime_type,
|
mime_type,
|
||||||
size,
|
size,
|
||||||
state,
|
state,
|
||||||
|
type,
|
||||||
filename,
|
filename,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
@@ -78,7 +84,10 @@ VALUES (
|
|||||||
@mime_type,
|
@mime_type,
|
||||||
@size,
|
@size,
|
||||||
@state,
|
@state,
|
||||||
|
@type,
|
||||||
@filename,
|
@filename,
|
||||||
|
@url,
|
||||||
|
@description,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -95,6 +104,9 @@ VALUES (
|
|||||||
"created_at": e.CreatedAt,
|
"created_at": e.CreatedAt,
|
||||||
"updated_at": e.UpdatedAt,
|
"updated_at": e.UpdatedAt,
|
||||||
"state": e.State,
|
"state": e.State,
|
||||||
|
"type": e.Type,
|
||||||
|
"url": e.URL,
|
||||||
|
"description": e.Description,
|
||||||
}
|
}
|
||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
@@ -111,10 +123,13 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
task_id,
|
task_id,
|
||||||
state,
|
state,
|
||||||
|
type,
|
||||||
object_key,
|
object_key,
|
||||||
mime_type,
|
mime_type,
|
||||||
size,
|
size,
|
||||||
filename,
|
filename,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -157,10 +172,13 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
task_id,
|
task_id,
|
||||||
state,
|
state,
|
||||||
|
type,
|
||||||
object_key,
|
object_key,
|
||||||
mime_type,
|
mime_type,
|
||||||
size,
|
size,
|
||||||
filename,
|
filename,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
|
|||||||
74
pkg/coredata/evidence_type.go
Normal file
74
pkg/coredata/evidence_type.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
EvidenceType uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EvidenceTypeFile EvidenceType = iota
|
||||||
|
EvidenceTypeLink
|
||||||
|
)
|
||||||
|
|
||||||
|
func (et EvidenceType) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(et.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EvidenceType) UnmarshalText(data []byte) error {
|
||||||
|
val := string(data)
|
||||||
|
|
||||||
|
switch val {
|
||||||
|
case EvidenceTypeFile.String():
|
||||||
|
*et = EvidenceTypeFile
|
||||||
|
case EvidenceTypeLink.String():
|
||||||
|
*et = EvidenceTypeLink
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid EvidenceType value: %q", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et EvidenceType) String() string {
|
||||||
|
var val string
|
||||||
|
|
||||||
|
switch et {
|
||||||
|
case EvidenceTypeFile:
|
||||||
|
val = "FILE"
|
||||||
|
case EvidenceTypeLink:
|
||||||
|
val = "LINK"
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EvidenceType) Scan(value any) error {
|
||||||
|
val, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid scan source for EvidenceType, expected string got %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return et.UnmarshalText([]byte(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et EvidenceType) Value() (driver.Value, error) {
|
||||||
|
return et.String(), nil
|
||||||
|
}
|
||||||
15
pkg/coredata/migrations/20250320T162619Z.sql
Normal file
15
pkg/coredata/migrations/20250320T162619Z.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Add evidence type enum
|
||||||
|
CREATE TYPE evidence_type AS ENUM (
|
||||||
|
'FILE',
|
||||||
|
'LINK'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE evidences
|
||||||
|
ADD COLUMN type evidence_type NOT NULL DEFAULT 'FILE',
|
||||||
|
ADD COLUMN url TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN description TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE evidences
|
||||||
|
ALTER COLUMN type DROP DEFAULT,
|
||||||
|
ALTER COLUMN url DROP DEFAULT,
|
||||||
|
ALTER COLUMN description DROP DEFAULT;
|
||||||
@@ -37,9 +37,12 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
CreateEvidenceRequest struct {
|
CreateEvidenceRequest struct {
|
||||||
TaskID gid.GID
|
TaskID gid.GID
|
||||||
Name string
|
Name string
|
||||||
File io.Reader
|
Type coredata.EvidenceType
|
||||||
|
File io.Reader
|
||||||
|
URL string
|
||||||
|
Description string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,50 +76,61 @@ func (s EvidenceService) Create(
|
|||||||
return nil, fmt.Errorf("cannot create evidence global id: %w", err)
|
return nil, fmt.Errorf("cannot create evidence global id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentType := "application/octet-stream"
|
evidence := &coredata.Evidence{
|
||||||
if req.Name != "" {
|
ID: evidenceID,
|
||||||
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
|
TaskID: req.TaskID,
|
||||||
contentType = detectedType
|
State: coredata.EvidenceStateValid,
|
||||||
|
Type: req.Type,
|
||||||
|
Filename: req.Name,
|
||||||
|
URL: req.URL,
|
||||||
|
Description: req.Description,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type == coredata.EvidenceTypeFile {
|
||||||
|
contentType := "application/octet-stream"
|
||||||
|
if req.Name != "" {
|
||||||
|
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
|
||||||
|
contentType = detectedType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
objectKey, err := uuid.NewV7()
|
objectKey, err := uuid.NewV7()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot generate object key: %w", err)
|
return nil, fmt.Errorf("cannot generate object key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
|
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
|
||||||
Bucket: aws.String(s.svc.bucket),
|
Bucket: aws.String(s.svc.bucket),
|
||||||
Key: aws.String(objectKey.String()),
|
Key: aws.String(objectKey.String()),
|
||||||
Body: req.File,
|
Body: req.File,
|
||||||
ContentType: aws.String(contentType),
|
ContentType: aws.String(contentType),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
|
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
|
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
|
||||||
Bucket: aws.String(s.svc.bucket),
|
Bucket: aws.String(s.svc.bucket),
|
||||||
Key: aws.String(objectKey.String()),
|
Key: aws.String(objectKey.String()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot get object metadata: %w", err)
|
return nil, fmt.Errorf("cannot get object metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("putObjectOutput", putObjectOutput)
|
fmt.Println("putObjectOutput", putObjectOutput)
|
||||||
|
|
||||||
|
evidence.ObjectKey = objectKey.String()
|
||||||
|
evidence.MimeType = contentType
|
||||||
|
evidence.Size = uint64(*headOutput.ContentLength)
|
||||||
|
} else if req.Type == coredata.EvidenceTypeLink {
|
||||||
|
evidence.MimeType = "text/uri-list"
|
||||||
|
evidence.Size = uint64(len(req.URL))
|
||||||
|
evidence.ObjectKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
task := &coredata.Task{}
|
task := &coredata.Task{}
|
||||||
evidence := &coredata.Evidence{
|
|
||||||
ID: evidenceID,
|
|
||||||
TaskID: req.TaskID,
|
|
||||||
State: coredata.EvidenceStateValid,
|
|
||||||
ObjectKey: objectKey.String(),
|
|
||||||
MimeType: contentType,
|
|
||||||
Size: uint64(*headOutput.ContentLength),
|
|
||||||
Filename: req.Name,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.svc.pg.WithTx(
|
err = s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -134,7 +148,7 @@ func (s EvidenceService) Create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO try do delete file from s3
|
// TODO try do delete file from s3 if it's a file type
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +165,10 @@ func (s EvidenceService) GenerateFileURL(
|
|||||||
return nil, fmt.Errorf("cannot get evidence: %w", err)
|
return nil, fmt.Errorf("cannot get evidence: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if evidence.Type == coredata.EvidenceTypeLink {
|
||||||
|
return nil, fmt.Errorf("cannot generate file URL for link type evidence")
|
||||||
|
}
|
||||||
|
|
||||||
presignClient := s3.NewPresignClient(s.svc.s3)
|
presignClient := s3.NewPresignClient(s.svc.s3)
|
||||||
|
|
||||||
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||||
|
|||||||
@@ -411,13 +411,22 @@ type EvidenceEdge {
|
|||||||
node: Evidence!
|
node: Evidence!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EvidenceType
|
||||||
|
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceType") {
|
||||||
|
FILE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeFile")
|
||||||
|
LINK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeLink")
|
||||||
|
}
|
||||||
|
|
||||||
type Evidence implements Node {
|
type Evidence implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fileUrl: String! @goField(forceResolver: true)
|
fileUrl: String @goField(forceResolver: true)
|
||||||
mimeType: String!
|
mimeType: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
state: EvidenceState!
|
state: EvidenceState!
|
||||||
|
type: EvidenceType!
|
||||||
filename: String!
|
filename: String!
|
||||||
|
url: String
|
||||||
|
description: String!
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
@@ -707,7 +716,10 @@ type UpdateControlPayload {
|
|||||||
input UploadEvidenceInput {
|
input UploadEvidenceInput {
|
||||||
taskId: ID!
|
taskId: ID!
|
||||||
name: String!
|
name: String!
|
||||||
file: Upload!
|
file: Upload
|
||||||
|
type: EvidenceType!
|
||||||
|
url: String
|
||||||
|
description: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadEvidencePayload {
|
type UploadEvidencePayload {
|
||||||
|
|||||||
@@ -141,14 +141,17 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Evidence struct {
|
Evidence struct {
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
FileURL func(childComplexity int) int
|
Description func(childComplexity int) int
|
||||||
Filename func(childComplexity int) int
|
FileURL func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
Filename func(childComplexity int) int
|
||||||
MimeType func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
Size func(childComplexity int) int
|
MimeType func(childComplexity int) int
|
||||||
State func(childComplexity int) int
|
Size func(childComplexity int) int
|
||||||
UpdatedAt func(childComplexity int) int
|
State func(childComplexity int) int
|
||||||
|
Type func(childComplexity int) int
|
||||||
|
URL func(childComplexity int) int
|
||||||
|
UpdatedAt func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
EvidenceConnection struct {
|
EvidenceConnection struct {
|
||||||
@@ -420,7 +423,7 @@ type ControlResolver interface {
|
|||||||
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error)
|
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error)
|
||||||
}
|
}
|
||||||
type EvidenceResolver interface {
|
type EvidenceResolver interface {
|
||||||
FileURL(ctx context.Context, obj *types.Evidence) (string, error)
|
FileURL(ctx context.Context, obj *types.Evidence) (*string, error)
|
||||||
}
|
}
|
||||||
type FrameworkResolver interface {
|
type FrameworkResolver interface {
|
||||||
Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error)
|
Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error)
|
||||||
@@ -711,6 +714,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.Evidence.CreatedAt(childComplexity), true
|
return e.complexity.Evidence.CreatedAt(childComplexity), true
|
||||||
|
|
||||||
|
case "Evidence.description":
|
||||||
|
if e.complexity.Evidence.Description == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Evidence.Description(childComplexity), true
|
||||||
|
|
||||||
case "Evidence.fileUrl":
|
case "Evidence.fileUrl":
|
||||||
if e.complexity.Evidence.FileURL == nil {
|
if e.complexity.Evidence.FileURL == nil {
|
||||||
break
|
break
|
||||||
@@ -753,6 +763,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.Evidence.State(childComplexity), true
|
return e.complexity.Evidence.State(childComplexity), true
|
||||||
|
|
||||||
|
case "Evidence.type":
|
||||||
|
if e.complexity.Evidence.Type == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Evidence.Type(childComplexity), true
|
||||||
|
|
||||||
|
case "Evidence.url":
|
||||||
|
if e.complexity.Evidence.URL == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Evidence.URL(childComplexity), true
|
||||||
|
|
||||||
case "Evidence.updatedAt":
|
case "Evidence.updatedAt":
|
||||||
if e.complexity.Evidence.UpdatedAt == nil {
|
if e.complexity.Evidence.UpdatedAt == nil {
|
||||||
break
|
break
|
||||||
@@ -2502,13 +2526,22 @@ type EvidenceEdge {
|
|||||||
node: Evidence!
|
node: Evidence!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EvidenceType
|
||||||
|
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceType") {
|
||||||
|
FILE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeFile")
|
||||||
|
LINK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeLink")
|
||||||
|
}
|
||||||
|
|
||||||
type Evidence implements Node {
|
type Evidence implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fileUrl: String! @goField(forceResolver: true)
|
fileUrl: String @goField(forceResolver: true)
|
||||||
mimeType: String!
|
mimeType: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
state: EvidenceState!
|
state: EvidenceState!
|
||||||
|
type: EvidenceType!
|
||||||
filename: String!
|
filename: String!
|
||||||
|
url: String
|
||||||
|
description: String!
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
@@ -2798,7 +2831,10 @@ type UpdateControlPayload {
|
|||||||
input UploadEvidenceInput {
|
input UploadEvidenceInput {
|
||||||
taskId: ID!
|
taskId: ID!
|
||||||
name: String!
|
name: String!
|
||||||
file: Upload!
|
file: Upload
|
||||||
|
type: EvidenceType!
|
||||||
|
url: String
|
||||||
|
description: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadEvidencePayload {
|
type UploadEvidencePayload {
|
||||||
@@ -5868,14 +5904,11 @@ func (ec *executionContext) _Evidence_fileUrl(ctx context.Context, field graphql
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(string)
|
res := resTmp.(*string)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
@@ -6005,6 +6038,44 @@ func (ec *executionContext) fieldContext_Evidence_state(_ context.Context, field
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Evidence_type(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Evidence_type(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Type, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(coredata.EvidenceType)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Evidence_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Evidence",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type EvidenceType does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Evidence_filename(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Evidence_filename(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Evidence_filename(ctx, field)
|
fc, err := ec.fieldContext_Evidence_filename(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6043,6 +6114,79 @@ func (ec *executionContext) fieldContext_Evidence_filename(_ context.Context, fi
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Evidence_url(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Evidence_url(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.URL, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Evidence_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Evidence",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Evidence_description(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Evidence_description(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Description, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Evidence_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Evidence",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Evidence_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Evidence_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Evidence_createdAt(ctx, field)
|
fc, err := ec.fieldContext_Evidence_createdAt(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6292,8 +6436,14 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi
|
|||||||
return ec.fieldContext_Evidence_size(ctx, field)
|
return ec.fieldContext_Evidence_size(ctx, field)
|
||||||
case "state":
|
case "state":
|
||||||
return ec.fieldContext_Evidence_state(ctx, field)
|
return ec.fieldContext_Evidence_state(ctx, field)
|
||||||
|
case "type":
|
||||||
|
return ec.fieldContext_Evidence_type(ctx, field)
|
||||||
case "filename":
|
case "filename":
|
||||||
return ec.fieldContext_Evidence_filename(ctx, field)
|
return ec.fieldContext_Evidence_filename(ctx, field)
|
||||||
|
case "url":
|
||||||
|
return ec.fieldContext_Evidence_url(ctx, field)
|
||||||
|
case "description":
|
||||||
|
return ec.fieldContext_Evidence_description(ctx, field)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
return ec.fieldContext_Evidence_createdAt(ctx, field)
|
return ec.fieldContext_Evidence_createdAt(ctx, field)
|
||||||
case "updatedAt":
|
case "updatedAt":
|
||||||
@@ -15668,7 +15818,7 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
|
|||||||
asMap[k] = v
|
asMap[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldsInOrder := [...]string{"taskId", "name", "file"}
|
fieldsInOrder := [...]string{"taskId", "name", "file", "type", "url", "description"}
|
||||||
for _, k := range fieldsInOrder {
|
for _, k := range fieldsInOrder {
|
||||||
v, ok := asMap[k]
|
v, ok := asMap[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -15691,11 +15841,32 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
|
|||||||
it.Name = data
|
it.Name = data
|
||||||
case "file":
|
case "file":
|
||||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file"))
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file"))
|
||||||
data, err := ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
|
data, err := ec.unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
it.File = data
|
it.File = data
|
||||||
|
case "type":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
|
||||||
|
data, err := ec.unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.Type = data
|
||||||
|
case "url":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("url"))
|
||||||
|
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.URL = data
|
||||||
|
case "description":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
|
||||||
|
data, err := ec.unmarshalNString2string(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.Description = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16652,11 +16823,8 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
|
|||||||
case "fileUrl":
|
case "fileUrl":
|
||||||
field := field
|
field := field
|
||||||
|
|
||||||
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
|
||||||
res = ec._Evidence_fileUrl(ctx, field, obj)
|
res = ec._Evidence_fileUrl(ctx, field, obj)
|
||||||
if res == graphql.Null {
|
|
||||||
atomic.AddUint32(&fs.Invalids, 1)
|
|
||||||
}
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16695,11 +16863,23 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
}
|
}
|
||||||
|
case "type":
|
||||||
|
out.Values[i] = ec._Evidence_type(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
case "filename":
|
case "filename":
|
||||||
out.Values[i] = ec._Evidence_filename(ctx, field, obj)
|
out.Values[i] = ec._Evidence_filename(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
}
|
}
|
||||||
|
case "url":
|
||||||
|
out.Values[i] = ec._Evidence_url(ctx, field, obj)
|
||||||
|
case "description":
|
||||||
|
out.Values[i] = ec._Evidence_description(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out.Values[i] = ec._Evidence_createdAt(ctx, field, obj)
|
out.Values[i] = ec._Evidence_createdAt(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
@@ -20101,6 +20281,33 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx context.Context, v any) (coredata.EvidenceType, error) {
|
||||||
|
tmp, err := graphql.UnmarshalString(v)
|
||||||
|
res := unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType[tmp]
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx context.Context, sel ast.SelectionSet, v coredata.EvidenceType) graphql.Marshaler {
|
||||||
|
res := graphql.MarshalString(marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType[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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType = map[string]coredata.EvidenceType{
|
||||||
|
"FILE": coredata.EvidenceTypeFile,
|
||||||
|
"LINK": coredata.EvidenceTypeLink,
|
||||||
|
}
|
||||||
|
marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType = map[coredata.EvidenceType]string{
|
||||||
|
coredata.EvidenceTypeFile: "FILE",
|
||||||
|
coredata.EvidenceTypeLink: "LINK",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler {
|
func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
|||||||
@@ -44,14 +44,25 @@ func NewEvidenceEdge(e *coredata.Evidence, orderBy coredata.EvidenceOrderField)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewEvidence(e *coredata.Evidence) *Evidence {
|
func NewEvidence(e *coredata.Evidence) *Evidence {
|
||||||
|
var fileURL *string = nil
|
||||||
|
|
||||||
|
var urlPtr *string = nil
|
||||||
|
if e.URL != "" {
|
||||||
|
urlCopy := e.URL
|
||||||
|
urlPtr = &urlCopy
|
||||||
|
}
|
||||||
|
|
||||||
return &Evidence{
|
return &Evidence{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
State: e.State,
|
State: e.State,
|
||||||
FileURL: "",
|
Type: e.Type,
|
||||||
Filename: e.Filename,
|
FileURL: fileURL,
|
||||||
MimeType: e.MimeType,
|
Filename: e.Filename,
|
||||||
Size: int(e.Size),
|
MimeType: e.MimeType,
|
||||||
CreatedAt: e.CreatedAt,
|
Size: int(e.Size),
|
||||||
UpdatedAt: e.UpdatedAt,
|
URL: urlPtr,
|
||||||
|
Description: e.Description,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,14 +195,17 @@ type DeleteVendorPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Evidence struct {
|
type Evidence struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
FileURL string `json:"fileUrl"`
|
FileURL *string `json:"fileUrl,omitempty"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
State coredata.EvidenceState `json:"state"`
|
State coredata.EvidenceState `json:"state"`
|
||||||
Filename string `json:"filename"`
|
Type coredata.EvidenceType `json:"type"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Filename string `json:"filename"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
URL *string `json:"url,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Evidence) IsNode() {}
|
func (Evidence) IsNode() {}
|
||||||
@@ -495,9 +498,12 @@ type UpdateVendorPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UploadEvidenceInput struct {
|
type UploadEvidenceInput struct {
|
||||||
TaskID gid.GID `json:"taskId"`
|
TaskID gid.GID `json:"taskId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
File graphql.Upload `json:"file"`
|
File *graphql.Upload `json:"file,omitempty"`
|
||||||
|
Type coredata.EvidenceType `json:"type"`
|
||||||
|
URL *string `json:"url,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadEvidencePayload struct {
|
type UploadEvidencePayload struct {
|
||||||
|
|||||||
@@ -45,15 +45,20 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FileURL is the resolver for the fileUrl field.
|
// FileURL is the resolver for the fileUrl field.
|
||||||
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (string, error) {
|
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
|
if obj.Type == coredata.EvidenceTypeLink {
|
||||||
|
return obj.URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
fileURL, err := svc.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute)
|
fileURL, err := svc.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cannot generate file URL: %w", err)
|
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return *fileURL, nil
|
result := *fileURL
|
||||||
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls is the resolver for the controls field.
|
// Controls is the resolver for the controls field.
|
||||||
@@ -436,10 +441,28 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
|
|||||||
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) {
|
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
|
||||||
|
|
||||||
|
var url string
|
||||||
|
if input.URL != nil {
|
||||||
|
url = *input.URL
|
||||||
|
}
|
||||||
|
|
||||||
req := probo.CreateEvidenceRequest{
|
req := probo.CreateEvidenceRequest{
|
||||||
TaskID: input.TaskID,
|
TaskID: input.TaskID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
File: input.File.File,
|
Type: input.Type,
|
||||||
|
URL: url,
|
||||||
|
Description: input.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Type == coredata.EvidenceTypeFile {
|
||||||
|
if input.File == nil {
|
||||||
|
return nil, fmt.Errorf("file is required for FILE type evidence")
|
||||||
|
}
|
||||||
|
req.File = input.File.File
|
||||||
|
} else if input.Type == coredata.EvidenceTypeLink {
|
||||||
|
if input.URL == nil || *input.URL == "" {
|
||||||
|
return nil, fmt.Errorf("URL is required for LINK type evidence")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
evidence, err := svc.Evidences.Create(ctx, req)
|
evidence, err := svc.Evidences.Create(ctx, req)
|
||||||
|
|||||||
Reference in New Issue
Block a user