Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/features/billing/services/billing.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useAuth } from '@/features/auth'
import { IS_FEDERATION } from '@/shared/config/mode'
import { useCurrentInstanceId } from '@/shared/lib/current-instance'
import { setMsspFlag } from '@/shared/lib/license-flags'
import { billingHttpService } from './billing-http.service'
import type { License, VersionInfo } from '../types/billing.types'

Expand Down Expand Up @@ -48,6 +49,7 @@ export function BillingProvider({ children }: { children: ReactNode }) {
billingHttpService.getVersion(),
])
setLicense(lic)
setMsspFlag(lic?.mssp === true)
setVersion(ver)
} catch {
setError(true)
Expand All @@ -59,6 +61,7 @@ export function BillingProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (!ready) {
setLicense(null)
setMsspFlag(false)
setVersion(null)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AgentInstallSelector } from '@/features/integrations/components/setup/A
import { AgentUninstallSection } from '@/features/integrations/components/setup/AgentUninstallSection'
import { useConectionKey } from '@/features/integrations/hooks/useConnectionKey'
import { buildAgentInstall } from '@/features/integrations/utils/agentInstallBuilder'
import { forwarderHost } from '@/features/integrations/components/setup/collector/ForwarderGuide'
import type { Integration } from '@/features/integrations/types'

interface AgentSetupProps {
Expand All @@ -18,9 +19,7 @@ export function AgentSetup({ integration: i }: AgentSetupProps) {
const { t } = useTranslation()
const { key } = useConectionKey()

const host = window.location.host.includes(':')
? window.location.host.split(':')[0]
: window.location.host
const host = forwarderHost()

// Always render the command even before the key loads: fall back to a masked
// placeholder (like the AS/400 and Forwarder guides) so the user never sees a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function CustomSetup({ integration }: { integration: Integration }) {
// slug if the catalog row has no explicit data type.
const name = integration.dataType || integration.moduleName?.toLowerCase() || 'my-integration'

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

const isHttp = proto === 'http' || proto === 'https'
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Section } from '@/features/integrations/components/ui/Section'
import { CodeBlock } from '@/features/integrations/components/ui/CodeBlock'
import { useConectionKey } from '@/features/integrations/hooks/useConnectionKey'
import { FlowNode, FlowEdge } from '@/shared/components/ui/flow-diagram'
import { getSupportTenant } from '@/shared/lib/current-tenant'
import { isMssp } from '@/shared/lib/license-flags'
import { RemoteEnablePanel, type RemoteEnableSelection } from './RemoteEnablePanel'
import { availableProtosFor, defaultPortFor, type Proto } from './protoCatalog'

Expand All @@ -23,10 +25,16 @@ import { availableProtosFor, defaultPortFor, type Proto } from './protoCatalog'
const SHARED = 'integrations.setup.collector.forwarder'
const LOGINPUT_PORT = '50052'

// The Forwarder runs on the UTMStack host; default the address to where the user
// is browsing from (stripped of any port).
// The Forwarder runs on the UTMStack host; default the address to where the
// user is browsing from (stripped of any port). In MSSP mode, when the operator
// has entered a child tenant, use that tenant's domain instead — the child is
// reached at its own subdomain, not the default's.
export function forwarderHost(): string {
if (typeof window === 'undefined') return 'utmstack-host'
if (isMssp()) {
const support = getSupportTenant()
if (support?.domain) return support.domain
}
const h = window.location.host
return h.includes(':') ? h.split(':')[0] : h
}
Expand Down Expand Up @@ -72,11 +80,10 @@ function FlowDiagram({ source, port, isMaster }: { source: string; port: string;

// ── Master command (POST endpoint + auth header) ─────────────────────────────

function MasterCommandSection({ selection }: { selection: RemoteEnableSelection }) {
function MasterCommandSection(_: { selection: RemoteEnableSelection }) {
const { t } = useTranslation()
const [tab, setTab] = useState<'simple' | 'batch'>('simple')
if (!selection.apiKey) return null
const host = typeof window === 'undefined' ? 'utmstack-host' : window.location.host
const host = forwarderHost()
const simpleCmd = `curl -k -X POST https://${host}:${LOGINPUT_PORT}/v1/ingest \\
-H "Content-Type: application/json" \\
-H "Utm-Api-Key: <YOUR_API_KEY>" \\
Expand Down Expand Up @@ -106,16 +113,16 @@ function MasterCommandSection({ selection }: { selection: RemoteEnableSelection
]
}'`
return (
<Section title={t(`${SHARED}.masterHeader.title`)} step={2}>
<p className="text-sm text-foreground/90">{t(`${SHARED}.masterHeader.body`, { name: selection.apiKey.name })}</p>
<Section title={t(`${SHARED}.instanceHeader.title`)} step={2}>
<p className="text-sm text-foreground/90">{t(`${SHARED}.instanceHeader.body`)}</p>
<div className="flex gap-0 border-b border-border mb-3">
{(['simple', 'batch'] as const).map((v) => (
<button
key={v}
onClick={() => setTab(v)}
className={cn('px-4 py-1.5 text-sm capitalize transition-colors', tab === v ? 'border-b-2 border-primary font-medium' : 'text-muted-foreground hover:text-foreground')}
>
{v === 'simple' ? t(`${SHARED}.masterHeader.tabSimple`) : t(`${SHARED}.masterHeader.tabBatch`)}
{v === 'simple' ? t(`${SHARED}.instanceHeader.tabSimple`) : t(`${SHARED}.instanceHeader.tabBatch`)}
</button>
))}
</div>
Expand Down Expand Up @@ -253,7 +260,7 @@ export function ForwarderGuide({ source, port, sourceType, defaultProto, childre
onRequestAddCollector={handleAddCollector}
/>

{selection.isMaster && selection.apiKey && <MasterCommandSection selection={selection} />}
{selection.isMaster && <MasterCommandSection selection={selection} />}

{/* Vendor-specific steps (device-side config). Hidden in master mode — it targets the forwarder, not master. */}
{!selection.isMaster && children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { Check, ChevronDown, Copy, Loader2, Plus, ShieldAlert, ShieldCheck, Upload } from 'lucide-react'
import { Check, ChevronDown, Copy, KeyRound, Loader2, Plus, ShieldAlert, ShieldCheck, Upload } from 'lucide-react'
import { cn } from '@/shared/lib/utils'
import { Button } from '@/shared/components/ui/button'
import { Section } from '@/features/integrations/components/ui/Section'
Expand All @@ -11,8 +11,6 @@ import { useBilling } from '@/features/billing/services/billing.context'
import { useCollectorIntegration } from '@/features/integrations/hooks/useCollectorIntegration'
import type { ForwarderCollector } from '@/features/integrations/types'
import { defaultPortFor, httpDefaultsFor, type HttpAuth, type Proto } from './protoCatalog'
import { useApiKeysList } from '@/features/api-keys/hooks/useApiKeysList'
import { ApiKeyPicker } from './ApiKeyPicker'

const ROOT = 'integrations.setup.remoteEnable'
const MASTER_ID = -1
Expand All @@ -22,7 +20,6 @@ export interface RemoteEnableSelection {
proto: Proto
port: string
isMaster: boolean
apiKey: { id: number; name: string } | null
}

interface RemoteEnablePanelProps {
Expand Down Expand Up @@ -148,13 +145,11 @@ export function RemoteEnablePanel({
const canUseMaster = license?.edition === 'enterprise' || !!license?.mssp

const { forwarders, setDataType, setCertificates, tlsStatus, dataTypeConfig } = useCollectorIntegration()
const apiKeys = useApiKeysList()

const allForwarders = forwarders.data ?? []

const initialProto = defaultProto ?? availableProtos[0]
const [collectorId, setCollectorId] = useState<number | null>(null)
const [apiKeyId, setApiKeyId] = useState<number | null>(null)
const [proto, setProto] = useState<Proto>(initialProto)
const [port, setPort] = useState(() => defaultPortFor(dataType, initialProto))
const httpDefaults = httpDefaultsFor(dataType)
Expand All @@ -178,7 +173,7 @@ export function RemoteEnablePanel({
const isMaster = collectorId === MASTER_ID
const masterOption: ForwarderCollector = {
id: MASTER_ID,
hostname: t(`${ROOT}.sendToMaster`),
hostname: t(`${ROOT}.sendToInstance`),
ip: '',
version: '',
status: 'online',
Expand All @@ -202,18 +197,12 @@ export function RemoteEnablePanel({
}, [allForwarders, collectorId])

useEffect(() => {
if (collectorId !== MASTER_ID) setApiKeyId(null)
}, [collectorId])

useEffect(() => {
const resolvedKey = apiKeys.data?.data.find((k) => k.id === apiKeyId) ?? null
onSelectionChange?.({
proto: isMaster ? 'https' : proto,
port,
isMaster,
apiKey: resolvedKey ? { id: resolvedKey.id, name: resolvedKey.name } : null,
})
}, [proto, port, isMaster, apiKeyId, apiKeys.data, onSelectionChange])
}, [proto, port, isMaster, onSelectionChange])

const isHttp = proto === 'http' || proto === 'https'
const needsCerts = proto === 'tls' || proto === 'https'
Expand Down Expand Up @@ -344,6 +333,7 @@ export function RemoteEnablePanel({
value={collectorId}
onChange={(id) => {
if (id === ADD_COLLECTOR_ID) {
setCollectorId(null)
onRequestAddCollector?.()
return
}
Expand Down Expand Up @@ -389,21 +379,17 @@ export function RemoteEnablePanel({
</div>

{isMaster && (
<label className="block">
<span className="mb-1 block text-[11px] font-medium text-muted-foreground">
{t(`${ROOT}.apiKeyLabel`)}
</span>
<ApiKeyPicker
keys={apiKeys.data?.data ?? []}
value={apiKeyId}
onChange={setApiKeyId}
onAddNew={() => navigate('/settings/api-keys')}
addLabel={t(`${ROOT}.apiKeyAddNew`)}
placeholder={t(`${ROOT}.apiKeyPlaceholder`)}
emptyLabel={t(`${ROOT}.apiKeyNone`)}
disabled={apiKeys.isLoading}
/>
</label>
<div className="flex flex-col items-start gap-1.5">
<p className="text-[11px] text-muted-foreground">{t(`${ROOT}.apiKeyHint`)}</p>
<Button
size="sm"
variant="outline"
onClick={() => navigate('/settings/api-keys')}
>
<KeyRound size={13} className="mr-1.5" />
{t(`${ROOT}.apiKeyAddNew`)}
</Button>
</div>
)}

{!isMaster && (<>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/tenants/components/TenantCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function enterTenant(tenant: Tenant): void {
id: tenant.id,
name: tenant.name,
access: tenant.supportAccess === 'FULL' ? 'FULL' : 'READ',
domain: tenant.domain,
})
window.location.assign('/home')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function TenantSwitcher() {
id: tenant.id,
name: tenant.name,
access: tenant.supportAccess === 'FULL' ? 'FULL' : 'READ',
domain: tenant.domain,
})
reloadAfterSwitch()
}
Expand Down
Loading
Loading