diff --git a/apps/web/src/components/directories/community-create-dialog.tsx b/apps/web/src/components/directories/community-create-dialog.tsx
index ac55f56..d19fd82 100644
--- a/apps/web/src/components/directories/community-create-dialog.tsx
+++ b/apps/web/src/components/directories/community-create-dialog.tsx
@@ -7,24 +7,40 @@ import { Label } from '@evobgp/ui/components/label'
import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
-import { useCreateCommunityMutation } from '@/queries/directories'
-import type { BgpCommunityCreate } from '@/types/api'
+import {
+ useCreateCommunityMutation,
+ useUpdateCommunityMutation,
+} from '@/queries/directories'
+import type { BgpCommunity, BgpCommunityCreate, BgpCommunityPatch } from '@/types/api'
-interface CommunityCreateDialogProps {
+interface CommunityFormDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
+ editTarget?: BgpCommunity | null
}
-export function CommunityCreateDialog({ open, onOpenChange }: CommunityCreateDialogProps) {
+/** @see https://reui.io/preview/base/form-7 */
+export function CommunityFormDialog({
+ open,
+ onOpenChange,
+ editTarget = null,
+}: CommunityFormDialogProps) {
const createMutation = useCreateCommunityMutation()
+ const updateMutation = useUpdateCommunityMutation()
+ const saving = createMutation.isPending || updateMutation.isPending
const [community, setCommunity] = useState('')
const [title, setTitle] = useState('')
useEffect(() => {
if (!open) return
- setCommunity('')
- setTitle('')
- }, [open])
+ if (editTarget) {
+ setCommunity(editTarget.community ?? '')
+ setTitle(editTarget.title ?? '')
+ } else {
+ setCommunity('')
+ setTitle('')
+ }
+ }, [editTarget, open])
async function save() {
const value = community.trim()
@@ -32,11 +48,19 @@ export function CommunityCreateDialog({ open, onOpenChange }: CommunityCreateDia
toast.error('Укажите community')
return
}
- const body: BgpCommunityCreate = { community: value }
- const t = title.trim()
- if (t) body.title = t
+ const titleTrimmed = title.trim()
try {
- await createMutation.mutateAsync(body)
+ if (editTarget) {
+ const body: BgpCommunityPatch = {
+ community: value,
+ title: titleTrimmed || '',
+ }
+ await updateMutation.mutateAsync({ id: editTarget.id, body })
+ } else {
+ const body: BgpCommunityCreate = { community: value }
+ if (titleTrimmed) body.title = titleTrimmed
+ await createMutation.mutateAsync(body)
+ }
onOpenChange(false)
} catch {
// toast in mutation
@@ -47,7 +71,7 @@ export function CommunityCreateDialog({ open, onOpenChange }: CommunityCreateDia
onOpenChange(false)}>
Отмена
- void save()}>
- Создать
+ void save()}>
+ {editTarget ? 'Сохранить' : 'Создать'}
>
}
@@ -82,3 +106,6 @@ export function CommunityCreateDialog({ open, onOpenChange }: CommunityCreateDia
)
}
+
+/** @deprecated Use CommunityFormDialog */
+export const CommunityCreateDialog = CommunityFormDialog
diff --git a/apps/web/src/components/directories/directories-communities-grid.tsx b/apps/web/src/components/directories/directories-communities-grid.tsx
index d9f3dc8..7cc0c71 100644
--- a/apps/web/src/components/directories/directories-communities-grid.tsx
+++ b/apps/web/src/components/directories/directories-communities-grid.tsx
@@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
+import { DirectoriesRowActions } from '@/components/directories/directories-row-actions'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { BgpCommunity } from '@/types/api'
@@ -11,12 +12,16 @@ import type { BgpCommunity } from '@/types/api'
export function DirectoriesCommunitiesGrid({
items,
isLoading = false,
+ canWrite = false,
+ onEdit,
}: {
items: BgpCommunity[]
isLoading?: boolean
+ canWrite?: boolean
+ onEdit?: (row: BgpCommunity) => void
}) {
- const columns = useMemo[]>(
- () => [
+ const columns = useMemo[]>(() => {
+ const cols: ColumnDef[] = [
{
accessorKey: 'title',
header: ({ column }) => ,
@@ -38,9 +43,23 @@ export function DirectoriesCommunitiesGrid({
cell: () => community,
meta: { headerTitle: 'Тип' },
},
- ],
- [],
- )
+ ]
+
+ if (canWrite && onEdit) {
+ cols.push({
+ id: 'actions',
+ enableSorting: false,
+ enableHiding: false,
+ header: () => Действия,
+ cell: ({ row }) => (
+ onEdit(row.original)} />
+ ),
+ meta: { headerTitle: 'Действия' },
+ })
+ }
+
+ return cols
+ }, [canWrite, onEdit])
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
data: items,
diff --git a/apps/web/src/components/directories/directories-doh-grid.tsx b/apps/web/src/components/directories/directories-doh-grid.tsx
index e049dd2..6591808 100644
--- a/apps/web/src/components/directories/directories-doh-grid.tsx
+++ b/apps/web/src/components/directories/directories-doh-grid.tsx
@@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
+import { DirectoriesRowActions } from '@/components/directories/directories-row-actions'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { DohProfile } from '@/types/api'
@@ -11,12 +12,16 @@ import type { DohProfile } from '@/types/api'
export function DirectoriesDohGrid({
items,
isLoading = false,
+ canWrite = false,
+ onEdit,
}: {
items: DohProfile[]
isLoading?: boolean
+ canWrite?: boolean
+ onEdit?: (row: DohProfile) => void
}) {
- const columns = useMemo[]>(
- () => [
+ const columns = useMemo[]>(() => {
+ const cols: ColumnDef[] = [
{
id: 'name',
accessorFn: (row) => row.name ?? row.url,
@@ -43,9 +48,23 @@ export function DirectoriesDohGrid({
cell: () => —,
meta: { headerTitle: 'По умолчанию' },
},
- ],
- [],
- )
+ ]
+
+ if (canWrite && onEdit) {
+ cols.push({
+ id: 'actions',
+ enableSorting: false,
+ enableHiding: false,
+ header: () => Действия,
+ cell: ({ row }) => (
+ onEdit(row.original)} />
+ ),
+ meta: { headerTitle: 'Действия' },
+ })
+ }
+
+ return cols
+ }, [canWrite, onEdit])
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
data: items,
diff --git a/apps/web/src/components/directories/directories-row-actions.tsx b/apps/web/src/components/directories/directories-row-actions.tsx
new file mode 100644
index 0000000..e93261b
--- /dev/null
+++ b/apps/web/src/components/directories/directories-row-actions.tsx
@@ -0,0 +1,36 @@
+import { MoreHorizontalIcon, PencilIcon } from 'lucide-react'
+
+import { Button } from '@evobgp/ui/components/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@evobgp/ui/components/dropdown-menu'
+
+/**
+ * Data-grid row actions — ⋯ menu (ReUI / shadcn DropdownMenu).
+ * @see https://reui.io/preview/base/components/c-dropdown-menu-12
+ * @see https://reui.io/docs/components/base/dropdown-menu
+ */
+export function DirectoriesRowActions({ onEdit }: { onEdit: () => void }) {
+ return (
+
+
+
+
+
+ }
+ />
+
+
+
+ Редактировать
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/directories/doh-profile-create-dialog.tsx b/apps/web/src/components/directories/doh-profile-create-dialog.tsx
index 68699b3..c14bda5 100644
--- a/apps/web/src/components/directories/doh-profile-create-dialog.tsx
+++ b/apps/web/src/components/directories/doh-profile-create-dialog.tsx
@@ -7,26 +7,47 @@ import { Label } from '@evobgp/ui/components/label'
import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
-import { useCreateDohProfileMutation } from '@/queries/directories'
-import type { DohProfileCreate } from '@/types/api'
+import {
+ useCreateDohProfileMutation,
+ useUpdateDohProfileMutation,
+} from '@/queries/directories'
+import type { DohProfile, DohProfileCreate, DohProfilePatch } from '@/types/api'
-interface DohProfileCreateDialogProps {
+interface DohProfileFormDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
+ editTarget?: DohProfile | null
}
-export function DohProfileCreateDialog({ open, onOpenChange }: DohProfileCreateDialogProps) {
+/** @see https://reui.io/preview/base/form-7 */
+export function DohProfileFormDialog({
+ open,
+ onOpenChange,
+ editTarget = null,
+}: DohProfileFormDialogProps) {
const createMutation = useCreateDohProfileMutation()
+ const updateMutation = useUpdateDohProfileMutation()
+ const saving = createMutation.isPending || updateMutation.isPending
const [name, setName] = useState('')
const [url, setUrl] = useState('')
const [timeoutMs, setTimeoutMs] = useState('')
useEffect(() => {
if (!open) return
- setName('')
- setUrl('')
- setTimeoutMs('')
- }, [open])
+ if (editTarget) {
+ setName(editTarget.name ?? '')
+ setUrl(editTarget.url ?? '')
+ setTimeoutMs(
+ editTarget.timeout_ms === null || editTarget.timeout_ms === undefined
+ ? ''
+ : String(editTarget.timeout_ms),
+ )
+ } else {
+ setName('')
+ setUrl('')
+ setTimeoutMs('')
+ }
+ }, [editTarget, open])
async function save() {
const trimmedUrl = url.trim()
@@ -45,20 +66,32 @@ export function DohProfileCreateDialog({ open, onOpenChange }: DohProfileCreateD
return
}
- const body: DohProfileCreate = { url: trimmedUrl }
- const n = name.trim()
- if (n) body.name = n
+ let timeout: number | null = null
if (timeoutMs.trim() !== '') {
const ms = Number(timeoutMs)
if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
toast.error('Timeout должен быть целым числом > 0')
return
}
- body.timeout_ms = ms
+ timeout = ms
}
+ const nameTrimmed = name.trim()
+
try {
- await createMutation.mutateAsync(body)
+ if (editTarget) {
+ const body: DohProfilePatch = {
+ url: trimmedUrl,
+ name: nameTrimmed || undefined,
+ timeout_ms: timeout,
+ }
+ await updateMutation.mutateAsync({ id: editTarget.id, body })
+ } else {
+ const body: DohProfileCreate = { url: trimmedUrl }
+ if (nameTrimmed) body.name = nameTrimmed
+ if (timeout !== null) body.timeout_ms = timeout
+ await createMutation.mutateAsync(body)
+ }
onOpenChange(false)
} catch {
// toast in mutation
@@ -69,7 +102,7 @@ export function DohProfileCreateDialog({ open, onOpenChange }: DohProfileCreateD
onOpenChange(false)}>
Отмена
- void save()}>
- Создать
+ void save()}>
+ {editTarget ? 'Сохранить' : 'Создать'}
>
}
@@ -116,3 +149,6 @@ export function DohProfileCreateDialog({ open, onOpenChange }: DohProfileCreateD
)
}
+
+/** @deprecated Use DohProfileFormDialog */
+export const DohProfileCreateDialog = DohProfileFormDialog
diff --git a/apps/web/src/queries/directories.ts b/apps/web/src/queries/directories.ts
index aa57574..7547bf8 100644
--- a/apps/web/src/queries/directories.ts
+++ b/apps/web/src/queries/directories.ts
@@ -5,9 +5,11 @@ import { apiJSON, apiMutate } from '@/lib/api-client'
import type {
BgpCommunity,
BgpCommunityCreate,
+ BgpCommunityPatch,
CommunitiesResponse,
DohProfile,
DohProfileCreate,
+ DohProfilePatch,
DohProfilesResponse,
} from '@/types/api'
@@ -47,6 +49,20 @@ export function useCreateCommunityMutation() {
})
}
+export function useUpdateCommunityMutation() {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: ({ id, body }: { id: string; body: BgpCommunityPatch }) =>
+ apiMutate(`/v1/communities/${id}`, 'PATCH', body, { idempotent: false }),
+ onSuccess: () => {
+ toast.success('Сообщество обновлено')
+ void qc.invalidateQueries({ queryKey: directoriesKeys.communities() })
+ },
+ onError: (e) =>
+ toast.error(e instanceof Error ? e.message : 'Не удалось обновить сообщество'),
+ })
+}
+
export function useCreateDohProfileMutation() {
const qc = useQueryClient()
return useMutation({
@@ -60,3 +76,17 @@ export function useCreateDohProfileMutation() {
toast.error(e instanceof Error ? e.message : 'Не удалось создать DoH-профиль'),
})
}
+
+export function useUpdateDohProfileMutation() {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: ({ id, body }: { id: string; body: DohProfilePatch }) =>
+ apiMutate(`/v1/doh-profiles/${id}`, 'PATCH', body, { idempotent: false }),
+ onSuccess: () => {
+ toast.success('DoH-профиль обновлён')
+ void qc.invalidateQueries({ queryKey: directoriesKeys.doh() })
+ },
+ onError: (e) =>
+ toast.error(e instanceof Error ? e.message : 'Не удалось обновить DoH-профиль'),
+ })
+}
diff --git a/apps/web/src/routes/_auth/directories.tsx b/apps/web/src/routes/_auth/directories.tsx
index 66646d2..7299b80 100644
--- a/apps/web/src/routes/_auth/directories.tsx
+++ b/apps/web/src/routes/_auth/directories.tsx
@@ -5,10 +5,14 @@ import { useState } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
-import { CommunityCreateDialog } from '@/components/directories/community-create-dialog'
+import {
+ CommunityFormDialog,
+} from '@/components/directories/community-create-dialog'
import { DirectoriesCommunitiesGrid } from '@/components/directories/directories-communities-grid'
import { DirectoriesDohGrid } from '@/components/directories/directories-doh-grid'
-import { DohProfileCreateDialog } from '@/components/directories/doh-profile-create-dialog'
+import {
+ DohProfileFormDialog,
+} from '@/components/directories/doh-profile-create-dialog'
import { PageHeader } from '@/components/page-header'
import { QueryState } from '@/components/query-state'
import { Badge } from '@/components/reui/badge'
@@ -17,6 +21,7 @@ import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons'
import { sessionCanWriteDirectories } from '@/lib/auth'
import { authSessionQueryOptions } from '@/queries/auth'
import { directoriesCommunitiesQueryOptions, directoriesDohQueryOptions } from '@/queries/directories'
+import type { BgpCommunity, DohProfile } from '@/types/api'
export const Route = createFileRoute('/_auth/directories')({
component: DirectoriesComponent,
@@ -24,7 +29,9 @@ export const Route = createFileRoute('/_auth/directories')({
function DirectoriesComponent() {
const [communityOpen, setCommunityOpen] = useState(false)
+ const [communityEdit, setCommunityEdit] = useState(null)
const [dohOpen, setDohOpen] = useState(false)
+ const [dohEdit, setDohEdit] = useState(null)
const sessionQ = useQuery(authSessionQueryOptions())
const canWrite = sessionCanWriteDirectories(sessionQ.data)
@@ -69,15 +76,35 @@ function DirectoriesComponent() {
},
]
+ function openCreateCommunity() {
+ setCommunityEdit(null)
+ setCommunityOpen(true)
+ }
+
+ function openEditCommunity(row: BgpCommunity) {
+ setCommunityEdit(row)
+ setCommunityOpen(true)
+ }
+
+ function openCreateDoh() {
+ setDohEdit(null)
+ setDohOpen(true)
+ }
+
+ function openEditDoh(row: DohProfile) {
+ setDohEdit(row)
+ setDohOpen(true)
+ }
+
const addCommunityButton = canWrite ? (
-