Files
EvoBGP/apps/web/src/routes/_auth/modules/$moduleId.tsx
T
DenozordecandClaude Sonnet 5.5 aba18e15b7
quality / changes (push) Successful in 9s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 23s
quality / web (push) Successful in 1m22s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 3m31s
feat(web): pages and tables as on shared mockup boards
ResourcePage и FrameDataGrid получили раскладку досок: заголовок Frame с поиском и фильтрами, линейные виды со счётчиками, подвал «Показано N из M». Страница модулей — KPI-полоса, кнопка создания в шапке, вкладка. Операции: действия применения и перезагрузки BIRD в шапке, баннер при ошибках задач. Сеть: баннер пиров на одобрение. Кнопки шапок — стандартной высоты.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 21:59:12 +07:00

155 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createFileRoute, Link } from '@tanstack/react-router'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { ArrowLeft, Pencil, RefreshCw } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { ModuleEditDialog } from '@/components/modules/module-edit-dialog'
import { ModuleEntriesSection } from '@/components/modules/module-entries-section'
import { ModuleKpiCards } from '@/components/modules/module-kpi-cards'
import { QueryState } from '@/components/query-state'
import { DetailPanel } from '@/components/reui-kit'
import { TableSkeleton } from '@/components/skeletons'
import { StatusBadge } from '@/components/status-badge'
import { sessionCanWriteModules } from '@/lib/auth'
import { moduleTypeRu } from '@/lib/ui-labels'
import { authSessionQueryOptions } from '@/queries/auth'
import {
directoriesCommunitiesQueryOptions,
directoriesDohQueryOptions,
} from '@/queries/directories'
import { moduleDetailQueryOptions, moduleEntriesQueryOptions, modulesKeys } from '@/queries/modules'
import type { AsEntry } from '@/types/api'
export const Route = createFileRoute('/_auth/modules/$moduleId')({
component: ModuleDetailComponent,
})
function ModuleDetailComponent() {
const { moduleId } = Route.useParams()
const queryClient = useQueryClient()
const [editOpen, setEditOpen] = useState(false)
const detail = useQuery(moduleDetailQueryOptions(moduleId))
const mod = detail.data
const sessionQ = useQuery(authSessionQueryOptions())
const canWrite = sessionCanWriteModules(sessionQ.data)
const communitiesQ = useQuery(directoriesCommunitiesQueryOptions())
const dohQ = useQuery(directoriesDohQueryOptions())
const entriesQuery = useQuery({
...moduleEntriesQueryOptions(moduleId, mod?.type ?? 'DOMAINS'),
enabled: !!mod,
})
const asEntriesQ = useQuery({
...moduleEntriesQueryOptions(moduleId, 'AS_PREFIXES'),
enabled: mod?.type === 'AS_PREFIXES',
select: (data) => data.items as unknown as AsEntry[],
})
async function refreshAll() {
await Promise.all([detail.refetch(), entriesQuery.refetch(), communitiesQ.refetch(), dohQ.refetch()])
}
async function onEntriesChanged() {
await entriesQuery.refetch()
if (mod?.type === 'AS_PREFIXES') {
await queryClient.invalidateQueries({ queryKey: modulesKeys.asEntries(moduleId) })
}
await detail.refetch()
}
const communities = communitiesQ.data?.items ?? []
const dohProfiles = dohQ.data?.items ?? []
const asEntries = mod?.type === 'AS_PREFIXES' ? (asEntriesQ.data ?? []) : []
const entriesCount = entriesQuery.data?.items.length ?? 0
const headerActions = (
<>
<Button variant="outline" render={<Link to="/modules" />}>
<ArrowLeft />
К списку
</Button>
{canWrite && mod ? (
<Button variant="outline" onClick={() => setEditOpen(true)}>
<Pencil />
Редактировать
</Button>
) : null}
<Button
variant="outline"
onClick={() => void refreshAll()}
disabled={detail.isFetching}
>
<RefreshCw className={detail.isFetching ? 'animate-spin' : ''} />
Обновить
</Button>
</>
)
return (
<QueryState
data={mod}
isLoading={detail.isLoading}
isError={detail.isError}
error={detail.error}
skeleton={<TableSkeleton rows={4} cols={2} />}
onRetry={() => detail.refetch()}
>
{(m) => (
<DetailPanel>
<DetailPanel.Header
title={m.name}
description={`Тип: ${moduleTypeRu(m.type)} (${m.type})`}
actions={headerActions}
>
{m.enabled ? (
<StatusBadge status="active" label="включён" />
) : (
<StatusBadge status="paused" label="выключен" />
)}
</DetailPanel.Header>
<DetailPanel.Section>
<ModuleKpiCards
mod={m}
communities={communities}
asEntries={asEntries}
entriesCount={entriesCount}
loading={detail.isLoading || communitiesQ.isLoading}
/>
</DetailPanel.Section>
<DetailPanel.Section title="Записи">
<ModuleEntriesSection
moduleId={moduleId}
mod={m}
items={entriesQuery.data?.items ?? []}
communities={communities}
isLoading={entriesQuery.isLoading}
isError={entriesQuery.isError}
error={entriesQuery.error}
onRetry={() => entriesQuery.refetch()}
onChanged={onEntriesChanged}
/>
</DetailPanel.Section>
{canWrite ? (
<ModuleEditDialog
open={editOpen}
onOpenChange={setEditOpen}
mod={m}
communities={communities}
dohProfiles={dohProfiles}
/>
) : null}
</DetailPanel>
)}
</QueryState>
)
}