1
+<template>
2
+ <div class="invoke-action-form">
3
+ <n-spin :show="loading">
4
+ <div class="flex flex-col gap-6">
5
+ <!-- Action Information -->
6
+ <div class="action-header p-4 rounded-lg border">
7
+ <div class="flex items-start justify-between mb-3">
8
+ <div class="flex-1">
9
+ <h4 class="text-lg font-semibold mb-2">{{ action.copilot_action_name }}</h4>
10
+ <p class="text-base font-medium opacity-90 leading-relaxed">{{ action.description }}</p>
11
+ </div>
12
+ <Badge :color="getTechnologyColor(action.technology)" class="ml-3">
13
+ <template #iconLeft><Icon :name="getTechnologyIcon(action.technology)" :size="14" /></template>
14
+ <template #value>{{ action.technology }}</template>
15
+ </Badge>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Target Agents Selection -->
20
+ <div class="form-section">
21
+ <div class="section-header mb-3">
22
+ <h5 class="font-semibold text-base">Target Agents</h5>
23
+ <span class="required-indicator">*</span>
24
+ </div>
25
+ <n-select
26
+ v-model:value="form.agent_names"
27
+ :options="agentOptions"
28
+ multiple
29
+ filterable
30
+ placeholder="Select target agents..."
31
+ :loading="loadingAgents"
32
+ clearable
33
+ size="large"
34
+ class="mb-2"
35
+ />
36
+ <p class="helper-text">Select one or more agents to run this action on</p>
37
+ </div>
38
+
39
+ <!-- Parameters Form -->
40
+ <div v-if="action.script_parameters.length > 0" class="form-section">
41
+ <div class="section-header mb-4">
42
+ <h5 class="font-semibold text-base">Parameters</h5>
43
+ </div>
44
+
45
+ <!-- Required Parameters -->
46
+ <div v-if="requiredParameters.length > 0" class="parameter-group mb-6">
47
+ <div class="parameter-group-header mb-4">
48
+ <h6 class="text-sm font-medium opacity-90">Required Parameters</h6>
49
+ <div class="parameter-group-line"></div>
50
+ </div>
51
+ <div class="grid grid-cols-1 gap-4">
52
+ <div v-for="param in requiredParameters" :key="param.name" class="parameter-field">
53
+ <div class="parameter-label mb-2">
54
+ <label class="font-medium text-sm">
55
+ {{ param.name }}
56
+ <span class="required-indicator">*</span>
57
+ </label>
58
+ <Badge v-if="param.type" color="primary" size="small" class="ml-2">
59
+ <template #value>{{ param.type }}</template>
60
+ </Badge>
61
+ </div>
62
+ <component
63
+ :is="getInputComponent(param.type)"
64
+ v-model:value="form.parameters[param.name]"
65
+ :placeholder="getPlaceholder(param)"
66
+ :options="param.enum?.map(e => ({ label: e, value: e }))"
67
+ clearable
68
+ size="large"
69
+ class="mb-1"
70
+ />
71
+ <p v-if="param.description" class="helper-text">{{ param.description }}</p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Optional Parameters -->
77
+ <div v-if="optionalParameters.length > 0" class="parameter-group">
78
+ <div class="parameter-group-header mb-4">
79
+ <h6 class="text-sm font-medium opacity-90">Optional Parameters</h6>
80
+ <div class="parameter-group-line"></div>
81
+ </div>
82
+ <div class="grid grid-cols-1 gap-4">
83
+ <div v-for="param in optionalParameters" :key="param.name" class="parameter-field">
84
+ <div class="parameter-label mb-2">
85
+ <label class="font-medium text-sm">{{ param.name }}</label>
86
+ <Badge v-if="param.type" color="primary" size="small" class="ml-2">
87
+ <template #value>{{ param.type }}</template>
88
+ </Badge>
89
+ </div>
90
+ <component
91
+ :is="getInputComponent(param.type)"
92
+ v-model:value="form.parameters[param.name]"
93
+ :placeholder="getPlaceholder(param)"
94
+ :options="param.enum?.map(e => ({ label: e, value: e }))"
95
+ clearable
96
+ size="large"
97
+ class="mb-1"
98
+ />
99
+ <p v-if="param.description" class="helper-text">{{ param.description }}</p>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div> <!-- Action Buttons -->
104
+ <div class="action-buttons flex justify-end gap-3 pt-6 mt-6 border-t border-opacity-20">
105
+ <n-button size="large" class="px-6" @click="$emit('close')">
106
+ Cancel
107
+ </n-button>
108
+ <n-button
109
+ type="primary"
110
+ size="large"
111
+ class="px-8"
112
+ :loading="loading"
113
+ :disabled="!isFormValid"
114
+ @click="handleSubmit"
115
+ >
116
+ <template v-if="!loading" #icon>
117
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
118
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
119
+ </svg>
120
+ </template>
121
+ {{ loading ? 'Invoking...' : 'Invoke Action' }}
122
+ </n-button>
123
+ </div>
124
+ </div>
125
+ </n-spin>
126
+ </div>
127
+</template>
128
+
129
+<script setup lang="ts">
130
+import type { ActiveResponseItem, InvokeCopilotActionRequest, ScriptParameter } from "@/types/copilotAction.d"
131
+import { NButton, NInput, NInputNumber, NSelect, NSpin, NSwitch, useMessage } from "naive-ui"
132
+import { computed, onMounted, ref } from "vue"
133
+import Api from "@/api"
134
+import Badge from "@/components/common/Badge.vue"
135
+import Icon from "@/components/common/Icon.vue"
136
+import { Technology } from "@/types/copilotAction.d"
137
+
138
+const { action } = defineProps<{
139
+ action: ActiveResponseItem
140
+}>()
141
+
142
+const emit = defineEmits<{
143
+ success: []
144
+ close: []
145
+}>()
146
+
147
+const message = useMessage()
148
+const loading = ref(false)
149
+const loadingAgents = ref(false)
150
+
151
+const agentOptions = ref<{ label: string; value: string }[]>([])
152
+
153
+const form = ref<{
154
+ agent_names: string[]
155
+ parameters: Record<string, any>
156
+}>({
157
+ agent_names: [],
158
+ parameters: {}
159
+})
160
+
161
+// Separate required and optional parameters
162
+const requiredParameters = computed(() => action.script_parameters.filter(p => p.required))
163
+const optionalParameters = computed(() => action.script_parameters.filter(p => !p.required))
164
+
165
+const isFormValid = computed(() => {
166
+ if (form.value.agent_names.length === 0) return false
167
+
168
+ // Check all required parameters are filled
169
+ for (const param of requiredParameters.value) {
170
+ const value = form.value.parameters[param.name]
171
+ if (value === null || value === undefined || value === '') {
172
+ return false
173
+ }
174
+ }
175
+
176
+ return true
177
+})
178
+
179
+function getInputComponent(type: string) {
180
+ switch (type.toLowerCase()) {
181
+ case 'int':
182
+ case 'integer':
183
+ case 'float':
184
+ case 'number':
185
+ return NInputNumber
186
+ case 'bool':
187
+ case 'boolean':
188
+ return NSwitch
189
+ case 'enum':
190
+ return NSelect
191
+ default:
192
+ return NInput
193
+ }
194
+}
195
+
196
+function getPlaceholder(param: ScriptParameter): string {
197
+ if (param.default !== null && param.default !== undefined) {
198
+ return `Default: ${param.default}`
199
+ }
200
+ return `Enter ${param.name}...`
201
+}
202
+
203
+function getTechnologyIcon(technology: string): string {
204
+ const iconMap: Record<string, string> = {
205
+ [Technology.WINDOWS]: "carbon:logo-windows",
206
+ [Technology.LINUX]: "carbon:logo-linux",
207
+ [Technology.MACOS]: "carbon:logo-apple",
208
+ [Technology.WAZUH]: "carbon:security",
209
+ [Technology.VELOCIRAPTOR]: "carbon:eagle",
210
+ [Technology.NETWORK]: "carbon:network-3",
211
+ [Technology.CLOUD]: "carbon:cloud"
212
+ }
213
+ return iconMap[technology] || "carbon:application"
214
+}
215
+
216
+function getTechnologyColor(technology: string): "primary" | "warning" | "success" | "danger" | undefined {
217
+ const colorMap: Record<string, "primary" | "warning" | "success" | "danger"> = {
218
+ [Technology.WINDOWS]: "primary",
219
+ [Technology.LINUX]: "warning",
220
+ [Technology.MACOS]: "success",
221
+ [Technology.WAZUH]: "success",
222
+ [Technology.VELOCIRAPTOR]: "primary",
223
+ [Technology.NETWORK]: "primary",
224
+ [Technology.CLOUD]: "success"
225
+ }
226
+ return colorMap[technology]
227
+}
228
+
229
+async function loadAgents() {
230
+ loadingAgents.value = true
231
+ try {
232
+ const response = await Api.agents.getAgents()
233
+ if (response.data.success) {
234
+ agentOptions.value = response.data.agents.map(agent => ({
235
+ label: `${agent.hostname} (${agent.ip_address})`,
236
+ value: agent.hostname
237
+ }))
238
+ } else {
239
+ message.error('Failed to load agents')
240
+ }
241
+ } catch {
242
+ message.error('Error loading agents')
243
+ } finally {
244
+ loadingAgents.value = false
245
+ }
246
+}
247
+
248
+async function handleSubmit() {
249
+ if (!isFormValid.value) return
250
+
251
+ loading.value = true
252
+ try {
253
+ // Prepare the payload
254
+ const payload: InvokeCopilotActionRequest = {
255
+ copilot_action_name: action.copilot_action_name,
256
+ agent_names: form.value.agent_names,
257
+ parameters: {
258
+ RepoURL: action.repo_url,
259
+ ScriptName: action.script_name || action.copilot_action_name,
260
+ ...form.value.parameters
261
+ }
262
+ }
263
+
264
+ const response = await Api.copilotAction.invokeAction(payload)
265
+
266
+ if (response.data.success) {
267
+ message.success(`Action invoked successfully on ${form.value.agent_names.length} agent(s). Check the appropriate Grafana dashboard for results.`)
268
+ emit('success')
269
+ } else {
270
+ message.error(response.data.message || 'Failed to invoke action')
271
+ }
272
+ } catch (error: any) {
273
+ message.error(error.response?.data?.message || 'Error invoking action')
274
+ } finally {
275
+ loading.value = false
276
+ }
277
+}
278
+
279
+// Initialize form with default values
280
+function initializeForm() {
281
+ const parameters: Record<string, any> = {}
282
+
283
+ action.script_parameters.forEach(param => {
284
+ if (param.default !== null && param.default !== undefined) {
285
+ parameters[param.name] = param.default
286
+ }
287
+ })
288
+
289
+ form.value.parameters = parameters
290
+}
291
+
292
+onMounted(() => {
293
+ loadAgents()
294
+ initializeForm()
295
+})
296
+</script>
297
+
298
+<style scoped>
299
+.invoke-action-form {
300
+ max-height: 70vh;
301
+ overflow-y: auto;
302
+}
303
+
304
+/* Form section styling */
305
+.form-section {
306
+ padding: 1rem;
307
+ border: 1px solid var(--border-color);
308
+ border-radius: 8px;
309
+}
310
+
311
+.section-header {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 0.5rem;
315
+}
316
+
317
+.required-indicator {
318
+ color: #f56565;
319
+ font-weight: 600;
320
+}
321
+
322
+/* Parameter groups */
323
+.parameter-group-header {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 0.75rem;
327
+}
328
+
329
+.parameter-group-line {
330
+ flex: 1;
331
+ height: 1px;
332
+ background: linear-gradient(to right, var(--border-color) 0%, transparent 100%);
333
+}
334
+
335
+.parameter-field {
336
+ position: relative;
337
+}
338
+
339
+.parameter-label {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 0.5rem;
343
+}
344
+
345
+.helper-text {
346
+ font-size: 0.875rem;
347
+ opacity: 0.7;
348
+ margin-top: 0.25rem;
349
+}
350
+
351
+/* Action header styling */
352
+.action-header {
353
+ border-color: var(--border-color);
354
+}
355
+
356
+/* Action buttons styling */
357
+.action-buttons {
358
+ border-color: var(--border-color);
359
+}
360
+
361
+/* CSS Variables for theme support */
362
+.light-theme {
363
+ --border-color: rgba(0, 0, 0, 0.1);
364
+}
365
+
366
+.dark-theme {
367
+ --border-color: rgba(255, 255, 255, 0.1);
368
+}
369
+
370
+/* Default fallback */
371
+:not(.light-theme):not(.dark-theme) {
372
+ --border-color: rgba(0, 0, 0, 0.1);
373
+}
374
+</style>