Гриды операций и расписания выровнены по эталону data-grid-filtering-2: явные размеры колонок (size в columnDef) вместо равномерного деления при table-layout: fixed — колонка названия доминирует, второстепенные сжаты по содержимому, пропали обрезанные имена («partner-dom…», «Обновлен...»). Убраны фейковые многоточия из JS-срезов: ID ревизии и варианты выбора в сравнении теперь показываются целиком, а усечение делает CSS-truncate. Подписи «Ревизия A/B» переведены на Field/FieldLabel вместо самодельных span, шапка блока diff дополнена заголовком «ревизия A → ревизия B» (CodeBlockTitle) слева от кнопок.
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { useMemo, useState } from 'react'
|
||
import { Boxes, RefreshCw, SearchIcon } from 'lucide-react'
|
||
|
||
import { CategoryBadge, ModeBadge } from '@/components/category-badge'
|
||
import { DataGridMonoCell, DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell'
|
||
import { LoadingButton } from '@/components/loading-button'
|
||
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 { moduleTypeRu } from '@/lib/ui-labels'
|
||
import type { ModuleRow } from '@/types/api'
|
||
|
||
const filterFields: FilterField[] = [
|
||
{
|
||
id: 'search',
|
||
label: 'Поиск',
|
||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||
type: 'text',
|
||
placeholder: 'Поиск модулей…',
|
||
},
|
||
]
|
||
|
||
export function ScheduleModulesGrid({
|
||
items,
|
||
refreshing,
|
||
onRefresh,
|
||
isLoading = false,
|
||
isError = false,
|
||
error = null,
|
||
onRetry,
|
||
}: {
|
||
items: ModuleRow[]
|
||
refreshing: Record<string, boolean>
|
||
onRefresh: (id: string) => void
|
||
isLoading?: boolean
|
||
isError?: boolean
|
||
error?: Error | null
|
||
onRetry?: () => void
|
||
}) {
|
||
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
|
||
createTextFilterQuery('search'),
|
||
)
|
||
|
||
const columns = useMemo<DataGridColumnDef<ModuleRow>[]>(
|
||
() => [
|
||
{
|
||
accessorKey: 'name',
|
||
header: ({ column }) => <DataGridColumnHeader column={column} title="Модуль" />,
|
||
cell: ({ row }) => <DataGridNameCell icon={Boxes} title={row.original.name} />,
|
||
meta: { headerTitle: 'Модуль' },
|
||
size: 280,
|
||
},
|
||
{
|
||
accessorKey: 'type',
|
||
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
|
||
cell: ({ row }) => <CategoryBadge>{moduleTypeRu(row.original.type)}</CategoryBadge>,
|
||
meta: { headerTitle: 'Тип' },
|
||
size: 150,
|
||
},
|
||
{
|
||
id: 'schedule',
|
||
accessorFn: (row) => row.cron_expr ?? String(row.refresh_interval_sec ?? ''),
|
||
header: ({ column }) => <DataGridColumnHeader column={column} title="Расписание" />,
|
||
cell: ({ row }) => (
|
||
<DataGridMonoCell className="text-xs text-muted-foreground">
|
||
{row.original.cron_expr ??
|
||
(row.original.refresh_interval_sec ? `${row.original.refresh_interval_sec} с` : '—')}
|
||
</DataGridMonoCell>
|
||
),
|
||
meta: { headerTitle: 'Расписание' },
|
||
size: 130,
|
||
},
|
||
{
|
||
id: 'last_refreshed_at',
|
||
accessorFn: (row) => row.last_refreshed_at ?? '',
|
||
header: ({ column }) => <DataGridColumnHeader column={column} title="Обновлено" />,
|
||
cell: ({ row }) => (
|
||
<DataGridMutedCell>
|
||
{row.original.last_refreshed_at
|
||
? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU')
|
||
: '—'}
|
||
</DataGridMutedCell>
|
||
),
|
||
meta: { headerTitle: 'Обновлено' },
|
||
size: 190,
|
||
},
|
||
{
|
||
id: 'enabled',
|
||
accessorFn: (row) => (row.enabled ? 'on' : 'off'),
|
||
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
|
||
cell: ({ row }) => (
|
||
<ModeBadge enabled={row.original.enabled} onLabel="Вкл" offLabel="Выкл" />
|
||
),
|
||
meta: { headerTitle: 'Статус' },
|
||
size: 110,
|
||
},
|
||
{
|
||
id: 'actions',
|
||
enableSorting: false,
|
||
size: 104,
|
||
header: () => null,
|
||
cell: ({ row }) => (
|
||
<div className="text-right">
|
||
<LoadingButton
|
||
size="sm"
|
||
variant="secondary"
|
||
type="button"
|
||
loading={!!refreshing[row.original.id]}
|
||
onClick={() => onRefresh(row.original.id)}
|
||
>
|
||
<RefreshCw />
|
||
Обновить
|
||
</LoadingButton>
|
||
</div>
|
||
),
|
||
},
|
||
],
|
||
[onRefresh, refreshing],
|
||
)
|
||
|
||
return (
|
||
<ResourcePage
|
||
title="Модули"
|
||
description="Расписание обновления и ручной запуск"
|
||
filterFields={filterFields}
|
||
filterQuery={filterQuery}
|
||
onFilterQueryChange={setFilterQuery}
|
||
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
|
||
getFilterFieldValue={(item, field) => {
|
||
if (field !== 'search') return undefined
|
||
return `${item.name} ${item.type} ${item.enabled ? 'вкл' : 'выкл'}`
|
||
}}
|
||
columns={columns}
|
||
data={items}
|
||
getRowId={(row) => row.id}
|
||
isLoading={isLoading}
|
||
isError={isError}
|
||
error={error}
|
||
onRetry={onRetry}
|
||
pinLastColumn
|
||
emptyState={{ title: 'Нет модулей' }}
|
||
/>
|
||
)
|
||
}
|