Add evidence type link
close #44 Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
||||
CheckCircle2,
|
||||
Plus,
|
||||
Trash2,
|
||||
Upload,
|
||||
FileIcon,
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
UserPlus,
|
||||
UserMinus,
|
||||
User,
|
||||
Link2,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -60,6 +60,9 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
|
||||
@@ -72,7 +75,6 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign
|
||||
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
|
||||
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
||||
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
// Function to format ISO8601 duration to human-readable format
|
||||
const formatDuration = (isoDuration: string): string => {
|
||||
@@ -147,6 +149,8 @@ const controlOverviewPageQuery = graphql`
|
||||
filename
|
||||
size
|
||||
state
|
||||
type
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
@@ -221,6 +225,8 @@ const uploadEvidenceMutation = graphql`
|
||||
filename
|
||||
fileUrl
|
||||
mimeType
|
||||
type
|
||||
url
|
||||
size
|
||||
state
|
||||
createdAt
|
||||
@@ -496,6 +502,13 @@ function ControlOverviewPageContent({
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
// Add state variables for the evidence dialog and link evidence
|
||||
const [evidenceDialogOpen, setEvidenceDialogOpen] = useState(false);
|
||||
const [linkEvidenceName, setLinkEvidenceName] = useState("");
|
||||
const [linkEvidenceUrl, setLinkEvidenceUrl] = useState("");
|
||||
const [linkEvidenceDescription, setLinkEvidenceDescription] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<"file" | "link">("file");
|
||||
|
||||
const tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
|
||||
|
||||
const getEvidenceConnectionId = useCallback(
|
||||
@@ -701,10 +714,12 @@ function ControlOverviewPageContent({
|
||||
|
||||
const handleUploadEvidence = (taskId: string, taskName: string) => {
|
||||
setTaskForEvidence({ id: taskId, name: taskName });
|
||||
// Instead of opening a modal, directly trigger the file input click
|
||||
if (hiddenFileInputRef.current) {
|
||||
hiddenFileInputRef.current.click();
|
||||
}
|
||||
setEvidenceDialogOpen(true);
|
||||
// Reset form fields
|
||||
setLinkEvidenceName("");
|
||||
setLinkEvidenceUrl("");
|
||||
setLinkEvidenceDescription("");
|
||||
setActiveTab("file");
|
||||
};
|
||||
|
||||
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -713,10 +728,10 @@ function ControlOverviewPageContent({
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
// Show toast for upload started
|
||||
// Show toast for add started
|
||||
toast({
|
||||
title: "Upload started",
|
||||
description: `Uploading ${file.name}...`,
|
||||
title: "Adding document",
|
||||
description: `Adding ${file.name}...`,
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
@@ -728,7 +743,9 @@ function ControlOverviewPageContent({
|
||||
input: {
|
||||
taskId: taskForEvidence.id,
|
||||
name: file.name,
|
||||
type: "FILE",
|
||||
file: null,
|
||||
description: "Document evidence",
|
||||
},
|
||||
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
||||
},
|
||||
@@ -737,8 +754,8 @@ function ControlOverviewPageContent({
|
||||
},
|
||||
onCompleted: () => {
|
||||
toast({
|
||||
title: "Evidence uploaded",
|
||||
description: "Evidence has been uploaded successfully.",
|
||||
title: "Document added",
|
||||
description: "Document evidence has been added successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
setTaskForEvidence(null);
|
||||
@@ -749,7 +766,81 @@ function ControlOverviewPageContent({
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error uploading evidence",
|
||||
title: "Error adding document",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinkEvidenceSubmit = () => {
|
||||
if (!taskForEvidence) return;
|
||||
|
||||
// Validate form
|
||||
if (!linkEvidenceName.trim()) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please provide a name for the evidence",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!linkEvidenceUrl.trim()) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please provide a URL for the evidence",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Description is now optional for link evidence
|
||||
// Remove the validation check for empty description
|
||||
|
||||
// Show toast for add started
|
||||
toast({
|
||||
title: "Adding link evidence",
|
||||
description: `Adding ${linkEvidenceName}...`,
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
// Get the evidence connection ID for this task
|
||||
const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id);
|
||||
|
||||
// Use a default description if none is provided
|
||||
const description =
|
||||
linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`;
|
||||
|
||||
uploadEvidence({
|
||||
variables: {
|
||||
input: {
|
||||
taskId: taskForEvidence.id,
|
||||
name: linkEvidenceName,
|
||||
type: "LINK",
|
||||
url: linkEvidenceUrl,
|
||||
description: description,
|
||||
file: null,
|
||||
},
|
||||
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
toast({
|
||||
title: "Link evidence added",
|
||||
description: "Link evidence has been added successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
setTaskForEvidence(null);
|
||||
setEvidenceDialogOpen(false);
|
||||
// Reset form fields
|
||||
setLinkEvidenceName("");
|
||||
setLinkEvidenceUrl("");
|
||||
setLinkEvidenceDescription("");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error adding link evidence",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -783,10 +874,10 @@ function ControlOverviewPageContent({
|
||||
const file = files[0];
|
||||
setUploadingTaskId(taskId);
|
||||
|
||||
// Show toast for upload started
|
||||
// Show toast for add started
|
||||
toast({
|
||||
title: "Upload started",
|
||||
description: `Uploading ${file.name}...`,
|
||||
title: "Adding document",
|
||||
description: `Adding ${file.name}...`,
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
@@ -798,7 +889,9 @@ function ControlOverviewPageContent({
|
||||
input: {
|
||||
taskId: taskId,
|
||||
name: file.name,
|
||||
type: "FILE",
|
||||
file: null,
|
||||
description: "Document evidence",
|
||||
},
|
||||
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
|
||||
},
|
||||
@@ -808,15 +901,15 @@ function ControlOverviewPageContent({
|
||||
onCompleted: () => {
|
||||
setUploadingTaskId(null);
|
||||
toast({
|
||||
title: "Evidence uploaded",
|
||||
description: "Evidence has been uploaded successfully.",
|
||||
title: "Document added",
|
||||
description: "Document evidence has been added successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setUploadingTaskId(null);
|
||||
toast({
|
||||
title: "Error uploading evidence",
|
||||
title: "Error adding document",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -850,9 +943,11 @@ function ControlOverviewPageContent({
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get file icon based on mime type
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.startsWith("image/")) {
|
||||
// Function to get file icon based on mime type and evidence type
|
||||
const getFileIcon = (mimeType: string, evidenceType: string) => {
|
||||
if (evidenceType === "LINK") {
|
||||
return <Link2 className="w-4 h-4 text-blue-600" />;
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
return <Image className="w-4 h-4 text-blue-500" />;
|
||||
} else if (mimeType.includes("pdf")) {
|
||||
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="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" />
|
||||
<span>Drag & drop files onto tasks to upload evidence</span>
|
||||
<span>Drag & drop files onto tasks to add evidence</span>
|
||||
</div>
|
||||
<Dialog
|
||||
open={isCreateTaskOpen}
|
||||
@@ -1246,9 +1341,9 @@ function ControlOverviewPageContent({
|
||||
{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="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">
|
||||
Drop file to upload evidence
|
||||
Drop file to add as evidence
|
||||
</p>
|
||||
</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="flex flex-col items-center gap-3 text-blue-600">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1491,8 +1586,9 @@ function ControlOverviewPageContent({
|
||||
handleUploadEvidence(task.id, task.name);
|
||||
}
|
||||
}}
|
||||
title="Add Evidence"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<FileText className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1547,17 +1643,31 @@ function ControlOverviewPageContent({
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-white p-2 rounded-md border border-gray-200">
|
||||
{getFileIcon(evidence.mimeType)}
|
||||
{getFileIcon(
|
||||
evidence.mimeType,
|
||||
evidence.type
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-800">
|
||||
{evidence.filename}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
|
||||
<span className="font-medium text-gray-600">
|
||||
{formatFileSize(evidence.size)}
|
||||
</span>
|
||||
<span>•</span>
|
||||
{evidence.type === "FILE" ? (
|
||||
<>
|
||||
<span className="font-medium text-gray-600">
|
||||
{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>
|
||||
{formatDate(evidence.createdAt)}
|
||||
</span>
|
||||
@@ -1565,38 +1675,45 @@ function ControlOverviewPageContent({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{evidence.mimeType.startsWith("image/") ? (
|
||||
<button
|
||||
onClick={() =>
|
||||
handlePreviewEvidence(evidence)
|
||||
}
|
||||
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
||||
title="Preview Image"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-blue-600" />
|
||||
</button>
|
||||
) : (
|
||||
{evidence.type === "FILE" ? (
|
||||
<>
|
||||
{evidence.mimeType.startsWith("image/") ? (
|
||||
<button
|
||||
onClick={() =>
|
||||
handlePreviewEvidence(evidence)
|
||||
}
|
||||
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
|
||||
title="Preview Image"
|
||||
>
|
||||
<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
|
||||
onClick={(e) => {
|
||||
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"
|
||||
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
|
||||
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>
|
||||
) : null}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -1632,6 +1749,99 @@ function ControlOverviewPageContent({
|
||||
</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 */}
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<4cf4f895bdcd936c74cb70f562210df7>>
|
||||
* @generated SignedSource<<5fff8009247229346c3cda94359f2129>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -14,7 +14,7 @@ export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
|
||||
};
|
||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
||||
readonly node: {
|
||||
readonly fileUrl?: string;
|
||||
readonly fileUrl?: string | null | undefined;
|
||||
readonly id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<084485ae03f7089bfdee310d631b01eb>>
|
||||
* @generated SignedSource<<e273ee54aacd3db274341d21b33809b2>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
||||
export type EvidenceType = "FILE" | "LINK";
|
||||
export type TaskState = "DONE" | "TODO";
|
||||
export type ControlOverviewPageQuery$variables = {
|
||||
controlId: string;
|
||||
@@ -44,6 +45,8 @@ export type ControlOverviewPageQuery$data = {
|
||||
readonly mimeType: string;
|
||||
readonly size: number;
|
||||
readonly state: EvidenceState;
|
||||
readonly type: EvidenceType;
|
||||
readonly url: string | null | undefined;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@@ -251,6 +254,20 @@ v15 = [
|
||||
"storageKey": null
|
||||
},
|
||||
(v5/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "url",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -480,7 +497,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "4bcea497959829f998cdab71797c8675",
|
||||
"cacheID": "6f419912ad357b36c51113ef7672ad51",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -503,11 +520,11 @@ return {
|
||||
},
|
||||
"name": "ControlOverviewPageQuery",
|
||||
"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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<313084c3c2422d51df42fe4d6b848d49>>
|
||||
* @generated SignedSource<<6726ce2446381674b1992ece913fd680>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,10 +10,14 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
||||
export type EvidenceType = "FILE" | "LINK";
|
||||
export type UploadEvidenceInput = {
|
||||
file: any;
|
||||
description: string;
|
||||
file?: any | null | undefined;
|
||||
name: string;
|
||||
taskId: string;
|
||||
type: EvidenceType;
|
||||
url?: string | null | undefined;
|
||||
};
|
||||
export type ControlOverviewPageUploadEvidenceMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
@@ -24,12 +28,14 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
|
||||
readonly evidenceEdge: {
|
||||
readonly node: {
|
||||
readonly createdAt: string;
|
||||
readonly fileUrl: string;
|
||||
readonly fileUrl: string | null | undefined;
|
||||
readonly filename: string;
|
||||
readonly id: string;
|
||||
readonly mimeType: string;
|
||||
readonly size: number;
|
||||
readonly state: EvidenceState;
|
||||
readonly type: EvidenceType;
|
||||
readonly url: string | null | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -101,6 +107,20 @@ v3 = {
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "url",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -194,16 +214,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "5edaf64bde176530d9952e3e8aeb15af",
|
||||
"cacheID": "de3681706266cd453ddccb5fe2900ea8",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "ControlOverviewPageUploadEvidenceMutation",
|
||||
"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;
|
||||
|
||||
@@ -28,15 +28,18 @@ import (
|
||||
|
||||
type (
|
||||
Evidence struct {
|
||||
ID gid.GID `db:"id"`
|
||||
TaskID gid.GID `db:"task_id"`
|
||||
State EvidenceState `db:"state"`
|
||||
ObjectKey string `db:"object_key"`
|
||||
MimeType string `db:"mime_type"`
|
||||
Size uint64 `db:"size"`
|
||||
Filename string `db:"filename"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ID gid.GID `db:"id"`
|
||||
TaskID gid.GID `db:"task_id"`
|
||||
State EvidenceState `db:"state"`
|
||||
Type EvidenceType `db:"type"`
|
||||
ObjectKey string `db:"object_key"`
|
||||
MimeType string `db:"mime_type"`
|
||||
Size uint64 `db:"size"`
|
||||
Filename string `db:"filename"`
|
||||
URL string `db:"url"`
|
||||
Description string `db:"description"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
Evidences []*Evidence
|
||||
@@ -66,7 +69,10 @@ INSERT INTO
|
||||
mime_type,
|
||||
size,
|
||||
state,
|
||||
type,
|
||||
filename,
|
||||
url,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
@@ -78,7 +84,10 @@ VALUES (
|
||||
@mime_type,
|
||||
@size,
|
||||
@state,
|
||||
@type,
|
||||
@filename,
|
||||
@url,
|
||||
@description,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
@@ -95,6 +104,9 @@ VALUES (
|
||||
"created_at": e.CreatedAt,
|
||||
"updated_at": e.UpdatedAt,
|
||||
"state": e.State,
|
||||
"type": e.Type,
|
||||
"url": e.URL,
|
||||
"description": e.Description,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
@@ -111,10 +123,13 @@ SELECT
|
||||
id,
|
||||
task_id,
|
||||
state,
|
||||
type,
|
||||
object_key,
|
||||
mime_type,
|
||||
size,
|
||||
filename,
|
||||
url,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -157,10 +172,13 @@ SELECT
|
||||
id,
|
||||
task_id,
|
||||
state,
|
||||
type,
|
||||
object_key,
|
||||
mime_type,
|
||||
size,
|
||||
filename,
|
||||
url,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
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 {
|
||||
TaskID gid.GID
|
||||
Name string
|
||||
File io.Reader
|
||||
TaskID gid.GID
|
||||
Name string
|
||||
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)
|
||||
}
|
||||
|
||||
contentType := "application/octet-stream"
|
||||
if req.Name != "" {
|
||||
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
|
||||
contentType = detectedType
|
||||
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"
|
||||
if req.Name != "" {
|
||||
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
|
||||
contentType = detectedType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectKey, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate object key: %w", err)
|
||||
}
|
||||
objectKey, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate object key: %w", err)
|
||||
}
|
||||
|
||||
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.svc.bucket),
|
||||
Key: aws.String(objectKey.String()),
|
||||
Body: req.File,
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
|
||||
}
|
||||
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.svc.bucket),
|
||||
Key: aws.String(objectKey.String()),
|
||||
Body: req.File,
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
|
||||
}
|
||||
|
||||
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(s.svc.bucket),
|
||||
Key: aws.String(objectKey.String()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get object metadata: %w", err)
|
||||
}
|
||||
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(s.svc.bucket),
|
||||
Key: aws.String(objectKey.String()),
|
||||
})
|
||||
if err != nil {
|
||||
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{}
|
||||
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(
|
||||
ctx,
|
||||
@@ -134,7 +148,7 @@ func (s EvidenceService) Create(
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -151,6 +165,10 @@ func (s EvidenceService) GenerateFileURL(
|
||||
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)
|
||||
|
||||
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
|
||||
@@ -411,13 +411,22 @@ type EvidenceEdge {
|
||||
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 {
|
||||
id: ID!
|
||||
fileUrl: String! @goField(forceResolver: true)
|
||||
fileUrl: String @goField(forceResolver: true)
|
||||
mimeType: String!
|
||||
size: Int!
|
||||
state: EvidenceState!
|
||||
type: EvidenceType!
|
||||
filename: String!
|
||||
url: String
|
||||
description: String!
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
@@ -707,7 +716,10 @@ type UpdateControlPayload {
|
||||
input UploadEvidenceInput {
|
||||
taskId: ID!
|
||||
name: String!
|
||||
file: Upload!
|
||||
file: Upload
|
||||
type: EvidenceType!
|
||||
url: String
|
||||
description: String!
|
||||
}
|
||||
|
||||
type UploadEvidencePayload {
|
||||
|
||||
@@ -141,14 +141,17 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Evidence struct {
|
||||
CreatedAt func(childComplexity int) int
|
||||
FileURL func(childComplexity int) int
|
||||
Filename func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
MimeType func(childComplexity int) int
|
||||
Size func(childComplexity int) int
|
||||
State func(childComplexity int) int
|
||||
UpdatedAt func(childComplexity int) int
|
||||
CreatedAt func(childComplexity int) int
|
||||
Description func(childComplexity int) int
|
||||
FileURL func(childComplexity int) int
|
||||
Filename func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
MimeType func(childComplexity int) int
|
||||
Size 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 {
|
||||
@@ -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)
|
||||
}
|
||||
type EvidenceResolver interface {
|
||||
FileURL(ctx context.Context, obj *types.Evidence) (string, error)
|
||||
FileURL(ctx context.Context, obj *types.Evidence) (*string, error)
|
||||
}
|
||||
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)
|
||||
@@ -711,6 +714,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
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":
|
||||
if e.complexity.Evidence.FileURL == nil {
|
||||
break
|
||||
@@ -753,6 +763,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
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":
|
||||
if e.complexity.Evidence.UpdatedAt == nil {
|
||||
break
|
||||
@@ -2502,13 +2526,22 @@ type EvidenceEdge {
|
||||
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 {
|
||||
id: ID!
|
||||
fileUrl: String! @goField(forceResolver: true)
|
||||
fileUrl: String @goField(forceResolver: true)
|
||||
mimeType: String!
|
||||
size: Int!
|
||||
state: EvidenceState!
|
||||
type: EvidenceType!
|
||||
filename: String!
|
||||
url: String
|
||||
description: String!
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
@@ -2798,7 +2831,10 @@ type UpdateControlPayload {
|
||||
input UploadEvidenceInput {
|
||||
taskId: ID!
|
||||
name: String!
|
||||
file: Upload!
|
||||
file: Upload
|
||||
type: EvidenceType!
|
||||
url: String
|
||||
description: String!
|
||||
}
|
||||
|
||||
type UploadEvidencePayload {
|
||||
@@ -5868,14 +5904,11 @@ func (ec *executionContext) _Evidence_fileUrl(ctx context.Context, field graphql
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(string)
|
||||
res := resTmp.(*string)
|
||||
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) {
|
||||
@@ -6005,6 +6038,44 @@ func (ec *executionContext) fieldContext_Evidence_state(_ context.Context, field
|
||||
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) {
|
||||
fc, err := ec.fieldContext_Evidence_filename(ctx, field)
|
||||
if err != nil {
|
||||
@@ -6043,6 +6114,79 @@ func (ec *executionContext) fieldContext_Evidence_filename(_ context.Context, fi
|
||||
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) {
|
||||
fc, err := ec.fieldContext_Evidence_createdAt(ctx, field)
|
||||
if err != nil {
|
||||
@@ -6292,8 +6436,14 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi
|
||||
return ec.fieldContext_Evidence_size(ctx, field)
|
||||
case "state":
|
||||
return ec.fieldContext_Evidence_state(ctx, field)
|
||||
case "type":
|
||||
return ec.fieldContext_Evidence_type(ctx, field)
|
||||
case "filename":
|
||||
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":
|
||||
return ec.fieldContext_Evidence_createdAt(ctx, field)
|
||||
case "updatedAt":
|
||||
@@ -15668,7 +15818,7 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"taskId", "name", "file"}
|
||||
fieldsInOrder := [...]string{"taskId", "name", "file", "type", "url", "description"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -15691,11 +15841,32 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
|
||||
it.Name = data
|
||||
case "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 {
|
||||
return it, err
|
||||
}
|
||||
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":
|
||||
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)
|
||||
if res == graphql.Null {
|
||||
atomic.AddUint32(&fs.Invalids, 1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -16695,11 +16863,23 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
|
||||
if out.Values[i] == graphql.Null {
|
||||
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":
|
||||
out.Values[i] = ec._Evidence_filename(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
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":
|
||||
out.Values[i] = ec._Evidence_createdAt(ctx, field, obj)
|
||||
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 {
|
||||
if v == nil {
|
||||
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 {
|
||||
var fileURL *string = nil
|
||||
|
||||
var urlPtr *string = nil
|
||||
if e.URL != "" {
|
||||
urlCopy := e.URL
|
||||
urlPtr = &urlCopy
|
||||
}
|
||||
|
||||
return &Evidence{
|
||||
ID: e.ID,
|
||||
State: e.State,
|
||||
FileURL: "",
|
||||
Filename: e.Filename,
|
||||
MimeType: e.MimeType,
|
||||
Size: int(e.Size),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
ID: e.ID,
|
||||
State: e.State,
|
||||
Type: e.Type,
|
||||
FileURL: fileURL,
|
||||
Filename: e.Filename,
|
||||
MimeType: e.MimeType,
|
||||
Size: int(e.Size),
|
||||
URL: urlPtr,
|
||||
Description: e.Description,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +195,17 @@ type DeleteVendorPayload struct {
|
||||
}
|
||||
|
||||
type Evidence struct {
|
||||
ID gid.GID `json:"id"`
|
||||
FileURL string `json:"fileUrl"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Size int `json:"size"`
|
||||
State coredata.EvidenceState `json:"state"`
|
||||
Filename string `json:"filename"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID gid.GID `json:"id"`
|
||||
FileURL *string `json:"fileUrl,omitempty"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Size int `json:"size"`
|
||||
State coredata.EvidenceState `json:"state"`
|
||||
Type coredata.EvidenceType `json:"type"`
|
||||
Filename string `json:"filename"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Evidence) IsNode() {}
|
||||
@@ -495,9 +498,12 @@ type UpdateVendorPayload struct {
|
||||
}
|
||||
|
||||
type UploadEvidenceInput struct {
|
||||
TaskID gid.GID `json:"taskId"`
|
||||
Name string `json:"name"`
|
||||
File graphql.Upload `json:"file"`
|
||||
TaskID gid.GID `json:"taskId"`
|
||||
Name string `json:"name"`
|
||||
File *graphql.Upload `json:"file,omitempty"`
|
||||
Type coredata.EvidenceType `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
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.
|
||||
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())
|
||||
|
||||
if obj.Type == coredata.EvidenceTypeLink {
|
||||
return obj.URL, nil
|
||||
}
|
||||
|
||||
fileURL, err := svc.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute)
|
||||
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.
|
||||
@@ -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) {
|
||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
|
||||
|
||||
var url string
|
||||
if input.URL != nil {
|
||||
url = *input.URL
|
||||
}
|
||||
|
||||
req := probo.CreateEvidenceRequest{
|
||||
TaskID: input.TaskID,
|
||||
Name: input.Name,
|
||||
File: input.File.File,
|
||||
TaskID: input.TaskID,
|
||||
Name: input.Name,
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user