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
ResourcePage и FrameDataGrid получили раскладку досок: заголовок Frame с поиском и фильтрами, линейные виды со счётчиками, подвал «Показано N из M». Страница модулей — KPI-полоса, кнопка создания в шапке, вкладка. Операции: действия применения и перезагрузки BIRD в шапке, баннер при ошибках задач. Сеть: баннер пиров на одобрение. Кнопки шапок — стандартной высоты. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
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>
|
||
)
|
||
}
|