Files
EvoBGP/apps/web/src/components/schedule/schedule-modules-grid.tsx
T
Denozordec ecd48642de fix(web): align grid columns with reui reference and clean labels
Гриды операций и расписания выровнены по эталону data-grid-filtering-2:
явные размеры колонок (size в columnDef) вместо равномерного деления
при table-layout: fixed — колонка названия доминирует, второстепенные
сжаты по содержимому, пропали обрезанные имена («partner-dom…»,
«Обновлен...»). Убраны фейковые многоточия из JS-срезов: ID ревизии и
варианты выбора в сравнении теперь показываются целиком, а усечение
делает CSS-truncate. Подписи «Ревизия A/B» переведены на Field/FieldLabel
вместо самодельных span, шапка блока diff дополнена заголовком
«ревизия A → ревизия B» (CodeBlockTitle) слева от кнопок.
2026-09-25 22:14:02 +07:00

149 lines
4.8 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 { 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: 'Нет модулей' }}
/>
)
}