Improve control view for mitigation
Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -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 "Map to
|
No controls linked to this mitigation yet. Click "Map
|
||||||
Controls" to link controls.
|
to Controls" 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 "Add Task" to create one.</p>
|
<p>No tasks yet. Click "Add Task" 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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user