main
vue 337 lines 8.87 KB
Raw
1 <template>
2 <n-form ref="formRef" :model="formValue" :rules label-placement="left" label-width="auto">
3 <!-- Technology Info Alert -->
4 <n-alert v-if="action?.technology" type="info" class="mb-4" size="small">
5 <template #header>
6 <div class="flex items-center gap-2">
7 <TechnologyIcon :technology="action.technology" :size="16" />
8 <span>{{ action.technology }} Action</span>
9 </div>
10 </template>
11 Only showing agents compatible with {{ action.technology }} technology
12 <template v-if="incompatibleAgentCount > 0">
13 ({{ incompatibleAgentCount }} incompatible agent{{ incompatibleAgentCount !== 1 ? "s" : "" }} hidden)
14 </template>
15 </n-alert>
16
17 <n-form-item label="Target Agents" path="agent_names" required>
18 <div class="flex w-full flex-col gap-2">
19 <n-select
20 v-model:value="formValue.agent_names"
21 :options="filteredAgentOptions"
22 :loading="loadingAgents"
23 multiple
24 filterable
25 placeholder="Select target agents"
26 :max-tag-count="3"
27 >
28 <template #empty>
29 <n-empty description="No compatible agents found" />
30 </template>
31 </n-select>
32 <div class="flex justify-end gap-2">
33 <n-button
34 secondary
35 type="primary"
36 size="small"
37 :disabled="loadingAgents || filteredAgentOptions.length === 0"
38 @click="selectAllAgents"
39 >
40 <template #icon>
41 <Icon name="carbon:checkbox-checked-filled" />
42 </template>
43 Select All
44 </n-button>
45 <n-button v-if="formValue.agent_names.length > 0" secondary size="small" @click="clearAllAgents">
46 <template #icon>
47 <Icon name="carbon:close" />
48 </template>
49 Clear
50 </n-button>
51 </div>
52 </div>
53 </n-form-item>
54
55 <!-- Parameters Section -->
56 <div v-if="action?.script_parameters?.length" class="mb-4">
57 <n-divider>Parameters</n-divider>
58 <div v-for="param in action.script_parameters" :key="param.name" class="mb-3">
59 <n-form-item :label="param.name" :path="`parameters.${param.name}`" :required="param.required">
60 <template #label>
61 <div class="flex items-center gap-2">
62 <span>{{ param.name }}</span>
63 <n-tag v-if="param.type" size="tiny" :bordered="false">
64 {{ param.type }}
65 </n-tag>
66 </div>
67 </template>
68
69 <!-- Boolean Parameter -->
70 <n-switch v-if="param.type === 'boolean'" v-model:value="formValue.parameters[param.name]" />
71
72 <!-- Integer Parameter -->
73 <n-input-number
74 v-else-if="param.type === 'integer'"
75 v-model:value="formValue.parameters[param.name] as number"
76 :placeholder="param.default?.toString() || 'Enter value...'"
77 class="w-full"
78 />
79
80 <!-- String with Enum (Select) -->
81 <n-select
82 v-else-if="param.enum?.length"
83 v-model:value="formValue.parameters[param.name] as string"
84 :options="param.enum.map(v => ({ label: v, value: v }))"
85 :placeholder="param.default?.toString() || 'Select value...'"
86 />
87
88 <!-- String Parameter -->
89 <n-input
90 v-else
91 v-model:value="formValue.parameters[param.name] as string"
92 :placeholder="param.default?.toString() || 'Enter value...'"
93 clearable
94 />
95
96 <template v-if="param.description" #feedback>
97 <span class="text-xs text-gray-500">{{ param.description }}</span>
98 </template>
99 </n-form-item>
100 </div>
101 </div>
102
103 <div class="flex justify-end gap-2">
104 <n-button @click="emit('close')">Cancel</n-button>
105 <n-button type="primary" :loading :disabled="!isFormValid" @click="handleInvoke">
106 <template #icon>
107 <Icon :name="PlayIcon" />
108 </template>
109 Invoke Action
110 </n-button>
111 </div>
112 </n-form>
113 </template>
114
115 <script setup lang="ts">
116 // TODO-FE: refactor
117 import type { FormInst, FormRules } from "naive-ui"
118 import type { Agent } from "@/types/agents.d"
119 import type { CopilotAction } from "@/types/copilotAction.d"
120 import {
121 NAlert,
122 NButton,
123 NDivider,
124 NEmpty,
125 NForm,
126 NFormItem,
127 NInput,
128 NInputNumber,
129 NSelect,
130 NSwitch,
131 NTag,
132 useMessage
133 } from "naive-ui"
134 import { computed, onBeforeMount, ref } from "vue"
135 import Api from "@/api"
136 import Icon from "@/components/common/Icon.vue"
137 import TechnologyIcon from "./TechnologyIcon.vue"
138
139 const { action } = defineProps<{
140 action: CopilotAction
141 }>()
142
143 const emit = defineEmits<{
144 (e: "success"): void
145 (e: "close"): void
146 }>()
147
148 const message = useMessage()
149 const formRef = ref<FormInst | null>(null)
150 const loading = ref(false)
151 const loadingAgents = ref(false)
152 const agents = ref<Agent[]>([])
153 const PlayIcon = "carbon:play"
154
155 const formValue = ref<{
156 agent_names: string[]
157 parameters: Record<string, string | number | boolean>
158 }>({
159 agent_names: [],
160 parameters: {}
161 })
162
163 // Initialize default parameter values
164 onBeforeMount(() => {
165 if (action?.script_parameters?.length) {
166 action.script_parameters.forEach(param => {
167 if (param.default !== undefined && param.default !== null) {
168 formValue.value.parameters[param.name] = param.default
169 }
170 })
171 }
172 getAgents()
173 })
174
175 // Helper function to determine if agent OS is compatible with technology
176 function isAgentCompatible(agent: Agent, technology: string): boolean {
177 const agentOS = agent.os?.toLowerCase() || ""
178 const tech = technology.toLowerCase()
179
180 switch (tech) {
181 case "linux":
182 return (
183 agentOS.includes("linux") ||
184 agentOS.includes("ubuntu") ||
185 agentOS.includes("debian") ||
186 agentOS.includes("centos") ||
187 agentOS.includes("red hat") ||
188 agentOS.includes("fedora") ||
189 agentOS.includes("suse")
190 )
191
192 case "windows":
193 return agentOS.includes("windows")
194
195 case "macos":
196 case "darwin":
197 return agentOS.includes("darwin") || agentOS.includes("macos")
198
199 // For other technologies or if no specific filtering is needed
200 default:
201 return true
202 }
203 }
204
205 // Filtered agent options based on technology
206 const filteredAgentOptions = computed(() => {
207 if (!action?.technology) {
208 return agents.value.map(a => ({
209 label: `${a.hostname} (${a.os})`,
210 value: a.hostname
211 }))
212 }
213
214 return agents.value
215 .filter(agent => isAgentCompatible(agent, action.technology))
216 .map(a => ({
217 label: `${a.hostname} (${a.os})`,
218 value: a.hostname
219 }))
220 })
221
222 // Count of incompatible agents (for display purposes)
223 const incompatibleAgentCount = computed(() => {
224 if (!action?.technology) return 0
225
226 return agents.value.filter(agent => !isAgentCompatible(agent, action.technology)).length
227 })
228
229 // Select all filtered agents
230 function selectAllAgents() {
231 formValue.value.agent_names = filteredAgentOptions.value.map(option => option.value)
232 message.success(
233 `Selected ${formValue.value.agent_names.length} agent${formValue.value.agent_names.length !== 1 ? "s" : ""}`
234 )
235 }
236
237 // Clear all selected agents
238 function clearAllAgents() {
239 formValue.value.agent_names = []
240 message.info("Cleared all agents")
241 }
242
243 const rules: FormRules = {
244 agent_names: {
245 type: "array",
246 required: true,
247 message: "Please select at least one agent",
248 trigger: ["blur", "change"]
249 }
250 }
251
252 // Add dynamic rules for required parameters
253 if (action?.script_parameters?.length) {
254 action.script_parameters.forEach(param => {
255 if (param.required) {
256 rules[`parameters.${param.name}`] = {
257 required: true,
258 message: `${param.name} is required`,
259 trigger: ["blur", "change"]
260 }
261 }
262 })
263 }
264
265 const isFormValid = computed(() => {
266 if (!formValue.value.agent_names.length) return false
267
268 // Check required parameters
269 if (action?.script_parameters?.length) {
270 for (const param of action.script_parameters) {
271 if (param.required) {
272 const value = formValue.value.parameters[param.name]
273 if (value === undefined || value === null || value === "") {
274 return false
275 }
276 }
277 }
278 }
279
280 return true
281 })
282
283 async function getAgents() {
284 loadingAgents.value = true
285 try {
286 const res = await Api.agents.getAgents()
287 if (res.data.success) {
288 agents.value = res.data.agents || []
289 } else {
290 message.error(res.data?.message || "Failed to load agents")
291 }
292 } catch (err: any) {
293 message.error(err.response?.data?.message || "Failed to load agents")
294 } finally {
295 loadingAgents.value = false
296 }
297 }
298
299 async function handleInvoke() {
300 if (!formRef.value) return
301
302 try {
303 await formRef.value.validate()
304 } catch {
305 return
306 }
307
308 loading.value = true
309
310 // Filter out undefined/null parameters
311 const cleanedParameters: Record<string, string | number> = {}
312 Object.entries(formValue.value.parameters).forEach(([key, value]) => {
313 if (value !== undefined && value !== null && value !== "") {
314 cleanedParameters[key] = typeof value === "boolean" ? (value ? "true" : "false") : value
315 }
316 })
317
318 try {
319 const res = await Api.copilotAction.invokeAction({
320 copilot_action_name: action.copilot_action_name,
321 agent_names: formValue.value.agent_names,
322 parameters: cleanedParameters
323 })
324
325 if (res.data.success) {
326 message.success(res.data?.message || "Action invoked successfully")
327 emit("success")
328 } else {
329 message.warning(res.data?.message || "Failed to invoke action")
330 }
331 } catch (err: any) {
332 message.error(err.response?.data?.message || "Failed to invoke action")
333 } finally {
334 loading.value = false
335 }
336 }
337 </script>