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

This commit is contained in:
Denozordec
2026-06-30 22:36:00 +07:00
parent d3a2d38b37
commit d2de8d3188
16 changed files with 162 additions and 214 deletions
+15 -58
View File
@@ -9,6 +9,7 @@ import type { CertStatus, Server } from "@/lib/data"
import type { CertificateDto } from "@mmapp/contracts/certificates"
import { Card, CardContent } from "@/components/ui/card"
import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar } from "@/components/data-page-toolbar"
import { CertificatesDataGrid } from "@/components/data-grids/certificates-data-grid"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -36,7 +37,6 @@ import {
} from "@/shared/api/certificates"
import { toast } from "sonner"
import {
SearchIcon,
ShieldCheckIcon,
BadgeCheckIcon,
AlertTriangleIcon,
@@ -237,58 +237,6 @@ function CertPartAcmeSettings({
)
}
function CertPartTableToolbar({
search,
setSearch,
statusFilter,
setStatusFilter,
filteredCount,
}: {
search: string
setSearch: (v: string) => void
statusFilter: CertStatus | "all"
setStatusFilter: (v: CertStatus | "all") => void
filteredCount: number
}) {
return (
<div className="flex items-center gap-3 px-4 py-3 border-b flex-wrap">
<div className="flex items-center gap-2 h-8 px-3 border border-input rounded-md bg-background min-w-[240px]">
<SearchIcon className="size-3.5 text-muted-foreground shrink-0" />
<input
className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground text-sm"
placeholder="Поиск по имени, CN, эмитенту…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5">
{(["all", "valid", "expired", "revoked"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setStatusFilter(s)}
className={cn(
"px-3 py-1 text-xs rounded whitespace-nowrap transition-colors",
statusFilter === s
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{s === "all"
? "Все"
: s === "valid"
? "Действующие"
: s === "expired"
? "Истёкшие"
: "Отозванные"}
</button>
))}
</div>
<span className="text-sm text-muted-foreground ml-auto">{filteredCount} сертификатов</span>
</div>
)
}
function CertPartReference() {
return (
<Card>
@@ -747,12 +695,21 @@ export default function CertificatesPage() {
)}
<DataPageCard>
<CertPartTableToolbar
<DataPageToolbar
search={search}
setSearch={setSearch}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
filteredCount={filtered.length}
onSearchChange={setSearch}
searchPlaceholder="Поиск по имени, CN, эмитенту…"
segmented={{
value: statusFilter,
onChange: setStatusFilter,
options: [
{ value: "all", label: "Все" },
{ value: "valid", label: "Действующие" },
{ value: "expired", label: "Истёкшие" },
{ value: "revoked", label: "Отозванные" },
],
}}
countLabel={`${filtered.length} сертификатов`}
/>
<CertificatesDataGrid
certificates={filtered}
+10 -12
View File
@@ -1083,17 +1083,16 @@ export default function DataCollectionPage() {
))}
</div>
<Card>
<CardHeader className="border-b border-border pb-4">
<CardTitle className="text-base">Планировщик сбора данных</CardTitle>
<CardDescription className="text-xs">
<DataPageCard>
<div className="border-b border-border px-5 py-4">
<p className="text-base font-medium">Планировщик сбора данных</p>
<p className="text-xs text-muted-foreground mt-1">
Интервалы и вкл/выкл по задачам. Переключатель сразу сохраняет задачу на бекенде; кнопка ниже интервалы и срок хранения.
</CardDescription>
</CardHeader>
<CardContent className="px-0 pb-0">
<DataCollectionSchedulerDataGrid rows={schedulerGridRows} />
<Separator />
<div className="space-y-3 px-5 py-4">
</p>
</div>
<DataCollectionSchedulerDataGrid rows={schedulerGridRows} />
<Separator />
<div className="space-y-3 px-5 py-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<p className="text-xs text-muted-foreground mb-1.5">Хранение сэмплов трафика (дней)</p>
@@ -1202,8 +1201,7 @@ export default function DataCollectionPage() {
</p>
</div>
</div>
</CardContent>
</Card>
</DataPageCard>
<Card>
<CardHeader className="border-b border-border flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
+2 -2
View File
@@ -1805,7 +1805,7 @@ export default function FiltersPage() {
<DataPageCard>
{/* selected server header */}
<div className="flex items-center gap-2.5 px-4 py-3 border-b bg-muted/10">
<div className="flex items-center gap-2.5 px-5 py-3 border-b bg-muted/10">
<StatusDot status={selectedServer.status} pulse={selectedServer.status === "online"} />
<Flag code={selectedServer.country} size={16} />
<span className="font-mono text-sm font-semibold">{selectedServer.name}</span>
@@ -1880,7 +1880,7 @@ export default function FiltersPage() {
{/* add rule shortcut */}
<button onClick={openCreate}
className="w-full flex items-center gap-2 px-4 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
className="w-full flex items-center gap-2 px-5 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
<PlusIcon className="size-3.5" />
Добавить правило для {selectedServer.name}
</button>
+45 -52
View File
@@ -9,11 +9,17 @@ import {
} from "@/components/data-grids/firewall-rules-data-grid"
import { FirewallScenarioRulesDataGrid } from "@/components/data-grids/firewall-scenario-rules-data-grid"
import { DataPageCard } from "@/components/data-page-card"
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
import { DataPageToolbarFrame } from "@/components/data-page-toolbar"
import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit"
import { firewallRules, type FirewallRule } from "@/lib/data"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import {
Sheet, SheetContent, SheetHeader, SheetTitle,
SheetDescription, SheetFooter, SheetClose,
@@ -1133,15 +1139,13 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
{/* Rules table */}
{rules.length > 0 ? (
<div className="rounded-lg border overflow-hidden">
<FirewallScenarioRulesDataGrid
rules={rules}
onToggleEnabled={toggleEnabled}
onMoveUp={(id) => moveRule(id, -1)}
onMoveDown={(id) => moveRule(id, 1)}
onRemove={removeRule}
/>
</div>
<FirewallScenarioRulesDataGrid
rules={rules}
onToggleEnabled={toggleEnabled}
onMoveUp={(id) => moveRule(id, -1)}
onMoveDown={(id) => moveRule(id, 1)}
onRemove={removeRule}
/>
) : (
!addOpen && (
<div className="text-center py-6 text-sm text-muted-foreground border rounded-lg border-dashed">
@@ -1784,52 +1788,41 @@ export default function FirewallPage() {
<SimulatorTab rules={rules} />
) : (
<DataPageCard>
{/* toolbar */}
<div className="flex items-center gap-3 px-5 py-3 border-b flex-wrap">
{/* IP family selector */}
<div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5">
{(["all", "ip", "ip6"] as IpFamily[]).map((f) => (
<button key={f} onClick={() => setIpFamily(f)}
className={cn(
"flex items-center gap-1.5 rounded px-3 py-1 text-xs transition-colors whitespace-nowrap",
ipFamily === f
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}>
{IP_FAMILY_LABELS[f]}
</button>
))}
</div>
{/* chain sub-tabs */}
<div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5">
{[{ value: "all", label: "Все" }, ...chainsInGroup.map((c) => ({ value: c, label: c }))].map((t) => (
<button key={t.value} onClick={() => setChainFilter(t.value)}
className={cn(
"flex items-center gap-1.5 rounded px-3 py-1 text-xs transition-colors",
chainFilter === t.value
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}>
{t.label}
<span className="opacity-50">{chainCounts[t.value] ?? 0}</span>
</button>
))}
</div>
{/* search */}
<div className="flex items-center gap-2 h-8 px-3 border border-input rounded-md bg-background min-w-[220px]">
<SearchIcon className="size-3.5 text-muted-foreground shrink-0" />
<input
className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground text-sm"
<DataPageToolbarFrame>
<SegmentedControl
value={ipFamily}
onChange={setIpFamily}
options={(["all", "ip", "ip6"] as IpFamily[]).map((f) => ({
value: f,
label: IP_FAMILY_LABELS[f],
}))}
/>
<SegmentedControl
value={chainFilter}
onChange={setChainFilter}
options={[
{ value: "all", label: "Все", count: chainCounts.all ?? 0 },
...chainsInGroup.map((c) => ({
value: c,
label: c,
count: chainCounts[c] ?? 0,
})),
]}
/>
<InputGroup className="min-w-[220px] max-w-sm">
<InputGroupAddon>
<SearchIcon className="size-3.5" />
</InputGroupAddon>
<InputGroupInput
placeholder="Поиск по адресу, действию…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<span className="text-sm text-muted-foreground ml-auto">{filteredRules.length} правил</span>
</div>
</InputGroup>
<span className="text-sm text-muted-foreground ml-auto">
{filteredRules.length} правил
</span>
</DataPageToolbarFrame>
<FirewallRulesDataGrid rules={filteredRules} onToggle={toggleRule} onEdit={openEdit} />
</DataPageCard>
+21 -21
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from "react"
import { PageHeader } from "@/components/page-header"
import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-grid"
import { GrePoolsDataGrid } from "@/components/data-grids/gre-pools-data-grid"
import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit"
@@ -25,7 +26,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { Flag } from "@/components/flag"
import {
PlusIcon, SearchIcon, RefreshCwIcon, MoreHorizontalIcon,
PlusIcon, RefreshCwIcon, MoreHorizontalIcon,
LockIcon, LockOpenIcon, ShieldCheckIcon, NetworkIcon,
EyeIcon, EyeOffIcon, ChevronDownIcon, ChevronRightIcon,
CodeXmlIcon, PencilIcon, PowerIcon, Trash2Icon, CopyIcon, CheckIcon,
@@ -454,23 +455,21 @@ export default function GrePage() {
{/* ── Tunnels ── */}
{pageTab === "tunnels" && (
<DataPageCard>
<div className="flex items-center gap-3 px-5 py-3 border-b flex-wrap">
<div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5">
{tunnelTabs.map((t) => (
<button key={t.value} onClick={() => setTabFilter(t.value)}
className={`flex items-center gap-1.5 rounded px-3 py-1 text-sm transition-colors ${tabFilter === t.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>
{t.label}
<span className="text-xs tabular-nums opacity-60">{t.count}</span>
</button>
))}
</div>
<div className="flex items-center gap-2 h-8 px-3 border border-input rounded-md bg-background min-w-[220px]">
<SearchIcon className="size-3.5 text-muted-foreground shrink-0" />
<input className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground text-sm"
placeholder="Поиск по имени, IP…" value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<span className="text-sm text-muted-foreground ml-auto">{filtered.length} туннелей</span>
</div>
<DataPageToolbar
segmented={{
value: tabFilter,
onChange: setTabFilter,
options: tunnelTabs.map((t) => ({
value: t.value,
label: t.label,
count: t.count,
})),
}}
search={search}
onSearchChange={setSearch}
searchPlaceholder="Поиск по имени, IP…"
countLabel={`${filtered.length} туннелей`}
/>
<GreTunnelsDataGrid
tunnels={filtered}
@@ -484,12 +483,13 @@ export default function GrePage() {
{/* ── IP Pools ── */}
{pageTab === "pools" && (
<DataPageCard>
<div className="flex items-center justify-between px-5 py-3 border-b">
<DataPageToolbarFrame className="justify-between">
<span className="text-sm text-muted-foreground">{displayPools.length} пула</span>
<Button size="sm" variant="outline" onClick={() => { setPForm(defaultPoolForm); setPoolOpen(true) }}>
<PlusIcon className="size-4" />Добавить пул
<PlusIcon className="size-4" />
Добавить пул
</Button>
</div>
</DataPageToolbarFrame>
<GrePoolsDataGrid pools={displayPools} />
<div className="border-t px-5 py-4">
+5 -4
View File
@@ -8,6 +8,7 @@ import {
inferCountry,
} from "@/components/data-grids/recursive-routes-data-grid"
import { FormField, SectionTitle } from "@/components/form-kit"
import { DataPageCard } from "@/components/data-page-card"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -654,9 +655,9 @@ export default function RecursiveRoutesPage() {
Раздел работает в режиме &quot;Живые данные&quot;. Переключи источник данных в настройках.
</Card>
) : (
<Card className="overflow-hidden py-0 gap-0">
<DataPageCard>
{currentServer && (
<div className="flex items-center gap-2.5 px-4 py-3 border-b bg-muted/10">
<div className="flex items-center gap-2.5 px-5 py-3 border-b bg-muted/10">
<StatusDot status={currentServer.status} pulse={currentServer.status === "online"} />
<Flag code={currentServer.country} size={16} />
<span className="font-mono text-sm font-semibold">{currentServer.name}</span>
@@ -683,11 +684,11 @@ export default function RecursiveRoutesPage() {
onDelete={(g) => setRows((prev) => prev.filter((r) => groupKeyOf(r) !== g.key))}
/>
<button onClick={openCreate}
className="w-full flex items-center gap-2 px-4 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
className="w-full flex items-center gap-2 px-5 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
<PlusIcon className="size-3.5" />
Добавить маршрут
</button>
</Card>
</DataPageCard>
)}
</div>
+22 -46
View File
@@ -11,6 +11,7 @@ import { Flag } from "@/components/flag"
import { StatusDot } from "@/components/status-dot"
import { Sparkline } from "@/components/sparkline"
import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar } from "@/components/data-page-toolbar"
import {
UptimeResourcesDataGrid,
type UptimeResourceRow,
@@ -981,48 +982,25 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
))}
</div>
{/* ── Toolbar ───────────────────────────────────────────────────────── */}
<div className="flex items-center gap-3 flex-wrap">
{/* Type filter */}
<div className="flex items-center gap-0.5 rounded-md border border-border bg-muted/40 p-0.5 shrink-0">
{typeOpts.map(o => (
<button key={o.value} onClick={() => setTypeFilter(o.value)}
className={cn(
"px-2.5 py-1 text-xs rounded transition-colors whitespace-nowrap",
typeFilter === o.value
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}>
{o.label}
</button>
))}
</div>
{/* Search */}
<div className="relative min-w-[180px] flex-1 max-w-xs">
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
<Input className="pl-8 h-8 text-sm" placeholder="Поиск по имени, площадке…"
value={resSearch} onChange={e => setResSearch(e.target.value)} />
{resSearch && (
<button onClick={() => setResSearch("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
<XIcon className="size-3.5" />
</button>
)}
</div>
<span className="text-xs text-muted-foreground ml-auto shrink-0">
{visible.length} из {rows.length} серверов
</span>
{/* Export CSV */}
<Button variant="outline" size="sm" className="h-8 gap-1.5 shrink-0" onClick={exportCsv}>
<DownloadIcon className="size-3.5" />CSV
</Button>
</div>
{/* ── Table ─────────────────────────────────────────────────────────── */}
<DataPageCard>
<DataPageToolbar
segmented={{
value: typeFilter,
onChange: setTypeFilter,
options: typeOpts,
}}
search={resSearch}
onSearchChange={setResSearch}
searchPlaceholder="Поиск по имени, площадке…"
countLabel={`${visible.length} из ${rows.length} серверов`}
actions={
<Button variant="outline" size="sm" className="h-8 gap-1.5 shrink-0" onClick={exportCsv}>
<DownloadIcon className="size-3.5" />
CSV
</Button>
}
/>
<UptimeResourcesDataGrid rows={visible} />
</DataPageCard>
@@ -2273,15 +2251,13 @@ export default function UptimePage() {
{/* ── history ── */}
{speedRuns.length > 0 && (
<Card className="overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b">
<DataPageCard>
<div className="flex items-center justify-between px-5 py-3 border-b">
<p className="text-sm font-medium">История тестов</p>
<span className="text-xs text-muted-foreground">{speedRuns.length} запусков</span>
</div>
<div className="overflow-x-auto">
<UptimeSpeedHistoryDataGrid runs={speedRuns} servers={allServers} />
</div>
</Card>
<UptimeSpeedHistoryDataGrid runs={speedRuns} servers={allServers} />
</DataPageCard>
)}
</div>
+11 -9
View File
@@ -36,7 +36,6 @@ interface CompactDataGridProps<T extends { id: string }> {
emptyDescription?: string
onRowClick?: (row: T) => void
getExpandedContent?: (row: T) => ReactNode
compact?: boolean
}
function CompactDataGrid<T extends { id: string }>({
@@ -47,12 +46,7 @@ function CompactDataGrid<T extends { id: string }>({
emptyDescription,
onRowClick,
getExpandedContent,
compact = false,
}: CompactDataGridProps<T>) {
const pad = compact ? "py-2" : DATA_GRID_CELL_PAD
const padFirst = compact ? "pl-4 py-2" : DATA_GRID_CELL_PAD_FIRST
const padLast = compact ? "pr-4 py-2" : DATA_GRID_CELL_PAD_LAST
const columnDefs = useMemo<ColumnDef<T>[]>(
() =>
columns.map((col, index) => ({
@@ -67,16 +61,24 @@ function CompactDataGrid<T extends { id: string }>({
headerTitle: col.header,
headerClassName:
col.headerClassName ??
(index === 0 ? padFirst : index === columns.length - 1 ? padLast : pad),
(index === 0
? DATA_GRID_CELL_PAD_FIRST
: index === columns.length - 1
? DATA_GRID_CELL_PAD_LAST
: DATA_GRID_CELL_PAD),
cellClassName:
col.cellClassName ??
(index === 0 ? padFirst : index === columns.length - 1 ? padLast : pad),
(index === 0
? DATA_GRID_CELL_PAD_FIRST
: index === columns.length - 1
? DATA_GRID_CELL_PAD_LAST
: DATA_GRID_CELL_PAD),
...(getExpandedContent && index === 0
? { expandedContent: getExpandedContent }
: {}),
},
})),
[columns, getExpandedContent, pad, padFirst, padLast],
[columns, getExpandedContent],
)
const table = useReactTable({
@@ -180,7 +180,6 @@ function DashboardActiveProbesDataGrid({
<CompactDataGrid
data={probes}
columns={columns}
compact
isLoading={isLoading}
emptyTitle={emptyTitle}
emptyDescription={emptyDescription}
@@ -159,7 +159,6 @@ function SettingsAccessSummaryDataGrid({
<CompactDataGrid
data={users}
columns={columns}
compact
emptyTitle="Нет пользователей"
/>
)
@@ -16,7 +16,11 @@ const DEFAULT_DATA_GRID_CLASS_NAMES: NonNullable<DataGridProps<object>["tableCla
bodyRow: "group/row",
}
const DATA_GRID_CONTAINER_CLASS = "rounded-none border-0"
const DATA_GRID_CONTAINER_CLASS = "w-full rounded-none border-0"
const DATA_PAGE_CARD_CLASS = "flex w-full flex-col overflow-hidden py-0 gap-0"
const DATA_PAGE_TOOLBAR_CLASS = "flex items-center gap-3 px-5 py-3 border-b flex-wrap"
export {
DATA_GRID_CELL_PAD,
@@ -25,4 +29,6 @@ export {
DEFAULT_DATA_GRID_LAYOUT,
DEFAULT_DATA_GRID_CLASS_NAMES,
DATA_GRID_CONTAINER_CLASS,
DATA_PAGE_CARD_CLASS,
DATA_PAGE_TOOLBAR_CLASS,
}
@@ -42,6 +42,7 @@ function DataGridShell<TData extends object>({
}: DataGridShellProps<TData>) {
return (
<DataGrid
className="w-full"
table={table}
recordCount={recordCount}
isLoading={isLoading}
+2 -2
View File
@@ -57,9 +57,9 @@ function SnapshotErrorCell({ error, className }: { error?: string; className?: s
}
function SnapshotDataGrid<T extends SnapshotRow>(
props: Omit<CompactDataGridProps<T>, "compact">,
props: CompactDataGridProps<T>,
) {
return <CompactDataGrid {...props} compact />
return <CompactDataGrid {...props} />
}
function TrafficSnapshotGrid({ servers }: { servers: TrafficServerSnapshot[] }) {
+2 -1
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from "react"
import { Card } from "@/components/ui/card"
import { DATA_PAGE_CARD_CLASS } from "@/components/data-grids/shared/data-grid-layout"
import { cn } from "@/lib/utils"
interface DataPageCardProps {
@@ -9,7 +10,7 @@ interface DataPageCardProps {
function DataPageCard({ children, className }: DataPageCardProps) {
return (
<Card className={cn("overflow-hidden py-0 gap-0", className)}>
<Card className={cn(DATA_PAGE_CARD_CLASS, className)}>
{children}
</Card>
)
+18 -3
View File
@@ -14,6 +14,21 @@ import {
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> {
search?: string
@@ -45,7 +60,7 @@ function DataPageToolbar<T extends string = string>({
className,
}: DataPageToolbarProps<T>) {
return (
<div className={cn("flex items-center gap-3 px-5 py-3 border-b flex-wrap", className)}>
<DataPageToolbarFrame className={className}>
{segmented && (
<SegmentedControl
value={segmented.value}
@@ -77,8 +92,8 @@ function DataPageToolbar<T extends string = string>({
<span className="text-sm text-muted-foreground ml-auto">{countLabel}</span>
)}
{actions}
</div>
</DataPageToolbarFrame>
)
}
export { DataPageToolbar, type DataPageToolbarProps }
export { DataPageToolbar, DataPageToolbarFrame, type DataPageToolbarProps }
+1 -1
View File
File diff suppressed because one or more lines are too long