diff --git a/frontend/src/features/soar/components/HttpParamsEditor.tsx b/frontend/src/features/soar/components/HttpParamsEditor.tsx index 8c86c3850..41ef00f6f 100644 --- a/frontend/src/features/soar/components/HttpParamsEditor.tsx +++ b/frontend/src/features/soar/components/HttpParamsEditor.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Trash2 } from 'lucide-react' import { Input } from '@/shared/components/ui/input' import { cn } from '@/shared/lib/utils' import { isValidHttpUrl, setHttpBodyError } from '../lib/http-node-validity' @@ -30,6 +31,8 @@ interface Props { onChange: (params: HttpParams) => void } +type PayloadTab = 'body' | 'headers' + // ponytail: URL split via one regex, body highlighted via Prism (already // vendored). Validity for save-blocking flows through http-node-validity — // no context wiring. @@ -45,6 +48,7 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }: const bodyRef = useRef(null) const [bodyText, setBodyText] = useState(() => bodyToText(p.body)) const [bodyError, setBodyError] = useState(null) + const [tab, setTab] = useState('body') useEffect(() => { setBodyText(bodyToText(p.body)) @@ -59,6 +63,10 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }: } }, [showBody, nodeId]) + useEffect(() => { + if (showBody) setTab('body') + }, [showBody]) + useEffect(() => () => setHttpBodyError(nodeId, null), [nodeId]) const commitUrl = (nextScheme: string, nextRest: string) => { @@ -126,6 +134,23 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }: } } + const switchTab = (next: PayloadTab) => { + if (next === tab) return + if (tab === 'body') commitBody(bodyText) + setTab(next) + } + + const headers = (showLabel: boolean) => ( + onChange({ ...p, headers: next })} + /> + ) + return (
@@ -183,32 +208,180 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }: ))}
- {showBody && ( + {showBody ? (
- - {!readOnly && ( -
- +
+ switchTab('body')} + label={t('soar.editor.canvas.http.body')} + /> + switchTab('headers')} + label={t('soar.editor.canvas.http.headers')} + /> +
+ {tab === 'body' ? ( +
+ {!readOnly && ( +
+ +
+ )} + commitBody(bodyText)} + textareaRef={bodyRef} + /> + {bodyError && ( +

+ {t('soar.editor.canvas.http.bodyInvalid')}: {bodyError} +

+ )}
- )} - commitBody(bodyText)} - textareaRef={bodyRef} - /> - {bodyError && ( -

- {t('soar.editor.canvas.http.bodyInvalid')}: {bodyError} -

+ ) : ( + headers(false) )}
+ ) : ( + headers(true) + )} +
+ ) +} + +function TabButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) { + return ( + + ) +} + +function HeaderRows({ + headers, + readOnly, + nodes, + currentNodeId, + showLabel = true, + onChange, +}: { + headers?: Record + readOnly?: boolean + nodes: Record + currentNodeId: string + showLabel?: boolean + onChange: (next: Record | undefined) => void +}) { + const { t } = useTranslation() + const entries = Object.entries(headers ?? {}) + const valueRefs = useRef>([]) + + const commit = (next: Array<[string, string]>) => { + const out: Record = {} + for (const [k, v] of next) { + if (k.trim()) out[k.trim()] = v + } + onChange(Object.keys(out).length > 0 ? out : undefined) + } + + const setAt = (i: number, patch: { key?: string; value?: string }) => { + const next = entries.map(([k, v], j) => + j === i ? ([patch.key ?? k, patch.value ?? v] as [string, string]) : ([k, v] as [string, string]), + ) + commit(next) + } + + const insertIntoValue = (i: number, token: string) => { + const el = valueRefs.current[i] + const cur = entries[i]?.[1] ?? '' + const start = el?.selectionStart ?? cur.length + const end = el?.selectionEnd ?? cur.length + setAt(i, { value: cur.slice(0, start) + token + cur.slice(end) }) + requestAnimationFrame(() => { + const el2 = valueRefs.current[i] + if (!el2) return + el2.focus() + const pos = start + token.length + el2.setSelectionRange(pos, pos) + }) + } + + return ( +
+
+ {showLabel && ( + + )} + {!readOnly && ( + + )} +
+ {entries.length === 0 && readOnly && ( +

—

+ )} +
+ {entries.map(([k, v], i) => ( +
+ setAt(i, { key: e.target.value })} + placeholder="Authorization" + className="h-7 w-2/5 font-mono text-[11px]" + /> + { + valueRefs.current[i] = el + }} + value={v} + readOnly={readOnly} + onChange={(e) => setAt(i, { value: e.target.value })} + placeholder="Bearer $(variables.apiToken)" + className="h-7 flex-1 font-mono text-[11px]" + /> + {!readOnly && ( + <> + insertIntoValue(i, token)} + /> + + + )} +
+ ))} +
) } diff --git a/frontend/src/features/soar/components/NodePalette.tsx b/frontend/src/features/soar/components/NodePalette.tsx index dce0b3c04..505ea262a 100644 --- a/frontend/src/features/soar/components/NodePalette.tsx +++ b/frontend/src/features/soar/components/NodePalette.tsx @@ -13,8 +13,7 @@ const ICONS: Record = { } /** Palette of draggable node types. Each row is one (executor, kind) pair — - * since some executors back both kinds (http, select via kind flag), the - * palette spells them out so the drag payload is unambiguous. */ + * the palette spells them out so the drag payload is unambiguous. */ export function NodePalette({ readOnly }: { readOnly?: boolean }) { const rows: Array<{ meta: ExecutorMeta; kind: NodeKind }> = [] for (const meta of EXECUTOR_CATALOG) { diff --git a/frontend/src/features/soar/types/soar.types.ts b/frontend/src/features/soar/types/soar.types.ts index 27a5d34e3..d25394d91 100644 --- a/frontend/src/features/soar/types/soar.types.ts +++ b/frontend/src/features/soar/types/soar.types.ts @@ -150,7 +150,7 @@ export interface ExecutorMeta { export const EXECUTOR_CATALOG: ExecutorMeta[] = [ { type: 'shell', label: 'Shell (endpoint agent)', kinds: ['executor'] }, - { type: 'http', label: 'HTTP call', kinds: ['executor', 'enrichment'], paramsPlaceholder: { method: 'GET', url: '' } }, + { type: 'http', label: 'HTTP call', kinds: ['enrichment'], paramsPlaceholder: { method: 'GET', url: '' } }, { type: 'llm_enrich', label: 'LLM enrichment', kinds: ['enrichment'], paramsPlaceholder: { prompt: '' } }, { type: 'llm_action', label: 'LLM action', kinds: ['executor'], paramsPlaceholder: { prompt: '' } }, { type: 'notify', label: 'Send notification', kinds: ['executor'], paramsPlaceholder: { message: '', type: 'INFO' } }, diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index 2cb935a27..400e222c0 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -4787,7 +4787,9 @@ "urlInvalid": "Keine gültige http(s)-URL.", "method": "Methode", "body": "Body (JSON)", - "bodyInvalid": "Ungültiges JSON" + "bodyInvalid": "Ungültiges JSON", + "headers": "HTTP-Kopfzeilen", + "addHeader": "Kopfzeile hinzufügen" }, "incident": { "name": "Incident-Name", diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index ce4090511..f64b8014f 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -5173,7 +5173,9 @@ "urlInvalid": "Not a valid http(s) URL.", "method": "Method", "body": "Body (JSON)", - "bodyInvalid": "Invalid JSON" + "bodyInvalid": "Invalid JSON", + "headers": "HTTP headers", + "addHeader": "Add header" }, "incident": { "name": "Incident name", diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index c3d2eabf3..5bcfdc39c 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -4909,7 +4909,9 @@ "urlInvalid": "URL http(s) inválida.", "method": "Método", "body": "Cuerpo (JSON)", - "bodyInvalid": "JSON inválido" + "bodyInvalid": "JSON inválido", + "headers": "Encabezados HTTP", + "addHeader": "Agregar encabezado" }, "incident": { "name": "Nombre del incidente", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index ce727a7c4..aa77acdf7 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -4787,7 +4787,9 @@ "urlInvalid": "URL http(s) invalide.", "method": "Méthode", "body": "Corps (JSON)", - "bodyInvalid": "JSON invalide" + "bodyInvalid": "JSON invalide", + "headers": "En-têtes HTTP", + "addHeader": "Ajouter un en-tête" }, "incident": { "name": "Nom de l'incident", diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index bcc9d65bc..b7129fc1e 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -4787,7 +4787,9 @@ "urlInvalid": "URL http(s) non valido.", "method": "Metodo", "body": "Corpo (JSON)", - "bodyInvalid": "JSON non valido" + "bodyInvalid": "JSON non valido", + "headers": "Intestazioni HTTP", + "addHeader": "Aggiungi intestazione" }, "incident": { "name": "Nome dell'incidente", diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index ed0bea69c..e95ba0418 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -4909,7 +4909,9 @@ "urlInvalid": "URL http(s) inválida.", "method": "Método", "body": "Corpo (JSON)", - "bodyInvalid": "JSON inválido" + "bodyInvalid": "JSON inválido", + "headers": "Cabeçalhos HTTP", + "addHeader": "Adicionar cabeçalho" }, "incident": { "name": "Nome do incidente", diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index 07e5c70e6..783059061 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -4579,7 +4579,9 @@ "urlInvalid": "Недопустимый http(s) URL.", "method": "Метод", "body": "Тело (JSON)", - "bodyInvalid": "Недопустимый JSON" + "bodyInvalid": "Недопустимый JSON", + "headers": "HTTP-заголовки", + "addHeader": "Добавить заголовок" }, "incident": { "name": "Название инцидента",