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 <noreply@anthropic.com>
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
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>
|
||
)
|
||
}
|