fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s

This commit is contained in:
Denozordec
2026-06-30 22:22:51 +07:00
parent a1a9124f3d
commit d3a2d38b37
63 changed files with 8509 additions and 3652 deletions
@@ -0,0 +1,219 @@
"use client"
import { useMemo } from "react"
import {
type ColumnDef,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import type { SchedulerJobStatusDto } from "@/lib/scheduler-settings"
import { FormToggle } from "@/components/form-kit"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
} from "@/components/data-grids/shared/data-grid-layout"
import { cn } from "@/lib/utils"
import { RefreshCwIcon } from "lucide-react"
export interface SchedulerJobGridRow {
id: string
jobKey: string
label: string
description: string
fixedSchedule: boolean
enabled: boolean
intervalValue: string
intervalReadOnly: boolean
intervalDisabled: boolean
defaultInterval: number
job?: SchedulerJobStatusDto
onEnabledChange?: (enabled: boolean) => void
onIntervalChange: (value: string) => void
onRunNow: () => void
runNowLoading: boolean
saveBusy: boolean
}
interface DataCollectionSchedulerDataGridProps {
rows: SchedulerJobGridRow[]
}
function DataCollectionSchedulerDataGrid({ rows }: DataCollectionSchedulerDataGridProps) {
const columns = useMemo<ColumnDef<SchedulerJobGridRow>[]>(
() => [
{
id: "task",
accessorKey: "label",
header: () => <span className="text-xs font-medium text-muted-foreground">Задача</span>,
enableSorting: false,
cell: ({ row }) => (
<div className="align-top">
<span className="font-medium text-sm">{row.original.label}</span>
<p className="text-[11px] text-muted-foreground mt-0.5 leading-snug">
{row.original.description}
</p>
<p className="text-[11px] text-muted-foreground font-mono mt-1">{row.original.jobKey}</p>
</div>
),
meta: {
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: cn(DATA_GRID_CELL_PAD_FIRST, "align-top"),
},
},
{
id: "enabled",
accessorKey: "enabled",
header: () => (
<span className="text-xs font-medium text-muted-foreground text-center block">Вкл</span>
),
enableSorting: false,
cell: ({ row }) => {
const { fixedSchedule, enabled, saveBusy, onEnabledChange } = row.original
return (
<div className="text-center align-top">
<span className={fixedSchedule ? "inline-flex pointer-events-none opacity-50" : "inline-flex"}>
<FormToggle
checked={enabled}
disabled={fixedSchedule || saveBusy}
onChange={(v) => {
if (fixedSchedule || saveBusy) return
onEnabledChange?.(v)
}}
/>
</span>
</div>
)
},
size: 56,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: cn(DATA_GRID_CELL_PAD, "align-top") },
},
{
id: "interval",
accessorKey: "intervalValue",
header: () => <span className="text-xs font-medium text-muted-foreground">Интервал (с)</span>,
enableSorting: false,
cell: ({ row }) => {
const r = row.original
return (
<div className="w-28 align-top">
<Input
value={r.intervalValue}
onChange={(e) => r.onIntervalChange(e.target.value)}
className="h-8 text-sm tabular-nums"
inputMode="numeric"
readOnly={r.intervalReadOnly}
disabled={r.intervalDisabled}
placeholder={String(r.defaultInterval)}
/>
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: cn(DATA_GRID_CELL_PAD, "align-top") },
},
{
id: "lastRun",
accessorFn: (row) => row.job?.lastFinishedAt ?? "",
header: () => <span className="text-xs font-medium text-muted-foreground">Последний прогон</span>,
enableSorting: false,
cell: ({ row }) => {
const j = row.original.job
return (
<div className="text-xs text-muted-foreground align-top">
{j?.lastFinishedAt ? new Date(j.lastFinishedAt).toLocaleString("ru-RU") : "—"}
{j?.lastDurationMs != null && (
<span className="block text-[11px]">{j.lastDurationMs} мс</span>
)}
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: cn(DATA_GRID_CELL_PAD, "align-top") },
},
{
id: "status",
accessorFn: (row) => row.job?.lastStatus ?? "",
header: () => <span className="text-xs font-medium text-muted-foreground">Статус</span>,
enableSorting: false,
cell: ({ row }) => {
const j = row.original.job
return (
<div className="flex flex-wrap items-center gap-1.5 align-top">
{j?.running ? (
<Badge variant="secondary" className="text-[10px]">
выполняется
</Badge>
) : null}
{j?.lastStatus ? (
<Badge
variant="outline"
className={cn(
"text-[10px]",
j.lastStatus === "ok" && "border-emerald-500/40 text-emerald-700 dark:text-emerald-400",
j.lastStatus === "error" && "border-destructive/50 text-destructive",
)}
>
{j.lastStatus}
</Badge>
) : null}
{j?.lastError ? (
<span className="text-[10px] text-destructive max-w-[200px] truncate block" title={j.lastError}>
{j.lastError}
</span>
) : null}
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: cn(DATA_GRID_CELL_PAD, "align-top") },
},
{
id: "runNow",
header: () => (
<span className="text-xs font-medium text-muted-foreground text-right block">Сейчас</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
return (
<div className="text-right align-top">
<Button
size="sm"
variant="outline"
className="h-8"
disabled={r.job?.running || r.runNowLoading}
onClick={r.onRunNow}
>
<RefreshCwIcon className={cn("size-3.5", r.runNowLoading && "animate-spin")} />
</Button>
</div>
)
},
meta: {
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: cn(DATA_GRID_CELL_PAD_LAST, "align-top"),
},
},
],
[],
)
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.id,
})
return (
<DataGridShell
table={table}
recordCount={rows.length}
tableClassNames={{ bodyRow: "group/row hover:bg-muted/40 text-sm" }}
/>
)
}
export { DataCollectionSchedulerDataGrid, type DataCollectionSchedulerDataGridProps }