fix(ui): ограничить трассировку и защитить повторную симуляцию

This commit is contained in:
Denozordec committed 2026-10-04 16:45:39 +07:00
1 parent 9231597d88
commit 21dc646553
7 files changed
+96 -18

No files matched your search

+46 -17
View File
@@ -988,7 +988,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
type="button"
onClick={() => isClickable && setExpanded(e => !e)}
className={cn(
"w-full flex items-center gap-3 px-5 py-2.5 text-left transition-colors",
"w-full min-h-11 flex flex-wrap sm:flex-nowrap items-center gap-3 px-5 py-2.5 text-left transition-colors",
isClickable ? "hover:bg-muted/30 cursor-pointer" : "cursor-default",
step.status === "pending" && "opacity-40",
step.status === "disabled" && "opacity-25",
@@ -999,7 +999,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
</span>
<div className="shrink-0"><ChainBadge chain={step.rule.chain} /></div>
<div className="shrink-0"><ActionBadge action={step.rule.action} /></div>
<span className="text-xs font-mono text-muted-foreground flex-1 truncate min-w-0">
<span className="text-xs font-mono text-muted-foreground flex-1 basis-full sm:basis-auto truncate min-w-0">
{step.rule.src || "any"} → {step.rule.dst || "any"}
{step.rule.proto && step.rule.proto !== "all" && (
<span className="opacity-50"> {step.rule.proto}</span>
@@ -1015,7 +1015,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
)}
{/* Status badge */}
<div className="shrink-0 min-w-[130px] flex justify-end">
<div className="shrink-0 min-w-0 sm:min-w-[130px] flex justify-end">
{step.status === "pending" && (
<span className="text-[10px] text-muted-foreground/40">ожидание</span>
)}
@@ -1030,7 +1030,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
</span>
)}
{step.status === "passthrough" && (
<span className="text-[10px] text-violet-500 font-mono font-semibold flex items-center gap-1">
<span className="text-[10px] text-violet-600 dark:text-violet-400 font-mono font-semibold flex items-center gap-1">
<SkipForwardIcon className="size-3" />passthrough
</span>
)}
@@ -1355,9 +1355,12 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
const [activePreset, setPreset] = useState<string | null>("https")
const [simState, setSimState] = useState<"idle" | "running" | "done">("idle")
const [steps, setSteps] = useState<SimStep[]>([])
const [tracePage, setTracePage] = useState(0)
const [speed, setSpeed] = useState<SimSpeed>("normal")
const [verdict, setVerdict] = useState<{ action: string; ruleIndex: number | null } | null>(null)
const cancelRef = useRef(false)
const runEpoch = useRef(0)
useEffect(() => () => { runEpoch.current += 1; cancelRef.current = true }, [])
// ── Scenarios state ─────────────────────────────────────────────────────────
const [scenarios, setScenarios] = useState<SimScenario[]>(() => {
@@ -1392,9 +1395,11 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
// ── Handlers ────────────────────────────────────────────────────────────────
function resetSim() {
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
}
@@ -1402,18 +1407,22 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
setActiveScenarioId(null)
setPreset(p.id)
setPacket({ ...DEFAULT_PKT, ...p.pkt })
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
}
function activateScenario(id: string | null) {
setActiveScenarioId(id)
setPreset(null)
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
if (id) {
const sc = scenarios.find(s => s.id === id)
@@ -1444,7 +1453,9 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
return
}
const epoch = ++runEpoch.current
cancelRef.current = false
setTracePage(0)
setSimState("running")
setVerdict(null)
setSteps(rules.map((r, i) => ({ rule: r, index: i, status: "pending", checks: [] })))
@@ -1453,10 +1464,10 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
let finalVerdict: { action: string; ruleIndex: number | null } | null = null
for (let i = 0; i < rules.length; i++) {
if (cancelRef.current) return
if (cancelRef.current || epoch !== runEpoch.current) return
setSteps(prev => prev.map((s, idx) => idx === i ? { ...s, status: "evaluating" } : s))
if (delay > 0) await new Promise<void>(res => setTimeout(res, delay))
if (cancelRef.current) return
if (cancelRef.current || epoch !== runEpoch.current) return
const result = evalRule(rules[i], packet)
setSteps(prev => prev.map((s, idx) =>
@@ -1471,12 +1482,21 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
if (delay > 0) await new Promise<void>(res => setTimeout(res, Math.max(delay * 0.25, 40)))
}
if (!cancelRef.current) {
if (!cancelRef.current && epoch === runEpoch.current) {
setVerdict(finalVerdict ?? { action: "accept (default)", ruleIndex: null })
setSimState("done")
}
}
const tracePageCount = Math.max(1, Math.ceil(steps.length / 50))
const currentTracePage = Math.min(tracePage, tracePageCount - 1)
const traceOffset = currentTracePage * 50
const traceChainCounts = useMemo(() => {
const counts = new Map<string, number>()
for (const step of steps) counts.set(step.rule.chain, (counts.get(step.rule.chain) ?? 0) + 1)
return counts
}, [steps])
const verdictColors = verdict ? getVerdictColors(verdict.action) : null
return (
@@ -1690,7 +1710,7 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
</Button>
{simState === "running" ? (
<Button size="sm" variant="destructive"
onClick={() => { cancelRef.current = true; setSimState("done") }}>
onClick={() => { runEpoch.current += 1; cancelRef.current = true; setSimState("done") }}>
<SquareIcon className="size-4" />Стоп
</Button>
) : (
@@ -1730,8 +1750,8 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
{/* ── Trace list ──────────────────────────────────────────────────────── */}
{steps.length > 0 && (
<Frame dense className="w-full">
<FramePanel className="p-0">
<div className="px-5 py-3 border-b flex items-center justify-between">
<FramePanel className="p-0" role="region" aria-label="Трассировка Firewall">
<div className="px-5 py-3 border-b flex items-center justify-between flex-wrap gap-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Трассировка · цепочка: <span className="text-foreground normal-case font-mono">{packet.chain}</span>
{activeScenario && (
@@ -1745,23 +1765,24 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
</span>
</div>
<div className="divide-y divide-border/60">
{steps.map((step, i) => {
{steps.slice(traceOffset, traceOffset + 50).map((step, pageIndex) => {
const i = traceOffset + pageIndex
const prevChain = i > 0 ? steps[i - 1].rule.chain : null
const showHeader = step.rule.chain !== prevChain
const showHeader = pageIndex === 0 || step.rule.chain !== prevChain
return (
<div key={step.rule.id}>
<div key={step.rule.id} data-firewall-trace-step={i + 1}>
{showHeader && (
<div className="px-5 py-1.5 bg-muted/40 border-b border-border/50 flex items-center gap-2">
<span className="text-[10px] font-mono font-semibold text-foreground/70 uppercase tracking-wider">
<span className="text-[10px] font-mono font-semibold text-foreground uppercase tracking-wider">
{step.rule.chain}
</span>
{CHAIN_TABLE_HINT[step.rule.chain] && (
<span className="text-[10px] text-muted-foreground/50">
<span className="text-[10px] text-muted-foreground">
· {CHAIN_TABLE_HINT[step.rule.chain]}
</span>
)}
<span className="text-[10px] text-muted-foreground/40 ml-auto">
{steps.filter(s => s.rule.chain === step.rule.chain).length} правил
<span className="text-[10px] text-muted-foreground ml-auto">
{traceChainCounts.get(step.rule.chain)} правил
</span>
</div>
)}
@@ -1770,6 +1791,14 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
)
})}
</div>
<div className="flex items-center justify-between flex-wrap gap-2 border-t px-5 py-3">
<span role="status" className="text-xs text-muted-foreground">Шаги {traceOffset + 1}–{Math.min(traceOffset + 50, steps.length)} из {steps.length}</span>
<div className="flex items-center flex-wrap gap-2">
<Button type="button" variant="outline" className="min-h-11" aria-label="Предыдущая страница трассировки" disabled={currentTracePage === 0} onClick={() => setTracePage(currentTracePage - 1)}>Назад</Button>
<Button type="button" variant="outline" className="min-h-11" aria-label="Следующая страница трассировки" disabled={currentTracePage >= tracePageCount - 1} onClick={() => setTracePage(currentTracePage + 1)}>Далее</Button>
{verdict?.ruleIndex != null && <Button type="button" variant="outline" className="min-h-11" onClick={() => setTracePage(Math.floor(verdict.ruleIndex! / 50))}>К правилу результата</Button>}
</div>
</div>
</FramePanel>
</Frame>
)}