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
220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
"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 }
|