Add requested evidence

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-02 10:10:49 +02:00
parent 2f9f04c984
commit c145125b1a
15 changed files with 1856 additions and 352 deletions

View File

@@ -17,30 +17,41 @@ import {
useRelayEnvironment, useRelayEnvironment,
} from "react-relay"; } from "react-relay";
import { import {
AlertTriangle,
ArrowRight,
Check,
CheckCircle2, CheckCircle2,
Plus,
Trash2,
FileIcon,
Loader2,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Clock,
Download, Download,
Eye, Eye,
File as FileGeneric, File as FileGeneric,
FileIcon,
FileText, FileText,
Image, Image,
X,
UserPlus,
UserMinus,
User,
Link2, Link2,
Loader2,
LucideIcon,
MoreHorizontal,
Plus,
Search, Search,
Link as LinkIcon,
CheckSquare,
ShieldCheck, ShieldCheck,
AlertTriangle, Trash2,
Upload,
User,
UserMinus,
UserPlus,
X,
} from "lucide-react"; } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -83,7 +94,6 @@ import { MitigationViewSkeleton } from "./MitigationPage";
import { MitigationViewUpdateTaskStateMutation as MitigationViewUpdateTaskStateMutationType } from "./__generated__/MitigationViewUpdateTaskStateMutation.graphql"; import { MitigationViewUpdateTaskStateMutation as MitigationViewUpdateTaskStateMutationType } from "./__generated__/MitigationViewUpdateTaskStateMutation.graphql";
import { MitigationViewCreateTaskMutation as MitigationViewCreateTaskMutationType } from "./__generated__/MitigationViewCreateTaskMutation.graphql"; import { MitigationViewCreateTaskMutation as MitigationViewCreateTaskMutationType } from "./__generated__/MitigationViewCreateTaskMutation.graphql";
import { MitigationViewDeleteTaskMutation as MitigationViewDeleteTaskMutationType } from "./__generated__/MitigationViewDeleteTaskMutation.graphql"; import { MitigationViewDeleteTaskMutation as MitigationViewDeleteTaskMutationType } from "./__generated__/MitigationViewDeleteTaskMutation.graphql";
import { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql";
import { MitigationViewDeleteEvidenceMutation as MitigationViewDeleteEvidenceMutationType } from "./__generated__/MitigationViewDeleteEvidenceMutation.graphql"; import { MitigationViewDeleteEvidenceMutation as MitigationViewDeleteEvidenceMutationType } from "./__generated__/MitigationViewDeleteEvidenceMutation.graphql";
import { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationType } from "./__generated__/MitigationViewAssignTaskMutation.graphql"; import { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationType } from "./__generated__/MitigationViewAssignTaskMutation.graphql";
import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql"; import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql";
@@ -105,6 +115,10 @@ import {
MitigationViewRisksQuery, MitigationViewRisksQuery,
MitigationViewRisksQuery$data, MitigationViewRisksQuery$data,
} from "./__generated__/MitigationViewRisksQuery.graphql"; } from "./__generated__/MitigationViewRisksQuery.graphql";
import { MitigationViewCreateEvidenceMutation as MitigationViewCreateEvidenceMutationType } from "./__generated__/MitigationViewCreateEvidenceMutation.graphql";
import { MitigationViewGetEvidenceFileUrlQuery as MitigationViewGetEvidenceFileUrlQueryType } from "./__generated__/MitigationViewGetEvidenceFileUrlQuery.graphql";
import { MitigationViewFulfillEvidenceMutation as MitigationViewFulfillEvidenceMutationType } from "./__generated__/MitigationViewFulfillEvidenceMutation.graphql";
import { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql";
// 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 => {
@@ -238,12 +252,12 @@ const deleteTaskMutation = graphql`
} }
`; `;
const uploadEvidenceMutation = graphql` const createEvidenceMutation = graphql`
mutation MitigationViewUploadEvidenceMutation( mutation MitigationViewCreateEvidenceMutation(
$input: UploadEvidenceInput! $input: CreateEvidenceInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
uploadEvidence(input: $input) { createEvidence(input: $input) {
evidenceEdge @appendEdge(connections: $connections) { evidenceEdge @appendEdge(connections: $connections) {
node { node {
id id
@@ -416,6 +430,30 @@ const deleteControlMappingMutation = graphql`
} }
`; `;
// Add a mutation to fulfill evidence
const fulfillEvidenceMutation = graphql`
mutation MitigationViewFulfillEvidenceMutation(
$input: FulfillEvidenceInput!
$connections: [ID!]!
) {
fulfillEvidence(input: $input) {
evidenceEdge @appendEdge(connections: $connections) {
node {
id
filename
fileUrl
mimeType
type
url
size
state
createdAt
}
}
}
}
`;
// Add query to fetch risks linked to a mitigation // Add query to fetch risks linked to a mitigation
const mitigationRisksQuery = graphql` const mitigationRisksQuery = graphql`
query MitigationViewRisksQuery($mitigationId: ID!) { query MitigationViewRisksQuery($mitigationId: ID!) {
@@ -641,9 +679,9 @@ function MitigationViewContent({
useMutation<MitigationViewCreateTaskMutationType>(createTaskMutation); useMutation<MitigationViewCreateTaskMutationType>(createTaskMutation);
const [deleteTask] = const [deleteTask] =
useMutation<MitigationViewDeleteTaskMutationType>(deleteTaskMutation); useMutation<MitigationViewDeleteTaskMutationType>(deleteTaskMutation);
const [uploadEvidence] = const [createEvidence] =
useMutation<MitigationViewUploadEvidenceMutationType>( useMutation<MitigationViewCreateEvidenceMutationType>(
uploadEvidenceMutation createEvidenceMutation
); );
const [deleteEvidence] = const [deleteEvidence] =
useMutation<MitigationViewDeleteEvidenceMutationType>( useMutation<MitigationViewDeleteEvidenceMutationType>(
@@ -653,6 +691,10 @@ function MitigationViewContent({
useMutation<MitigationViewAssignTaskMutationType>(assignTaskMutation); useMutation<MitigationViewAssignTaskMutationType>(assignTaskMutation);
const [unassignTask] = const [unassignTask] =
useMutation<MitigationViewUnassignTaskMutationType>(unassignTaskMutation); useMutation<MitigationViewUnassignTaskMutationType>(unassignTaskMutation);
const [fulfillEvidence] =
useMutation<MitigationViewFulfillEvidenceMutationType>(
fulfillEvidenceMutation
);
const [updateMitigationState] = const [updateMitigationState] =
useMutation<MitigationViewUpdateMitigationStateMutationType>( useMutation<MitigationViewUpdateMitigationStateMutationType>(
@@ -723,6 +765,14 @@ function MitigationViewContent({
const [linkEvidenceDescription, setLinkEvidenceDescription] = useState(""); const [linkEvidenceDescription, setLinkEvidenceDescription] = useState("");
const [activeTab, setActiveTab] = useState<"file" | "link">("file"); const [activeTab, setActiveTab] = useState<"file" | "link">("file");
// State for fulfill evidence dialog
const [fulfillEvidenceDialogOpen, setFulfillEvidenceDialogOpen] =
useState(false);
const [evidenceToFulfill, setEvidenceToFulfill] = useState<{
id: string;
filename: string;
} | null>(null);
// Add state for selected task panel // Add state for selected task panel
const [selectedTask, setSelectedTask] = useState<(typeof tasks)[0] | null>( const [selectedTask, setSelectedTask] = useState<(typeof tasks)[0] | null>(
null null
@@ -955,7 +1005,7 @@ function MitigationViewContent({
); );
}; };
const handleUploadEvidence = (taskId: string, taskName: string) => { const handleCreateEvidence = (taskId: string, taskName: string) => {
setTaskForEvidence({ id: taskId, name: taskName }); setTaskForEvidence({ id: taskId, name: taskName });
setEvidenceDialogOpen(true); setEvidenceDialogOpen(true);
// Reset form fields // Reset form fields
@@ -980,7 +1030,7 @@ function MitigationViewContent({
// Get the evidence connection ID for this task // Get the evidence connection ID for this task
const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id); const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id);
uploadEvidence({ createEvidence({
variables: { variables: {
input: { input: {
taskId: taskForEvidence.id, taskId: taskForEvidence.id,
@@ -1043,7 +1093,7 @@ function MitigationViewContent({
const description = const description =
linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`; linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`;
uploadEvidence({ createEvidence({
variables: { variables: {
input: { input: {
taskId: taskForEvidence.id, taskId: taskForEvidence.id,
@@ -1109,7 +1159,7 @@ function MitigationViewContent({
// Get the evidence connection ID for this task // Get the evidence connection ID for this task
const evidenceConnectionId = getEvidenceConnectionId(taskId); const evidenceConnectionId = getEvidenceConnectionId(taskId);
uploadEvidence({ createEvidence({
variables: { variables: {
input: { input: {
taskId: taskId, taskId: taskId,
@@ -1236,6 +1286,126 @@ function MitigationViewContent({
setIsDeleteEvidenceOpen(true); setIsDeleteEvidenceOpen(true);
}; };
const handleFulfillEvidence = (evidenceId: string, filename: string) => {
setEvidenceToFulfill({ id: evidenceId, filename });
setFulfillEvidenceDialogOpen(true);
setActiveTab("file");
setLinkEvidenceUrl("");
setLinkEvidenceDescription("");
};
const handleFulfillEvidenceWithFile = (
e: React.ChangeEvent<HTMLInputElement>
) => {
if (!e.target.files || e.target.files.length === 0 || !evidenceToFulfill)
return;
const file = e.target.files[0];
// Show toast for upload started
toast({
title: "Uploading evidence",
description: `Uploading ${file.name}...`,
variant: "default",
});
const evidenceId = evidenceToFulfill.id;
// Get connection ID for the parent task (assuming it's available in the view)
const task = tasks.find((task) =>
task.evidences?.edges.some((edge) => edge?.node?.id === evidenceId)
);
const evidenceConnectionId = task?.id
? getEvidenceConnectionId(task.id)
: null;
fulfillEvidence({
variables: {
input: {
evidenceId: evidenceId,
file: null,
},
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
},
uploadables: {
"input.file": file,
},
onCompleted: () => {
setFulfillEvidenceDialogOpen(false);
setEvidenceToFulfill(null);
toast({
title: "Evidence uploaded",
description: "Evidence has been fulfilled successfully.",
variant: "default",
});
// Reset the file input
if (hiddenFileInputRef.current) {
hiddenFileInputRef.current.value = "";
}
},
onError: (error) => {
toast({
title: "Error uploading evidence",
description: error.message,
variant: "destructive",
});
},
});
};
const handleFulfillEvidenceWithLink = () => {
if (!evidenceToFulfill) return;
// Validate URL
if (!linkEvidenceUrl.trim()) {
toast({
title: "Error",
description: "Please provide a URL for the evidence",
variant: "destructive",
});
return;
}
// Get connection ID for the parent task
const task = tasks.find((task) =>
task.evidences?.edges.some(
(edge) => edge?.node?.id === evidenceToFulfill.id
)
);
const evidenceConnectionId = task?.id
? getEvidenceConnectionId(task.id)
: null;
fulfillEvidence({
variables: {
input: {
evidenceId: evidenceToFulfill.id,
url: linkEvidenceUrl,
},
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
},
onCompleted: () => {
setFulfillEvidenceDialogOpen(false);
setEvidenceToFulfill(null);
setLinkEvidenceUrl("");
setLinkEvidenceDescription("");
toast({
title: "Evidence updated",
description: "Evidence has been fulfilled with link successfully.",
variant: "default",
});
},
onError: (error) => {
toast({
title: "Error updating evidence",
description: error.message,
variant: "destructive",
});
},
});
};
const confirmDeleteEvidence = () => { const confirmDeleteEvidence = () => {
if (!evidenceToDelete) return; if (!evidenceToDelete) return;
@@ -1789,7 +1959,7 @@ function MitigationViewContent({
> >
<TabsList className="mb-4"> <TabsList className="mb-4">
<TabsTrigger value="tasks" className="flex items-center gap-2"> <TabsTrigger value="tasks" className="flex items-center gap-2">
<CheckSquare className="w-4 h-4" /> <Check className="w-4 h-4" />
Tasks Tasks
{tasks.length > 0 && ( {tasks.length > 0 && (
<span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5"> <span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5">
@@ -1830,7 +2000,7 @@ function MitigationViewContent({
className="flex items-center gap-1" className="flex items-center gap-1"
onClick={handleOpenControlMappingDialog} onClick={handleOpenControlMappingDialog}
> >
<LinkIcon className="w-4 h-4" /> <Link2 className="w-4 h-4" />
<span>Map to Controls</span> <span>Map to Controls</span>
</Button> </Button>
</div> </div>
@@ -1967,7 +2137,7 @@ function MitigationViewContent({
{isLinkingControl ? ( {isLinkingControl ? (
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<LinkIcon className="w-4 h-4" /> <Link2 className="w-4 h-4" />
)} )}
<span className="ml-1">Link</span> <span className="ml-1">Link</span>
</Button> </Button>
@@ -2442,7 +2612,7 @@ function MitigationViewContent({
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (task?.id && task?.name) { if (task?.id && task?.name) {
handleUploadEvidence(task.id, task.name); handleCreateEvidence(task.id, task.name);
} }
}} }}
title="Add Evidence" title="Add Evidence"
@@ -2512,16 +2682,31 @@ function MitigationViewContent({
)} )}
</div> </div>
<div> <div>
<div className="text-sm font-medium text-gray-800"> <div className="text-sm font-medium text-gray-800 flex items-center gap-2">
{evidence.filename} {evidence.filename}
{evidence.state === "REQUESTED" && (
<Badge
variant="outline"
className="text-xs bg-yellow-50 text-yellow-700 border-yellow-200"
>
Requested
</Badge>
)}
</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" ? ( {evidence.state === "REQUESTED" ? (
<span className="italic text-yellow-600">
Waiting for fulfillment
</span>
) : 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>
<span>
{formatDate(evidence.createdAt)}
</span>
</> </>
) : evidence.url ? ( ) : evidence.url ? (
<> <>
@@ -2529,16 +2714,34 @@ function MitigationViewContent({
{evidence.url} {evidence.url}
</span> </span>
<span>•</span> <span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</> </>
) : null} ) : (
<span> <span>
{formatDate(evidence.createdAt)} {formatDate(evidence.createdAt)}
</span> </span>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{evidence.type === "FILE" ? ( {evidence.state === "REQUESTED" ? (
<button
onClick={(e) => {
e.stopPropagation();
handleFulfillEvidence(
evidence.id,
evidence.filename
);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Fulfill Evidence"
>
<Upload className="w-4 h-4 text-green-600" />
</button>
) : evidence.type === "FILE" ? (
<> <>
{evidence.mimeType.startsWith( {evidence.mimeType.startsWith(
"image/" "image/"
@@ -2583,13 +2786,11 @@ function MitigationViewContent({
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (task.id) { handleDeleteEvidence(
handleDeleteEvidence( evidence.id,
evidence.id, evidence.filename,
evidence.filename, task.id
task.id );
);
}
}} }}
className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all" className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all"
title="Delete" title="Delete"
@@ -2947,7 +3148,7 @@ function MitigationViewContent({
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => onClick={() =>
handleUploadEvidence(selectedTask.id, selectedTask.name) handleCreateEvidence(selectedTask.id, selectedTask.name)
} }
className="flex items-center gap-1 text-xs" className="flex items-center gap-1 text-xs"
> >
@@ -2974,16 +3175,31 @@ function MitigationViewContent({
{getFileIcon(evidence.mimeType, evidence.type)} {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 flex items-center gap-2">
{evidence.filename} {evidence.filename}
{evidence.state === "REQUESTED" && (
<Badge
variant="outline"
className="text-xs bg-yellow-50 text-yellow-700 border-yellow-200"
>
Requested
</Badge>
)}
</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" ? ( {evidence.state === "REQUESTED" ? (
<span className="italic text-yellow-600">
Waiting for fulfillment
</span>
) : 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>
<span>
{formatDate(evidence.createdAt)}
</span>
</> </>
) : evidence.url ? ( ) : evidence.url ? (
<> <>
@@ -2991,20 +3207,41 @@ function MitigationViewContent({
{evidence.url} {evidence.url}
</span> </span>
<span>•</span> <span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</> </>
) : null} ) : (
<span>{formatDate(evidence.createdAt)}</span> <span>
{formatDate(evidence.createdAt)}
</span>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{evidence.type === "FILE" ? ( {evidence.state === "REQUESTED" ? (
<button
onClick={(e) => {
e.stopPropagation();
handleFulfillEvidence(
evidence.id,
evidence.filename
);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Fulfill Evidence"
>
<Upload className="w-4 h-4 text-green-600" />
</button>
) : evidence.type === "FILE" ? (
<> <>
{evidence.mimeType.startsWith("image/") ? ( {evidence.mimeType.startsWith("image/") ? (
<button <button
onClick={() => onClick={(e) => {
handlePreviewEvidence(evidence) e.stopPropagation();
} handlePreviewEvidence(evidence);
}}
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="Preview Image" title="Preview Image"
> >
@@ -3013,7 +3250,7 @@ function MitigationViewContent({
) : ( ) : (
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.stopPropagation();
handlePreviewEvidence(evidence); handlePreviewEvidence(evidence);
}} }}
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"
@@ -3026,7 +3263,7 @@ function MitigationViewContent({
) : evidence.url ? ( ) : evidence.url ? (
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.stopPropagation();
if (evidence.url) { if (evidence.url) {
window.open(evidence.url, "_blank"); window.open(evidence.url, "_blank");
} }
@@ -3039,11 +3276,11 @@ function MitigationViewContent({
) : null} ) : null}
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.stopPropagation();
handleDeleteEvidence( handleDeleteEvidence(
evidence.id, evidence.id,
evidence.filename, evidence.filename,
selectedTask.id task.id
); );
}} }}
className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all" className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all"
@@ -3353,6 +3590,104 @@ function MitigationViewContent({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Fulfill Evidence Dialog */}
<Dialog
open={fulfillEvidenceDialogOpen}
onOpenChange={setFulfillEvidenceDialogOpen}
>
<DialogContent className="sm:max-w-[525px]">
<DialogHeader>
<DialogTitle>Fulfill Evidence</DialogTitle>
<DialogDescription>
Provide the requested evidence: {evidenceToFulfill?.filename}
</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>Upload a document to fulfill this evidence request.</p>
<Button
onClick={() => {
if (hiddenFileInputRef.current) {
// Set a temp onchange handler for the file input
const originalOnChange =
hiddenFileInputRef.current.onchange;
hiddenFileInputRef.current.onchange = (e) => {
hiddenFileInputRef.current!.onchange =
originalOnChange as any;
handleFulfillEvidenceWithFile({
target: {
files: (e.target as HTMLInputElement)?.files,
},
} as React.ChangeEvent<HTMLInputElement>);
};
hiddenFileInputRef.current.click();
setFulfillEvidenceDialogOpen(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-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={() => setFulfillEvidenceDialogOpen(false)}
>
Cancel
</Button>
{activeTab === "link" && (
<Button onClick={handleFulfillEvidenceWithLink}>
Submit Link
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</PageTemplate> </PageTemplate>
); );
} }

View File

@@ -0,0 +1,229 @@
/**
* @generated SignedSource<<beb765793d4559382ee625dffd7a510c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type CreateEvidenceInput = {
description: string;
file?: any | null | undefined;
name: string;
taskId: string;
type: EvidenceType;
url?: string | null | undefined;
};
export type MitigationViewCreateEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateEvidenceInput;
};
export type MitigationViewCreateEvidenceMutation$data = {
readonly createEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: 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;
};
};
};
};
export type MitigationViewCreateEvidenceMutation = {
response: MitigationViewCreateEvidenceMutation$data;
variables: MitigationViewCreateEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"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,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationViewCreateEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationViewCreateEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "ca3148cb5b3c59fec3bd9ff9c281f364",
"id": null,
"metadata": {},
"name": "MitigationViewCreateEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MitigationViewCreateEvidenceMutation(\n $input: CreateEvidenceInput!\n) {\n createEvidence(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 = "dac05ab414b2e09e801df5b0be9532a3";
export default node;

View File

@@ -0,0 +1,226 @@
/**
* @generated SignedSource<<c12714c0dab6d31865a2b49936566f05>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type FulfillEvidenceInput = {
evidenceId: string;
file?: any | null | undefined;
url?: string | null | undefined;
};
export type MitigationViewFulfillEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: FulfillEvidenceInput;
};
export type MitigationViewFulfillEvidenceMutation$data = {
readonly fulfillEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: 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;
};
};
};
};
export type MitigationViewFulfillEvidenceMutation = {
response: MitigationViewFulfillEvidenceMutation$data;
variables: MitigationViewFulfillEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"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,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationViewFulfillEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "FulfillEvidencePayload",
"kind": "LinkedField",
"name": "fulfillEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationViewFulfillEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "FulfillEvidencePayload",
"kind": "LinkedField",
"name": "fulfillEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "147fdc881f797ace12dc1242cab782ad",
"id": null,
"metadata": {},
"name": "MitigationViewFulfillEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MitigationViewFulfillEvidenceMutation(\n $input: FulfillEvidenceInput!\n) {\n fulfillEvidence(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 = "c0a1e6be43759cfbcd7e25a3b597926a";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<5baf9229b2f520bcfa43960bd6af05e0>> * @generated SignedSource<<3188657dfa050c818bf0dc06688d7fab>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,7 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID"; export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK"; export type EvidenceType = "FILE" | "LINK";
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED"; export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<9d4db1a6dc5ce7a35be41b6f42bc91d2>> * @generated SignedSource<<6e5efc598a8e6657fd0a1375e6104ff6>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,7 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID"; export type EvidenceState = "FULLFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK"; export type EvidenceType = "FILE" | "LINK";
export type UploadEvidenceInput = { export type UploadEvidenceInput = {
description: string; description: string;

View File

@@ -29,7 +29,7 @@ import (
type ( type (
Evidence struct { Evidence struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
TaskID *gid.GID `db:"task_id"` TaskID gid.GID `db:"task_id"`
State EvidenceState `db:"state"` State EvidenceState `db:"state"`
Type EvidenceType `db:"type"` Type EvidenceType `db:"type"`
ObjectKey string `db:"object_key"` ObjectKey string `db:"object_key"`

View File

@@ -24,9 +24,8 @@ type (
) )
const ( const (
EvidenceStateValid EvidenceState = iota EvidenceStateRequested EvidenceState = iota
EvidenceStateInvalid EvidenceStateFulfilled
EvidenceStateExpired
) )
func (es EvidenceState) MarshalText() ([]byte, error) { func (es EvidenceState) MarshalText() ([]byte, error) {
@@ -37,12 +36,10 @@ func (es *EvidenceState) UnmarshalText(data []byte) error {
val := string(data) val := string(data)
switch val { switch val {
case EvidenceStateValid.String(): case EvidenceStateRequested.String():
*es = EvidenceStateValid *es = EvidenceStateRequested
case EvidenceStateInvalid.String(): case EvidenceStateFulfilled.String():
*es = EvidenceStateInvalid *es = EvidenceStateFulfilled
case EvidenceStateExpired.String():
*es = EvidenceStateExpired
default: default:
return fmt.Errorf("invalid EvidenceState value: %q", val) return fmt.Errorf("invalid EvidenceState value: %q", val)
} }
@@ -54,12 +51,10 @@ func (es EvidenceState) String() string {
var val string var val string
switch es { switch es {
case EvidenceStateValid: case EvidenceStateRequested:
val = "VALID" val = "REQUESTED"
case EvidenceStateInvalid: case EvidenceStateFulfilled:
val = "INVALID" val = "FULFILLED"
case EvidenceStateExpired:
val = "EXPIRED"
} }
return val return val

View File

@@ -0,0 +1,10 @@
CREATE TYPE evidence_state_new AS ENUM ('FULFILLED', 'REQUESTED');
ALTER TABLE evidences ALTER COLUMN state TYPE text;
UPDATE evidences SET state = 'FULFILLED';
ALTER TABLE evidences ALTER COLUMN state TYPE evidence_state_new USING state::evidence_state_new;
DROP TYPE evidence_state;
ALTER TYPE evidence_state_new RENAME TO evidence_state;

View File

@@ -0,0 +1 @@
ALTER TABLE evidences ALTER COLUMN task_id SET NOT NULL;

View File

@@ -61,7 +61,7 @@ LOOP:
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case <-time.After(1 * time.Second): case <-time.After(60 * time.Second):
ctx := context.Background() ctx := context.Background()
if err := m.batchSendEmails(ctx); err != nil { if err := m.batchSendEmails(ctx); err != nil {
m.l.ErrorCtx(ctx, "cannot send email", log.Error(err)) m.l.ErrorCtx(ctx, "cannot send email", log.Error(err))

View File

@@ -37,6 +37,19 @@ type (
svc *TenantService svc *TenantService
} }
RequestEvidenceRequest struct {
TaskID gid.GID
Type coredata.EvidenceType
Name string
Description string
}
FulfilledEvidenceRequest struct {
EvidenceID gid.GID
File io.Reader
URL string
}
CreateEvidenceRequest struct { CreateEvidenceRequest struct {
TaskID gid.GID TaskID gid.GID
Name string Name string
@@ -67,6 +80,63 @@ func (s EvidenceService) Get(
return evidence, nil return evidence, nil
} }
func (s EvidenceService) Request(
ctx context.Context,
req RequestEvidenceRequest,
) (*coredata.Evidence, error) {
evidenceID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create evidence global id: %w", err)
}
now := time.Now()
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
State: coredata.EvidenceStateRequested,
Type: req.Type,
Filename: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return evidence.Insert(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, fmt.Errorf("cannot insert evidence: %w", err)
}
return evidence, nil
}
func (s EvidenceService) Fulfill(
ctx context.Context,
req FulfilledEvidenceRequest,
) (*coredata.Evidence, error) {
evidence := &coredata.Evidence{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := evidence.LoadByID(ctx, conn, s.svc.scope, req.EvidenceID); err != nil {
return fmt.Errorf("cannot load evidence: %w", err)
}
evidence.State = coredata.EvidenceStateFulfilled
return evidence.Update(ctx, conn, s.svc.scope)
},
)
}
func (s EvidenceService) Create( func (s EvidenceService) Create(
ctx context.Context, ctx context.Context,
req CreateEvidenceRequest, req CreateEvidenceRequest,
@@ -79,8 +149,8 @@ func (s EvidenceService) Create(
evidence := &coredata.Evidence{ evidence := &coredata.Evidence{
ID: evidenceID, ID: evidenceID,
TaskID: &req.TaskID, TaskID: req.TaskID,
State: coredata.EvidenceStateValid, State: coredata.EvidenceStateFulfilled,
Type: req.Type, Type: req.Type,
Filename: req.Name, Filename: req.Name,
URL: req.URL, URL: req.URL,

View File

@@ -67,15 +67,13 @@ enum TaskState
enum EvidenceState enum EvidenceState
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceState") { @goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceState") {
VALID FULFILLED
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateValid")
INVALID
@goEnum( @goEnum(
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateInvalid" value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateFulfilled"
) )
EXPIRED REQUESTED
@goEnum( @goEnum(
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateExpired" value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateRequested"
) )
} }
@@ -751,7 +749,9 @@ type Mutation {
deleteRiskMapping(input: DeleteRiskMappingInput!): DeleteRiskMappingPayload! deleteRiskMapping(input: DeleteRiskMappingInput!): DeleteRiskMappingPayload!
# Evidence mutations # Evidence mutations
uploadEvidence(input: UploadEvidenceInput!): UploadEvidencePayload! requestEvidence(input: RequestEvidenceInput!): RequestEvidencePayload!
fulfillEvidence(input: FulfillEvidenceInput!): FulfillEvidencePayload!
createEvidence(input: CreateEvidenceInput!): CreateEvidencePayload!
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload! deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
# Policy mutations # Policy mutations
@@ -939,12 +939,25 @@ input DeleteRiskMappingInput {
mitigationId: ID! mitigationId: ID!
} }
input UploadEvidenceInput { input RequestEvidenceInput {
taskId: ID! taskId: ID!
name: String! name: String!
type: EvidenceType!
description: String!
}
input FulfillEvidenceInput {
evidenceId: ID!
file: Upload file: Upload
url: String
}
input CreateEvidenceInput {
taskId: ID!
name: String!
type: EvidenceType! type: EvidenceType!
url: String url: String
file: Upload
description: String! description: String!
} }
@@ -1102,7 +1115,15 @@ type DeleteRiskMappingPayload {
success: Boolean! success: Boolean!
} }
type UploadEvidencePayload { type RequestEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type FulfillEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type CreateEvidencePayload {
evidenceEdge: EvidenceEdge! evidenceEdge: EvidenceEdge!
} }

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,19 @@ type CreateControlMappingPayload struct {
Success bool `json:"success"` Success bool `json:"success"`
} }
type CreateEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
Type coredata.EvidenceType `json:"type"`
URL *string `json:"url,omitempty"`
File *graphql.Upload `json:"file,omitempty"`
Description string `json:"description"`
}
type CreateEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type CreateFrameworkInput struct { type CreateFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"` OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"` Name string `json:"name"`
@@ -306,6 +319,16 @@ type FrameworkEdge struct {
Node *Framework `json:"node"` Node *Framework `json:"node"`
} }
type FulfillEvidenceInput struct {
EvidenceID gid.GID `json:"evidenceId"`
File *graphql.Upload `json:"file,omitempty"`
URL *string `json:"url,omitempty"`
}
type FulfillEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type ImportFrameworkInput struct { type ImportFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"` OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"` File graphql.Upload `json:"file"`
@@ -463,6 +486,17 @@ type RemoveUserPayload struct {
Success bool `json:"success"` Success bool `json:"success"`
} }
type RequestEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
Type coredata.EvidenceType `json:"type"`
Description string `json:"description"`
}
type RequestEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type Risk struct { type Risk struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -624,19 +658,6 @@ type UpdateVendorPayload struct {
Vendor *Vendor `json:"vendor"` Vendor *Vendor `json:"vendor"`
} }
type UploadEvidenceInput struct {
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 {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type User struct { type User struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`

View File

@@ -718,20 +718,62 @@ func (r *mutationResolver) DeleteRiskMapping(ctx context.Context, input types.De
}, nil }, nil
} }
// UploadEvidence is the resolver for the uploadEvidence field. // RequestEvidence is the resolver for the requestEvidence field.
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) { func (r *mutationResolver) RequestEvidence(ctx context.Context, input types.RequestEvidenceInput) (*types.RequestEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID()) svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
var url string evidence, err := svc.Evidences.Request(
if input.URL != nil { ctx,
url = *input.URL probo.RequestEvidenceRequest{
TaskID: input.TaskID,
Name: input.Name,
Type: input.Type,
Description: input.Description,
},
)
if err != nil {
panic(fmt.Errorf("cannot request evidence: %w", err))
} }
return &types.RequestEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// FulfillEvidence is the resolver for the fulfillEvidence field.
func (r *mutationResolver) FulfillEvidence(ctx context.Context, input types.FulfillEvidenceInput) (*types.FulfillEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.EvidenceID.TenantID())
req := probo.FulfilledEvidenceRequest{
EvidenceID: input.EvidenceID,
}
if input.File != nil {
req.File = input.File.File
}
if input.URL != nil {
req.URL = *input.URL
}
evidence, err := svc.Evidences.Fulfill(ctx, req)
if err != nil {
panic(fmt.Errorf("cannot fulfill evidence: %w", err))
}
return &types.FulfillEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// CreateEvidence is the resolver for the createEvidence field.
func (r *mutationResolver) CreateEvidence(ctx context.Context, input types.CreateEvidenceInput) (*types.CreateEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
req := probo.CreateEvidenceRequest{ req := probo.CreateEvidenceRequest{
TaskID: input.TaskID, TaskID: input.TaskID,
Name: input.Name, Name: input.Name,
Type: input.Type, Type: input.Type,
URL: url,
Description: input.Description, Description: input.Description,
} }
@@ -751,7 +793,7 @@ func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.Uploa
panic(fmt.Errorf("failed to create evidence: %w", err)) panic(fmt.Errorf("failed to create evidence: %w", err))
} }
return &types.UploadEvidencePayload{ return &types.CreateEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt), EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil }, nil
} }