quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 39s
quality / web (push) Successful in 1m29s
quality / go (push) Successful in 1m3s
quality / bird2 (push) Successful in 15s
CD / quality (push) Successful in 3m43s
CD / publish (push) Failing after 8m29s
Единый kitDataGridTableLayout и ResourcePage/FrameDataGrid на всех списках. Календарь задач переведён на EventCalendar, lookup — на cascader, у операций появился вид доски. Удалены settings-7 и самописные DataGridSection/toolbar. Co-authored-by: Cursor <[email protected]>
150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { useNavigate } from '@tanstack/react-router'
|
|
import { Boxes, SearchIcon } from 'lucide-react'
|
|
|
|
import { CategoryBadge } from '@/components/category-badge'
|
|
import { DataGridMonoCell, DataGridMutedCell, DataGridNameCell } from '@/components/data-grid-cell'
|
|
import { Badge } from '@/components/reui/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 type { LookupMatch } from '@/types/api'
|
|
|
|
/**
|
|
* Lookup matches grid — ResourcePage hideHeader inside wizard-2 Frame.
|
|
* @see https://reui.io/preview/base/wizard-2
|
|
* @see https://reui.io/preview/base/data-grid-filtering-2
|
|
* @see https://reui.io/docs/components/base/badge
|
|
*/
|
|
const filterFields: FilterField[] = [
|
|
{
|
|
id: 'search',
|
|
label: 'Поиск',
|
|
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
|
type: 'text',
|
|
placeholder: 'Фильтр совпадений…',
|
|
},
|
|
]
|
|
|
|
export function LookupMatchesGrid({
|
|
items,
|
|
isLoading = false,
|
|
}: {
|
|
items: LookupMatch[]
|
|
isLoading?: boolean
|
|
}) {
|
|
const navigate = useNavigate()
|
|
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
|
|
createTextFilterQuery('search'),
|
|
)
|
|
|
|
const columns = useMemo<DataGridColumnDef<LookupMatch>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: 'layer',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Слой" />,
|
|
cell: ({ row }) => (
|
|
<Badge
|
|
variant={row.original.layer === 'entry' ? 'info-light' : 'primary-light'}
|
|
size="sm"
|
|
>
|
|
{row.original.layer}
|
|
</Badge>
|
|
),
|
|
meta: { headerTitle: 'Слой' },
|
|
},
|
|
{
|
|
accessorKey: 'module_name',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Модуль" />,
|
|
cell: ({ row }) => (
|
|
<DataGridNameCell
|
|
icon={Boxes}
|
|
title={row.original.module_name}
|
|
subtitle={row.original.module_type}
|
|
/>
|
|
),
|
|
meta: { headerTitle: 'Модуль' },
|
|
},
|
|
{
|
|
accessorKey: 'matched_value',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Совпадение" />,
|
|
cell: ({ row }) => (
|
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
<DataGridMonoCell>{row.original.matched_value}</DataGridMonoCell>
|
|
<DataGridMutedCell>
|
|
{row.original.resolved_ip
|
|
? `${row.original.match_kind} · via ${row.original.resolved_ip}`
|
|
: row.original.match_kind}
|
|
</DataGridMutedCell>
|
|
</div>
|
|
),
|
|
meta: { headerTitle: 'Совпадение' },
|
|
},
|
|
{
|
|
id: 'community',
|
|
accessorFn: (row) => row.community_title || row.community || '',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Community" />,
|
|
cell: ({ row }) => {
|
|
const title = row.original.community_title?.trim()
|
|
const value = row.original.community?.trim()
|
|
if (!title && !value) {
|
|
return <DataGridMutedCell>—</DataGridMutedCell>
|
|
}
|
|
return (
|
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
<DataGridMonoCell>{title || value || '—'}</DataGridMonoCell>
|
|
{title && value && title !== value ? (
|
|
<DataGridMutedCell>{value}</DataGridMutedCell>
|
|
) : null}
|
|
</div>
|
|
)
|
|
},
|
|
meta: { headerTitle: 'Community' },
|
|
},
|
|
{
|
|
id: 'source',
|
|
enableSorting: false,
|
|
header: 'Источник',
|
|
cell: ({ row }) =>
|
|
row.original.source ? (
|
|
<CategoryBadge>{row.original.source}</CategoryBadge>
|
|
) : (
|
|
<DataGridMutedCell>—</DataGridMutedCell>
|
|
),
|
|
meta: { headerTitle: 'Источник' },
|
|
},
|
|
],
|
|
[],
|
|
)
|
|
|
|
return (
|
|
<ResourcePage
|
|
title="Совпадения"
|
|
description="Entries и snapshots · клик по строке открывает модуль"
|
|
hideHeader
|
|
filterFields={filterFields}
|
|
filterQuery={filterQuery}
|
|
onFilterQueryChange={setFilterQuery}
|
|
onClearFilters={() => setFilterQuery(createTextFilterQuery('search'))}
|
|
getFilterFieldValue={(item, field) => {
|
|
if (field !== 'search') return undefined
|
|
return `${item.layer} ${item.module_name} ${item.module_type} ${item.matched_value} ${item.community ?? ''} ${item.community_title ?? ''} ${item.source ?? ''}`
|
|
}}
|
|
columns={columns}
|
|
data={items}
|
|
getRowId={(row) =>
|
|
`${row.layer}|${row.module_id}|${row.match_kind}|${row.matched_value}|${row.entry_id ?? ''}|${row.source ?? ''}|${row.community_id ?? ''}`
|
|
}
|
|
isLoading={isLoading}
|
|
onRowClick={(row) =>
|
|
void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } })
|
|
}
|
|
emptyState={{ title: 'Нет совпадений' }}
|
|
/>
|
|
)
|
|
}
|