diff --git a/apps/web/src/components/access/api-key-create-dialog.tsx b/apps/web/src/components/access/api-key-create-dialog.tsx
index 750ec86..47ed82c 100644
--- a/apps/web/src/components/access/api-key-create-dialog.tsx
+++ b/apps/web/src/components/access/api-key-create-dialog.tsx
@@ -2,16 +2,10 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels'
@@ -68,48 +62,48 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea
}
return (
-
+ >
+ }
+ >
+
+
+ setName(e.target.value)}
+ placeholder="CI / оператор UI"
+ />
+
+ v && setRole(v as ApiKeyRole)}
+ />
+
+
+ setExpiresLocal(e.target.value)}
+ />
+
+
)
}
diff --git a/apps/web/src/components/confirm-dialog.tsx b/apps/web/src/components/confirm-dialog.tsx
index 76b3860..87acb56 100644
--- a/apps/web/src/components/confirm-dialog.tsx
+++ b/apps/web/src/components/confirm-dialog.tsx
@@ -1,53 +1,126 @@
+import { Button } from '@evobgp/ui/components/button'
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from '@evobgp/ui/components/alert-dialog'
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from '@evobgp/ui/components/drawer'
import type { ReactElement, ReactNode } from 'react'
-interface ConfirmDialogProps {
- trigger: ReactElement
+type ConfirmDialogBaseProps = {
title: string
description?: ReactNode
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
onConfirm: () => void
+ confirmDisabled?: boolean
+ confirmLoading?: boolean
+ confirmLoadingLabel?: string
}
-export function ConfirmDialog({
- trigger,
+type ConfirmDialogWithTrigger = ConfirmDialogBaseProps & {
+ trigger: ReactElement
+ open?: never
+ onOpenChange?: never
+}
+
+type ConfirmDialogControlled = ConfirmDialogBaseProps & {
+ trigger?: never
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+type ConfirmDialogProps = ConfirmDialogWithTrigger | ConfirmDialogControlled
+
+function ConfirmDrawerBody({
title,
description,
confirmLabel = 'Подтвердить',
cancelLabel = 'Отмена',
destructive,
onConfirm,
-}: ConfirmDialogProps) {
+ confirmDisabled,
+ confirmLoading,
+ confirmLoadingLabel,
+ controlled,
+}: ConfirmDialogBaseProps & { controlled?: boolean }) {
+ const confirmText =
+ confirmLoading && confirmLoadingLabel
+ ? confirmLoadingLabel
+ : confirmLoading
+ ? `${confirmLabel}…`
+ : confirmLabel
+
return (
-
-
-
-
- {title}
- {description ? {description} : null}
-
-
- {cancelLabel}
-
+
+ {title}
+ {description ? {description} : null}
+
+
+ }>
+ {cancelLabel}
+
+ {controlled ? (
+
-
-
-
+ {confirmText}
+
+ ) : (
+
+ }
+ onClick={onConfirm}
+ >
+ {confirmText}
+
+ )}
+
+ >
+ )
+}
+
+export function ConfirmDialog(props: ConfirmDialogProps) {
+ const bodyProps: ConfirmDialogBaseProps = {
+ title: props.title,
+ description: props.description,
+ confirmLabel: props.confirmLabel,
+ cancelLabel: props.cancelLabel,
+ destructive: props.destructive,
+ onConfirm: props.onConfirm,
+ confirmDisabled: props.confirmDisabled,
+ confirmLoading: props.confirmLoading,
+ confirmLoadingLabel: props.confirmLoadingLabel,
+ }
+
+ if (props.trigger) {
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
)
}
diff --git a/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx
index 2cd7afe..e56d036 100644
--- a/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx
+++ b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx
@@ -1,16 +1,10 @@
import { useEffect, useState } from 'react'
import { Button } from '@evobgp/ui/components/button'
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { CommunitySelect } from '@/components/modules/community-select'
import { SelectField } from '@/components/select-field'
@@ -60,48 +54,48 @@ export function FirewallRuleCreateDialog({
}
return (
-
+ >
+ }
+ >
+ v && setAction(v as 'block' | 'accept')}
+ />
+
+
+
+ setComment(e.target.value)}
+ />
+
+
)
}
diff --git a/apps/web/src/components/form-drawer.tsx b/apps/web/src/components/form-drawer.tsx
new file mode 100644
index 0000000..9fbd67c
--- /dev/null
+++ b/apps/web/src/components/form-drawer.tsx
@@ -0,0 +1,47 @@
+import type { ReactNode } from 'react'
+
+import { cn } from '@evobgp/ui/lib/utils'
+import {
+ Drawer,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+} from '@evobgp/ui/components/drawer'
+import { ScrollArea } from '@evobgp/ui/components/scroll-area'
+
+interface FormDrawerProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ title: string
+ description?: string
+ children: ReactNode
+ footer: ReactNode
+ className?: string
+}
+
+export function FormDrawer({
+ open,
+ onOpenChange,
+ title,
+ description,
+ children,
+ footer,
+ className,
+}: FormDrawerProps) {
+ return (
+
+
+
+ {title}
+ {description ? {description} : null}
+
+
+ {children}
+
+ {footer}
+
+
+ )
+}
diff --git a/apps/web/src/components/modules/module-as-entry-dialog.tsx b/apps/web/src/components/modules/module-as-entry-dialog.tsx
index 45cb21b..ed989fb 100644
--- a/apps/web/src/components/modules/module-as-entry-dialog.tsx
+++ b/apps/web/src/components/modules/module-as-entry-dialog.tsx
@@ -1,17 +1,10 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { CommunitySelect } from '@/components/modules/community-select'
import { ApiError, apiMutate } from '@/lib/api-client'
@@ -72,45 +65,43 @@ export function ModuleAsEntryDialog({
}
return (
-
+ >
+ }
+ >
+
+
+ setForm((s) => ({ ...s, asn: Number(e.target.value) }))}
+ />
+
+ setForm((s) => ({ ...s, community_id: v }))}
+ communities={communities}
+ nullable
+ />
+
)
}
diff --git a/apps/web/src/components/modules/module-cdn-source-dialog.tsx b/apps/web/src/components/modules/module-cdn-source-dialog.tsx
index d607417..6774f84 100644
--- a/apps/web/src/components/modules/module-cdn-source-dialog.tsx
+++ b/apps/web/src/components/modules/module-cdn-source-dialog.tsx
@@ -1,21 +1,14 @@
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
+import { Button } from '@evobgp/ui/components/button'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
-import { SelectField } from '@/components/select-field'
-import { Button } from '@evobgp/ui/components/button'
-
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { CommunitySelect } from '@/components/modules/community-select'
+import { SelectField } from '@/components/select-field'
import { ApiError, apiMutate } from '@/lib/api-client'
import { normalizeCdnSourceKind } from '@/lib/modules/helpers'
import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api'
@@ -154,107 +147,107 @@ export function ModuleCdnSourceDialog({
}
return (
-
+ >
+ }
+ >
+
+
+ setForm((s) => ({ ...s, url: e.target.value }))}
+ />
+
+ v && setForm((s) => ({ ...s, source_kind: v }))}
+ />
+
+
+
setForm((s) => ({ ...s, prefix_path: e.target.value }))}
+ />
+ {form.source_kind === 'json' && !form.prefix_path?.trim() ? (
+
+ Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов.
+
+ ) : null}
+
+ setForm((s) => ({ ...s, community_id: v }))}
+ communities={communities}
+ nullable
+ />
+
+
+
+ setForm((s) => ({
+ ...s,
+ refresh_interval_sec: e.target.value ? Number(e.target.value) : null,
+ }))
+ }
+ />
+
+
+
+
+ {previewError ? (
+ {previewError}
+ ) : previewOk ? (
+
+ Всего: {previewTotal}
+ {previewTruncated ? (
+ (обрезано)
+ ) : null}
+
+ ) : null}
+
+ {previewItems.length > 0 ? (
+
+ {previewItems.map((item, i) => (
+ -
+ {item}
+
+ ))}
+
+ ) : null}
+
+
)
}
diff --git a/apps/web/src/components/modules/module-domain-entry-dialog.tsx b/apps/web/src/components/modules/module-domain-entry-dialog.tsx
index c6bc8e7..5f2bbe9 100644
--- a/apps/web/src/components/modules/module-domain-entry-dialog.tsx
+++ b/apps/web/src/components/modules/module-domain-entry-dialog.tsx
@@ -1,16 +1,10 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { CommunitySelect } from '@/components/modules/community-select'
import { ApiError, apiMutate } from '@/lib/api-client'
@@ -70,39 +64,39 @@ export function ModuleDomainEntryDialog({
}
return (
-
+ >
+ }
+ >
+
+
+ setForm((s) => ({ ...s, fqdn: e.target.value }))}
+ />
+
+ setForm((s) => ({ ...s, community_id: v }))}
+ communities={communities}
+ nullable
+ />
+
)
}
diff --git a/apps/web/src/components/modules/module-entries-section.tsx b/apps/web/src/components/modules/module-entries-section.tsx
index d03b5f6..c2dbc11 100644
--- a/apps/web/src/components/modules/module-entries-section.tsx
+++ b/apps/web/src/components/modules/module-entries-section.tsx
@@ -2,18 +2,9 @@ import { useState } from 'react'
import { Plus } from 'lucide-react'
import { toast } from 'sonner'
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from '@evobgp/ui/components/alert-dialog'
import { Button } from '@evobgp/ui/components/button'
+import { ConfirmDialog } from '@/components/confirm-dialog'
import { DataGridCard } from '@/components/data-grid-shell'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
@@ -218,24 +209,17 @@ export function ModuleEntriesSection({
/>
) : null}
- !open && setDeleteTarget(null)}>
-
-
- Удалить запись?
- {deleteDescription(deleteTarget)}
-
-
- Отмена
- void confirmDelete()}
- >
- {deleting ? 'Удаление…' : 'Удалить'}
-
-
-
-
+ !open && setDeleteTarget(null)}
+ title="Удалить запись?"
+ description={deleteDescription(deleteTarget)}
+ confirmLabel="Удалить"
+ confirmLoadingLabel="Удаление…"
+ destructive
+ confirmLoading={deleting}
+ onConfirm={() => void confirmDelete()}
+ />
>
)
}
diff --git a/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx b/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx
index 08ee885..c54887b 100644
--- a/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx
+++ b/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx
@@ -1,16 +1,10 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { CommunitySelect } from '@/components/modules/community-select'
import { ApiError, apiMutate } from '@/lib/api-client'
@@ -70,39 +64,39 @@ export function ModuleIpRangeEntryDialog({
}
return (
-
+ >
+ }
+ >
+
+
+ setForm((s) => ({ ...s, prefix: e.target.value }))}
+ />
+
+ setForm((s) => ({ ...s, community_id: v ?? '' }))}
+ communities={communities}
+ placeholder="Выберите community"
+ />
+
)
}
diff --git a/apps/web/src/components/network/peer-form-dialog.tsx b/apps/web/src/components/network/peer-form-dialog.tsx
index f89ff2c..09786b1 100644
--- a/apps/web/src/components/network/peer-form-dialog.tsx
+++ b/apps/web/src/components/network/peer-form-dialog.tsx
@@ -2,18 +2,11 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
+import { Checkbox } from '@evobgp/ui/components/checkbox'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
-import { Checkbox } from '@evobgp/ui/components/checkbox'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { useCreatePeerMutation, useUpdatePeerMutation } from '@/queries/network'
@@ -102,74 +95,74 @@ export function PeerFormDialog({
}
return (
-
+ >
+ }
+ >
+
+
+ setName(e.target.value)}
+ />
+
+
+
+ setNeighbor(e.target.value)}
+ required
+ />
+
+
+
+ setRemoteAsn(e.target.value)}
+ required
+ />
+
+ setBgpSpeakerId(v || null)}
+ placeholder="Все спикеры"
+ />
+
+
+
+
+ Выключенный пир не попадает в конфиг BIRD до следующей ревизии.
+
+
+
setEnabled(v === true)}
+ />
+
+
)
}
diff --git a/apps/web/src/components/network/speaker-form-dialog.tsx b/apps/web/src/components/network/speaker-form-dialog.tsx
index 1db5689..56010ac 100644
--- a/apps/web/src/components/network/speaker-form-dialog.tsx
+++ b/apps/web/src/components/network/speaker-form-dialog.tsx
@@ -2,17 +2,10 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
+import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { useCreateSpeakerMutation } from '@/queries/network'
@@ -98,72 +91,72 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
}
return (
-
+ >
+ }
+ >
+
+
+ handleEndpointChange(e.target.value)}
+ />
+
+ setRole(v ?? 'replica')}
+ />
+
+
+ setAgentDomain(e.target.value)}
+ />
+
+
+
+ handleNodeIpv4Change(e.target.value)}
+ />
+
+
+
+ {
+ setBgpSourceManual(true)
+ setBgpSourceIpv4(e.target.value)
+ }}
+ />
+
+
)
}
diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo
index 17f5ace..f58e79f 100644
--- a/apps/web/tsconfig.tsbuildinfo
+++ b/apps/web/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/form-drawer.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/packages/ui/src/components/drawer.tsx b/packages/ui/src/components/drawer.tsx
new file mode 100644
index 0000000..ffe1f0b
--- /dev/null
+++ b/packages/ui/src/components/drawer.tsx
@@ -0,0 +1,226 @@
+import * as React from "react"
+import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"
+
+import { cn } from "@evobgp/ui/lib/utils"
+
+type DrawerContextProps = {
+ hasSnapPoints: boolean
+ modal: DrawerPrimitive.Root.Props["modal"]
+ showSwipeHandle: boolean
+ swipeDirection: NonNullable
+}
+
+const DrawerContext = React.createContext(null)
+
+function useDrawer() {
+ const context = React.useContext(DrawerContext)
+
+ if (!context) {
+ throw new Error("useDrawer must be used within a Drawer.")
+ }
+
+ return context
+}
+
+function Drawer({
+ modal = true,
+ showSwipeHandle = false,
+ snapPoints,
+ swipeDirection = "down",
+ ...props
+}: DrawerPrimitive.Root.Props & {
+ showSwipeHandle?: boolean
+}) {
+ const hasSnapPoints = snapPoints != null && snapPoints.length > 0
+ const contextValue = React.useMemo(
+ () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
+ [hasSnapPoints, modal, showSwipeHandle, swipeDirection]
+ )
+
+ return (
+
+
+
+ )
+}
+
+function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
+ return
+}
+
+function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
+ return
+}
+
+function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
+ return
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: DrawerPrimitive.Backdrop.Props) {
+ return (
+
+ )
+}
+
+function DrawerSwipeHandle({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: DrawerPrimitive.Popup.Props) {
+ const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer()
+ const swipeAxis =
+ swipeDirection === "down" || swipeDirection === "up" ? "y" : "x"
+
+ return (
+
+ {modal === true && (
+
+ )}
+
+
+ {showSwipeHandle && }
+
+ {children}
+
+
+
+
+ )
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
+ return (
+
+ )
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: DrawerPrimitive.Description.Props) {
+ return (
+
+ )
+}
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerSwipeHandle,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}