Files
EvoBGP/apps/web/src/components/schedule/schedule-jobs-grid.tsx
T
Denozordec 95d4305241
quality / commitlint (push) Skipped
quality / changes (push) Successful in 11s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 1m3s
quality / web (push) Successful in 1m58s
quality / go (push) Successful in 3m21s
quality / bird2 (push) Successful in 19s
CD / quality (push) Successful in 7m6s
CD / publish (push) Successful in 9m18s
feat(web): show job trigger and module details in jobs list
Колонка «Вид» в журнале задач (Операции → Задачи и Расписание) стала информативнее: подзаголовок с именами обновляемых модулей для tenant_refresh (из meta.module_ids), сообщение apply_summary для deploy_apply и бейдж триггера («по расписанию», «вручную», «после правки данных»). Поиск по журналу учитывает новые поля.

Бэкенд: meta.trigger теперь записывается для module_refresh и deploy_apply при ручных запусках (POST /v1/modules/{id}/refresh, /v1/apply, /v1/speakers/{id}/apply), а deploy_apply из цепочки обновления наследует триггер родительской задачи.
2026-09-25 16:16:53 +07:00

169 lines
5.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 { useMemo, useState } from 'react'
import { ListTodo, SearchIcon } from 'lucide-react'
import { DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
import {
ResourcePage,
createTextFilterQuery,
type DataGridColumnDef,
} from '@/components/reui-kit'
import { Badge } from '@/components/reui/badge'
import { isRefreshJobKind, jobKindRu, jobSubtitleRu, jobTriggerRu } from '@/lib/ui-labels'
import type { JobRow } from '@/types/api'
const FAILED_STATUSES = ['failed', 'error', 'cancelled']
const filterFields: FilterField[] = [
{
id: 'search',
label: 'Поиск',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
placeholder: 'Поиск задач…',
},
]
function tabFilter(item: JobRow, tabId: string): boolean {
if (tabId === 'refresh') return isRefreshJobKind(item.kind)
if (tabId === 'failed') return FAILED_STATUSES.includes(item.status.toLowerCase())
return true
}
export function ScheduleJobsGrid({
items,
nameById = new Map<string, string>(),
isLoading = false,
}: {
items: JobRow[]
nameById?: Map<string, string>
isLoading?: boolean
}) {
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
createTextFilterQuery('search'),
)
const tabs = useMemo(
() => [
{ id: 'all', label: 'Все', count: items.length },
{
id: 'refresh',
label: 'Обновление',
count: items.filter((job) => isRefreshJobKind(job.kind)).length,
},
{
id: 'failed',
label: 'С ошибкой',
count: items.filter((job) => FAILED_STATUSES.includes(job.status.toLowerCase())).length,
},
],
[items],
)
const columns = useMemo<DataGridColumnDef<JobRow>[]>(
() => [
{
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => {
const trigger = jobTriggerRu(row.original.meta?.trigger)
return (
<DataGridNameCell
icon={ListTodo}
title={
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate">{jobKindRu(row.original.kind)}</span>
{trigger ? (
<Badge variant="outline" size="sm" className="shrink-0 font-normal">
{trigger}
</Badge>
) : null}
</span>
}
subtitle={jobSubtitleRu(row.original.kind, row.original.meta, nameById)}
/>
)
},
meta: { headerTitle: 'Вид' },
},
{
accessorKey: 'status',
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
cell: ({ row }) => {
const finished = row.original.finished_at
const hint = finished
? new Date(finished).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
: undefined
return <StatusBadge status={row.original.status} hint={hint} />
},
meta: { headerTitle: 'Статус' },
},
{
id: 'created_at',
accessorFn: (row) => row.created_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Создана" />,
cell: ({ row }) => (
<DataGridMutedCell>
{row.original.created_at
? new Date(row.original.created_at).toLocaleString('ru-RU')
: '—'}
</DataGridMutedCell>
),
meta: { headerTitle: 'Создана' },
},
{
id: 'finished_at',
accessorFn: (row) => row.finished_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Завершена" />,
cell: ({ row }) => (
<DataGridMutedCell>
{row.original.finished_at
? new Date(row.original.finished_at).toLocaleString('ru-RU')
: '—'}
</DataGridMutedCell>
),
meta: { headerTitle: 'Завершена' },
},
{
accessorKey: 'error',
enableSorting: false,
header: 'Ошибка',
cell: ({ row }) => (
<span className="max-w-xs truncate text-xs text-destructive">
{row.original.error ?? ''}
</span>
),
meta: { headerTitle: 'Ошибка' },
},
],
[nameById],
)
return (
<ResourcePage
title="Задачи"
description="Последние задачи из API"
tabs={tabs}
tabFilter={tabFilter}
filterFields={filterFields}
filterQuery={filterQuery}
onFilterQueryChange={setFilterQuery}
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
getFilterFieldValue={(item, field) => {
if (field !== 'search') return undefined
const subtitle = jobSubtitleRu(item.kind, item.meta, nameById)
const trigger = jobTriggerRu(item.meta?.trigger)
return `${jobKindRu(item.kind)} ${item.status} ${item.error ?? ''} ${item.job_id} ${subtitle ?? ''} ${trigger ?? ''}`
}}
columns={columns}
data={items}
getRowId={(row) => row.job_id}
isLoading={isLoading}
virtualization={items.length > 80}
emptyState={{ title: 'Нет задач в выборке' }}
/>
)
}