feat: refactor components to utilize DataGridCard for improved UI consistency
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 57s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m27s
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 57s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m27s
Updated various components including AccessApiKeysCard, FirewallClientsGrid, FirewallRulesGrid, ModuleEntriesSection, and DirectoriesComponent to replace traditional card and table structures with the new DataGridCard component. This change enhances the user interface by providing a more consistent layout and improved loading states. Additionally, integrated QueryState for better handling of loading and error scenarios across these components, streamlining the overall user experience.
This commit is contained in:
1 parent
5d1102b497
commit
b321aa5321
33 files changed
+2281
-1330
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Pencil, Plus, Trash2 } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
@@ -13,25 +13,16 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@evobgp/ui/components/alert-dialog'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@evobgp/ui/components/table'
|
||||
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
import { ModuleEntriesGrid, type ModuleEntryDeleteTarget } from '@/components/modules/module-entries-grid'
|
||||
import { ModuleAsEntryDialog } from '@/components/modules/module-as-entry-dialog'
|
||||
import { ModuleCdnSourceDialog } from '@/components/modules/module-cdn-source-dialog'
|
||||
import { ModuleDomainEntryDialog } from '@/components/modules/module-domain-entry-dialog'
|
||||
import { ModuleIpRangeEntryDialog } from '@/components/modules/module-ip-range-entry-dialog'
|
||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||
import { communityLabel } from '@/lib/modules/helpers'
|
||||
import { formatDateTime } from '@/lib/modules/display'
|
||||
import type {
|
||||
AsEntry,
|
||||
BgpCommunity,
|
||||
@@ -53,11 +44,7 @@ interface ModuleEntriesSectionProps {
|
||||
onChanged: () => void | Promise<void>
|
||||
}
|
||||
|
||||
type DeleteTarget =
|
||||
| { kind: 'domain'; entry: DomainEntry }
|
||||
| { kind: 'ip-range'; entry: IpRangeEntry }
|
||||
| { kind: 'cdn'; entry: CdnSource }
|
||||
| { kind: 'as'; entry: AsEntry }
|
||||
type DeleteTarget = ModuleEntryDeleteTarget
|
||||
|
||||
const CARD_META: Record<
|
||||
ModuleRow['type'],
|
||||
@@ -150,49 +137,45 @@ export function ModuleEntriesSection({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3 border-b py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle className="text-base">{meta.title}</CardTitle>
|
||||
<CardDescription>{meta.description}</CardDescription>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 self-start sm:self-auto">
|
||||
<Button size="sm" onClick={openCreate}>
|
||||
<Plus />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<QueryState
|
||||
data={items}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
empty={items.length === 0}
|
||||
emptyTitle={meta.emptyTitle}
|
||||
emptyDescription={meta.emptyDescription}
|
||||
skeleton={<TableSkeleton rows={5} cols={3} />}
|
||||
onRetry={onRetry}
|
||||
>
|
||||
{(rows) => (
|
||||
<EntriesTable
|
||||
mod={mod}
|
||||
rows={rows}
|
||||
communities={communities}
|
||||
onEdit={(target) => {
|
||||
if (target.kind === 'domain') setEditDomain(target.entry)
|
||||
if (target.kind === 'ip-range') setEditIpRange(target.entry)
|
||||
if (target.kind === 'cdn') setEditCdn(target.entry)
|
||||
if (target.kind === 'as') setEditAs(target.entry)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
onDelete={setDeleteTarget}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DataGridCard
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
actions={
|
||||
<Button size="sm" type="button" onClick={openCreate}>
|
||||
<Plus />
|
||||
Добавить
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<QueryState
|
||||
data={items}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
empty={items.length === 0}
|
||||
emptyTitle={meta.emptyTitle}
|
||||
emptyDescription={meta.emptyDescription}
|
||||
skeleton={<TableSkeleton rows={5} cols={3} />}
|
||||
onRetry={onRetry}
|
||||
>
|
||||
{(rows) => (
|
||||
<ModuleEntriesGrid
|
||||
mod={mod}
|
||||
rows={rows}
|
||||
communities={communities}
|
||||
isLoading={isLoading}
|
||||
onEdit={(target) => {
|
||||
if (target.kind === 'domain') setEditDomain(target.entry)
|
||||
if (target.kind === 'ip-range') setEditIpRange(target.entry)
|
||||
if (target.kind === 'cdn') setEditCdn(target.entry)
|
||||
if (target.kind === 'as') setEditAs(target.entry)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
onDelete={setDeleteTarget}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
|
||||
{mod.type === 'DOMAINS' ? (
|
||||
<ModuleDomainEntryDialog
|
||||
@@ -270,168 +253,3 @@ function deleteDescription(target: DeleteTarget | null): string {
|
||||
return `AS${target.entry.asn}`
|
||||
}
|
||||
}
|
||||
|
||||
function EntriesTable({
|
||||
mod,
|
||||
rows,
|
||||
communities,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
mod: ModuleRow
|
||||
rows: Record<string, unknown>[]
|
||||
communities: BgpCommunity[]
|
||||
onEdit: (target: DeleteTarget) => void
|
||||
onDelete: (target: DeleteTarget) => void
|
||||
}) {
|
||||
if (mod.type === 'DOMAINS') {
|
||||
const entries = rows as unknown as DomainEntry[]
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>FQDN</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell className="font-mono text-sm">{entry.fqdn}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{communityLabel(entry.community_id, communities)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RowActions
|
||||
onEdit={() => onEdit({ kind: 'domain', entry })}
|
||||
onDelete={() => onDelete({ kind: 'domain', entry })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
if (mod.type === 'IP_RANGES') {
|
||||
const entries = rows as unknown as IpRangeEntry[]
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Префикс (CIDR)</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell className="font-mono text-sm">{entry.prefix}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{communityLabel(entry.community_id, communities)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RowActions
|
||||
onEdit={() => onEdit({ kind: 'ip-range', entry })}
|
||||
onDelete={() => onDelete({ kind: 'ip-range', entry })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
if (mod.type === 'CDN_CIDRS') {
|
||||
const entries = rows as unknown as CdnSource[]
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead>Тип</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead>Обновлено</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell className="max-w-xs truncate font-mono text-xs">{entry.url}</TableCell>
|
||||
<TableCell className="text-sm">{entry.source_kind}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{communityLabel(entry.community_id, communities)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{formatDateTime(entry.last_refreshed_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RowActions
|
||||
onEdit={() => onEdit({ kind: 'cdn', entry })}
|
||||
onDelete={() => onDelete({ kind: 'cdn', entry })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
const entries = rows as unknown as AsEntry[]
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ASN</TableHead>
|
||||
<TableHead>Имя</TableHead>
|
||||
<TableHead>Префиксов</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell className="font-mono text-sm">{entry.asn}</TableCell>
|
||||
<TableCell className="text-sm">{entry.asn_name ?? '—'}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{entry.prefix_count ?? '—'}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{communityLabel(entry.community_id, communities)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RowActions
|
||||
onEdit={() => onEdit({ kind: 'as', entry })}
|
||||
onDelete={() => onDelete({ kind: 'as', entry })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
function RowActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () => void }) {
|
||||
return (
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onEdit} aria-label="Редактировать">
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-destructive"
|
||||
onClick={onDelete}
|
||||
aria-label="Удалить"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user