127 lines
4.9 KiB
TypeScript
127 lines
4.9 KiB
TypeScript
|
|
import { Fragment, useState } from 'react';
|
||
|
|
import { ChevronRight, ChevronDown } from 'lucide-react';
|
||
|
|
import { useC2Tasks } from '@/hooks/useC2';
|
||
|
|
import { C2TaskStatusBadge } from './C2TaskStatusBadge';
|
||
|
|
|
||
|
|
interface C2TasksPanelProps {
|
||
|
|
simulationId: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element {
|
||
|
|
const query = useC2Tasks(simulationId, { enabled: true });
|
||
|
|
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
||
|
|
|
||
|
|
const tasks = query.data?.tasks ?? [];
|
||
|
|
const isRefreshing = query.isFetching && !query.isLoading;
|
||
|
|
|
||
|
|
function toggleExpand(id: number, completed: boolean) {
|
||
|
|
if (!completed) return;
|
||
|
|
setExpandedIds((prev) => {
|
||
|
|
const next = new Set(prev);
|
||
|
|
if (next.has(id)) {
|
||
|
|
next.delete(id);
|
||
|
|
} else {
|
||
|
|
next.add(id);
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
data-testid="c2-tasks-panel"
|
||
|
|
className="card-product flex flex-col gap-md"
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<h3 className="text-[16px] font-medium text-ink">C2 Tasks</h3>
|
||
|
|
{isRefreshing && (
|
||
|
|
<span
|
||
|
|
data-testid="c2-task-refresh-indicator"
|
||
|
|
className="text-[12px] text-graphite"
|
||
|
|
>
|
||
|
|
Refreshing…
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{tasks.length === 0 ? (
|
||
|
|
<div className="border border-hairline rounded-none px-md py-md">
|
||
|
|
<p className="text-[14px] text-graphite">
|
||
|
|
No C2 tasks yet. Use Execute via C2 to launch commands.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="border border-hairline overflow-x-auto">
|
||
|
|
<table className="w-full text-[14px]">
|
||
|
|
<thead>
|
||
|
|
<tr className="bg-cloud border-b border-hairline">
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink w-8" aria-label="Expand" />
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink">Task</th>
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink">Command</th>
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink">Source</th>
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink">Status</th>
|
||
|
|
<th className="px-md py-sm text-left font-medium text-ink">Completed at</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{tasks.map((task) => {
|
||
|
|
const isExpanded = expandedIds.has(task.id);
|
||
|
|
const canExpand = task.completed && Boolean(task.output);
|
||
|
|
return (
|
||
|
|
<Fragment key={task.id}>
|
||
|
|
<tr
|
||
|
|
data-testid="c2-task-row"
|
||
|
|
onClick={() => toggleExpand(task.id, task.completed)}
|
||
|
|
className={`border-b border-hairline ${canExpand ? 'cursor-pointer hover:bg-cloud' : ''}`}
|
||
|
|
>
|
||
|
|
<td className="px-md py-sm text-graphite">
|
||
|
|
{canExpand ? (
|
||
|
|
isExpanded ? (
|
||
|
|
<ChevronDown size={14} aria-hidden />
|
||
|
|
) : (
|
||
|
|
<ChevronRight size={14} aria-hidden />
|
||
|
|
)
|
||
|
|
) : null}
|
||
|
|
</td>
|
||
|
|
<td className="px-md py-sm font-mono">#{task.mythic_task_display_id}</td>
|
||
|
|
<td
|
||
|
|
className="px-md py-sm font-mono max-w-[200px] truncate"
|
||
|
|
title={task.command}
|
||
|
|
>
|
||
|
|
{task.command}
|
||
|
|
</td>
|
||
|
|
<td className="px-md py-sm">
|
||
|
|
<span className="inline-flex items-center rounded-pill px-3 py-[4px] text-[12px] leading-[1.3] font-bold bg-cloud text-graphite border border-hairline">
|
||
|
|
{task.mapping_applied ? 'MIMIC' : 'IMPORT'}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td className="px-md py-sm">
|
||
|
|
<C2TaskStatusBadge status={task.status} />
|
||
|
|
</td>
|
||
|
|
<td className="px-md py-sm font-mono text-graphite">
|
||
|
|
{task.completed_at ?? '—'}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
{isExpanded && task.output && (
|
||
|
|
<tr className="border-b border-hairline bg-cloud">
|
||
|
|
<td colSpan={6} className="px-md py-sm">
|
||
|
|
<pre
|
||
|
|
data-testid="c2-task-output"
|
||
|
|
className="font-mono text-[12px] whitespace-pre-wrap text-ink"
|
||
|
|
>
|
||
|
|
{task.output}
|
||
|
|
</pre>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
)}
|
||
|
|
</Fragment>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|