From d87d4939fa301a65fc830ad0f10e9da45dfd999c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 2 Jul 2026 07:34:22 -0600 Subject: [PATCH 1/5] fix[backend](soar): added soar rules filter types --- backend/modules/soar/domain/filter.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/modules/soar/domain/filter.go b/backend/modules/soar/domain/filter.go index 25ece3d95..4325706e4 100644 --- a/backend/modules/soar/domain/filter.go +++ b/backend/modules/soar/domain/filter.go @@ -3,9 +3,23 @@ package domain type OperatorType string const ( - OperatorIS OperatorType = "IS" - OperatorIsOneOf OperatorType = "IS_ONE_OF" - OperatorIsNotOneOf OperatorType = "IS_NOT_ONE_OF" + OperatorIS OperatorType = "IS" + OperatorISNot OperatorType = "IS_NOT" + + OperatorContains OperatorType = "CONTAINS" + OperatorNotContains OperatorType = "NOT_CONTAINS" + + OperatorExists OperatorType = "EXISTS" + OperatorNotExists OperatorType = "NOT_EXISTS" + + OperatorStartWith OperatorType = "START_WITH" + OperatorNotStartWith OperatorType = "NOT_START_WITH" + + OperatorEndsWith OperatorType = "ENDS_WITH" + OperatorNotEndsWith OperatorType = "NOT_ENDS_WITH" + + OperatorIsOneOf OperatorType = "IS_ONE_OF" + OperatorIsNotOneOf OperatorType = "IS_NOT_ONE_OF" ) type FilterType struct { From f7d172952cabe6cc71300db4aacb8612bd95e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 2 Jul 2026 07:36:14 -0600 Subject: [PATCH 2/5] fix[frontend](soar): added missing soar rule filter --- .../features/soar/components/FlowEditor.tsx | 6 ++- frontend/src/features/soar/lib/flow-yaml.ts | 27 +++++++------ .../src/features/soar/types/soar.types.ts | 38 +++++++++++++++++-- frontend/src/shared/i18n/locales/de.json | 9 +++++ frontend/src/shared/i18n/locales/en.json | 9 +++++ frontend/src/shared/i18n/locales/es.json | 9 +++++ frontend/src/shared/i18n/locales/fr.json | 9 +++++ frontend/src/shared/i18n/locales/it.json | 9 +++++ frontend/src/shared/i18n/locales/pt.json | 9 +++++ frontend/src/shared/i18n/locales/ru.json | 9 +++++ 10 files changed, 116 insertions(+), 18 deletions(-) diff --git a/frontend/src/features/soar/components/FlowEditor.tsx b/frontend/src/features/soar/components/FlowEditor.tsx index 8d52d2611..11958dd3b 100644 --- a/frontend/src/features/soar/components/FlowEditor.tsx +++ b/frontend/src/features/soar/components/FlowEditor.tsx @@ -11,7 +11,7 @@ import { VariablesManager } from '@/features/datasources/components/VariablesMan import { soarFlowsService, SoarHttpError } from '../services/soar-flows.service' import { flowToForm, formToInput, flowFormToYaml, yamlToFlowForm, type FlowFormState } from '../lib/flow-yaml' import { ALERT_FIELDS, COMMON_PLATFORMS, defaultShellForPlatform, shellsForPlatform } from '../lib/alert-fields' -import { SOAR_OPERATORS, type Flow, type FlowCondition, type SoarOperator } from '../types/soar.types' +import { SOAR_MULTI_VALUE_OPERATORS, SOAR_NO_VALUE_OPERATORS, SOAR_OPERATORS, type Flow, type FlowCondition, type SoarOperator } from '../types/soar.types' const SELECT = 'h-8 rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring' @@ -381,7 +381,9 @@ function ConditionsEditor({ ))} - setAt(i, { value: e.target.value })} placeholder={c.operator === 'IS' ? t('soar.editor.value') : t('soar.editor.valueList')} className="h-8 min-w-[140px] flex-1 font-mono text-xs" /> + {!SOAR_NO_VALUE_OPERATORS.includes(c.operator) && ( + setAt(i, { value: e.target.value })} placeholder={SOAR_MULTI_VALUE_OPERATORS.includes(c.operator) ? t('soar.editor.valueList') : t('soar.editor.value')} className="h-8 min-w-[140px] flex-1 font-mono text-xs" /> + )} {!readOnly && ( +
+ {i > 0 && ( +
+
+ +
)} +
+ {i + 1} + { + refs.current[i] = el + }} + value={cmd.command} + readOnly={readOnly} + onFocus={() => setActive(i)} + onClick={() => setActive(i)} + onChange={(e) => onChange(commands.map((x, k) => (k === i ? { ...x, command: e.target.value } : x)))} + placeholder='net user "$(target.user)" /active:no' + className="h-8 flex-1 font-mono text-xs" + /> + {!readOnly && ( + + )} +
))}
{!readOnly && ( - )} diff --git a/frontend/src/features/soar/lib/flow-yaml.ts b/frontend/src/features/soar/lib/flow-yaml.ts index c68ab4a0a..d96c01781 100644 --- a/frontend/src/features/soar/lib/flow-yaml.ts +++ b/frontend/src/features/soar/lib/flow-yaml.ts @@ -1,5 +1,5 @@ import { dump, load } from 'js-yaml' -import { SOAR_MULTI_VALUE_OPERATORS, SOAR_NO_VALUE_OPERATORS, SOAR_OPERATORS, type Flow, type FlowCondition, type SaveFlowInput, type SoarOperator } from '../types/soar.types' +import { SOAR_CONDITIONS, SOAR_MULTI_VALUE_OPERATORS, SOAR_NO_VALUE_OPERATORS, SOAR_OPERATORS, type Flow, type FlowCommand, type FlowCondition, type SaveFlowInput, type SoarCondition, type SoarOperator } from '../types/soar.types' /** Structured editing state for a flow. `active` is managed by the toggle (not in * the YAML file), so callers preserve it across the code round-trip. */ @@ -7,7 +7,7 @@ export interface FlowFormState { name: string description: string conditions: FlowCondition[] - commands: string[] + commands: FlowCommand[] shell: string agentPlatform: string defaultAgent: string @@ -23,6 +23,16 @@ function isRecord(v: unknown): v is Record { const str = (v: unknown): string => (v == null ? '' : String(v)) const strArray = (v: unknown): string[] => (Array.isArray(v) ? v.map(String) : []) +/** Accept either the object form `{command, condition?}` or a legacy bare + * string (older YAML files). Unknown/invalid condition values are dropped. */ +function parseCommand(c: unknown): FlowCommand { + if (typeof c === 'string') return { command: c } + if (!isRecord(c)) return { command: '' } + const command = str(c.command) + const cond = str(c.condition) + return SOAR_CONDITIONS.includes(cond as SoarCondition) ? { command, condition: cond as SoarCondition } : { command } +} + function parseCond(c: unknown): FlowCondition { const o = isRecord(c) ? c : {} const op = (SOAR_OPERATORS.includes(str(o.operator) as SoarOperator) ? o.operator : 'IS') as SoarOperator @@ -53,7 +63,7 @@ export function flowToForm(f?: Flow): FlowFormState { name: f?.name ?? '', description: f?.description ?? '', conditions: f?.conditions?.length ? f.conditions.map((c) => ({ ...c })) : [{ operator: 'IS', field: '', value: '' }], - commands: f?.commands?.length ? [...f.commands] : [''], + commands: f?.commands?.length ? f.commands.map((c) => ({ ...c })) : [{ command: '' }], shell: f?.shell ?? '', agentPlatform: f?.agentPlatform ?? '', defaultAgent: f?.defaultAgent ?? '', @@ -63,11 +73,15 @@ export function flowToForm(f?: Flow): FlowFormState { } export function formToInput(f: FlowFormState): SaveFlowInput { + const commands = f.commands + .map((c) => ({ ...c, command: c.command.trim() })) + .filter((c) => c.command) + .map((c, i) => (i === 0 ? { command: c.command } : { command: c.command, condition: c.condition ?? 'Always' })) return { name: f.name.trim(), description: f.description.trim(), conditions: f.conditions.filter((c) => c.field.trim()).map(normalizeCond), - commands: f.commands.map((c) => c.trim()).filter(Boolean), + commands, active: f.active, agentPlatform: f.agentPlatform.trim(), defaultAgent: f.defaultAgent.trim(), @@ -103,12 +117,12 @@ export function yamlToFlowForm(content: string): FlowParseResult { if (!isRecord(raw)) return { ok: false, error: 'root must be a flow mapping' } const conditions = Array.isArray(raw.conditions) ? raw.conditions.map(parseCond) : [] - const commands = Array.isArray(raw.commands) ? raw.commands.map(String) : [] + const commands = Array.isArray(raw.commands) ? raw.commands.map(parseCommand) : [] const form: FlowFormState = { name: str(raw.name), description: str(raw.description), conditions: conditions.length ? conditions : [{ operator: 'IS', field: '', value: '' }], - commands: commands.length ? commands : [''], + commands: commands.length ? commands : [{ command: '' }], shell: str(raw.shell), agentPlatform: str(raw.agentPlatform), defaultAgent: str(raw.defaultAgent), diff --git a/frontend/src/features/soar/types/soar.types.ts b/frontend/src/features/soar/types/soar.types.ts index c87f9e88a..0ac1ba6c4 100644 --- a/frontend/src/features/soar/types/soar.types.ts +++ b/frontend/src/features/soar/types/soar.types.ts @@ -41,13 +41,24 @@ export interface FlowCondition { value: unknown } +export type SoarCondition = 'OnSuccess' | 'OnFailure' | 'Always' +export const SOAR_CONDITIONS: SoarCondition[] = ['OnSuccess', 'OnFailure', 'Always'] + +/** Join semantic for a command relative to the previous one: + * OnSuccess → `&&`, OnFailure → `||`, Always → `;`. Absent on the first + * command (nothing to chain against). */ +export interface FlowCommand { + command: string + condition?: SoarCondition +} + /** An alert-response flow (file-backed YAML). Identity is `relPath`. */ export interface Flow { relPath: string name: string description: string conditions: FlowCondition[] - commands: string[] + commands: FlowCommand[] active: boolean agentPlatform: string defaultAgent: string @@ -62,7 +73,7 @@ export interface SaveFlowInput { name: string description: string conditions: FlowCondition[] - commands: string[] + commands: FlowCommand[] active: boolean agentPlatform: string defaultAgent: string From b80f41c62dbe54405b4efb208b93f959f8cc8870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 2 Jul 2026 08:46:52 -0600 Subject: [PATCH 5/5] fix[frontend](soar): added prebuilt templates for soar rules --- .../soar/components/TemplatePicker.tsx | 112 ++++++++++++++++++ .../src/features/soar/pages/FlowsPage.tsx | 35 +++++- .../soar/services/soar-templates.service.ts | 15 +++ .../src/features/soar/types/soar.types.ts | 15 +++ frontend/src/shared/i18n/locales/de.json | 14 +++ frontend/src/shared/i18n/locales/en.json | 14 +++ frontend/src/shared/i18n/locales/es.json | 14 +++ frontend/src/shared/i18n/locales/fr.json | 14 +++ frontend/src/shared/i18n/locales/it.json | 14 +++ frontend/src/shared/i18n/locales/pt.json | 14 +++ frontend/src/shared/i18n/locales/ru.json | 14 +++ 11 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/soar/components/TemplatePicker.tsx create mode 100644 frontend/src/features/soar/services/soar-templates.service.ts diff --git a/frontend/src/features/soar/components/TemplatePicker.tsx b/frontend/src/features/soar/components/TemplatePicker.tsx new file mode 100644 index 000000000..deb5bfe62 --- /dev/null +++ b/frontend/src/features/soar/components/TemplatePicker.tsx @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { AlertTriangle, FileText, Loader2, Lock, X } from 'lucide-react' +import { Button } from '@/shared/components/ui/button' +import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll' +import { soarTemplatesService } from '../services/soar-templates.service' +import type { ActionTemplate } from '../types/soar.types' + +const PAGE_SIZE = 20 + +export function TemplatePicker({ + onPick, + onScratch, + onClose, +}: { + onPick: (t: ActionTemplate) => void + onScratch: () => void + onClose: () => void +}) { + const { t } = useTranslation() + const [items, setItems] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(0) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + + const loadPage = useCallback(async (p: number) => { + setLoading(true) + setError(false) + try { + const res = await soarTemplatesService.list({ page: p, size: PAGE_SIZE }) + setTotal(res.total) + setItems((prev) => (p === 0 ? res.data : [...prev, ...res.data])) + } catch { + setError(true) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + loadPage(page) + }, [page, loadPage]) + + return ( +
+
e.stopPropagation()}> +
+
+

{t('soar.templates.title')}

+

{t('soar.templates.subtitle')}

+
+ +
+ +
+ {loading && items.length === 0 ? ( +
+ {t('soar.templates.loading')} +
+ ) : error && items.length === 0 ? ( +
+ {t('soar.templates.loadError')} + +
+ ) : items.length === 0 ? ( +
{t('soar.templates.empty')}
+ ) : ( +
+ {items.map((tpl) => ( + + ))} + setPage((p) => p + 1)} + hasMore={items.length < total} + loading={loading} + endLabel={t('common.allLoaded', { count: total })} + /> +
+ )} +
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/src/features/soar/pages/FlowsPage.tsx b/frontend/src/features/soar/pages/FlowsPage.tsx index b88a93e00..bd998fb2f 100644 --- a/frontend/src/features/soar/pages/FlowsPage.tsx +++ b/frontend/src/features/soar/pages/FlowsPage.tsx @@ -9,8 +9,9 @@ import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll' import { soarFlowsService } from '../services/soar-flows.service' import { soarExecutionsService } from '../services/soar-executions.service' import { FlowEditor } from '../components/FlowEditor' +import { TemplatePicker } from '../components/TemplatePicker' import { ExecutionsView } from '../components/ExecutionsView' -import type { Flow } from '../types/soar.types' +import type { ActionTemplate, Flow } from '../types/soar.types' type PageTab = 'flows' | 'executions' @@ -85,8 +86,27 @@ function FlowsTab() { const [loading, setLoading] = useState(true) const [error, setError] = useState(false) const [editing, setEditing] = useState<{ flow?: Flow; creating: boolean } | null>(null) + const [pickingTemplate, setPickingTemplate] = useState(false) const [stats, setStats] = useState>({}) + const startFromTemplate = (tpl: ActionTemplate) => { + const seed: Flow = { + relPath: '', + name: tpl.title, + description: tpl.description ?? '', + conditions: [], + commands: [{ command: tpl.command }], + active: true, + agentPlatform: '', + defaultAgent: '', + shell: '', + excludedAgents: [], + systemOwner: false, + } + setPickingTemplate(false) + setEditing({ flow: seed, creating: true }) + } + useEffect(() => { const h = setTimeout(() => { setDebounced(search.trim()) @@ -186,7 +206,7 @@ function FlowsTab() {
-
@@ -239,6 +259,17 @@ function FlowsTab() { }} /> )} + + {pickingTemplate && ( + { + setPickingTemplate(false) + setEditing({ creating: true }) + }} + onClose={() => setPickingTemplate(false)} + /> + )} ) } diff --git a/frontend/src/features/soar/services/soar-templates.service.ts b/frontend/src/features/soar/services/soar-templates.service.ts new file mode 100644 index 000000000..bc100e261 --- /dev/null +++ b/frontend/src/features/soar/services/soar-templates.service.ts @@ -0,0 +1,15 @@ +import { createApiClient } from '@/shared/lib/api-client' +import type { ActionTemplate, ActionTemplateListQuery } from '../types/soar.types' + +const api = createApiClient() +const BASE = '/soar/action-templates' + +export const soarTemplatesService = { + // Returns { data: ActionTemplate[], total } — total from X-Total-Count. + list: (q: ActionTemplateListQuery = {}) => { + const p = new URLSearchParams() + p.set('page', String(q.page ?? 0)) + p.set('size', String(q.size ?? 20)) + return api.getPaged(`${BASE}?${p.toString()}`) + }, +} diff --git a/frontend/src/features/soar/types/soar.types.ts b/frontend/src/features/soar/types/soar.types.ts index 0ac1ba6c4..b6c5fea3d 100644 --- a/frontend/src/features/soar/types/soar.types.ts +++ b/frontend/src/features/soar/types/soar.types.ts @@ -90,6 +90,21 @@ export interface FlowListQuery { size?: number } +/** Reusable command template served from `/soar/action-templates`. Selecting + * one seeds the flow-creation form. */ +export interface ActionTemplate { + id: number + title: string + description?: string + command: string + systemOwner: boolean +} + +export interface ActionTemplateListQuery { + page?: number // 0-based + size?: number +} + export type ExecutionStatus = 'EXECUTED' | 'PENDING' | 'FAILED' export type NonExecutionCause = 'AGENT_OFFLINE' | 'AGENT_NOT_FOUND' | 'UNKNOWN' diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index b8460ee42..7f5e5e569 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -4458,6 +4458,15 @@ "loadError": "Flows konnten nicht geladen werden.", "retry": "Erneut versuchen", "empty": "Keine Flows für diese Ansicht.", + "templates": { + "title": "Neuen Flow starten", + "subtitle": "Wähle eine Aktionsvorlage oder starte bei Null.", + "loading": "Vorlagen werden geladen…", + "loadError": "Vorlagen konnten nicht geladen werden.", + "empty": "Keine Vorlagen verfügbar.", + "cancel": "Abbrechen", + "scratch": "Bei Null anfangen" + }, "tabs": { "flows": "Flows", "executions": "Ausführungsverlauf", @@ -4514,6 +4523,11 @@ "commands": "Befehle", "commandsHint": "Werden der Reihe nach auf dem Agenten ausgeführt. $(field.path) für Alert-Werte, $[variables.NAME] für gespeicherte Variablen.", "addCommand": "Befehl hinzufügen", + "connector": { + "always": "immer", + "success": "bei Erfolg", + "failure": "bei Fehler" + }, "agentPlatform": "Agent-Plattform", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index 84e8272fd..5ea8dda9e 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -4622,6 +4622,15 @@ "loadError": "Could not load flows.", "retry": "Retry", "empty": "No flows match this view.", + "templates": { + "title": "Start a new flow", + "subtitle": "Pick an action template or start from scratch.", + "loading": "Loading templates…", + "loadError": "Could not load templates.", + "empty": "No templates available.", + "cancel": "Cancel", + "scratch": "Start from scratch" + }, "tabs": { "flows": "Flows", "executions": "Execution history", @@ -4678,6 +4687,11 @@ "commands": "Commands", "commandsHint": "Run in order on the agent. Use $(field.path) for alert values and $[variables.NAME] for stored variables.", "addCommand": "Add command", + "connector": { + "always": "always", + "success": "on success", + "failure": "on failure" + }, "agentPlatform": "Agent platform", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index 389a97938..b15efd41f 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -4546,6 +4546,15 @@ "loadError": "No se pudieron cargar los flujos.", "retry": "Reintentar", "empty": "Ningún flujo coincide con esta vista.", + "templates": { + "title": "Nuevo flujo", + "subtitle": "Elige una plantilla de acción o empieza desde cero.", + "loading": "Cargando plantillas…", + "loadError": "No se pudieron cargar las plantillas.", + "empty": "No hay plantillas disponibles.", + "cancel": "Cancelar", + "scratch": "Empezar desde cero" + }, "tabs": { "flows": "Flujos", "executions": "Historial de ejecuciones", @@ -4602,6 +4611,11 @@ "commands": "Comandos", "commandsHint": "Se ejecutan en orden en el agente. Usa $(field.path) para valores de la alerta y $[variables.NAME] para variables guardadas.", "addCommand": "Agregar comando", + "connector": { + "always": "siempre", + "success": "si tuvo éxito", + "failure": "si falló" + }, "agentPlatform": "Plataforma del agente", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index e743100f6..422f57e90 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -4458,6 +4458,15 @@ "loadError": "Impossible de charger les flux.", "retry": "Réessayer", "empty": "Aucun flux ne correspond à cette vue.", + "templates": { + "title": "Nouveau flux", + "subtitle": "Choisis un modèle d'action ou pars de zéro.", + "loading": "Chargement des modèles…", + "loadError": "Impossible de charger les modèles.", + "empty": "Aucun modèle disponible.", + "cancel": "Annuler", + "scratch": "Partir de zéro" + }, "tabs": { "flows": "Flux", "executions": "Historique d'exécutions", @@ -4514,6 +4523,11 @@ "commands": "Commandes", "commandsHint": "Exécutées dans l'ordre sur l'agent. Utilisez $(field.path) pour les valeurs de l'alerte et $[variables.NAME] pour les variables enregistrées.", "addCommand": "Ajouter une commande", + "connector": { + "always": "toujours", + "success": "en cas de succès", + "failure": "en cas d'échec" + }, "agentPlatform": "Plateforme de l'agent", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index 67ecb53d8..2209c0481 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -4458,6 +4458,15 @@ "loadError": "Impossibile caricare i flussi.", "retry": "Riprova", "empty": "Nessun flusso corrisponde a questa vista.", + "templates": { + "title": "Nuovo flusso", + "subtitle": "Scegli un modello di azione o parti da zero.", + "loading": "Caricamento modelli…", + "loadError": "Impossibile caricare i modelli.", + "empty": "Nessun modello disponibile.", + "cancel": "Annulla", + "scratch": "Parti da zero" + }, "tabs": { "flows": "Flussi", "executions": "Cronologia esecuzioni", @@ -4514,6 +4523,11 @@ "commands": "Comandi", "commandsHint": "Eseguiti in ordine sull'agente. Usa $(field.path) per i valori dell'alert e $[variables.NAME] per le variabili salvate.", "addCommand": "Aggiungi comando", + "connector": { + "always": "sempre", + "success": "se riuscito", + "failure": "se fallito" + }, "agentPlatform": "Piattaforma dell'agente", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index 328f71a7f..a54532b41 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -4546,6 +4546,15 @@ "loadError": "Não foi possível carregar os fluxos.", "retry": "Tentar de novo", "empty": "Nenhum fluxo corresponde a esta visão.", + "templates": { + "title": "Novo fluxo", + "subtitle": "Escolha um modelo de ação ou comece do zero.", + "loading": "Carregando modelos…", + "loadError": "Não foi possível carregar os modelos.", + "empty": "Nenhum modelo disponível.", + "cancel": "Cancelar", + "scratch": "Começar do zero" + }, "tabs": { "flows": "Fluxos", "executions": "Histórico de execuções", @@ -4602,6 +4611,11 @@ "commands": "Comandos", "commandsHint": "Executam em ordem no agente. Use $(field.path) para valores do alerta e $[variables.NAME] para variáveis salvas.", "addCommand": "Adicionar comando", + "connector": { + "always": "sempre", + "success": "em caso de sucesso", + "failure": "em caso de falha" + }, "agentPlatform": "Plataforma do agente", "shell": "Shell", "shellAuto": "auto", diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index f3b2b3d36..ec4013459 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -4281,6 +4281,15 @@ "loadError": "Не удалось загрузить потоки.", "retry": "Повторить", "empty": "Нет потоков для этого вида.", + "templates": { + "title": "Новый поток", + "subtitle": "Выберите шаблон действия или начните с нуля.", + "loading": "Загрузка шаблонов…", + "loadError": "Не удалось загрузить шаблоны.", + "empty": "Шаблоны отсутствуют.", + "cancel": "Отмена", + "scratch": "Начать с нуля" + }, "tabs": { "flows": "Потоки", "executions": "История выполнений", @@ -4337,6 +4346,11 @@ "commands": "Команды", "commandsHint": "Выполняются по порядку на агенте. Используйте $(field.path) для значений алерта и $[variables.NAME] для сохранённых переменных.", "addCommand": "Добавить команду", + "connector": { + "always": "всегда", + "success": "при успехе", + "failure": "при ошибке" + }, "agentPlatform": "Платформа агента", "shell": "Оболочка", "shellAuto": "авто",