'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ArrowLeft, Cloud, Loader2, Plus, RefreshCw, Settings, Trash2, } from 'lucide-react'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { useParams } from 'next/navigation'; import { useState } from 'react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { CloudflareService, cloudflareQueryKey, NodeService, type CloudflareGroupPayload, } from '@/lib/services/openflare'; import { getErrorMessage } from '../../../websites/components/website-utils'; import { GroupDialog } from '../../components/group-dialog'; import { MemberAddDialog } from '../../components/member-add-dialog'; export function CloudflareGroupDetailPageClient() { const t = useTranslations('cloudflare'); const params = useParams<{ id: string }>(); const groupID = Number(params.id); const queryClient = useQueryClient(); const [editOpen, setEditOpen] = useState(false); const [addOpen, setAddOpen] = useState(false); const detailQuery = useQuery({ queryKey: [...cloudflareQueryKey, 'groups', groupID], queryFn: () => CloudflareService.getGroup(groupID), enabled: Number.isInteger(groupID) && groupID > 0, refetchInterval: 5000, }); const domainsQuery = useQuery({ queryKey: [...cloudflareQueryKey, 'domains', 'available'], queryFn: () => CloudflareService.listAvailableDomains(), }); const nodesQuery = useQuery({ queryKey: ['openflare', 'nodes'], queryFn: () => NodeService.listNodes(), }); const invalidate = async () => queryClient.invalidateQueries({ queryKey: cloudflareQueryKey }); const updateGroupMutation = useMutation({ mutationFn: (payload: CloudflareGroupPayload) => CloudflareService.updateGroup(groupID, payload), onSuccess: async () => { toast.success(t('groupUpdated')); setEditOpen(false); await invalidate(); }, onError: (error) => toast.error(getErrorMessage(error)), }); const addMutation = useMutation({ mutationFn: async ({ domainIDs, proxied, }: { domainIDs: number[]; proxied: boolean; }) => { const results = await Promise.allSettled( domainIDs.map((domainID) => CloudflareService.createMember(groupID, { zone_domain_id: domainID, proxied, }), ), ); const failed = results.filter((r) => r.status === 'rejected'); const succeeded = results.length - failed.length; return { succeeded, failed: failed.length, total: results.length }; }, onSuccess: async ({ succeeded, failed, total }) => { if (failed === 0) { toast.success( total === 1 ? t('memberAddedOne') : t('memberAddedMany', { count: succeeded }), ); } else if (succeeded === 0) { toast.error(t('memberAddFailed', { count: failed })); } else { toast.warning(t('memberPartial', { succeeded, failed })); } setAddOpen(false); await invalidate(); }, onError: (error) => toast.error(getErrorMessage(error)), }); const proxiedMutation = useMutation({ mutationFn: ({ memberID, proxied, }: { memberID: number; proxied: boolean; }) => CloudflareService.updateMember(groupID, memberID, proxied), onSuccess: async () => { toast.success(t('proxyUpdated')); await invalidate(); }, onError: (error) => toast.error(getErrorMessage(error)), }); const syncMutation = useMutation({ mutationFn: (memberID: number) => CloudflareService.syncMember(groupID, memberID), onSuccess: () => toast.success(t('memberSyncQueued')), onError: (error) => toast.error(getErrorMessage(error)), }); const removeMutation = useMutation({ mutationFn: (memberID: number) => CloudflareService.removeMember(groupID, memberID), onSuccess: async () => { toast.success(t('memberDeleted')); await invalidate(); }, onError: (error) => toast.error(getErrorMessage(error)), }); if (detailQuery.isLoading) return (
); if (detailQuery.isError || !detailQuery.data) return (
void detailQuery.refetch()} />
); const { group, members } = detailQuery.data; return (

{group.name}

{t('currentPoint')} {t('activeNode', { name: group.active_node.name, ip: group.active_node.ip, })} {group.enabled ? t('syncEnabled') : t('syncDisabled')} {t('primaryNode', { name: group.primary_node.name })} {t('backupNode', { name: group.backup_node?.name ?? t('backupUnset'), })} {t('members')} {t('membersDesc')} {members.length === 0 ? (

{t('noMembers')}

) : ( {t('columns.domain')} {t('columns.desiredIp')} {t('columns.status')} {t('columns.proxied')} {t('columns.actions')} {members.map((member) => (
{member.domain}
{member.last_error ? (

{member.last_error}

) : null}
{member.desired_ip || t('pendingSync')} {member.sync_status} proxiedMutation.mutate({ memberID: member.id, proxied, }) } />
))}
)}
updateGroupMutation.mutate(payload)} /> addMutation.mutate({ domainIDs, proxied }) } />
); }