1- import { useMemo , useState , type ReactNode } from 'react'
1+ import { useMemo , useRef , useState , type ReactNode } from 'react'
22import { useTranslation } from 'react-i18next'
33import { ChevronDown , Forward , Server , ShieldCheck } from 'lucide-react'
44import { cn } from '@/shared/lib/utils'
55import { Section } from '@/features/integrations/components/ui/Section'
66import { CodeBlock } from '@/features/integrations/components/ui/CodeBlock'
77import { useConectionKey } from '@/features/integrations/hooks/useConnectionKey'
88import { FlowNode , FlowEdge } from '@/shared/components/ui/flow-diagram'
9- import { useCollectorIntegration } from '@/features/integrations/hooks/useCollectorIntegration'
109import { RemoteEnablePanel , type RemoteEnableSelection } from './RemoteEnablePanel'
1110import { availableProtosFor , defaultPortFor , type Proto } from './protoCatalog'
1211
@@ -66,6 +65,24 @@ function FlowDiagram({ source, port }: { source: string; port: string }) {
6665 )
6766}
6867
68+ // ── Master command (POST endpoint + auth header) ─────────────────────────────
69+
70+ function MasterCommandSection ( { selection } : { selection : RemoteEnableSelection } ) {
71+ const { t } = useTranslation ( )
72+ if ( ! selection . apiKey ) return null
73+ const host = forwarderHost ( )
74+ // ponytail: secret is generated once server-side; user must paste it themselves
75+ const cmd = `POST ${ selection . proto } ://${ host } :8080/v1/logs
76+ Authorization: Bearer <YOUR_API_KEY_SECRET>
77+ Content-Type: application/json`
78+ return (
79+ < Section title = { t ( `${ SHARED } .masterHeader.title` ) } step = { 2 } >
80+ < p className = "text-sm text-foreground/90" > { t ( `${ SHARED } .masterHeader.body` , { name : selection . apiKey . name } ) } </ p >
81+ < CodeBlock code = { cmd } />
82+ </ Section >
83+ )
84+ }
85+
6986// ── Manual (CLI) command — collapsible, reactive to the RemoteEnablePanel ────
7087// selection above it. Replaces what used to be a fixed "Optional — Enable TLS
7188// encryption" block: the command shown here always matches whatever
@@ -115,22 +132,22 @@ function ManualCommandSection({ sourceType, selection }: { sourceType: string; s
115132
116133// ── Forwarder install section ─────────────────────────────────────────────────
117134
118- export function ForwarderInstall ( { source } : { source : string } ) {
119- const { t } = useTranslation ( )
120- const { key } = useConectionKey ( )
121- const host = forwarderHost ( )
122-
123- // Token is always shown as ••••••• in the UI; the real value is copied on click.
124- // Falls back to "*******" if the key hasn't loaded or the request failed.
125- const token = key . data ?. connectionKey ?? '*' . repeat ( 7 )
126- const installCmd = `sudo bash -c "
135+ function buildForwarderInstallCmd ( host : string , token : string ) : string {
136+ return `sudo bash -c "
127137 apt update -y && apt install wget -y && \\
128138 mkdir -p /opt/utmstack-forwarder && \\
129139 wget --no-check-certificate -P /opt/utmstack-forwarder \\
130140 https://${ host } :9001/private/dependencies/collector/forwarder/utmstack_forwarder && \\
131141 chmod 755 /opt/utmstack-forwarder/utmstack_forwarder && \\
132142 /opt/utmstack-forwarder/utmstack_forwarder install ${ host } <secret>${ token } </secret> yes
133143"`
144+ }
145+
146+ export function ForwarderInstall ( { source } : { source : string } ) {
147+ const { t } = useTranslation ( )
148+ const { key } = useConectionKey ( )
149+ const token = key . data ?. connectionKey ?? '*' . repeat ( 7 )
150+ const installCmd = buildForwarderInstallCmd ( forwarderHost ( ) , token )
134151
135152 return (
136153 < Section title = { t ( `${ SHARED } .install.title` ) } step = { 1 } >
@@ -160,14 +177,21 @@ interface ForwarderGuideProps {
160177
161178export function ForwarderGuide ( { source, port, sourceType, defaultProto, children } : ForwarderGuideProps ) {
162179 const { t } = useTranslation ( )
163- const { forwarders } = useCollectorIntegration ( )
164- const hasOnlineForwarder = ( forwarders . data ?? [ ] ) . some ( ( f ) => f . status === 'online' )
165180 const availableProtos = useMemo ( ( ) => availableProtosFor ( sourceType ) , [ sourceType ] )
166181 const initialProto = defaultProto ?? availableProtos [ 0 ]
167182 const [ selection , setSelection ] = useState < RemoteEnableSelection > ( ( ) => ( {
168183 proto : initialProto ,
169184 port : defaultPortFor ( sourceType , initialProto ) || port ,
185+ isMaster : false ,
186+ apiKey : null ,
170187 } ) )
188+ const [ installOpen , setInstallOpen ] = useState ( false )
189+ const installRef = useRef < HTMLDivElement > ( null )
190+
191+ const handleAddCollector = ( ) => {
192+ setInstallOpen ( true )
193+ setTimeout ( ( ) => installRef . current ?. scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) , 50 )
194+ }
171195
172196 return (
173197 < div className = "space-y-4" >
@@ -179,21 +203,56 @@ export function ForwarderGuide({ source, port, sourceType, defaultProto, childre
179203 </ p >
180204 </ Section >
181205
182- { ! forwarders . isLoading && ! hasOnlineForwarder && < ForwarderInstall source = { source } /> }
183-
184206 < RemoteEnablePanel
185207 dataType = { sourceType }
186208 availableProtos = { availableProtos }
187209 defaultProto = { defaultProto }
188- step = { 2 }
210+ step = { 1 }
189211 onSelectionChange = { setSelection }
212+ onRequestAddCollector = { handleAddCollector }
190213 />
191214
192- { /* Vendor-specific steps (device-side config). */ }
193- { children }
215+ { selection . isMaster && selection . apiKey && < MasterCommandSection selection = { selection } /> }
216+
217+ { /* Vendor-specific steps (device-side config). Hidden in master mode — it targets the forwarder, not master. */ }
218+ { ! selection . isMaster && children }
194219
195- < ManualCommandSection sourceType = { sourceType } selection = { selection } />
196- < ForwarderUninstallSection />
220+ { ! selection . isMaster && (
221+ < div ref = { installRef } >
222+ < ForwarderInstallSection source = { source } open = { installOpen } onToggle = { ( ) => setInstallOpen ( ( o ) => ! o ) } />
223+ </ div >
224+ ) }
225+ { ! selection . isMaster && < ManualCommandSection sourceType = { sourceType } selection = { selection } /> }
226+ { ! selection . isMaster && < ForwarderUninstallSection /> }
227+ </ div >
228+ )
229+ }
230+
231+ function ForwarderInstallSection ( { source, open, onToggle } : { source : string ; open : boolean ; onToggle : ( ) => void } ) {
232+ const { t } = useTranslation ( )
233+ const { key } = useConectionKey ( )
234+
235+ const token = key . data ?. connectionKey ?? '*' . repeat ( 7 )
236+ const installCmd = buildForwarderInstallCmd ( forwarderHost ( ) , token )
237+
238+ return (
239+ < div className = "overflow-hidden rounded-lg border border-border" >
240+ < button
241+ onClick = { onToggle }
242+ className = "flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium hover:bg-muted/40 transition-colors"
243+ >
244+ < span > { t ( `${ SHARED } .install.title` ) } </ span >
245+ < ChevronDown size = { 14 } className = { cn ( 'shrink-0 text-muted-foreground transition-transform duration-200' , open && 'rotate-180' ) } />
246+ </ button >
247+ { open && (
248+ < div className = "space-y-3 border-t border-border px-4 pb-4 pt-3" >
249+ < p className = "text-sm text-foreground/90" > { t ( `${ SHARED } .install.body` ) } </ p >
250+ < CodeBlock code = { installCmd } />
251+ < p className = "rounded-md bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground" >
252+ { t ( `${ SHARED } .install.reuse` , { source } ) }
253+ </ p >
254+ </ div >
255+ ) }
197256 </ div >
198257 )
199258}
0 commit comments