1
1
import {
2
+ ActionIcon ,
2
3
Box ,
3
4
Card ,
4
5
Group ,
6
+ Loader ,
5
7
Paper ,
6
8
SimpleGrid ,
7
9
Stack ,
@@ -11,13 +13,16 @@ import {
11
13
Tooltip
12
14
} from '@mantine/core'
13
15
import { PiCloudArrowUpDuotone , PiUsersDuotone , PiWarningCircle } from 'react-icons/pi'
14
- import { TbWifi , TbWifiOff } from 'react-icons/tb'
16
+ import { UpdateNodeCommand } from '@remnawave/backend-contract'
17
+ import { TbPower , TbWifi , TbWifiOff } from 'react-icons/tb'
15
18
import { HiOutlineServer } from 'react-icons/hi'
16
19
import { useTranslation } from 'react-i18next'
17
20
import { motion } from 'framer-motion'
18
21
import { memo , useMemo } from 'react'
19
22
23
+ import { QueryKeys , useDisableNode , useEnableNode } from '@shared/api/hooks'
20
24
import { XtlsLogo } from '@shared/ui/logos/xtls-logo'
25
+ import { queryClient } from '@shared/api'
21
26
import { Logo } from '@shared/ui'
22
27
23
28
import { IProps } from './interface'
@@ -27,13 +32,47 @@ export const NodeDetailsCardWidget = memo(({ node, fetchedNode }: IProps) => {
27
32
28
33
const nodeData = fetchedNode || node
29
34
35
+ const mutationParams = {
36
+ route : {
37
+ uuid : nodeData . uuid
38
+ } ,
39
+ mutationFns : {
40
+ onSuccess : async ( nodeData : UpdateNodeCommand . Response [ 'response' ] ) => {
41
+ await queryClient . setQueryData (
42
+ QueryKeys . nodes . getNode ( { uuid : nodeData . uuid } ) . queryKey ,
43
+ nodeData
44
+ )
45
+ }
46
+ }
47
+ }
48
+
49
+ const { mutate : disableNode , isPending : isDisableNodePending } = useDisableNode ( mutationParams )
50
+ const { mutate : enableNode , isPending : isEnableNodePending } = useEnableNode ( mutationParams )
51
+
52
+ const isConfigMissing = useMemo ( ( ) => {
53
+ return (
54
+ node . configProfile . activeConfigProfileUuid === null ||
55
+ node . configProfile . activeInbounds . length === 0
56
+ )
57
+ } , [ node . configProfile ] )
58
+
30
59
const { icon, color, backgroundColor, borderColor, boxShadow } = useMemo ( ( ) => {
31
60
let icon : React . ReactNode
32
61
let color = 'red'
33
62
let backgroundColor = 'rgba(239, 68, 68, 0.15)'
34
63
let borderColor = 'rgba(239, 68, 68, 0.3)'
35
64
let boxShadow = 'rgba(239, 68, 68, 0.2)'
36
65
66
+ if ( isConfigMissing ) {
67
+ icon = < PiWarningCircle size = { 18 } style = { { color : 'var(--mantine-color-red-6)' } } />
68
+ color = 'red'
69
+ backgroundColor = 'rgba(239, 68, 68, 0.15)'
70
+ borderColor = 'rgba(239, 68, 68, 0.3)'
71
+ boxShadow = 'rgba(239, 68, 68, 0.2)'
72
+
73
+ return { icon, color, backgroundColor, borderColor, boxShadow }
74
+ }
75
+
37
76
if ( nodeData . isConnected ) {
38
77
icon = < TbWifi size = { 18 } style = { { color : 'var(--mantine-color-teal-6)' } } />
39
78
color = 'teal'
@@ -68,6 +107,14 @@ export const NodeDetailsCardWidget = memo(({ node, fetchedNode }: IProps) => {
68
107
return { icon, color, backgroundColor, borderColor, boxShadow }
69
108
} , [ nodeData . isConnected , nodeData . isConnecting , nodeData . isDisabled , t ] )
70
109
110
+ const handleToggleNodeStatus = ( ) => {
111
+ if ( nodeData . isDisabled ) {
112
+ enableNode ( { } )
113
+ } else {
114
+ disableNode ( { } )
115
+ }
116
+ }
117
+
71
118
return (
72
119
< Card
73
120
p = "lg"
@@ -115,29 +162,98 @@ export const NodeDetailsCardWidget = memo(({ node, fetchedNode }: IProps) => {
115
162
</ Box >
116
163
</ Group >
117
164
118
- < motion . div
119
- animate = { {
120
- scale : nodeData ?. isConnected ? [ 1 , 1.1 , 1 ] : 1 ,
121
- opacity : nodeData ?. isConnected ? [ 1 , 0.8 , 1 ] : 0.6
122
- } }
123
- transition = { {
124
- duration : nodeData ?. isConnected ? 2 : 0 ,
125
- repeat : nodeData ?. isConnected ? Infinity : 0
126
- } }
127
- >
128
- < ThemeIcon
129
- color = { color }
130
- size = "lg"
131
- style = { {
132
- backgroundColor,
133
- border : `1px solid ${ borderColor } ` ,
134
- boxShadow : `0 0 15px ${ boxShadow } `
165
+ < Group gap = "xs" >
166
+ { ! isConfigMissing && (
167
+ < Tooltip label = { nodeData . isDisabled ? 'Enable Node' : 'Disable Node' } >
168
+ < ActionIcon
169
+ color = { nodeData . isDisabled ? 'teal' : 'red' }
170
+ disabled = { isDisableNodePending || isEnableNodePending }
171
+ onClick = { handleToggleNodeStatus }
172
+ size = "md"
173
+ style = { {
174
+ backgroundColor : nodeData . isDisabled
175
+ ? 'rgba(45, 212, 191, 0.15)'
176
+ : 'rgba(239, 68, 68, 0.15)' ,
177
+ border : `1px solid ${
178
+ nodeData . isDisabled
179
+ ? 'rgba(45, 212, 191, 0.3)'
180
+ : 'rgba(239, 68, 68, 0.3)'
181
+ } `,
182
+ boxShadow : `0 0 10px ${
183
+ nodeData . isDisabled
184
+ ? 'rgba(45, 212, 191, 0.2)'
185
+ : 'rgba(239, 68, 68, 0.2)'
186
+ } `
187
+ } }
188
+ variant = "light"
189
+ >
190
+ { isDisableNodePending || isEnableNodePending ? (
191
+ < Loader
192
+ color = { nodeData . isDisabled ? 'teal' : 'red' }
193
+ size = "xs"
194
+ />
195
+ ) : (
196
+ < TbPower
197
+ size = { 16 }
198
+ style = { {
199
+ color : nodeData . isDisabled
200
+ ? 'var(--mantine-color-teal-4)'
201
+ : 'var(--mantine-color-red-4)'
202
+ } }
203
+ />
204
+ ) }
205
+ </ ActionIcon >
206
+ </ Tooltip >
207
+ ) }
208
+
209
+ { isConfigMissing && (
210
+ < Tooltip label = "Config profile or inbounds is missing" >
211
+ < ActionIcon
212
+ color = "gray"
213
+ disabled
214
+ size = "md"
215
+ style = { {
216
+ backgroundColor : 'rgba(107, 114, 128, 0.15)' ,
217
+ border : `1px solid rgba(107, 114, 128, 0.3)` ,
218
+ boxShadow : `0 0 10px rgba(107, 114, 128, 0.2)` ,
219
+ opacity : 0.7
220
+ } }
221
+ variant = "light"
222
+ >
223
+ < TbPower
224
+ size = { 16 }
225
+ style = { {
226
+ color : 'var(--mantine-color-teal-4)'
227
+ } }
228
+ />
229
+ </ ActionIcon >
230
+ </ Tooltip >
231
+ ) }
232
+
233
+ < motion . div
234
+ animate = { {
235
+ scale : nodeData ?. isConnected ? [ 1 , 1.1 , 1 ] : 1 ,
236
+ opacity : nodeData ?. isConnected ? [ 1 , 0.8 , 1 ] : 0.6
237
+ } }
238
+ transition = { {
239
+ duration : nodeData ?. isConnected ? 2 : 0 ,
240
+ repeat : nodeData ?. isConnected ? Infinity : 0
135
241
} }
136
- variant = "light"
137
242
>
138
- { icon }
139
- </ ThemeIcon >
140
- </ motion . div >
243
+ < ThemeIcon
244
+ color = { color }
245
+ size = "lg"
246
+ style = { {
247
+ backgroundColor,
248
+ border : `1px solid ${ borderColor } ` ,
249
+ boxShadow : `0 0 15px ${ boxShadow } `
250
+ } }
251
+ variant = "light"
252
+ >
253
+ { icon }
254
+ </ ThemeIcon >
255
+ </ motion . div >
256
+ </ Group >
141
257
</ Group >
142
258
143
259
{ nodeData . isConnected && (
0 commit comments