fix(config): добавить новые зависимости и обновить конфигурацию компонентов
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
|
||||
import { firewallRules, type FirewallRule } from "@/lib/data"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -341,22 +342,6 @@ function fmtHits(n: number): string {
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<button type="button" role="switch" aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
)}>
|
||||
<span className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0",
|
||||
)} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionBadge({ action }: { action: string }) {
|
||||
const cls = ACTION_STYLES[action] ?? "bg-muted text-muted-foreground border-border"
|
||||
return (
|
||||
@@ -375,29 +360,6 @@ function ChainBadge({ chain }: { chain: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, hint, required, children }: {
|
||||
label: string; hint?: string; required?: boolean; children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">
|
||||
{label}{required && <span className="text-destructive ml-0.5">*</span>}
|
||||
</label>
|
||||
{children}
|
||||
{hint && <p className="text-xs text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{children}</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NativeSelect({ value, onChange, children, className }: {
|
||||
value: string; onChange: (v: string) => void; children: React.ReactNode; className?: string
|
||||
}) {
|
||||
@@ -507,67 +469,67 @@ function RuleSheet({ open, onClose, initialRule, chainGroup }: {
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionTitle>Цепочка и действие</SectionTitle>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Цепочка" required>
|
||||
<FormField label="Цепочка" required>
|
||||
<NativeSelect value={form.chain} onChange={(v) => set("chain", v)}>
|
||||
{chainsForGroup.map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field label="Действие" required>
|
||||
</FormField>
|
||||
<FormField label="Действие" required>
|
||||
<NativeSelect value={form.action} onChange={(v) => set("action", v)}>
|
||||
{actions.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Matching */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionTitle>Условие совпадения</SectionTitle>
|
||||
<Field label="Протокол">
|
||||
<FormField label="Протокол">
|
||||
<NativeSelect value={form.proto} onChange={(v) => set("proto", v)}>
|
||||
{["all","tcp","udp","icmp","gre","esp","ah","ipencap","ospf"].map((p) =>
|
||||
<option key={p} value={p}>{p}</option>
|
||||
)}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Src-address / Address-list" hint="IP, CIDR или имя address-list">
|
||||
<FormField label="Src-address / Address-list" hint="IP, CIDR или имя address-list">
|
||||
<Input className="font-mono h-8" placeholder="10.0.0.0/8"
|
||||
value={form.srcAddrList} onChange={(e) => set("srcAddrList", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Dst-address / Address-list">
|
||||
</FormField>
|
||||
<FormField label="Dst-address / Address-list">
|
||||
<Input className="font-mono h-8" placeholder="0.0.0.0/0"
|
||||
value={form.dstAddrList} onChange={(e) => set("dstAddrList", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Src-port" hint="TCP/UDP, напр. 1024-65535">
|
||||
<FormField label="Src-port" hint="TCP/UDP, напр. 1024-65535">
|
||||
<Input className="font-mono h-8" placeholder="—"
|
||||
value={form.srcPort} onChange={(e) => set("srcPort", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Dst-port">
|
||||
</FormField>
|
||||
<FormField label="Dst-port">
|
||||
<Input className="font-mono h-8" placeholder="443"
|
||||
value={form.dstPort} onChange={(e) => set("dstPort", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="In-interface" hint="Входящий интерфейс">
|
||||
<FormField label="In-interface" hint="Входящий интерфейс">
|
||||
<Input className="font-mono h-8" placeholder="wan-msk"
|
||||
value={form.inIface} onChange={(e) => set("inIface", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Out-interface">
|
||||
</FormField>
|
||||
<FormField label="Out-interface">
|
||||
<Input className="font-mono h-8" placeholder="lan"
|
||||
value={form.outIface} onChange={(e) => set("outIface", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<Field label="Connection-state" hint="Через запятую: new, established, related, invalid">
|
||||
<FormField label="Connection-state" hint="Через запятую: new, established, related, invalid">
|
||||
<Input className="font-mono h-8" placeholder="new,established"
|
||||
value={form.connState} onChange={(e) => set("connState", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{/* Log + Comment */}
|
||||
@@ -578,18 +540,18 @@ function RuleSheet({ open, onClose, initialRule, chainGroup }: {
|
||||
<p className="text-sm font-medium">Log</p>
|
||||
<p className="text-xs text-muted-foreground">Записывать совпадения в системный лог</p>
|
||||
</div>
|
||||
<Toggle checked={form.log} onChange={(v) => set("log", v)} />
|
||||
<FormToggle checked={form.log} onChange={(v) => set("log", v)} />
|
||||
</div>
|
||||
{form.log && (
|
||||
<Field label="Log-prefix" hint="Метка в логе, например FW-DROP">
|
||||
<FormField label="Log-prefix" hint="Метка в логе, например FW-DROP">
|
||||
<Input className="font-mono h-8" placeholder="FW-RULE"
|
||||
value={form.logPrefix} onChange={(e) => set("logPrefix", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
)}
|
||||
<Field label="Комментарий">
|
||||
<FormField label="Комментарий">
|
||||
<Input className="h-8" placeholder="Описание правила"
|
||||
value={form.comment} onChange={(e) => set("comment", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{/* Enabled */}
|
||||
@@ -598,7 +560,7 @@ function RuleSheet({ open, onClose, initialRule, chainGroup }: {
|
||||
<p className="text-sm font-medium">Правило включено</p>
|
||||
<p className="text-xs text-muted-foreground">Отключённые правила сохраняются, но не применяются</p>
|
||||
</div>
|
||||
<Toggle checked={form.enabled} onChange={(v) => set("enabled", v)} />
|
||||
<FormToggle checked={form.enabled} onChange={(v) => set("enabled", v)} />
|
||||
</div>
|
||||
|
||||
{/* CLI preview */}
|
||||
@@ -1039,14 +1001,14 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
<div className="flex flex-col gap-3">
|
||||
<SectionTitle>Название</SectionTitle>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Название сценария" required>
|
||||
<FormField label="Название сценария" required>
|
||||
<Input className="h-8" placeholder="Блокировка Tor Exit"
|
||||
value={name} onChange={e => setName(e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Описание">
|
||||
</FormField>
|
||||
<FormField label="Описание">
|
||||
<Input className="h-8" placeholder="Краткое описание"
|
||||
value={desc} onChange={e => setDesc(e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1054,7 +1016,7 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
<div className="flex flex-col gap-3">
|
||||
<SectionTitle>Тестовый пакет по умолчанию</SectionTitle>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
<Field label="Направление / цепочка">
|
||||
<FormField label="Направление / цепочка">
|
||||
<NativeSelect value={pkt.chain} onChange={v => setP("chain", v)}>
|
||||
<optgroup label="Полный маршрут">
|
||||
<option value="forward">forward — транзит</option>
|
||||
@@ -1067,40 +1029,40 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</optgroup>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field label="Протокол">
|
||||
</FormField>
|
||||
<FormField label="Протокол">
|
||||
<NativeSelect value={pkt.proto} onChange={v => setP("proto", v)}>
|
||||
{PROTO_OPTS.map(p => <option key={p} value={p}>{p}</option>)}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field label="Conn-state">
|
||||
</FormField>
|
||||
<FormField label="Conn-state">
|
||||
<Input className="font-mono h-8" value={pkt.connState}
|
||||
placeholder="new" onChange={e => setP("connState", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Src IP">
|
||||
</FormField>
|
||||
<FormField label="Src IP">
|
||||
<Input className="font-mono h-8" value={pkt.srcAddr}
|
||||
onChange={e => setP("srcAddr", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Dst IP">
|
||||
</FormField>
|
||||
<FormField label="Dst IP">
|
||||
<Input className="font-mono h-8" value={pkt.dstAddr}
|
||||
onChange={e => setP("dstAddr", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Dst Port">
|
||||
</FormField>
|
||||
<FormField label="Dst Port">
|
||||
<Input className="font-mono h-8" value={pkt.dstPort}
|
||||
placeholder="443" onChange={e => setP("dstPort", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="In-interface">
|
||||
</FormField>
|
||||
<FormField label="In-interface">
|
||||
<Input className="font-mono h-8" value={pkt.inIface}
|
||||
placeholder="lan" onChange={e => setP("inIface", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Out-interface">
|
||||
</FormField>
|
||||
<FormField label="Out-interface">
|
||||
<Input className="font-mono h-8" value={pkt.outIface}
|
||||
placeholder="wan-msk" onChange={e => setP("outIface", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Dst addr-list">
|
||||
</FormField>
|
||||
<FormField label="Dst addr-list">
|
||||
<Input className="font-mono h-8" value={pkt.dstAddrList}
|
||||
placeholder="" onChange={e => setP("dstAddrList", e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1170,7 +1132,7 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle checked={addForm.enabled} onChange={v => setAF("enabled", v)} />
|
||||
<FormToggle checked={addForm.enabled} onChange={v => setAF("enabled", v)} />
|
||||
<span className="text-xs text-muted-foreground">Включено</span>
|
||||
</div>
|
||||
<Button size="sm" onClick={addRule}><PlusIcon className="size-4" />Добавить</Button>
|
||||
@@ -1783,7 +1745,7 @@ function RulesTable({ rules, onToggle, onEdit }: {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Toggle checked={r.enabled} onChange={() => onToggle(r.id)} />
|
||||
<FormToggle checked={r.enabled} onChange={() => onToggle(r.id)} />
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user