Files
EvoBGP/apps/web/src/routes/_auth/schedule.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 <noreply@anthropic.com>
2026-10-23 21:59:12 +07:00

130 lines
4.5 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 } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, Clock, ListTodo, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import { useState } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { Badge } from '@/components/reui/badge'
import { PageHeader } from '@/components/page-header'
import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
import { ScheduleJobsCard } from '@/components/schedule/schedule-jobs-card'
import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid'
import { apiMutate } from '@/lib/api-client'
import { moduleNameById } from '@/queries/overview'
import { modulesListQueryOptions } from '@/queries/modules'
import { operationsJobsQueryOptions } from '@/queries/operations'
export const Route = createFileRoute('/_auth/schedule')({
component: ScheduleComponent,
})
function ScheduleComponent() {
const modulesQ = useQuery(modulesListQueryOptions())
const jobsQ = useQuery(operationsJobsQueryOptions())
const qc = useQueryClient()
const [refreshing, setRefreshing] = useState<Record<string, boolean>>({})
const modules = modulesQ.data?.items ?? []
const jobs = jobsQ.data?.items ?? []
const loading = modulesQ.isLoading || jobsQ.isLoading
const running = jobs.filter((j) => j.status === 'running' || j.status === 'queued').length
const failed = jobs.filter((j) =>
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
).length
const items: KpiStatItem[] = [
{
label: 'Всего задач',
value: jobs.length,
icon: <ListTodo className="size-4" />,
footer: (
<Badge variant="primary-light" size="sm">
в выборке
</Badge>
),
},
{
label: 'В работе',
value: running,
icon: <Clock className="size-4" />,
footer: (
<Badge variant={running > 0 ? 'info-light' : 'outline'} size="sm">
очередь и выполнение
</Badge>
),
},
{
label: 'С ошибкой',
value: failed,
icon: <AlertTriangle className="size-4" />,
variant: failed > 0 ? 'warning' : 'default',
footer: (
<Badge variant={failed > 0 ? 'warning-light' : 'success-light'} size="sm">
{failed > 0 ? 'требуют внимания' : 'без ошибок'}
</Badge>
),
},
]
const refreshMutation = useMutation({
mutationFn: async (id: string) => apiMutate<{ job_id?: string }>(`/v1/modules/${id}/refresh`, 'POST'),
onMutate: (id) => setRefreshing((s) => ({ ...s, [id]: true })),
onSuccess: (data, id) => {
if (data === undefined) toast.message('Обновление не требуется (тип IP_RANGES)')
else toast.success('Задача поставлена в очередь')
void qc.invalidateQueries({ queryKey: ['operations'] })
void qc.invalidateQueries({ queryKey: ['modules'] })
setRefreshing((s) => ({ ...s, [id]: false }))
},
onError: (e, id) => {
toast.error(e instanceof Error ? e.message : 'Не удалось запустить')
setRefreshing((s) => ({ ...s, [id]: false }))
},
})
return (
<div className="flex flex-col gap-6">
<PageHeader
title="Расписание и задачи"
description="Интервалы обновления модулей и ручной запуск"
actions={
<Button
variant="outline"
onClick={() => {
void modulesQ.refetch()
void jobsQ.refetch()
}}
disabled={loading}
>
<RefreshCw className={loading ? 'animate-spin' : ''} />
Обновить
</Button>
}
/>
<KpiStatGrid items={items} isLoading={loading} skeletonCount={3} />
<ScheduleModulesGrid
items={modules}
refreshing={refreshing}
onRefresh={(id) => refreshMutation.mutate(id)}
isLoading={modulesQ.isLoading}
isError={modulesQ.isError}
error={modulesQ.error instanceof Error ? modulesQ.error : null}
onRetry={() => void modulesQ.refetch()}
/>
<ScheduleJobsCard
jobs={jobs}
nameById={moduleNameById(modules)}
isLoading={jobsQ.isLoading}
isError={jobsQ.isError}
error={jobsQ.error}
onRetry={() => jobsQ.refetch()}
/>
</div>
)
}