Docker images / prepare-release (push) Successful in 9s
Docker images / backend-test (push) Successful in 2m18s
Docker images / frontend-image (push) Successful in 4m37s
Docker images / updater-image (push) Successful in 46s
Docker images / backend-image (push) Successful in 3m6s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 16s
Сохранять снимки в S3-compatible бакет, скачивать и удалять их вместе с локальными файлами. Привести /backups к DNA /servers: Frame, KPI, фильтры и реальное восстановление. Co-authored-by: Cursor <[email protected]>
110 lines
2.7 KiB
TypeScript
110 lines
2.7 KiB
TypeScript
"use client"
|
|
|
|
import { ReactNode } from "react"
|
|
import { ListFilterIcon, SearchIcon } from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupInput,
|
|
} from "@/components/ui/input-group"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Filters,
|
|
type Filter,
|
|
type FilterFieldConfig,
|
|
} from "@/components/reui/filters"
|
|
import { SegmentedControl } from "@/components/form-kit"
|
|
import { DATA_PAGE_TOOLBAR_CLASS } from "@/components/data-grids/shared/data-grid-layout"
|
|
|
|
function DataPageToolbarFrame({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div className={cn(DATA_PAGE_TOOLBAR_CLASS, className)}>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface DataPageToolbarProps<T extends string = string> {
|
|
leading?: ReactNode
|
|
search?: string
|
|
onSearchChange?: (value: string) => void
|
|
searchPlaceholder?: string
|
|
segmented?: {
|
|
value: T
|
|
onChange: (value: T) => void
|
|
options: { value: T; label: string; count?: number }[]
|
|
}
|
|
filters?: Filter[]
|
|
onFiltersChange?: (filters: Filter[]) => void
|
|
filterFields?: FilterFieldConfig[]
|
|
countLabel?: string
|
|
actions?: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
function DataPageToolbar<T extends string = string>({
|
|
leading,
|
|
search,
|
|
onSearchChange,
|
|
searchPlaceholder = "Поиск…",
|
|
segmented,
|
|
filters,
|
|
onFiltersChange,
|
|
filterFields,
|
|
countLabel,
|
|
actions,
|
|
className,
|
|
}: DataPageToolbarProps<T>) {
|
|
return (
|
|
<DataPageToolbarFrame className={className}>
|
|
{leading}
|
|
{segmented && (
|
|
<SegmentedControl
|
|
value={segmented.value}
|
|
onChange={segmented.onChange}
|
|
options={segmented.options}
|
|
/>
|
|
)}
|
|
{filterFields && filters && onFiltersChange && (
|
|
<Filters
|
|
filters={filters}
|
|
fields={filterFields}
|
|
onChange={onFiltersChange}
|
|
size="sm"
|
|
trigger={
|
|
<Button type="button" variant="outline" size="sm">
|
|
<ListFilterIcon className="size-3.5" />
|
|
Фильтры
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
{onSearchChange != null && (
|
|
<InputGroup className="min-w-[220px] max-w-sm">
|
|
<InputGroupAddon>
|
|
<SearchIcon className="size-3.5" />
|
|
</InputGroupAddon>
|
|
<InputGroupInput
|
|
placeholder={searchPlaceholder}
|
|
value={search ?? ""}
|
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
/>
|
|
</InputGroup>
|
|
)}
|
|
{countLabel && (
|
|
<span className="text-sm text-muted-foreground ml-auto">{countLabel}</span>
|
|
)}
|
|
{actions}
|
|
</DataPageToolbarFrame>
|
|
)
|
|
}
|
|
|
|
export { DataPageToolbar, DataPageToolbarFrame, type DataPageToolbarProps }
|