fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
This commit is contained in:
+18
-167
@@ -2,6 +2,13 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import {
|
||||
FirewallRulesDataGrid,
|
||||
ActionBadge,
|
||||
ChainBadge,
|
||||
} from "@/components/data-grids/firewall-rules-data-grid"
|
||||
import { FirewallScenarioRulesDataGrid } from "@/components/data-grids/firewall-scenario-rules-data-grid"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
|
||||
import { firewallRules, type FirewallRule } from "@/lib/data"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
@@ -342,23 +349,7 @@ function fmtHits(n: number): string {
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function ActionBadge({ action }: { action: string }) {
|
||||
const cls = ACTION_STYLES[action] ?? "bg-muted text-muted-foreground border-border"
|
||||
return (
|
||||
<span className={cn("text-[11px] font-mono font-medium px-2 py-0.5 rounded border whitespace-nowrap", cls)}>
|
||||
{action}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ChainBadge({ chain }: { chain: string }) {
|
||||
const cls = CHAIN_STYLES[chain] ?? "bg-muted text-muted-foreground"
|
||||
return (
|
||||
<span className={cn("text-[11px] font-mono px-2 py-0.5 rounded", cls)}>
|
||||
{chain}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
// ActionBadge, ChainBadge — из firewall-rules-data-grid
|
||||
|
||||
function NativeSelect({ value, onChange, children, className }: {
|
||||
value: string; onChange: (v: string) => void; children: React.ReactNode; className?: string
|
||||
@@ -1143,55 +1134,13 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
{/* Rules table */}
|
||||
{rules.length > 0 ? (
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/30 text-muted-foreground">
|
||||
<th className="text-left px-3 py-2 w-6">#</th>
|
||||
<th className="text-left px-3 py-2">Цепочка</th>
|
||||
<th className="text-left px-3 py-2">Действие</th>
|
||||
<th className="text-left px-3 py-2">Src</th>
|
||||
<th className="text-left px-3 py-2">Dst</th>
|
||||
<th className="text-left px-3 py-2">Порт</th>
|
||||
<th className="text-left px-3 py-2">Iface</th>
|
||||
<th className="text-left px-3 py-2">Комментарий</th>
|
||||
<th className="w-24 px-2 py-2" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{rules.map((r, i) => (
|
||||
<tr key={r.id} className={cn(
|
||||
"hover:bg-muted/20 transition-colors",
|
||||
!r.enabled && "opacity-40",
|
||||
)}>
|
||||
<td className="px-3 py-1.5 text-muted-foreground tabular-nums">{i + 1}</td>
|
||||
<td className="px-3 py-1.5"><ChainBadge chain={r.chain} /></td>
|
||||
<td className="px-3 py-1.5"><ActionBadge action={r.action} /></td>
|
||||
<td className="px-3 py-1.5 font-mono text-muted-foreground max-w-[90px] truncate">{r.src || "any"}</td>
|
||||
<td className="px-3 py-1.5 font-mono text-muted-foreground max-w-[90px] truncate">{r.dst || "any"}</td>
|
||||
<td className="px-3 py-1.5 font-mono text-muted-foreground">{r.port || "—"}</td>
|
||||
<td className="px-3 py-1.5 font-mono text-muted-foreground">{r.iface || "—"}</td>
|
||||
<td className="px-3 py-1.5 text-muted-foreground/70 max-w-[110px] truncate">{r.comment || "—"}</td>
|
||||
<td className="px-2 py-1.5">
|
||||
<div className="flex items-center gap-0.5 justify-end">
|
||||
<button type="button" onClick={() => toggleEnabled(r.id)}
|
||||
title={r.enabled ? "Отключить" : "Включить"}
|
||||
className="p-0.5 text-muted-foreground/40 hover:text-foreground transition-colors">
|
||||
<PowerIcon className="size-3.5" />
|
||||
</button>
|
||||
<button type="button" onClick={() => moveRule(r.id, -1)} disabled={i === 0}
|
||||
className="p-0.5 text-muted-foreground/40 hover:text-foreground disabled:opacity-20 transition-colors">▲</button>
|
||||
<button type="button" onClick={() => moveRule(r.id, 1)} disabled={i === rules.length - 1}
|
||||
className="p-0.5 text-muted-foreground/40 hover:text-foreground disabled:opacity-20 transition-colors">▼</button>
|
||||
<button type="button" onClick={() => removeRule(r.id)}
|
||||
className="p-0.5 ml-0.5 text-muted-foreground/40 hover:text-red-500 transition-colors">
|
||||
<Trash2Icon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<FirewallScenarioRulesDataGrid
|
||||
rules={rules}
|
||||
onToggleEnabled={toggleEnabled}
|
||||
onMoveUp={(id) => moveRule(id, -1)}
|
||||
onMoveDown={(id) => moveRule(id, 1)}
|
||||
onRemove={removeRule}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
!addOpen && (
|
||||
@@ -1683,104 +1632,6 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Rules Table ──────────────────────────────────────────────────────────────
|
||||
|
||||
function RulesTable({ rules, onToggle, onEdit }: {
|
||||
rules: FirewallRule[]
|
||||
onToggle: (id: string) => void
|
||||
onEdit: (r: FirewallRule) => void
|
||||
}) {
|
||||
if (rules.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground">
|
||||
<ShieldOffIcon className="size-10 mb-3 opacity-30" />
|
||||
<p className="text-sm font-medium">Правила не найдены</p>
|
||||
<p className="text-xs mt-1">Попробуйте изменить фильтр или добавьте новое правило</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-xs text-muted-foreground">
|
||||
<th className="text-left font-medium px-5 py-3 w-8">#</th>
|
||||
<th className="text-left font-medium px-4 py-3">Цепочка</th>
|
||||
<th className="text-left font-medium px-4 py-3">Действие</th>
|
||||
<th className="text-left font-medium px-4 py-3">Источник</th>
|
||||
<th className="text-left font-medium px-4 py-3">Назначение</th>
|
||||
<th className="text-left font-medium px-4 py-3">Протокол</th>
|
||||
<th className="text-left font-medium px-4 py-3">Порт</th>
|
||||
<th className="text-left font-medium px-4 py-3">Интерфейс</th>
|
||||
<th className="text-right font-medium px-4 py-3">Пакетов</th>
|
||||
<th className="text-left font-medium px-4 py-3 w-12">Вкл</th>
|
||||
<th className="w-10 px-3 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{rules.map((r, i) => (
|
||||
<tr key={r.id}
|
||||
className={cn("hover:bg-muted/40 transition-colors", !r.enabled && "opacity-40")}>
|
||||
<td className="px-5 py-2.5 font-mono text-xs text-muted-foreground">{i + 1}</td>
|
||||
<td className="px-4 py-2.5"><ChainBadge chain={r.chain} /></td>
|
||||
<td className="px-4 py-2.5"><ActionBadge action={r.action} /></td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground max-w-[140px] truncate">
|
||||
{r.src || "any"}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground max-w-[140px] truncate">
|
||||
{r.dst || "any"}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-xs font-mono">{r.proto}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">{r.port || "—"}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">{r.iface || "—"}</td>
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
<span className={cn(
|
||||
"text-xs font-mono tabular-nums",
|
||||
r.hits > 1_000_000 ? "text-emerald-600 dark:text-emerald-400 font-semibold"
|
||||
: r.hits > 10_000 ? "text-foreground"
|
||||
: "text-muted-foreground",
|
||||
)}>
|
||||
{fmtHits(r.hits)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<FormToggle checked={r.enabled} onChange={() => onToggle(r.id)} />
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={
|
||||
<Button variant="ghost" size="icon" className="size-7">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
} />
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit(r)}>
|
||||
<PencilIcon className="size-4" />Редактировать
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CopyIcon className="size-4" />Дублировать
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onToggle(r.id)}>
|
||||
<PowerIcon className="size-4" />
|
||||
{r.enabled ? "Отключить" : "Включить"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon className="size-4" />Удалить правило
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
export default function FirewallPage() {
|
||||
const [rules, setRules] = useState<FirewallRule[]>(firewallRules)
|
||||
@@ -1932,7 +1783,7 @@ export default function FirewallPage() {
|
||||
) : chainGroup === "simulator" ? (
|
||||
<SimulatorTab rules={rules} />
|
||||
) : (
|
||||
<Card>
|
||||
<DataPageCard>
|
||||
{/* toolbar */}
|
||||
<div className="flex items-center gap-3 px-5 py-3 border-b flex-wrap">
|
||||
{/* IP family selector */}
|
||||
@@ -1980,8 +1831,8 @@ export default function FirewallPage() {
|
||||
<span className="text-sm text-muted-foreground ml-auto">{filteredRules.length} правил</span>
|
||||
</div>
|
||||
|
||||
<RulesTable rules={filteredRules} onToggle={toggleRule} onEdit={openEdit} />
|
||||
</Card>
|
||||
<FirewallRulesDataGrid rules={filteredRules} onToggle={toggleRule} onEdit={openEdit} />
|
||||
</DataPageCard>
|
||||
)}
|
||||
|
||||
{/* RouterOS reference */}
|
||||
|
||||
Reference in New Issue
Block a user