admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / frontend / src / playbook / nodes / CommandNode.tsx
3958 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import { useRef } from 'react' import { Handle, Position, useReactFlow, type NodeProps } from '@xyflow/react' import type { CommandNode as CommandNodeType } from '../types' /** Ticket-derived placeholder tokens - see app/ai/guardrails.py's * PLACEHOLDER_* constants, which resolve these at investigate/execute * time (e.g. "{{username}}@{{ip_address}}" becomes "[email protected]"). Kept * as a single source of truth for the token strings on this side too. */ const PLACEHOLDERS = [ { label: '+ Username', token: '{{username}}' }, { label: '+ IP Address', token: '{{ip_address}}' }, { label: '+ Password', token: '{{password}}' }, // Makes this a "command runner" playbook: the command is supplied at run // time (AI-proposed, human-editable) instead of fixed here. Global-admin // only, always human-in-the-loop - see app/routers/playbooks.py. { label: '+ AI Command', token: '{{command}}' }, ] export function CommandNode({ id, data }: NodeProps<CommandNodeType>) { const { updateNodeData, deleteElements } = useReactFlow() const textareaRef = useRef<HTMLTextAreaElement>(null) const insertPlaceholder = (token: string) => { const el = textareaRef.current const current = data.command || '' if (!el) { updateNodeData(id, { command: current + token }) return } const start = el.selectionStart ?? current.length const end = el.selectionEnd ?? current.length const next = current.slice(0, start) + token + current.slice(end) updateNodeData(id, { command: next }) // Restore focus/cursor after the inserted token once the value commits. requestAnimationFrame(() => { el.focus() const pos = start + token.length el.setSelectionRange(pos, pos) }) } return ( <div className="w-72 rounded-md border border-cortex-border bg-cortex-panel shadow-lg"> <Handle type="target" position={Position.Top} className="!bg-accent-blue !w-2.5 !h-2.5 !border-cortex-bg" /> <div className="flex items-center justify-between px-3 py-1.5 border-b border-cortex-border bg-cortex-panel2 rounded-t-md"> <span className="text-[10px] font-display font-bold uppercase tracking-widest text-accent-blue">Command</span> <button onClick={() => deleteElements({ nodes: [{ id }] })} className="text-cortex-muted hover:text-accent-red text-xs leading-none" title="Delete node" > × </button> </div> <div className="p-3 space-y-2"> <input value={data.label} onChange={(e) => updateNodeData(id, { label: e.target.value })} placeholder="Step label" className="w-full bg-cortex-panel2 border border-cortex-border rounded px-2 py-1 text-xs focus:outline-none focus:border-accent-blue nodrag" /> <textarea ref={textareaRef} value={data.command} onChange={(e) => updateNodeData(id, { command: e.target.value })} placeholder="Shell command to run" rows={2} className="w-full bg-cortex-panel2 border border-cortex-border rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-accent-blue nodrag resize-none" /> <div className="flex flex-wrap gap-1 nodrag"> {PLACEHOLDERS.map((p) => ( <button key={p.token} type="button" onClick={() => insertPlaceholder(p.token)} title={`Insert ${p.token} - resolved from the ticket at investigate/execute time`} className="text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded border border-cortex-border text-cortex-muted hover:border-accent-blue hover:text-accent-blue transition" > {p.label} </button> ))} </div> </div> <Handle type="source" position={Position.Bottom} className="!bg-accent-blue !w-2.5 !h-2.5 !border-cortex-bg" /> </div> ) } |