main
vue 315 lines 8.04 KB
Raw
1 <template>
2 <n-modal
3 v-model:show="showModal"
4 preset="card"
5 title="Bulk Delete Agents"
6 class="bulk-delete-modal"
7 :style="{ maxWidth: '600px' }"
8 >
9 <n-tabs v-model:value="activeTab" type="line">
10 <n-tab-pane name="selection" tab="By Selection">
11 <div class="tab-content">
12 <n-alert v-if="!selectedAgents.length" type="warning" class="mb-4">
13 No agents selected. Please select agents from the list first.
14 </n-alert>
15 <template v-else>
16 <p class="mb-4">
17 You are about to delete
18 <strong>{{ selectedAgents.length }}</strong>
19 selected agent(s).
20 </p>
21 <n-scrollbar style="max-height: 200px" class="mb-4">
22 <div class="selected-agents-list">
23 <n-tag
24 v-for="agent in selectedAgents"
25 :key="agent.agent_id"
26 closable
27 class="mr-2 mb-2"
28 @close="$emit('remove-selection', agent)"
29 >
30 {{ agent.hostname }} ({{ agent.agent_id }})
31 </n-tag>
32 </div>
33 </n-scrollbar>
34 </template>
35 </div>
36 </n-tab-pane>
37
38 <n-tab-pane name="filter" tab="By Filter">
39 <div class="tab-content">
40 <n-form :model="filterForm" label-placement="top">
41 <n-alert type="info" class="mb-4">
42 At least one filter must be specified. This will delete all matching agents.
43 </n-alert>
44
45 <n-form-item label="Customer Code" path="customer_code">
46 <n-select
47 v-model:value="filterForm.customer_code"
48 :options="customerOptions"
49 placeholder="Select customer (optional)"
50 clearable
51 filterable
52 />
53 </n-form-item>
54
55 <n-form-item label="Agent Status" path="status">
56 <n-select
57 v-model:value="filterForm.status"
58 :options="statusOptions"
59 placeholder="Select status (optional)"
60 clearable
61 />
62 </n-form-item>
63
64 <n-form-item label="Disconnected Days" path="disconnected_days">
65 <n-input-number
66 v-model:value="filterForm.disconnected_days"
67 :min="1"
68 :max="365"
69 placeholder="Agents disconnected for more than X days"
70 clearable
71 class="w-full"
72 />
73 </n-form-item>
74 </n-form>
75 </div>
76 </n-tab-pane>
77 </n-tabs>
78
79 <!-- Results Section -->
80 <template v-if="deleteResults">
81 <n-divider />
82 <div class="results-section">
83 <n-alert :type="deleteResults.success ? 'success' : 'warning'" class="mb-4">
84 {{ deleteResults.message }}
85 </n-alert>
86 <div class="results-stats mb-4">
87 <n-space>
88 <n-tag type="info">Total: {{ deleteResults.total_requested }}</n-tag>
89 <n-tag type="success">Successful: {{ deleteResults.successful_deletions }}</n-tag>
90 <n-tag v-if="deleteResults.failed_deletions > 0" type="error">
91 Failed: {{ deleteResults.failed_deletions }}
92 </n-tag>
93 </n-space>
94 </div>
95 <n-collapse v-if="deleteResults.results.length > 0">
96 <n-collapse-item title="Deletion Details" name="details">
97 <n-scrollbar style="max-height: 200px">
98 <div v-for="result in deleteResults.results" :key="result.agent_id" class="result-item">
99 <n-tag :type="result.success ? 'success' : 'error'" size="small">
100 {{ result.success ? "" : "" }}
101 </n-tag>
102 <span class="ml-2">{{ result.agent_id }}: {{ result.message }}</span>
103 </div>
104 </n-scrollbar>
105 </n-collapse-item>
106 </n-collapse>
107 </div>
108 </template>
109
110 <template #footer>
111 <n-space justify="end">
112 <n-button @click="closeModal">Cancel</n-button>
113 <n-button type="error" :loading :disabled="!canDelete" @click="confirmDelete">
114 <template #icon>
115 <Icon :name="DeleteIcon" />
116 </template>
117 Delete Agents
118 </n-button>
119 </n-space>
120 </template>
121 </n-modal>
122
123 <!-- Confirmation Dialog -->
124 <n-modal v-model:show="showConfirmDialog" preset="dialog" type="warning" title="Confirm Bulk Deletion">
125 <template #default>
126 <p>Are you sure you want to delete these agents?</p>
127 <p class="mt-2 text-red-500">This action cannot be undone.</p>
128 <template v-if="activeTab === 'filter'">
129 <p class="mt-2">
130 <strong>Filters:</strong>
131 </p>
132 <ul class="ml-4 list-disc">
133 <li v-if="filterForm.customer_code">Customer: {{ filterForm.customer_code }}</li>
134 <li v-if="filterForm.status">Status: {{ filterForm.status }}</li>
135 <li v-if="filterForm.disconnected_days">
136 Disconnected for: {{ filterForm.disconnected_days }}+ days
137 </li>
138 </ul>
139 </template>
140 <template v-else>
141 <p class="mt-2">{{ selectedAgents.length }} agent(s) will be deleted.</p>
142 </template>
143 </template>
144 <template #action>
145 <n-button @click="showConfirmDialog = false">Cancel</n-button>
146 <n-button type="error" :loading @click="executeDelete">Confirm Delete</n-button>
147 </template>
148 </n-modal>
149 </template>
150
151 <script setup lang="ts">
152 // TODO-FE: refactor
153 import type { Agent, BulkDeleteAgentsResponse, BulkDeleteFilterRequest } from "@/types/agents.d"
154 import {
155 NAlert,
156 NButton,
157 NCollapse,
158 NCollapseItem,
159 NDivider,
160 NForm,
161 NFormItem,
162 NInputNumber,
163 NModal,
164 NScrollbar,
165 NSelect,
166 NSpace,
167 NTabPane,
168 NTabs,
169 NTag,
170 useMessage
171 } from "naive-ui"
172 import { computed, ref, watch } from "vue"
173 import Api from "@/api"
174 import Icon from "@/components/common/Icon.vue"
175
176 const props = defineProps<{
177 show: boolean
178 selectedAgents: Agent[]
179 customers: string[]
180 }>()
181
182 const emit = defineEmits<{
183 (e: "update:show", value: boolean): void
184 (e: "remove-selection", agent: Agent): void
185 (e: "deleted"): void
186 }>()
187
188 const message = useMessage()
189 const DeleteIcon = "carbon:trash-can"
190
191 const showModal = computed({
192 get: () => props.show,
193 set: value => emit("update:show", value)
194 })
195
196 const activeTab = ref<"selection" | "filter">("selection")
197 const loading = ref(false)
198 const showConfirmDialog = ref(false)
199 const deleteResults = ref<BulkDeleteAgentsResponse | null>(null)
200
201 const filterForm = ref<BulkDeleteFilterRequest>({
202 customer_code: undefined,
203 status: undefined,
204 disconnected_days: undefined
205 })
206
207 const customerOptions = computed(() => {
208 return props.customers.map(code => ({ label: code, value: code }))
209 })
210
211 const statusOptions = [
212 { label: "Disconnected", value: "disconnected" },
213 { label: "Never Connected", value: "never_connected" },
214 { label: "Active", value: "active" }
215 ]
216
217 const hasFilters = computed(() => {
218 return filterForm.value.customer_code || filterForm.value.status || filterForm.value.disconnected_days
219 })
220
221 const canDelete = computed(() => {
222 if (activeTab.value === "selection") {
223 return props.selectedAgents.length > 0
224 }
225 return hasFilters.value
226 })
227
228 function closeModal() {
229 showModal.value = false
230 deleteResults.value = null
231 }
232
233 function confirmDelete() {
234 showConfirmDialog.value = true
235 }
236
237 async function executeDelete() {
238 showConfirmDialog.value = false
239 loading.value = true
240 deleteResults.value = null
241
242 try {
243 let response
244 if (activeTab.value === "selection") {
245 const agentIds = props.selectedAgents.map(a => a.agent_id)
246 response = await Api.agents.bulkDeleteAgents(agentIds)
247 } else {
248 response = await Api.agents.bulkDeleteAgentsByFilter(filterForm.value)
249 }
250
251 deleteResults.value = response.data
252
253 if (response.data.success) {
254 message.success(response.data.message)
255 emit("deleted")
256 } else {
257 message.warning(response.data.message)
258 }
259 } catch (err: any) {
260 message.error(err.response?.data?.detail || err.response?.data?.message || "Failed to delete agents")
261 } finally {
262 loading.value = false
263 }
264 }
265
266 // Reset form when modal closes
267 watch(showModal, newVal => {
268 if (!newVal) {
269 filterForm.value = {
270 customer_code: undefined,
271 status: undefined,
272 disconnected_days: undefined
273 }
274 deleteResults.value = null
275 }
276 })
277
278 // Switch to selection tab if agents are selected
279 watch(
280 () => props.selectedAgents.length,
281 len => {
282 if (len > 0) {
283 activeTab.value = "selection"
284 }
285 }
286 )
287 </script>
288
289 <style lang="scss" scoped>
290 .bulk-delete-modal {
291 .tab-content {
292 padding-top: 16px;
293 }
294
295 .selected-agents-list {
296 display: flex;
297 flex-wrap: wrap;
298 }
299
300 .result-item {
301 display: flex;
302 align-items: center;
303 padding: 4px 0;
304 border-bottom: 1px solid var(--border-color);
305
306 &:last-child {
307 border-bottom: none;
308 }
309 }
310
311 .w-full {
312 width: 100%;
313 }
314 }
315 </style>