Backend: AgentRegistryService gains findByApplicationAndEnvironment() and environment-aware addGroupCommandWithReplies() overload. AgentCommandController and ApplicationConfigController accept optional environment query parameter. When set, commands only target agents in that environment. Backward compatible — null means all environments. Frontend: All command mutations (config update, route control, traced processors, tap config, route recording) now pass selectedEnv to the backend via query parameter. Prevents cross-environment command leakage — e.g., updating config for prod no longer pushes to dev agents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
8.1 KiB
TypeScript
235 lines
8.1 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { useAuthStore } from '../../auth/auth-store'
|
|
import { config } from '../../config'
|
|
|
|
// ── Application Config ────────────────────────────────────────────────────
|
|
|
|
export interface TapDefinition {
|
|
tapId: string
|
|
processorId: string
|
|
target: 'INPUT' | 'OUTPUT' | 'BOTH'
|
|
expression: string
|
|
language: string
|
|
attributeName: string
|
|
attributeType: 'BUSINESS_OBJECT' | 'CORRELATION' | 'EVENT' | 'CUSTOM'
|
|
enabled: boolean
|
|
version: number
|
|
}
|
|
|
|
export interface ApplicationConfig {
|
|
application: string
|
|
version: number
|
|
updatedAt?: string
|
|
engineLevel?: string
|
|
payloadCaptureMode?: string
|
|
applicationLogLevel?: string
|
|
agentLogLevel?: string
|
|
metricsEnabled: boolean
|
|
samplingRate: number
|
|
tracedProcessors: Record<string, string>
|
|
taps: TapDefinition[]
|
|
tapVersion: number
|
|
routeRecording: Record<string, boolean>
|
|
compressSuccess: boolean
|
|
}
|
|
|
|
/** Authenticated fetch using the JWT from auth store. Paths are relative to apiBaseUrl. */
|
|
function authFetch(path: string, init?: RequestInit): Promise<Response> {
|
|
const token = useAuthStore.getState().accessToken
|
|
const headers = new Headers(init?.headers)
|
|
if (token) headers.set('Authorization', `Bearer ${token}`)
|
|
headers.set('X-Cameleer-Protocol-Version', '1')
|
|
return fetch(`${config.apiBaseUrl}${path}`, { ...init, headers })
|
|
}
|
|
|
|
export function useAllApplicationConfigs() {
|
|
return useQuery({
|
|
queryKey: ['applicationConfig', 'all'],
|
|
queryFn: async () => {
|
|
const res = await authFetch('/config')
|
|
if (!res.ok) throw new Error('Failed to fetch configs')
|
|
return res.json() as Promise<ApplicationConfig[]>
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useApplicationConfig(application: string | undefined) {
|
|
return useQuery({
|
|
queryKey: ['applicationConfig', application],
|
|
queryFn: async () => {
|
|
const res = await authFetch(`/config/${application}`)
|
|
if (!res.ok) throw new Error('Failed to fetch config')
|
|
return res.json() as Promise<ApplicationConfig>
|
|
},
|
|
enabled: !!application,
|
|
})
|
|
}
|
|
|
|
export interface ConfigUpdateResponse {
|
|
config: ApplicationConfig
|
|
pushResult: CommandGroupResponse
|
|
}
|
|
|
|
export function useUpdateApplicationConfig() {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: async ({ config, environment }: { config: ApplicationConfig; environment?: string }) => {
|
|
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
|
|
const res = await authFetch(`/config/${config.application}${envParam}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to update config')
|
|
return res.json() as Promise<ConfigUpdateResponse>
|
|
},
|
|
onSuccess: (result) => {
|
|
queryClient.setQueryData(['applicationConfig', result.config.application], result.config)
|
|
queryClient.invalidateQueries({ queryKey: ['applicationConfig', 'all'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
// ── Processor → Route Mapping ─────────────────────────────────────────────
|
|
|
|
export function useProcessorRouteMapping(application?: string) {
|
|
return useQuery({
|
|
queryKey: ['config', application, 'processor-routes'],
|
|
queryFn: async () => {
|
|
const res = await authFetch(`/config/${application}/processor-routes`)
|
|
if (!res.ok) throw new Error('Failed to fetch processor-route mapping')
|
|
return res.json() as Promise<Record<string, string>>
|
|
},
|
|
enabled: !!application,
|
|
})
|
|
}
|
|
|
|
// ── Group Command Response ───────────────────────────────────────────────
|
|
|
|
export interface CommandGroupResponse {
|
|
success: boolean
|
|
total: number
|
|
responded: number
|
|
responses: { agentId: string; status: string; message: string }[]
|
|
timedOut: string[]
|
|
}
|
|
|
|
// ── Generic Group Command (kept for non-config commands) ──────────────────
|
|
|
|
interface SendGroupCommandParams {
|
|
group: string
|
|
type: string
|
|
payload: Record<string, unknown>
|
|
environment?: string
|
|
}
|
|
|
|
export function useSendGroupCommand() {
|
|
return useMutation({
|
|
mutationFn: async ({ group, type, payload, environment }: SendGroupCommandParams) => {
|
|
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
|
|
const res = await authFetch(`/agents/groups/${encodeURIComponent(group)}/commands${envParam}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type, payload }),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to send command')
|
|
return res.json() as Promise<CommandGroupResponse>
|
|
},
|
|
})
|
|
}
|
|
|
|
// ── Test Expression ───────────────────────────────────────────────────────
|
|
|
|
export function useTestExpression() {
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
application,
|
|
expression,
|
|
language,
|
|
body,
|
|
target,
|
|
}: {
|
|
application: string
|
|
expression: string
|
|
language: string
|
|
body: string
|
|
target: string
|
|
}) => {
|
|
const res = await authFetch(
|
|
`/config/${encodeURIComponent(application)}/test-expression`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ expression, language, body, target }),
|
|
},
|
|
)
|
|
if (!res.ok) {
|
|
if (res.status === 404) throw new Error('No live agent available')
|
|
if (res.status === 504) throw new Error('Expression test timed out')
|
|
throw new Error('Failed to test expression')
|
|
}
|
|
return res.json() as Promise<{ result?: string; error?: string }>
|
|
},
|
|
})
|
|
}
|
|
|
|
// ── Route Control ────────────────────────────────────────────────────────
|
|
|
|
export function useSendRouteCommand() {
|
|
return useMutation({
|
|
mutationFn: async ({ application, action, routeId, environment }: {
|
|
application: string
|
|
action: 'start' | 'stop' | 'suspend' | 'resume'
|
|
routeId: string
|
|
environment?: string
|
|
}) => {
|
|
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
|
|
const res = await authFetch(`/agents/groups/${encodeURIComponent(application)}/commands${envParam}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type: 'route-control', payload: { routeId, action, nonce: crypto.randomUUID() } }),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to send route command')
|
|
return res.json() as Promise<CommandGroupResponse>
|
|
},
|
|
})
|
|
}
|
|
|
|
// ── Replay Exchange ───────────────────────────────────────────────────────
|
|
|
|
export interface ReplayResult {
|
|
status: string
|
|
message: string
|
|
data?: string
|
|
}
|
|
|
|
export function useReplayExchange() {
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
agentId,
|
|
routeId,
|
|
headers,
|
|
body,
|
|
originalExchangeId,
|
|
}: {
|
|
agentId: string
|
|
routeId: string
|
|
headers?: Record<string, string>
|
|
body: string
|
|
originalExchangeId?: string
|
|
}): Promise<ReplayResult> => {
|
|
const res = await authFetch(`/agents/${encodeURIComponent(agentId)}/replay`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ routeId, body, headers: headers ?? {}, originalExchangeId }),
|
|
})
|
|
if (!res.ok) {
|
|
if (res.status === 404) throw new Error('Agent not found')
|
|
if (res.status === 504) throw new Error('Replay timed out — agent did not respond')
|
|
throw new Error('Failed to send replay command')
|
|
}
|
|
return res.json() as Promise<ReplayResult>
|
|
},
|
|
})
|
|
}
|