Add evidence type link

close #44

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-20 17:25:29 +01:00
parent 7117c6a210
commit b8bbeaba11
13 changed files with 798 additions and 167 deletions

View File

@@ -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">
{evidence.type === "FILE" ? (
<>
<span className="font-medium text-gray-600"> <span className="font-medium text-gray-600">
{formatFileSize(evidence.size)} {formatFileSize(evidence.size)}
</span> </span>
<span>•</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,6 +1675,8 @@ function ControlOverviewPageContent({
</div> </div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{evidence.type === "FILE" ? (
<>
{evidence.mimeType.startsWith("image/") ? ( {evidence.mimeType.startsWith("image/") ? (
<button <button
onClick={() => onClick={() =>
@@ -1587,16 +1699,21 @@ function ControlOverviewPageContent({
<Download className="w-4 h-4 text-blue-600" /> <Download className="w-4 h-4 text-blue-600" />
</button> </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 <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"

View 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;
}; };
}; };

View File

@@ -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;

View File

@@ -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;

View File

@@ -31,10 +31,13 @@ type (
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"`
Type EvidenceType `db:"type"`
ObjectKey string `db:"object_key"` ObjectKey string `db:"object_key"`
MimeType string `db:"mime_type"` MimeType string `db:"mime_type"`
Size uint64 `db:"size"` Size uint64 `db:"size"`
Filename string `db:"filename"` Filename string `db:"filename"`
URL string `db:"url"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at"`
} }
@@ -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

View 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
}

View 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;

View File

@@ -39,7 +39,10 @@ type (
CreateEvidenceRequest struct { CreateEvidenceRequest struct {
TaskID gid.GID TaskID gid.GID
Name string Name string
Type coredata.EvidenceType
File io.Reader File io.Reader
URL string
Description string
} }
) )
@@ -73,6 +76,19 @@ 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)
} }
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
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" contentType := "application/octet-stream"
if req.Name != "" { if req.Name != "" {
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" { if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
@@ -105,19 +121,17 @@ func (s EvidenceService) Create(
fmt.Println("putObjectOutput", putObjectOutput) fmt.Println("putObjectOutput", putObjectOutput)
task := &coredata.Task{} evidence.ObjectKey = objectKey.String()
evidence := &coredata.Evidence{ evidence.MimeType = contentType
ID: evidenceID, evidence.Size = uint64(*headOutput.ContentLength)
TaskID: req.TaskID, } else if req.Type == coredata.EvidenceTypeLink {
State: coredata.EvidenceStateValid, evidence.MimeType = "text/uri-list"
ObjectKey: objectKey.String(), evidence.Size = uint64(len(req.URL))
MimeType: contentType, evidence.ObjectKey = ""
Size: uint64(*headOutput.ContentLength),
Filename: req.Name,
CreatedAt: now,
UpdatedAt: now,
} }
task := &coredata.Task{}
err = s.svc.pg.WithTx( err = s.svc.pg.WithTx(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
@@ -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{

View File

@@ -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 {

View File

@@ -142,12 +142,15 @@ type ComplexityRoot struct {
Evidence struct { Evidence struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
FileURL func(childComplexity int) int FileURL func(childComplexity int) int
Filename func(childComplexity int) int Filename func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
MimeType func(childComplexity int) int MimeType func(childComplexity int) int
Size func(childComplexity int) int Size func(childComplexity int) int
State func(childComplexity int) int State func(childComplexity int) int
Type func(childComplexity int) int
URL func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -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)) {

View File

@@ -44,13 +44,24 @@ 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,
FileURL: fileURL,
Filename: e.Filename, Filename: e.Filename,
MimeType: e.MimeType, MimeType: e.MimeType,
Size: int(e.Size), Size: int(e.Size),
URL: urlPtr,
Description: e.Description,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
} }

View File

@@ -196,11 +196,14 @@ 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"`
Type coredata.EvidenceType `json:"type"`
Filename string `json:"filename"` Filename string `json:"filename"`
URL *string `json:"url,omitempty"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
@@ -497,7 +500,10 @@ 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 {

View File

@@ -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)