fix(ui): ограничить трассировку и защитить повторную симуляцию
This commit is contained in:
1 parent
9231597d88
commit
21dc646553
7 files changed
+96
-18
No files matched your search
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user