Skip to content

Commit 27aa7bf

Browse files
committed
fix: resolve restart_nodes type mismatch
1 parent ea2050e commit 27aa7bf

File tree

6 files changed

+184
-3
lines changed

6 files changed

+184
-3
lines changed
File renamed without changes.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { useGetAllCores, useModifyCoreConfig } from '@/service/api'
2+
import { CoreResponse } from '@/service/api'
3+
import Core from './core'
4+
import { useState, useEffect, useMemo } from 'react'
5+
import CoreConfigModal, { coreConfigFormSchema, CoreConfigFormValues } from '@/components/dialogs/core-config-modal'
6+
import { useForm } from 'react-hook-form'
7+
import { zodResolver } from '@hookform/resolvers/zod'
8+
import { ScrollArea } from '@/components/ui/scroll-area'
9+
import { toast } from 'sonner'
10+
import { useTranslation } from 'react-i18next'
11+
import { queryClient } from '@/utils/query-client'
12+
import useDirDetection from '@/hooks/use-dir-detection'
13+
import { Skeleton } from '@/components/ui/skeleton'
14+
import { Card } from '@/components/ui/card'
15+
import { Input } from '@/components/ui/input'
16+
import { Button } from '@/components/ui/button'
17+
import { RefreshCw, Search, X } from 'lucide-react'
18+
import { cn } from '@/lib/utils'
19+
20+
const initialDefaultValues: Partial<CoreConfigFormValues> = {
21+
name: '',
22+
config: JSON.stringify({}, null, 2),
23+
excluded_inbound_ids: [],
24+
restart_nodes: true,
25+
}
26+
27+
interface CoresProps {
28+
isDialogOpen?: boolean
29+
onOpenChange?: (open: boolean) => void
30+
cores?: CoreResponse[]
31+
onEditCore?: (coreId: number | string) => void
32+
onDuplicateCore?: (coreId: number | string) => void
33+
onDeleteCore?: (coreName: string, coreId: number) => void
34+
}
35+
36+
export default function Cores({ isDialogOpen, onOpenChange, cores, onEditCore, onDuplicateCore, onDeleteCore }: CoresProps) {
37+
const [editingCore, setEditingCore] = useState<CoreResponse | null>(null)
38+
const [searchQuery, setSearchQuery] = useState('')
39+
const { t } = useTranslation()
40+
const modifyCoreMutation = useModifyCoreConfig()
41+
const dir = useDirDetection()
42+
43+
const { data: coresData, isLoading, isFetching, refetch } = useGetAllCores({})
44+
45+
useEffect(() => {
46+
const handleOpenDialog = () => onOpenChange?.(true)
47+
window.addEventListener('openCoreDialog', handleOpenDialog)
48+
return () => window.removeEventListener('openCoreDialog', handleOpenDialog)
49+
}, [onOpenChange])
50+
51+
const form = useForm<CoreConfigFormValues>({
52+
resolver: zodResolver(coreConfigFormSchema),
53+
defaultValues: initialDefaultValues,
54+
})
55+
56+
const handleEdit = (core: CoreResponse) => {
57+
setEditingCore(core)
58+
form.reset({
59+
name: core.name,
60+
config: JSON.stringify(core.config, null, 2),
61+
excluded_inbound_ids: core.exclude_inbound_tags
62+
? core.exclude_inbound_tags
63+
.join(',')
64+
.split(',')
65+
.map((id: string) => id.trim())
66+
.filter((id: string) => id.trim() !== '')
67+
: [],
68+
})
69+
onOpenChange?.(true)
70+
}
71+
72+
const handleToggleStatus = async (core: CoreResponse) => {
73+
try {
74+
await modifyCoreMutation.mutateAsync({
75+
coreId: core.id,
76+
data: {
77+
name: core.name,
78+
config: core.config,
79+
exclude_inbound_tags: core.exclude_inbound_tags,
80+
},
81+
params: {
82+
restart_nodes: true,
83+
},
84+
})
85+
86+
toast.success(
87+
t('core.toggleSuccess', {
88+
name: core.name,
89+
}),
90+
)
91+
92+
queryClient.invalidateQueries({
93+
queryKey: ['/api/cores'],
94+
})
95+
} catch (error) {
96+
toast.error(
97+
t('core.toggleFailed', {
98+
name: core.name,
99+
}),
100+
)
101+
}
102+
}
103+
104+
const handleModalClose = (open: boolean) => {
105+
if (!open) {
106+
setEditingCore(null)
107+
form.reset(initialDefaultValues)
108+
// Refresh cores data when modal closes
109+
refetch()
110+
}
111+
onOpenChange?.(open)
112+
}
113+
114+
const coresList = cores || coresData?.cores || []
115+
116+
const filteredCores = useMemo(() => {
117+
if (!searchQuery.trim()) return coresList
118+
const query = searchQuery.toLowerCase().trim()
119+
return coresList.filter((core: CoreResponse) => core.name?.toLowerCase().includes(query))
120+
}, [coresList, searchQuery])
121+
122+
const handleRefreshClick = async () => {
123+
await refetch()
124+
}
125+
126+
return (
127+
<div className={cn('flex w-full flex-col gap-4 pt-4', dir === 'rtl' && 'rtl')}>
128+
<div className="flex items-center gap-2 md:gap-3">
129+
{/* Search Input */}
130+
<div className="relative w-full md:w-[calc(100%/3-10px)]" dir={dir}>
131+
<Search className={cn('absolute', dir === 'rtl' ? 'right-2' : 'left-2', 'top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground')} />
132+
<Input placeholder={t('search')} value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className={cn('pl-8 pr-10', dir === 'rtl' && 'pl-10 pr-8')} />
133+
{searchQuery && (
134+
<button onClick={() => setSearchQuery('')} className={cn('absolute', dir === 'rtl' ? 'left-2' : 'right-2', 'top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground')}>
135+
<X className="h-4 w-4" />
136+
</button>
137+
)}
138+
</div>
139+
<Button
140+
size="icon-md"
141+
variant="ghost"
142+
onClick={handleRefreshClick}
143+
className={cn('border', isFetching && 'opacity-70')}
144+
aria-label={t('autoRefresh.refreshNow')}
145+
title={t('autoRefresh.refreshNow')}
146+
>
147+
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />
148+
</Button>
149+
</div>
150+
<ScrollArea dir={dir} className="h-[calc(100vh-8rem)]">
151+
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
152+
{isLoading
153+
? [...Array(6)].map((_, i) => (
154+
<Card key={i} className="px-4 py-5">
155+
<div className="flex items-center gap-2 sm:gap-3">
156+
<Skeleton className="h-2 w-2 shrink-0 rounded-full" />
157+
<Skeleton className="h-5 w-24 sm:w-32" />
158+
<div className="ml-auto shrink-0">
159+
<Skeleton className="h-8 w-8" />
160+
</div>
161+
</div>
162+
</Card>
163+
))
164+
: filteredCores.map((core: CoreResponse) => (
165+
<Core
166+
key={core.id}
167+
core={core}
168+
onEdit={onEditCore ? () => onEditCore(core.id) : () => handleEdit(core)}
169+
onToggleStatus={handleToggleStatus}
170+
onDuplicate={onDuplicateCore ? () => onDuplicateCore(core.id) : undefined}
171+
onDelete={onDeleteCore ? () => onDeleteCore(core.name, core.id) : undefined}
172+
/>
173+
))}
174+
</div>
175+
</ScrollArea>
176+
177+
<CoreConfigModal isDialogOpen={!!isDialogOpen} onOpenChange={handleModalClose} form={form} editingCore={!!editingCore} editingCoreId={editingCore?.id} />
178+
</div>
179+
)
180+
}

dashboard/src/components/settings/cores.tsx renamed to dashboard/src/components/cores/cores.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const initialDefaultValues: Partial<CoreConfigFormValues> = {
2121
name: '',
2222
config: JSON.stringify({}, null, 2),
2323
excluded_inbound_ids: [],
24+
restart_nodes: true,
2425
}
2526

2627
interface CoresProps {
File renamed without changes.

dashboard/src/components/dialogs/core-config-modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const coreConfigFormSchema = z.object({
3535
excluded_inbound_ids: z.array(z.string()).optional(),
3636
public_key: z.string().optional(),
3737
private_key: z.string().optional(),
38-
restart_nodes: z.boolean().default(true),
38+
restart_nodes: z.boolean().optional(),
3939
})
4040

4141
export type CoreConfigFormValues = z.infer<typeof coreConfigFormSchema>
@@ -489,7 +489,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
489489
exclude_inbound_tags: excludeInboundTags,
490490
},
491491
params: {
492-
restart_nodes: values.restart_nodes,
492+
restart_nodes: values.restart_nodes ?? true,
493493
},
494494
})
495495
} else {

dashboard/src/pages/_dashboard.nodes.cores.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useState, useMemo } from 'react'
2-
import Cores from '@/components/settings/cores'
2+
import Cores from '@/components/cores/cores-list'
33
import { useGetAllCores, useDeleteCoreConfig, useCreateCoreConfig } from '@/service/api'
44
import { toast } from 'sonner'
55
import { useTranslation } from 'react-i18next'

0 commit comments

Comments
 (0)