Improve control view for mitigation

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-01 09:41:47 +02:00
parent ca7bf09437
commit dbc39a53c5

View File

@@ -36,6 +36,8 @@ import {
Link2, Link2,
Search, Search,
Link as LinkIcon, Link as LinkIcon,
CheckSquare,
ShieldCheck,
} from "lucide-react"; } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -445,6 +447,9 @@ function MitigationViewContent({
const navigate = useNavigate(); const navigate = useNavigate();
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
// Add state for main content tabs
const [mainContentTab, setMainContentTab] = useState<string>("tasks");
// Add URLSearchParams handling for task persistence // Add URLSearchParams handling for task persistence
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const taskIdFromUrl = searchParams.get("taskId"); const taskIdFromUrl = searchParams.get("taskId");
@@ -883,7 +888,6 @@ function MitigationViewContent({
setLinkEvidenceName(""); setLinkEvidenceName("");
setLinkEvidenceUrl(""); setLinkEvidenceUrl("");
setLinkEvidenceDescription(""); setLinkEvidenceDescription("");
setActiveTab("file");
}; };
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1662,7 +1666,36 @@ function MitigationViewContent({
</Card> </Card>
</div> </div>
{/* Control Mapping Section */} {/* Main Content Tabs */}
<Tabs
defaultValue="tasks"
value={mainContentTab}
onValueChange={setMainContentTab}
>
<TabsList className="mb-4">
<TabsTrigger value="tasks" className="flex items-center gap-2">
<CheckSquare className="w-4 h-4" />
Tasks
{tasks.length > 0 && (
<span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5">
{tasks.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="controls" className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4" />
Controls
{linkedControlsData?.mitigation?.controls?.edges &&
linkedControlsData.mitigation.controls.edges.length > 0 && (
<span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5">
{linkedControlsData.mitigation.controls.edges.length}
</span>
)}
</TabsTrigger>
</TabsList>
{/* Controls Tab Content */}
<TabsContent value="controls">
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">Controls</h2> <h2 className="text-xl font-semibold">Controls</h2>
@@ -1738,8 +1771,8 @@ function MitigationViewContent({
<div className="grid grid-cols-1 gap-2 max-h-[50vh] overflow-y-auto pr-2"> <div className="grid grid-cols-1 gap-2 max-h-[50vh] overflow-y-auto pr-2">
{filteredControls().length === 0 ? ( {filteredControls().length === 0 ? (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No controls found. Try adjusting your search or select a No controls found. Try adjusting your search or select
different framework. a different framework.
</div> </div>
) : ( ) : (
filteredControls().map((control) => { filteredControls().map((control) => {
@@ -1750,7 +1783,9 @@ function MitigationViewContent({
className="border overflow-hidden" className="border overflow-hidden"
> >
<div <div
className={`p-4 ${isLinked ? "bg-blue-50" : ""}`} className={`p-4 ${
isLinked ? "bg-blue-50" : ""
}`}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
@@ -1767,7 +1802,9 @@ function MitigationViewContent({
</Badge> </Badge>
)} )}
</div> </div>
<h3 className="font-medium">{control.name}</h3> <h3 className="font-medium">
{control.name}
</h3>
{control.description && ( {control.description && (
<p className="text-sm text-gray-500 mt-1 line-clamp-2"> <p className="text-sm text-gray-500 mt-1 line-clamp-2">
{control.description} {control.description}
@@ -1834,22 +1871,36 @@ function MitigationViewContent({
<CardContent className="p-4"> <CardContent className="p-4">
{linkedControlsData?.mitigation?.controls?.edges && {linkedControlsData?.mitigation?.controls?.edges &&
linkedControlsData.mitigation.controls.edges.length > 0 ? ( linkedControlsData.mitigation.controls.edges.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left font-medium text-sm py-2 px-4">
ID
</th>
<th className="text-left font-medium text-sm py-2 px-4">
Control Name
</th>
<th className="text-right font-medium text-sm py-2 px-4">
Actions
</th>
</tr>
</thead>
<tbody>
{getLinkedControls().map((control: ControlNode) => ( {getLinkedControls().map((control: ControlNode) => (
<Card key={control.id} className="border overflow-hidden"> <tr
<div className="p-3"> key={control.id}
<div className="flex items-center gap-2 mb-1"> className="border-b hover:bg-gray-50"
<div className="font-mono text-sm px-1 py-0.5 rounded-sm bg-lime-100 border border-lime-200 text-lime-800 font-bold"> >
<td className="py-3 px-4">
<div className="font-mono text-sm px-1.5 py-0.5 rounded-sm bg-lime-100 border border-lime-200 text-lime-800 font-bold inline-block">
{control.referenceId} {control.referenceId}
</div> </div>
</div> </td>
<h3 className="font-medium text-sm">{control.name}</h3> <td className="py-3 px-4 font-medium text-sm">
{control.description && ( {control.name}
<p className="text-xs text-gray-500 mt-1 line-clamp-2"> </td>
{control.description} <td className="py-3 px-4 text-right">
</p>
)}
<div className="flex justify-end mt-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -1859,21 +1910,25 @@ function MitigationViewContent({
> >
Unlink Unlink
</Button> </Button>
</div> </td>
</div> </tr>
</Card>
))} ))}
</tbody>
</table>
</div> </div>
) : ( ) : (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No controls linked to this mitigation yet. Click &quot;Map to No controls linked to this mitigation yet. Click &quot;Map
Controls&quot; to link controls. to Controls&quot; to link controls.
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</TabsContent>
{/* Tasks Tab Content */}
<TabsContent value="tasks">
<div> <div>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold">Tasks</h2> <h2 className="text-xl font-semibold">Tasks</h2>
@@ -1882,7 +1937,10 @@ function MitigationViewContent({
<FileIcon className="w-4 h-4 mr-2 text-blue-500" /> <FileIcon className="w-4 h-4 mr-2 text-blue-500" />
<span>Drag & drop files onto tasks to add evidence</span> <span>Drag & drop files onto tasks to add evidence</span>
</div> </div>
<Dialog open={isCreateTaskOpen} onOpenChange={setIsCreateTaskOpen}> <Dialog
open={isCreateTaskOpen}
onOpenChange={setIsCreateTaskOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="flex items-center gap-1"> <Button size="sm" className="flex items-center gap-1">
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@@ -1899,80 +1957,57 @@ function MitigationViewContent({
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium"> <Label htmlFor="task-name">Task Name</Label>
Task Name
</label>
<Input <Input
id="name" id="task-name"
value={newTaskName} value={newTaskName}
onChange={(e) => setNewTaskName(e.target.value)} onChange={(e) => setNewTaskName(e.target.value)}
placeholder="Enter task name" placeholder="What needs to be done?"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label <Label htmlFor="task-description">
htmlFor="description"
className="text-sm font-medium"
>
Description (optional) Description (optional)
</label> </Label>
<Textarea <Textarea
id="description" id="task-description"
value={newTaskDescription} value={newTaskDescription}
onChange={(e) => setNewTaskDescription(e.target.value)} onChange={(e) =>
placeholder="Enter task description" setNewTaskDescription(e.target.value)
}
placeholder="Add more details about the task"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label <Label>Time Estimate (optional)</Label>
htmlFor="timeEstimate" <div className="flex items-center gap-2">
className="text-sm font-medium" <div className="w-full">
>
Time Estimate (optional)
</label>
<div className="grid grid-cols-3 gap-4">
<div>
<label
htmlFor="days"
className="text-xs text-gray-500 block mb-1"
>
Days
</label>
<Input <Input
id="days"
type="number" type="number"
min="0" min="0"
value={timeEstimateDays} value={timeEstimateDays}
onChange={(e) => setTimeEstimateDays(e.target.value)} onChange={(e) =>
placeholder="0" setTimeEstimateDays(e.target.value)
}
placeholder="Days"
/> />
<p className="text-xs text-gray-500 mt-1">Days</p>
</div> </div>
<div> <div className="w-full">
<label
htmlFor="hours"
className="text-xs text-gray-500 block mb-1"
>
Hours
</label>
<Input <Input
id="hours"
type="number" type="number"
min="0" min="0"
max="23" max="23"
value={timeEstimateHours} value={timeEstimateHours}
onChange={(e) => setTimeEstimateHours(e.target.value)} onChange={(e) =>
placeholder="0" setTimeEstimateHours(e.target.value)
}
placeholder="Hours"
/> />
<p className="text-xs text-gray-500 mt-1">Hours</p>
</div> </div>
<div> <div className="w-full">
<label
htmlFor="minutes"
className="text-xs text-gray-500 block mb-1"
>
Minutes
</label>
<Input <Input
id="minutes"
type="number" type="number"
min="0" min="0"
max="59" max="59"
@@ -1980,8 +2015,11 @@ function MitigationViewContent({
onChange={(e) => onChange={(e) =>
setTimeEstimateMinutes(e.target.value) setTimeEstimateMinutes(e.target.value)
} }
placeholder="0" placeholder="Minutes"
/> />
<p className="text-xs text-gray-500 mt-1">
Minutes
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -2094,7 +2132,9 @@ function MitigationViewContent({
: "text-blue-500" : "text-blue-500"
}`} }`}
> >
<span className="inline-block w-4 h-4 mr-1">⏱️</span> <span className="inline-block w-4 h-4 mr-1">
⏱️
</span>
<span>{formatDuration(task.timeEstimate)}</span> <span>{formatDuration(task.timeEstimate)}</span>
</p> </p>
)} )}
@@ -2301,15 +2341,19 @@ function MitigationViewContent({
</div> </div>
{/* Evidence section */} {/* Evidence section */}
{task?.evidences?.edges && task.evidences.edges.length > 0 && ( {task?.evidences?.edges &&
task.evidences.edges.length > 0 && (
<> <>
<div <div
className="bg-gray-50 border-t border-gray-200 px-4 py-2.5 flex justify-between items-center cursor-pointer hover:bg-gray-100 transition-colors" className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 cursor-pointer hover:bg-gray-100"
onClick={() => task.id && toggleEvidenceList(task.id)} onClick={(e) => {
e.stopPropagation(); // Prevent task selection
if (task.id) toggleEvidenceList(task.id);
}}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileIcon className="w-4 h-4 text-blue-500" /> <FileText className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-gray-700"> <span className="text-xs text-gray-600">
{task.evidences.edges.length}{" "} {task.evidences.edges.length}{" "}
{task.evidences.edges.length === 1 {task.evidences.edges.length === 1
? "Evidence" ? "Evidence"
@@ -2337,7 +2381,10 @@ function MitigationViewContent({
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="bg-white p-2 rounded-md border border-gray-200"> <div className="bg-white p-2 rounded-md border border-gray-200">
{getFileIcon(evidence.mimeType, 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">
@@ -2359,18 +2406,23 @@ function MitigationViewContent({
<span>•</span> <span>•</span>
</> </>
) : null} ) : 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.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"
> >
@@ -2379,7 +2431,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"
@@ -2392,7 +2444,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");
} }
@@ -2405,8 +2457,8 @@ function MitigationViewContent({
) : null} ) : null}
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.stopPropagation();
if (task?.id) { if (task.id) {
handleDeleteEvidence( handleDeleteEvidence(
evidence.id, evidence.id,
evidence.filename, evidence.filename,
@@ -2431,12 +2483,14 @@ function MitigationViewContent({
))} ))}
{tasks.length === 0 && ( {tasks.length === 0 && (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-12 bg-gray-50 rounded-lg border border-gray-200">
<p>No tasks yet. Click &quot;Add Task&quot; to create one.</p> <p>No tasks yet. Click &quot;Add Task&quot; to create one.</p>
</div> </div>
)} )}
</div> </div>
</div> </div>
</TabsContent>
</Tabs>
{/* Right task panel */} {/* Right task panel */}
<Sheet open={isTaskPanelOpen} onOpenChange={handleCloseTaskPanel}> <Sheet open={isTaskPanelOpen} onOpenChange={handleCloseTaskPanel}>