Skip to content

Commit 49014f8

Browse files
Backlog/v12 integrations collector (#2463)
* fix[frontend](integrations): standarized collector colorscheme * fix[frontend](integrations):added missing translations * fix[frontend](integrations):added send to master option on collector * fix[frontend](integrations): added api key selectors on collector integrations * fix[frontend](integrations): added sent to master command section (step 2 )
1 parent 09fd2b8 commit 49014f8

31 files changed

Lines changed: 652 additions & 102 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { apiKeysHttpService } from '../services/api-keys-http.service'
3+
4+
export const API_KEYS_LIST_QUERY_KEY = ['api-keys', 'list'] as const
5+
6+
export function useApiKeysList() {
7+
return useQuery({
8+
queryKey: API_KEYS_LIST_QUERY_KEY,
9+
queryFn: () => apiKeysHttpService.list(1, 200),
10+
staleTime: 30_000,
11+
})
12+
}

‎frontend/src/features/integrations/components/setup/CustomSetup.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function CustomSetup({ integration }: { integration: Integration }) {
3737
// slug if the catalog row has no explicit data type.
3838
const name = integration.dataType || integration.moduleName?.toLowerCase() || 'my-integration'
3939

40-
const [selection, setSelection] = useState<RemoteEnableSelection>({ proto: 'udp', port: '7100' })
40+
const [selection, setSelection] = useState<RemoteEnableSelection>({ proto: 'udp', port: '7100', isMaster: false, apiKey: null })
4141
const { proto, port } = selection
4242

4343
const isHttp = proto === 'http' || proto === 'https'
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import { ChevronDown, Plus } from 'lucide-react'
3+
import type { ApiKey } from '@/features/api-keys/types/api-key.types'
4+
5+
export interface ApiKeyPickerProps {
6+
keys: ApiKey[]
7+
value: number | null
8+
onChange: (id: number) => void
9+
onAddNew: () => void
10+
addLabel: string
11+
placeholder: string
12+
emptyLabel: string
13+
disabled?: boolean
14+
}
15+
16+
export function ApiKeyPicker({ keys, value, onChange, onAddNew, addLabel, placeholder, emptyLabel, disabled }: ApiKeyPickerProps) {
17+
const [open, setOpen] = useState(false)
18+
const ref = useRef<HTMLDivElement>(null)
19+
const selected = keys.find((k) => k.id === value) ?? null
20+
21+
useEffect(() => {
22+
if (!open) return
23+
const onDoc = (e: MouseEvent) => ref.current && !ref.current.contains(e.target as Node) && setOpen(false)
24+
document.addEventListener('mousedown', onDoc)
25+
return () => document.removeEventListener('mousedown', onDoc)
26+
}, [open])
27+
28+
return (
29+
<div className="relative" ref={ref}>
30+
<button
31+
type="button"
32+
onClick={() => setOpen((v) => !v)}
33+
disabled={disabled}
34+
className="flex h-9 w-full items-center justify-between gap-2 rounded-md border border-border bg-background px-3 text-sm text-foreground disabled:opacity-70"
35+
>
36+
<span className="truncate">{selected ? selected.name : placeholder}</span>
37+
<ChevronDown size={14} className="shrink-0 text-muted-foreground" />
38+
</button>
39+
{open && (
40+
<div className="absolute left-0 top-full z-30 mt-1 w-full rounded-md border border-border bg-popover py-1 shadow-lg">
41+
{keys.length === 0 && (
42+
<span className="block px-3 py-1.5 text-sm text-muted-foreground">{emptyLabel}</span>
43+
)}
44+
{keys.map((k) => (
45+
<button
46+
key={k.id}
47+
type="button"
48+
onClick={() => { onChange(k.id); setOpen(false) }}
49+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted"
50+
>
51+
<span className="truncate">{k.name}</span>
52+
</button>
53+
))}
54+
<button
55+
type="button"
56+
onClick={() => { onAddNew(); setOpen(false) }}
57+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted"
58+
>
59+
<Plus size={12} className="shrink-0 text-muted-foreground" />
60+
<span className="truncate">{addLabel}</span>
61+
</button>
62+
</div>
63+
)}
64+
</div>
65+
)
66+
}

‎frontend/src/features/integrations/components/setup/collector/ForwarderGuide.tsx‎

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { useMemo, useState, type ReactNode } from 'react'
1+
import { useMemo, useRef, useState, type ReactNode } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import { ChevronDown, Forward, Server, ShieldCheck } from 'lucide-react'
44
import { cn } from '@/shared/lib/utils'
55
import { Section } from '@/features/integrations/components/ui/Section'
66
import { CodeBlock } from '@/features/integrations/components/ui/CodeBlock'
77
import { useConectionKey } from '@/features/integrations/hooks/useConnectionKey'
88
import { FlowNode, FlowEdge } from '@/shared/components/ui/flow-diagram'
9-
import { useCollectorIntegration } from '@/features/integrations/hooks/useCollectorIntegration'
109
import { RemoteEnablePanel, type RemoteEnableSelection } from './RemoteEnablePanel'
1110
import { 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

161178
export 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

Comments
 (0)