refactor(web): общие форматтеры, единая навигация, RBAC-гейтинг и чистка мёртвого кода

- lib/format.ts: ru-RU форматтеры дат/чисел в одном месте — убраны дубли
  из 8 компонентов (agent-card, fleet-grid, blocked-ips/ports, facts,
  lifecycle, host-firewall, lists-columns, system-monitor)
- lib/nav.ts: единый конфиг маршрутов — sidebar, ⌘K-поиск и breadcrumbs
  рендерятся из одного источника (было 3 расходящихся копии)
- lib/permissions.ts + useCan: клиентский RBAC — кнопки создания/подтверждения
  скрываются без fw:*:write/admin (сервер остаётся авторитетным)
- удалён мёртвый код ~2500 строк: stepper, data-grid dnd/virtual/visibility
  варианты, settings-shell
This commit is contained in:
Denozordec
2026-09-20 19:36:37 +07:00
parent 7a3f1fad25
commit 3dc8e6d5e2
26 changed files with 275 additions and 2178 deletions
+6 -2
View File
@@ -47,6 +47,7 @@ import { ConfirmDialog } from '@/components/confirm-dialog'
import { QueryState } from '@/components/query-state'
import { agentsQueryOptions, settingsQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCan } from '@/lib/permissions'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@evofw/ui/components/button'
import {
@@ -97,6 +98,7 @@ function AgentsPage() {
const agentsQ = useQuery(agentsQueryOptions())
const settingsQ = useQuery(settingsQueryOptions())
const { copyToClipboard } = useCopyToClipboard()
const canWrite = useCan()('fw:agents:write')
const [createOpen, setCreateOpen] = useState(false)
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null)
const [filters, setFilters] = useState<Filter[]>([])
@@ -418,12 +420,12 @@ function AgentsPage() {
</ToggleGroup>
)
const addButton = (
const addButton = canWrite ? (
<Button size="sm" onClick={() => setCreateOpen(true)}>
<Plus data-icon="inline-start" />
Добавить агента
</Button>
)
) : null
return (
<PageShell>
@@ -454,6 +456,7 @@ function AgentsPage() {
>
Показать
</Button>
{canWrite ? (
<Button
size="sm"
disabled={
@@ -464,6 +467,7 @@ function AgentsPage() {
<Check data-icon="inline-start" />
Approve all
</Button>
) : null}
</div>
</FrameHeader>
</Frame>
+8 -4
View File
@@ -15,6 +15,7 @@ import {
import { ConfirmDialog } from '@/components/confirm-dialog'
import { listsQueryOptions, evobgpCommunitiesQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCan } from '@/lib/permissions'
import {
Autocomplete,
AutocompleteContent,
@@ -67,6 +68,7 @@ const CREATE_SOURCE_ITEMS = [
function ListsPage() {
const navigate = useNavigate()
const qc = useQueryClient()
const canWrite = useCan()('fw:lists:write')
const [createOpen, setCreateOpen] = useState(false)
const [name, setName] = useState('')
const [source, setSource] = useState<CreateSource>('static')
@@ -199,10 +201,12 @@ function ListsPage() {
/>
Обновить
</Button>
<Button size="sm" onClick={() => setCreateOpen(true)}>
<Plus data-icon="inline-start" />
Создать
</Button>
{canWrite ? (
<Button size="sm" onClick={() => setCreateOpen(true)}>
<Plus data-icon="inline-start" />
Создать
</Button>
) : null}
</>
}
/>
+4 -2
View File
@@ -13,6 +13,7 @@ import { ConfirmDialog } from '@/components/confirm-dialog'
import { PolicySetIcon } from '@/components/rules/policy-set-icon'
import { policySetsQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCan } from '@/lib/permissions'
import { Button } from '@evofw/ui/components/button'
import { Field, FieldLabel } from '@evofw/ui/components/field'
import { Input } from '@evofw/ui/components/input'
@@ -200,12 +201,13 @@ function PolicySetsPage() {
[navigate],
)
const addButton = (
const canCreate = useCan()('fw:policies:write')
const addButton = canCreate ? (
<Button size="sm" onClick={() => setSheetOpen(true)}>
<Plus data-icon="inline-start" />
Новый набор
</Button>
)
) : null
return (
<PageShell>
+14 -11
View File
@@ -14,12 +14,12 @@ import { LoadingButton } from '@/components/loading-button'
import { SettingRow } from '@/components/setting-row'
import { settingsQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCan } from '@/lib/permissions'
import { Input } from '@evofw/ui/components/input'
import { Switch } from '@evofw/ui/components/switch'
/**
* Control plane settings — single page: PageShell + Frame + SettingRow.
* SettingsShell (reui-kit) — только при 2+ секциях.
* Preview: https://reui.io/preview/base/settings-16 · https://reui.io/preview/base/settings-3
*/
@@ -30,6 +30,7 @@ export const Route = createFileRoute('/_auth/settings')({
function SettingsPage() {
const qc = useQueryClient()
const settingsQ = useQuery(settingsQueryOptions())
const canSave = useCan()('fw:settings:admin')
const [form, setForm] = useState<Record<string, string>>({})
// Seed the form once — a background refetch must not wipe in-progress edits.
@@ -134,16 +135,18 @@ function SettingsPage() {
</div>
</FramePanel>
</Frame>
<div className="flex justify-end">
<LoadingButton
onClick={() => save.mutate()}
isLoading={save.isPending}
disabled={!initialized.current}
loadingLabel="Сохранение…"
>
Сохранить
</LoadingButton>
</div>
{canSave ? (
<div className="flex justify-end">
<LoadingButton
onClick={() => save.mutate()}
isLoading={save.isPending}
disabled={!initialized.current}
loadingLabel="Сохранение…"
>
Сохранить
</LoadingButton>
</div>
) : null}
</PageShell>
)
}