@cryptotaxi247 / CoPilot / commits / 55b1c9b7

feat: enhance customer agents tab with sorting and export (#661)

- Sort agents alphabetically by hostname for easier navigation - Open agent details in new tab to preserve customer view context - Add CSV export button with customer_code, agent_id, hostname, ip_address, os, wazuh_last_seen, and velociraptor_last_seen fields

oliver-oat committed Feb 2, 2026 at 19:54 UTC 55b1c9b7c6ea269e2f5e2f63789384a48903b942
1 file changed +71 -7
frontend/src/components/customers/CustomerAgents.vue
+71 -7
@@ -1,17 +1,25 @@
1 <template>
2 <n-spin :show="loading">
3 <div class="flex min-h-28 flex-col gap-2">
4 + <div v-if="sortedList.length" class="flex justify-end mb-2">
5 + <n-button type="primary" @click="exportToCSV">
6 + <template #icon>
7 + <Icon :name="DownloadIcon" />
8 + </template>
9 + Export
10 + </n-button>
11 + </div>
12 <AgentCard
5 - v-for="agent in list"
13 + v-for="agent in sortedList"
14 :key="agent.agent_id"
15 :agent
16 embedded
17 show-actions
18 class="item-appear item-appear-bottom item-appear-005"
19 @delete="getAgents()"
12 - @click="gotoAgent(agent.agent_id)"
20 + @click="openAgentInNewTab(agent.agent_id)"
21 />
14 - <n-empty v-if="!list.length" description="No Agents found" class="h-48 justify-center" />
22 + <n-empty v-if="!sortedList.length" description="No Agents found" class="h-48 justify-center" />
23 </div>
24 </n-spin>
25 </template>
@@ -19,22 +27,78 @@
27 <script setup lang="ts">
28 import type { Agent } from "@/types/agents.d"
29 import type { Customer } from "@/types/customers.d"
22 -import { NEmpty, NSpin, useMessage } from "naive-ui"
23 -import { onBeforeMount, ref, toRefs } from "vue"
30 +import { NButton, NEmpty, NSpin, useMessage } from "naive-ui"
31 +import { computed, onBeforeMount, ref, toRefs } from "vue"
32 +import { useRouter } from "vue-router"
33 import Api from "@/api"
34 import AgentCard from "@/components/agents/AgentCard.vue"
26 -import { useGoto } from "@/composables/useGoto"
35 +import Icon from "@/components/common/Icon.vue"
36
37 const props = defineProps<{
38 customer: Customer
39 }>()
40 const { customer } = toRefs(props)
41
42 +const DownloadIcon = "carbon:download"
43 +
44 const loading = ref(false)
34 -const { gotoAgent } = useGoto()
45 +const router = useRouter()
46 const message = useMessage()
47 const list = ref<Agent[] | []>([])
48
49 +const sortedList = computed(() => {
50 + return [...list.value].sort((a, b) => {
51 + return a.hostname.toLowerCase().localeCompare(b.hostname.toLowerCase())
52 + })
53 +})
54 +
55 +function exportToCSV() {
56 + if (!sortedList.value.length) {
57 + message.warning("No agents to export")
58 + return
59 + }
60 +
61 + // CSV headers
62 + const headers = ["customer_code", "agent_id", "hostname", "ip_address", "os", "wazuh_last_seen", "velociraptor_last_seen"]
63 +
64 + // Create CSV content
65 + const csvContent = [
66 + headers.join(","),
67 + ...sortedList.value.map(agent => {
68 + return [
69 + agent.customer_code || "",
70 + agent.agent_id,
71 + `"${agent.hostname.replace(/"/g, '""')}"`, // Escape quotes in hostname
72 + agent.ip_address,
73 + `"${agent.os.replace(/"/g, '""')}"`, // Escape quotes in OS
74 + agent.wazuh_last_seen,
75 + agent.velociraptor_last_seen || ""
76 + ].join(",")
77 + })
78 + ].join("\n")
79 +
80 + // Create blob and download
81 + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" })
82 + const link = document.createElement("a")
83 + const url = URL.createObjectURL(blob)
84 +
85 + const filename = `agents_${customer.value.customer_code}_${new Date().toISOString().split("T")[0]}.csv`
86 +
87 + link.setAttribute("href", url)
88 + link.setAttribute("download", filename)
89 + link.style.visibility = "hidden"
90 + document.body.appendChild(link)
91 + link.click()
92 + document.body.removeChild(link)
93 +
94 + message.success("Export completed successfully")
95 +}
96 +
97 +function openAgentInNewTab(agentId: string) {
98 + const route = router.resolve({ name: "Agent", params: { id: agentId } })
99 + window.open(route.href, "_blank")
100 +}
101 +
102 function getAgents() {
103 loading.value = true
104