Skip to content

Commit f54e686

Browse files
committed
Merge branch 'release/v12.0.0' of https://github.com/utmstack/UTMStack into release/v12.0.0
2 parents c645627 + fd0957c commit f54e686

11 files changed

Lines changed: 241 additions & 35 deletions

File tree

frontend/src/features/soar/components/HttpParamsEditor.tsx

Lines changed: 194 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useRef, useState } from 'react'
22
import { useTranslation } from 'react-i18next'
3+
import { Trash2 } from 'lucide-react'
34
import { Input } from '@/shared/components/ui/input'
45
import { cn } from '@/shared/lib/utils'
56
import { isValidHttpUrl, setHttpBodyError } from '../lib/http-node-validity'
@@ -30,6 +31,8 @@ interface Props {
3031
onChange: (params: HttpParams) => void
3132
}
3233

34+
type PayloadTab = 'body' | 'headers'
35+
3336
// ponytail: URL split via one regex, body highlighted via Prism (already
3437
// vendored). Validity for save-blocking flows through http-node-validity —
3538
// no context wiring.
@@ -45,6 +48,7 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }:
4548
const bodyRef = useRef<HTMLTextAreaElement>(null)
4649
const [bodyText, setBodyText] = useState(() => bodyToText(p.body))
4750
const [bodyError, setBodyError] = useState<string | null>(null)
51+
const [tab, setTab] = useState<PayloadTab>('body')
4852

4953
useEffect(() => {
5054
setBodyText(bodyToText(p.body))
@@ -59,6 +63,10 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }:
5963
}
6064
}, [showBody, nodeId])
6165

66+
useEffect(() => {
67+
if (showBody) setTab('body')
68+
}, [showBody])
69+
6270
useEffect(() => () => setHttpBodyError(nodeId, null), [nodeId])
6371

6472
const commitUrl = (nextScheme: string, nextRest: string) => {
@@ -126,6 +134,23 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }:
126134
}
127135
}
128136

137+
const switchTab = (next: PayloadTab) => {
138+
if (next === tab) return
139+
if (tab === 'body') commitBody(bodyText)
140+
setTab(next)
141+
}
142+
143+
const headers = (showLabel: boolean) => (
144+
<HeaderRows
145+
headers={p.headers}
146+
readOnly={readOnly}
147+
nodes={nodes}
148+
currentNodeId={nodeId}
149+
showLabel={showLabel}
150+
onChange={(next) => onChange({ ...p, headers: next })}
151+
/>
152+
)
153+
129154
return (
130155
<div className="space-y-2">
131156
<div>
@@ -183,32 +208,180 @@ export function HttpParamsEditor({ nodeId, nodes, params, readOnly, onChange }:
183208
))}
184209
</select>
185210
</div>
186-
{showBody && (
211+
{showBody ? (
187212
<div>
188-
<label className="mb-1 block text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
189-
{t('soar.editor.canvas.http.body')}
190-
</label>
191-
{!readOnly && (
192-
<div className="mb-1 flex flex-wrap items-center gap-1.5">
193-
<InsertFieldMenu nodes={nodes} currentNodeId={nodeId} onInsert={insertIntoBody} />
213+
<div className="flex gap-1 border-b border-border">
214+
<TabButton
215+
active={tab === 'body'}
216+
onClick={() => switchTab('body')}
217+
label={t('soar.editor.canvas.http.body')}
218+
/>
219+
<TabButton
220+
active={tab === 'headers'}
221+
onClick={() => switchTab('headers')}
222+
label={t('soar.editor.canvas.http.headers')}
223+
/>
224+
</div>
225+
{tab === 'body' ? (
226+
<div>
227+
{!readOnly && (
228+
<div className="mb-1 flex flex-wrap items-center gap-1.5">
229+
<InsertFieldMenu nodes={nodes} currentNodeId={nodeId} onInsert={insertIntoBody} />
230+
</div>
231+
)}
232+
<JsonCodeEditor
233+
value={bodyText}
234+
readOnly={readOnly}
235+
placeholder='{"foo":"bar"}'
236+
invalid={Boolean(bodyError)}
237+
onChange={setBodyText}
238+
onBlur={() => commitBody(bodyText)}
239+
textareaRef={bodyRef}
240+
/>
241+
{bodyError && (
242+
<p className="mt-1 text-[10px] text-red-500">
243+
{t('soar.editor.canvas.http.bodyInvalid')}: {bodyError}
244+
</p>
245+
)}
194246
</div>
195-
)}
196-
<JsonCodeEditor
197-
value={bodyText}
198-
readOnly={readOnly}
199-
placeholder='{"foo":"bar"}'
200-
invalid={Boolean(bodyError)}
201-
onChange={setBodyText}
202-
onBlur={() => commitBody(bodyText)}
203-
textareaRef={bodyRef}
204-
/>
205-
{bodyError && (
206-
<p className="mt-1 text-[10px] text-red-500">
207-
{t('soar.editor.canvas.http.bodyInvalid')}: {bodyError}
208-
</p>
247+
) : (
248+
headers(false)
209249
)}
210250
</div>
251+
) : (
252+
headers(true)
253+
)}
254+
</div>
255+
)
256+
}
257+
258+
function TabButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) {
259+
return (
260+
<button
261+
type="button"
262+
onClick={onClick}
263+
className={cn(
264+
'rounded-t border-b-2 px-2 py-1 text-[10px] font-medium uppercase tracking-wider transition-colors',
265+
active
266+
? 'border-primary text-foreground'
267+
: 'border-transparent text-muted-foreground hover:text-foreground',
211268
)}
269+
>
270+
{label}
271+
</button>
272+
)
273+
}
274+
275+
function HeaderRows({
276+
headers,
277+
readOnly,
278+
nodes,
279+
currentNodeId,
280+
showLabel = true,
281+
onChange,
282+
}: {
283+
headers?: Record<string, string>
284+
readOnly?: boolean
285+
nodes: Record<string, FlowNode>
286+
currentNodeId: string
287+
showLabel?: boolean
288+
onChange: (next: Record<string, string> | undefined) => void
289+
}) {
290+
const { t } = useTranslation()
291+
const entries = Object.entries(headers ?? {})
292+
const valueRefs = useRef<Array<HTMLInputElement | null>>([])
293+
294+
const commit = (next: Array<[string, string]>) => {
295+
const out: Record<string, string> = {}
296+
for (const [k, v] of next) {
297+
if (k.trim()) out[k.trim()] = v
298+
}
299+
onChange(Object.keys(out).length > 0 ? out : undefined)
300+
}
301+
302+
const setAt = (i: number, patch: { key?: string; value?: string }) => {
303+
const next = entries.map(([k, v], j) =>
304+
j === i ? ([patch.key ?? k, patch.value ?? v] as [string, string]) : ([k, v] as [string, string]),
305+
)
306+
commit(next)
307+
}
308+
309+
const insertIntoValue = (i: number, token: string) => {
310+
const el = valueRefs.current[i]
311+
const cur = entries[i]?.[1] ?? ''
312+
const start = el?.selectionStart ?? cur.length
313+
const end = el?.selectionEnd ?? cur.length
314+
setAt(i, { value: cur.slice(0, start) + token + cur.slice(end) })
315+
requestAnimationFrame(() => {
316+
const el2 = valueRefs.current[i]
317+
if (!el2) return
318+
el2.focus()
319+
const pos = start + token.length
320+
el2.setSelectionRange(pos, pos)
321+
})
322+
}
323+
324+
return (
325+
<div>
326+
<div className="mb-1 flex items-center justify-between">
327+
{showLabel && (
328+
<label className="block text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
329+
{t('soar.editor.canvas.http.headers')}
330+
</label>
331+
)}
332+
{!readOnly && (
333+
<button
334+
type="button"
335+
onClick={() => commit([...entries, ['', '']])}
336+
className="rounded px-1.5 py-0.5 text-[10px] text-primary hover:bg-muted"
337+
>
338+
{t('soar.editor.canvas.http.addHeader')}
339+
</button>
340+
)}
341+
</div>
342+
{entries.length === 0 && readOnly && (
343+
<p className="text-[10px] text-muted-foreground"></p>
344+
)}
345+
<div className="space-y-1">
346+
{entries.map(([k, v], i) => (
347+
<div key={i} className="flex items-center gap-1">
348+
<Input
349+
value={k}
350+
readOnly={readOnly}
351+
onChange={(e) => setAt(i, { key: e.target.value })}
352+
placeholder="Authorization"
353+
className="h-7 w-2/5 font-mono text-[11px]"
354+
/>
355+
<Input
356+
ref={(el) => {
357+
valueRefs.current[i] = el
358+
}}
359+
value={v}
360+
readOnly={readOnly}
361+
onChange={(e) => setAt(i, { value: e.target.value })}
362+
placeholder="Bearer $(variables.apiToken)"
363+
className="h-7 flex-1 font-mono text-[11px]"
364+
/>
365+
{!readOnly && (
366+
<>
367+
<InsertFieldMenu
368+
nodes={nodes}
369+
currentNodeId={currentNodeId}
370+
onInsert={(token) => insertIntoValue(i, token)}
371+
/>
372+
<button
373+
type="button"
374+
onClick={() => commit(entries.filter((_, j) => j !== i))}
375+
className="rounded p-1 text-muted-foreground hover:text-red-500"
376+
title={t('soar.editor.canvas.deleteNode')}
377+
>
378+
<Trash2 size={12} />
379+
</button>
380+
</>
381+
)}
382+
</div>
383+
))}
384+
</div>
212385
</div>
213386
)
214387
}

frontend/src/features/soar/components/NodePalette.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ const ICONS: Record<string, typeof Terminal> = {
1313
}
1414

1515
/** Palette of draggable node types. Each row is one (executor, kind) pair —
16-
* since some executors back both kinds (http, select via kind flag), the
17-
* palette spells them out so the drag payload is unambiguous. */
16+
* the palette spells them out so the drag payload is unambiguous. */
1817
export function NodePalette({ readOnly }: { readOnly?: boolean }) {
1918
const rows: Array<{ meta: ExecutorMeta; kind: NodeKind }> = []
2019
for (const meta of EXECUTOR_CATALOG) {

frontend/src/features/soar/types/soar.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export interface ExecutorMeta {
150150

151151
export const EXECUTOR_CATALOG: ExecutorMeta[] = [
152152
{ type: 'shell', label: 'Shell (endpoint agent)', kinds: ['executor'] },
153-
{ type: 'http', label: 'HTTP call', kinds: ['executor', 'enrichment'], paramsPlaceholder: { method: 'GET', url: '' } },
153+
{ type: 'http', label: 'HTTP call', kinds: ['enrichment'], paramsPlaceholder: { method: 'GET', url: '' } },
154154
{ type: 'llm_enrich', label: 'LLM enrichment', kinds: ['enrichment'], paramsPlaceholder: { prompt: '' } },
155155
{ type: 'llm_action', label: 'LLM action', kinds: ['executor'], paramsPlaceholder: { prompt: '' } },
156156
{ type: 'notify', label: 'Send notification', kinds: ['executor'], paramsPlaceholder: { message: '', type: 'INFO' } },

frontend/src/shared/i18n/locales/de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4787,7 +4787,9 @@
47874787
"urlInvalid": "Keine gültige http(s)-URL.",
47884788
"method": "Methode",
47894789
"body": "Body (JSON)",
4790-
"bodyInvalid": "Ungültiges JSON"
4790+
"bodyInvalid": "Ungültiges JSON",
4791+
"headers": "HTTP-Kopfzeilen",
4792+
"addHeader": "Kopfzeile hinzufügen"
47914793
},
47924794
"incident": {
47934795
"name": "Incident-Name",

frontend/src/shared/i18n/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5173,7 +5173,9 @@
51735173
"urlInvalid": "Not a valid http(s) URL.",
51745174
"method": "Method",
51755175
"body": "Body (JSON)",
5176-
"bodyInvalid": "Invalid JSON"
5176+
"bodyInvalid": "Invalid JSON",
5177+
"headers": "HTTP headers",
5178+
"addHeader": "Add header"
51775179
},
51785180
"incident": {
51795181
"name": "Incident name",

frontend/src/shared/i18n/locales/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4909,7 +4909,9 @@
49094909
"urlInvalid": "URL http(s) inválida.",
49104910
"method": "Método",
49114911
"body": "Cuerpo (JSON)",
4912-
"bodyInvalid": "JSON inválido"
4912+
"bodyInvalid": "JSON inválido",
4913+
"headers": "Encabezados HTTP",
4914+
"addHeader": "Agregar encabezado"
49134915
},
49144916
"incident": {
49154917
"name": "Nombre del incidente",

frontend/src/shared/i18n/locales/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4787,7 +4787,9 @@
47874787
"urlInvalid": "URL http(s) invalide.",
47884788
"method": "Méthode",
47894789
"body": "Corps (JSON)",
4790-
"bodyInvalid": "JSON invalide"
4790+
"bodyInvalid": "JSON invalide",
4791+
"headers": "En-têtes HTTP",
4792+
"addHeader": "Ajouter un en-tête"
47914793
},
47924794
"incident": {
47934795
"name": "Nom de l'incident",

frontend/src/shared/i18n/locales/it.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4787,7 +4787,9 @@
47874787
"urlInvalid": "URL http(s) non valido.",
47884788
"method": "Metodo",
47894789
"body": "Corpo (JSON)",
4790-
"bodyInvalid": "JSON non valido"
4790+
"bodyInvalid": "JSON non valido",
4791+
"headers": "Intestazioni HTTP",
4792+
"addHeader": "Aggiungi intestazione"
47914793
},
47924794
"incident": {
47934795
"name": "Nome dell'incidente",

frontend/src/shared/i18n/locales/pt.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4909,7 +4909,9 @@
49094909
"urlInvalid": "URL http(s) inválida.",
49104910
"method": "Método",
49114911
"body": "Corpo (JSON)",
4912-
"bodyInvalid": "JSON inválido"
4912+
"bodyInvalid": "JSON inválido",
4913+
"headers": "Cabeçalhos HTTP",
4914+
"addHeader": "Adicionar cabeçalho"
49134915
},
49144916
"incident": {
49154917
"name": "Nome do incidente",

frontend/src/shared/i18n/locales/ru.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4579,7 +4579,9 @@
45794579
"urlInvalid": "Недопустимый http(s) URL.",
45804580
"method": "Метод",
45814581
"body": "Тело (JSON)",
4582-
"bodyInvalid": "Недопустимый JSON"
4582+
"bodyInvalid": "Недопустимый JSON",
4583+
"headers": "HTTP-заголовки",
4584+
"addHeader": "Добавить заголовок"
45834585
},
45844586
"incident": {
45854587
"name": "Название инцидента",

0 commit comments

Comments
 (0)