refactor: enhance schedule components with improved filtering and layout
CI / changes (push) Successful in 11s
CI / openapi (push) Has been skipped
CI / commitlint (push) Has been skipped
CI / web (push) Successful in 51s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m20s
CI / changes (push) Successful in 11s
CI / openapi (push) Has been skipped
CI / commitlint (push) Has been skipped
CI / web (push) Successful in 51s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m20s
Updated the ScheduleAgendaPanel to include a job filter feature, allowing users to filter jobs by type (all, refresh, failed). Refactored the layout to integrate a new ScheduleCalendarView for better organization. Enhanced the ScheduleJobsGrid to conditionally display pagination based on the number of items. Additionally, modified the Schedule component to utilize the new ScheduleJobsCard for improved job display and loading states, ensuring a more cohesive user experience.
This commit is contained in:
@@ -1,15 +1,69 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { format, isSameDay, parseISO } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { CalendarDays, Clock } from 'lucide-react'
|
||||
|
||||
import { PanelCard } from '@/components/panel-card'
|
||||
import { IllustratedEmptyState } from '@/components/patterns/illustrated-empty-state'
|
||||
import { PanelCard, panelCardContentFlushClassName } from '@/components/panel-card'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Calendar } from '@evobgp/ui/components/calendar'
|
||||
import { Item } from '@evobgp/ui/components/item'
|
||||
import { ScrollArea } from '@evobgp/ui/components/scroll-area'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@evobgp/ui/components/select'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import { jobKindRu } from '@/lib/ui-labels'
|
||||
import type { JobRow } from '@/types/api'
|
||||
|
||||
/** schedule-1 inspired agenda: calendar + day job list. */
|
||||
import { ScheduleCalendarView } from './schedule-calendar-view'
|
||||
|
||||
type JobFilter = 'all' | 'refresh' | 'failed'
|
||||
|
||||
const FILTER_ITEMS: { value: JobFilter; label: string }[] = [
|
||||
{ value: 'all', label: 'Все задачи' },
|
||||
{ value: 'refresh', label: 'Обновление' },
|
||||
{ value: 'failed', label: 'С ошибкой' },
|
||||
]
|
||||
|
||||
function jobTimestamp(job: JobRow): string | undefined {
|
||||
return job.created_at ?? job.started_at ?? job.finished_at ?? undefined
|
||||
}
|
||||
|
||||
function matchesFilter(job: JobRow, filter: JobFilter): boolean {
|
||||
if (filter === 'refresh') return job.kind === 'module_refresh'
|
||||
if (filter === 'failed')
|
||||
return ['failed', 'error', 'cancelled'].includes(job.status.toLowerCase())
|
||||
return true
|
||||
}
|
||||
|
||||
function ScheduleJobCard({ job }: { job: JobRow }) {
|
||||
const ts = jobTimestamp(job)
|
||||
const timeLabel = ts
|
||||
? format(parseISO(ts), 'd MMM · HH:mm', { locale: ru })
|
||||
: '—'
|
||||
|
||||
return (
|
||||
<Item variant="outline" size="xs" className="flex items-start gap-3 py-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<p className="text-foreground text-sm leading-tight font-medium">{jobKindRu(job.kind)}</p>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<StatusBadge status={job.status} />
|
||||
<span className="text-muted-foreground flex items-center gap-1 text-xs">
|
||||
<Clock className="size-3 shrink-0" aria-hidden />
|
||||
{timeLabel}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-mono text-xs">{job.job_id}</p>
|
||||
</div>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
|
||||
/** schedule-1 layout: calendar column + day job list. */
|
||||
export function ScheduleAgendaPanel({
|
||||
jobs,
|
||||
isLoading,
|
||||
@@ -18,25 +72,12 @@ export function ScheduleAgendaPanel({
|
||||
isLoading?: boolean
|
||||
}) {
|
||||
const [date, setDate] = useState<Date>(new Date())
|
||||
|
||||
const dayJobs = useMemo(
|
||||
() =>
|
||||
jobs.filter((job) => {
|
||||
const raw = job.created_at ?? job.started_at ?? job.finished_at
|
||||
if (!raw) return false
|
||||
try {
|
||||
return isSameDay(parseISO(raw), date)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
[jobs, date],
|
||||
)
|
||||
const [filter, setFilter] = useState<JobFilter>('all')
|
||||
|
||||
const markedDays = useMemo(() => {
|
||||
const days = new Set<string>()
|
||||
for (const job of jobs) {
|
||||
const raw = job.created_at ?? job.started_at ?? job.finished_at
|
||||
const raw = jobTimestamp(job)
|
||||
if (!raw) continue
|
||||
try {
|
||||
days.add(format(parseISO(raw), 'yyyy-MM-dd'))
|
||||
@@ -47,44 +88,85 @@ export function ScheduleAgendaPanel({
|
||||
return days
|
||||
}, [jobs])
|
||||
|
||||
const dayJobs = useMemo(
|
||||
() =>
|
||||
jobs.filter((job) => {
|
||||
const raw = jobTimestamp(job)
|
||||
if (!raw) return false
|
||||
try {
|
||||
return isSameDay(parseISO(raw), date) && matchesFilter(job, filter)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
[jobs, date, filter],
|
||||
)
|
||||
|
||||
const headingLabel = format(date, 'EEEE, d MMMM', { locale: ru })
|
||||
|
||||
return (
|
||||
<PanelCard
|
||||
title="Календарь задач"
|
||||
description="Задачи refresh и apply по дням (schedule-1 pattern)"
|
||||
className="h-full"
|
||||
description="Задачи refresh и apply по дням"
|
||||
contentClassName={cn(panelCardContentFlushClassName, 'p-0')}
|
||||
>
|
||||
<div className="grid gap-4 p-4 lg:grid-cols-[minmax(0,280px)_1fr]">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(d) => d && setDate(d)}
|
||||
locale={ru}
|
||||
modifiers={{
|
||||
hasJob: (d) => markedDays.has(format(d, 'yyyy-MM-dd')),
|
||||
}}
|
||||
modifiersClassNames={{ hasJob: 'font-bold underline' }}
|
||||
/>
|
||||
<ScrollArea className="h-64 lg:h-auto">
|
||||
{isLoading ? (
|
||||
<p className="text-muted-foreground text-sm">Загрузка…</p>
|
||||
) : dayJobs.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Нет задач за {format(date, 'd MMMM yyyy', { locale: ru })}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2 pr-3">
|
||||
{dayJobs.map((job) => (
|
||||
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{jobKindRu(job.kind)}</span>
|
||||
<StatusBadge status={job.status} />
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">{job.job_id}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</ScrollArea>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<div className="border-border shrink-0 border-b p-5 lg:w-[370px] lg:border-r lg:border-b-0">
|
||||
<ScheduleCalendarView
|
||||
selected={date}
|
||||
onSelect={(d) => d && setDate(d)}
|
||||
datesWithEvents={markedDays}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4 p-5">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-foreground text-sm font-semibold capitalize">{headingLabel}</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{isLoading
|
||||
? 'Загрузка…'
|
||||
: dayJobs.length > 0
|
||||
? `${dayJobs.length} ${dayJobs.length === 1 ? 'задача' : dayJobs.length < 5 ? 'задачи' : 'задач'}`
|
||||
: 'Нет задач за выбранный день'}
|
||||
</p>
|
||||
</div>
|
||||
<Select value={filter} onValueChange={(v) => v && setFilter(v as JobFilter)}>
|
||||
<SelectTrigger size="sm" className="w-full sm:w-44">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FILTER_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[280px]">
|
||||
{isLoading ? (
|
||||
<p className="text-muted-foreground text-sm">Загрузка задач…</p>
|
||||
) : dayJobs.length === 0 ? (
|
||||
<IllustratedEmptyState
|
||||
icon={CalendarDays}
|
||||
title="Нет задач"
|
||||
description={`За ${format(date, 'd MMMM yyyy', { locale: ru })} задачи не найдены. Выберите другой день или измените фильтр.`}
|
||||
/>
|
||||
) : (
|
||||
<ScrollArea className="max-h-[320px] pr-3">
|
||||
<ul className="space-y-2.5">
|
||||
{dayJobs.map((job) => (
|
||||
<li key={job.job_id}>
|
||||
<ScheduleJobCard job={job} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelCard>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { useState, type ComponentPropsWithoutRef } from 'react'
|
||||
import { DayButton } from 'react-day-picker'
|
||||
import { format } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { Calendar, CalendarDayButton } from '@evobgp/ui/components/calendar'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@evobgp/ui/components/select'
|
||||
|
||||
const MONTHS_RU = Array.from({ length: 12 }, (_, i) =>
|
||||
format(new Date(2024, i, 1), 'LLLL', { locale: ru }),
|
||||
)
|
||||
|
||||
const CURRENT_YEAR = new Date().getFullYear()
|
||||
const YEARS = Array.from({ length: 11 }, (_, i) => CURRENT_YEAR - 5 + i)
|
||||
|
||||
const TODAY_WEEKDAY = format(new Date(), 'EEEEEE', { locale: ru }).toUpperCase()
|
||||
|
||||
export function ScheduleCalendarView({
|
||||
selected,
|
||||
onSelect,
|
||||
datesWithEvents = new Set<string>(),
|
||||
}: {
|
||||
selected: Date | undefined
|
||||
onSelect: (date: Date | undefined) => void
|
||||
datesWithEvents?: Set<string>
|
||||
}) {
|
||||
const [month, setMonth] = useState<Date>(selected ?? new Date())
|
||||
|
||||
const stepMonth = (delta: number) =>
|
||||
setMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + delta, 1))
|
||||
|
||||
const handleMonthSelect = (value: string) => {
|
||||
const i = MONTHS_RU.indexOf(value)
|
||||
if (i >= 0) setMonth(new Date(month.getFullYear(), i, 1))
|
||||
}
|
||||
|
||||
const handleYearSelect = (value: string) => {
|
||||
const y = parseInt(value, 10)
|
||||
if (!Number.isNaN(y)) setMonth(new Date(y, month.getMonth(), 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 select-none">
|
||||
<div className="flex w-full grow items-center justify-between gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 shrink-0 p-0"
|
||||
onClick={() => stepMonth(-1)}
|
||||
aria-label="Предыдущий месяц"
|
||||
>
|
||||
<ChevronLeft className="size-3.5" aria-hidden />
|
||||
</Button>
|
||||
|
||||
<Select
|
||||
value={MONTHS_RU[month.getMonth()]}
|
||||
onValueChange={(value) => value && handleMonthSelect(value)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="min-w-0 flex-1 capitalize">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MONTHS_RU.map((m) => (
|
||||
<SelectItem key={m} value={m} className="capitalize">
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={String(month.getFullYear())}
|
||||
onValueChange={(value) => value && handleYearSelect(value)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-22 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{YEARS.map((y) => (
|
||||
<SelectItem key={y} value={String(y)}>
|
||||
{y}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 shrink-0 p-0"
|
||||
onClick={() => stepMonth(1)}
|
||||
aria-label="Следующий месяц"
|
||||
>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
locale={ru}
|
||||
showOutsideDays
|
||||
hideNavigation
|
||||
className="w-full bg-transparent p-0 md:[--cell-size:--spacing(11)]"
|
||||
formatters={{
|
||||
formatWeekdayName: (date) =>
|
||||
date.toLocaleString('ru-RU', { weekday: 'short' }).replace('.', '').toUpperCase(),
|
||||
}}
|
||||
classNames={{
|
||||
month_caption: 'hidden',
|
||||
nav: 'hidden',
|
||||
weekdays: 'flex gap-1',
|
||||
weekday:
|
||||
'flex-1 flex items-center justify-center h-6 text-[0.65rem] font-medium text-muted-foreground',
|
||||
week: 'flex gap-1 mt-1',
|
||||
day: 'flex-1 aspect-square p-0',
|
||||
day_button: cn(
|
||||
'bg-muted/50 hover:bg-muted rounded-md',
|
||||
'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:hover:bg-primary data-[selected-single=true]:hover:text-primary-foreground!',
|
||||
),
|
||||
outside: 'opacity-60',
|
||||
disabled: 'opacity-60',
|
||||
today: cn('bg-accent text-foreground rounded-md'),
|
||||
}}
|
||||
components={{
|
||||
Weekday: ({ children, className: cls, ...props }: ComponentPropsWithoutRef<'th'>) => {
|
||||
const isToday = children === TODAY_WEEKDAY
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
className={cn(
|
||||
'flex h-6! flex-1 items-center justify-center rounded-md text-xs font-medium',
|
||||
isToday ? 'bg-accent text-foreground!' : 'text-muted-foreground',
|
||||
cls,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
},
|
||||
DayButton: ({
|
||||
children,
|
||||
modifiers,
|
||||
day,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) => {
|
||||
const dateKey = format(day.date, 'yyyy-MM-dd')
|
||||
const hasEvents = !modifiers.outside && datesWithEvents.has(dateKey)
|
||||
|
||||
return (
|
||||
<CalendarDayButton day={day} modifiers={modifiers} {...props}>
|
||||
{hasEvents ? (
|
||||
<span
|
||||
className="bg-primary text-primary-foreground in-data-[selected-single=true]:bg-primary-foreground! size-1 rounded-full"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<span className="size-1" aria-hidden />
|
||||
)}
|
||||
{children}
|
||||
</CalendarDayButton>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||
import type { JobRow } from '@/types/api'
|
||||
|
||||
import { ScheduleJobsGrid } from './schedule-jobs-grid'
|
||||
|
||||
type JobTab = 'all' | 'refresh' | 'failed'
|
||||
|
||||
function filterJobs(items: JobRow[], tab: JobTab): JobRow[] {
|
||||
if (tab === 'refresh') return items.filter((j) => j.kind === 'module_refresh')
|
||||
if (tab === 'failed')
|
||||
return items.filter((j) => ['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()))
|
||||
return items
|
||||
}
|
||||
|
||||
function tabCounts(items: JobRow[]) {
|
||||
return {
|
||||
all: items.length,
|
||||
refresh: items.filter((j) => j.kind === 'module_refresh').length,
|
||||
failed: items.filter((j) => ['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()))
|
||||
.length,
|
||||
}
|
||||
}
|
||||
|
||||
/** Jobs data-grid with status tabs (data-grid-filtering pattern). */
|
||||
export function ScheduleJobsCard({
|
||||
jobs,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
jobs: JobRow[]
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
error: unknown
|
||||
onRetry: () => void
|
||||
}) {
|
||||
const [tab, setTab] = useState<JobTab>('all')
|
||||
const counts = useMemo(() => tabCounts(jobs), [jobs])
|
||||
const filtered = useMemo(() => filterJobs(jobs, tab), [jobs, tab])
|
||||
|
||||
return (
|
||||
<DataGridCard title="Задачи" description="Последние задачи из API">
|
||||
<div className="border-b px-5 py-3">
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as JobTab)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">Все ({counts.all})</TabsTrigger>
|
||||
<TabsTrigger value="refresh">Обновление ({counts.refresh})</TabsTrigger>
|
||||
<TabsTrigger value="failed">С ошибкой ({counts.failed})</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<QueryState
|
||||
data={filtered}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
empty={filtered.length === 0}
|
||||
emptyTitle="Нет задач в выборке"
|
||||
skeleton={<TableSkeleton rows={6} cols={5} />}
|
||||
onRetry={onRetry}
|
||||
>
|
||||
{(items) => (
|
||||
<ScheduleJobsGrid
|
||||
items={items}
|
||||
isLoading={isLoading && items.length > 0}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
)
|
||||
}
|
||||
@@ -90,6 +90,7 @@ export function ScheduleJobsGrid({
|
||||
recordCount={filteredCount}
|
||||
isLoading={isLoading}
|
||||
emptyMessage="Нет задач"
|
||||
showPagination={items.length > 10}
|
||||
searchValue={globalFilter}
|
||||
onSearchChange={setGlobalFilter}
|
||||
searchPlaceholder="Поиск задач…"
|
||||
|
||||
@@ -2,13 +2,12 @@ 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 { useEffect, useState } from 'react'
|
||||
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { ScheduleAgendaPanel } from '@/components/schedule/schedule-agenda-panel'
|
||||
import { ScheduleJobsGrid } from '@/components/schedule/schedule-jobs-grid'
|
||||
import { ScheduleJobsCard } from '@/components/schedule/schedule-jobs-card'
|
||||
import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
@@ -18,7 +17,6 @@ import { SectionCardsSkeleton } from '@/components/skeletons'
|
||||
import { operationsJobsQueryOptions } from '@/queries/operations'
|
||||
import { modulesListQueryOptions } from '@/queries/modules'
|
||||
import { apiMutate } from '@/lib/api-client'
|
||||
import type { JobRow } from '@/types/api'
|
||||
|
||||
export const Route = createFileRoute('/_auth/schedule')({
|
||||
component: ScheduleComponent,
|
||||
@@ -38,6 +36,30 @@ function ScheduleComponent() {
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
|
||||
// #region agent log
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:7311/ingest/6b35c3ae-1bcd-4c9c-81eb-f157c9347393', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': 'ce85d7' },
|
||||
body: JSON.stringify({
|
||||
sessionId: 'ce85d7',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'C',
|
||||
location: 'schedule.tsx:mount',
|
||||
message: 'schedule page data loaded',
|
||||
data: {
|
||||
modules: modules.length,
|
||||
jobs: jobs.length,
|
||||
loading,
|
||||
modulesError: modulesQ.isError,
|
||||
jobsError: jobsQ.isError,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {})
|
||||
}, [modules.length, jobs.length, loading, modulesQ.isError, jobsQ.isError])
|
||||
// #endregion
|
||||
|
||||
const items: SectionCardItem[] = [
|
||||
{ label: 'Всего задач', value: jobs.length, icon: <ListTodo className="size-4" />, hint: 'в выборке' },
|
||||
{ label: 'В работе', value: running, icon: <Clock className="size-4" />, hint: 'в очереди и выполняются' },
|
||||
@@ -115,43 +137,13 @@ function ScheduleComponent() {
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
|
||||
<DataGridCard title="Задачи" description="Последние задачи из API">
|
||||
<JobsTabs jobs={jobs} loading={jobsQ.isLoading} />
|
||||
</DataGridCard>
|
||||
<ScheduleJobsCard
|
||||
jobs={jobs}
|
||||
isLoading={jobsQ.isLoading}
|
||||
isError={jobsQ.isError}
|
||||
error={jobsQ.error}
|
||||
onRetry={() => jobsQ.refetch()}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function JobsTabs({ jobs, loading }: { jobs: JobRow[]; loading: boolean }) {
|
||||
const refresh = jobs.filter((j) => j.kind === 'module_refresh')
|
||||
const failed = jobs.filter((j) =>
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
)
|
||||
|
||||
return (
|
||||
<BadgeTabs
|
||||
defaultValue="all"
|
||||
listClassName="mx-3 mb-0 w-auto"
|
||||
items={[
|
||||
{ value: 'all', label: 'Все', count: jobs.length },
|
||||
{ value: 'refresh', label: 'Обновление', count: refresh.length, badgeVariant: 'info-light' },
|
||||
{
|
||||
value: 'failed',
|
||||
label: 'С ошибкой',
|
||||
count: failed.length,
|
||||
badgeVariant: failed.length > 0 ? 'destructive-light' : 'primary-light',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TabsContent value="all" className="mt-0">
|
||||
<ScheduleJobsGrid items={jobs} isLoading={loading} />
|
||||
</TabsContent>
|
||||
<TabsContent value="refresh" className="mt-0">
|
||||
<ScheduleJobsGrid items={refresh} isLoading={loading} />
|
||||
</TabsContent>
|
||||
<TabsContent value="failed" className="mt-0">
|
||||
<ScheduleJobsGrid items={failed} isLoading={loading} />
|
||||
</TabsContent>
|
||||
</BadgeTabs>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user