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
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:
@@ -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}
|
||||
|
||||
@@ -1083,14 +1083,13 @@ 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">
|
||||
</p>
|
||||
</div>
|
||||
<DataCollectionSchedulerDataGrid rows={schedulerGridRows} />
|
||||
<Separator />
|
||||
<div className="space-y-3 px-5 py-4">
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,7 +1139,6 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
|
||||
{/* Rules table */}
|
||||
{rules.length > 0 ? (
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<FirewallScenarioRulesDataGrid
|
||||
rules={rules}
|
||||
onToggleEnabled={toggleEnabled}
|
||||
@@ -1141,7 +1146,6 @@ function ScenarioSheet({ open, onClose, initial, onSave }: {
|
||||
onMoveDown={(id) => moveRule(id, 1)}
|
||||
onRemove={removeRule}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
!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
@@ -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">
|
||||
|
||||
@@ -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() {
|
||||
Раздел работает в режиме "Живые данные". Переключи источник данных в настройках.
|
||||
</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>
|
||||
|
||||
|
||||
+21
-45
@@ -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>
|
||||
</DataPageCard>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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[] }) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user