main
vue 340 lines 8.1 KB
Raw
1 <template>
2 <div class="page page-wrapped page-without-footer flex flex-col">
3 <div class="wrapper flex grow gap-4">
4 <div class="sidebar p-px">
5 <AgentToolbar
6 v-model="textFilter"
7 :syncing="loadingSync"
8 :agents-length="agents.length"
9 :agents-filtered-length="agentsFiltered.length"
10 :agents-critical
11 :agents-online
12 hide-selection-switch
13 :selection-mode
14 :selected-count="selectedAgents.length"
15 @run="runCommand($event)"
16 @click="routeAgent($event.agent_id).navigate()"
17 @bulk-delete="showBulkDeleteModal = true"
18 @update:selection-mode="selectionMode = $event"
19 @clear-selection="clearSelection"
20 />
21 </div>
22 <div class="main flex grow flex-col overflow-hidden">
23 <n-spin class="flex h-full w-full flex-col overflow-hidden" :show="loadingAgents">
24 <n-scrollbar class="grow">
25 <div class="agents-list flex grow flex-col gap-3 p-px">
26 <template v-if="agentsFiltered.length">
27 <AgentCard
28 v-for="agent in itemsPaginated"
29 :key="agent.agent_id"
30 :agent
31 :selectable="selectionMode"
32 :selected="isAgentSelected(agent)"
33 show-actions
34 hoverable
35 clickable
36 class="item-appear item-appear-bottom item-appear-005"
37 @delete="getAgents()"
38 @click="handleAgentClick(agent)"
39 @toggle-selection="toggleAgentSelection(agent)"
40 />
41 </template>
42 <template v-else>
43 <n-empty
44 v-if="!loadingAgents"
45 description="No items found"
46 class="h-48 justify-center"
47 />
48 </template>
49 </div>
50 </n-scrollbar>
51 </n-spin>
52
53 <div class="pagination-wrapper">
54 <n-pagination v-model:page="page" :page-size :page-slot="5" :item-count="agentsFiltered.length" />
55 </div>
56 </div>
57 </div>
58
59 <!-- Bulk Delete Modal -->
60 <BulkDeleteModal
61 v-model:show="showBulkDeleteModal"
62 :selected-agents
63 :customers="uniqueCustomers"
64 @remove-selection="removeFromSelection"
65 @deleted="onBulkDeleteComplete"
66 />
67 </div>
68 </template>
69
70 <script setup lang="ts">
71 import type { Agent } from "@/types/agents.d"
72 import _debounce from "lodash/debounce"
73 import _split from "lodash/split"
74 import { NEmpty, NPagination, NScrollbar, NSpin, useMessage } from "naive-ui"
75 import { computed, onBeforeMount, ref, watch } from "vue"
76 import Api from "@/api"
77 import AgentCard from "@/components/agents/AgentCard.vue"
78 import AgentToolbar from "@/components/agents/AgentToolbar.vue"
79 import BulkDeleteModal from "@/components/agents/BulkDeleteModal.vue"
80 import { useNavigation } from "@/composables/useNavigation"
81 import { AgentStatus } from "@/types/agents.d"
82
83 const message = useMessage()
84 const { routeAgent } = useNavigation()
85 const loadingAgents = ref(false)
86 const loadingSync = ref(false)
87 const agents = ref<Agent[]>([])
88 const textFilter = ref("")
89 const page = ref(1)
90 const pageSize = ref(20)
91
92 // Selection mode state
93 const selectionMode = ref(true)
94 const selectedAgents = ref<Agent[]>([])
95 const showBulkDeleteModal = ref(false)
96
97 const textFilterDebounced = ref("")
98
99 const update = _debounce(value => {
100 textFilterDebounced.value = value
101 }, 100)
102
103 watch(textFilter, val => {
104 update(val)
105 })
106
107 const agentsFiltered = computed(() => {
108 return agents.value
109 .filter(({ hostname, ip_address, agent_id, label }) =>
110 (hostname + ip_address + agent_id + label)
111 .toString()
112 .toLowerCase()
113 .includes(textFilterDebounced.value.toString().toLowerCase())
114 )
115 .sort((a, b) => Number.parseInt(a.agent_id) - Number.parseInt(b.agent_id))
116 })
117
118 const itemsPaginated = computed(() => {
119 const from = (page.value - 1) * pageSize.value
120 const to = page.value * pageSize.value
121
122 return agentsFiltered.value.slice(from, to)
123 })
124
125 const agentsCritical = computed(() => {
126 return agents.value.filter(({ critical_asset }) => critical_asset)
127 })
128
129 const agentsOnline = computed(() => {
130 return agents.value.filter(({ wazuh_agent_status }) => wazuh_agent_status === AgentStatus.Active)
131 })
132
133 // Get unique customer codes for filter dropdown
134 const uniqueCustomers = computed(() => {
135 const codes = new Set(agents.value.map(a => a.customer_code).filter(Boolean))
136 return [...codes] as string[]
137 })
138
139 // Selection helpers
140 function isAgentSelected(agent: Agent): boolean {
141 return selectedAgents.value.some(a => a.agent_id === agent.agent_id)
142 }
143
144 function toggleAgentSelection(agent: Agent) {
145 const index = selectedAgents.value.findIndex(a => a.agent_id === agent.agent_id)
146 if (index === -1) {
147 selectedAgents.value.push(agent)
148 } else {
149 selectedAgents.value.splice(index, 1)
150 }
151 }
152
153 function removeFromSelection(agent: Agent) {
154 const index = selectedAgents.value.findIndex(a => a.agent_id === agent.agent_id)
155 if (index !== -1) {
156 selectedAgents.value.splice(index, 1)
157 }
158 }
159
160 function clearSelection() {
161 selectedAgents.value = []
162 }
163
164 function handleAgentClick(agent: Agent) {
165 routeAgent(agent.agent_id).navigate()
166 }
167
168 function onBulkDeleteComplete() {
169 clearSelection()
170 getAgents()
171 }
172
173 function runCommand(command: string) {
174 if (command === "sync-agents") {
175 syncAgents()
176 } else if (_split(command, ":").length) {
177 syncVulnerabilities(_split(command, ":")[1] ?? "")
178 }
179 }
180
181 function getAgents() {
182 loadingAgents.value = true
183
184 Api.agents
185 .getAgents()
186 .then(res => {
187 if (res.data.success) {
188 agents.value = res.data.agents || []
189 } else {
190 message.error(res.data?.message || "An error occurred. Please try again later.")
191 }
192 })
193 .catch(err => {
194 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
195 })
196 .finally(() => {
197 loadingAgents.value = false
198 })
199 }
200
201 function syncAgents() {
202 loadingSync.value = true
203
204 Api.agents
205 .syncAgents()
206 .then(res => {
207 if (res.data.success) {
208 message.success("Agents Synced Successfully")
209 getAgents()
210 } else {
211 message.error("An error occurred. Please try again later.")
212 }
213 })
214 .catch(err => {
215 if (err.response?.status === 401) {
216 message.error(err.response?.data?.message || "Sync returned Unauthorized.")
217 } else {
218 message.error(err.response?.data?.message || "Failed to Sync Agents")
219 }
220 })
221 .finally(() => {
222 loadingSync.value = false
223 })
224 }
225
226 function syncVulnerabilities(customerCode: string) {
227 loadingSync.value = true
228
229 Api.agents
230 .syncVulnerabilities(customerCode)
231 .then(res => {
232 if (res.data.success) {
233 message.success("Agent vulnerabilities synced successfully")
234 getAgents()
235 } else {
236 message.error("An error occurred. Please try again later.")
237 }
238 })
239 .catch(err => {
240 if (err.response?.status === 401) {
241 message.error(err.response?.data?.message || "Sync returned Unauthorized.")
242 } else {
243 message.error(err.response?.data?.message || "Failed to Sync Agents")
244 }
245 })
246 .finally(() => {
247 loadingSync.value = false
248 })
249 }
250
251 onBeforeMount(() => {
252 getAgents()
253 })
254 </script>
255
256 <style lang="scss" scoped>
257 .page {
258 container-type: inline-size;
259
260 .wrapper {
261 position: relative;
262 height: 100%;
263 overflow: hidden;
264
265 @media (max-width: 1023px) {
266 flex-direction: column;
267 }
268
269 .sidebar {
270 .agent-toolbar {
271 height: 100%;
272 }
273 }
274
275 :deep() {
276 .n-spin-content {
277 overflow: hidden;
278 max-height: 100%;
279 }
280 }
281
282 .main {
283 position: relative;
284 border-radius: var(--border-radius);
285
286 :deep() {
287 .n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical {
288 right: 0;
289 bottom: 50px;
290 }
291 }
292
293 .pagination-wrapper {
294 --size: 10px;
295 position: absolute;
296 bottom: 0;
297 right: 0;
298 background-color: var(--bg-body-color);
299 padding-left: var(--size);
300 padding-top: var(--size);
301 border-top-left-radius: var(--size);
302
303 &::before,
304 &::after {
305 content: "";
306 position: absolute;
307 width: var(--size);
308 height: var(--size);
309 left: calc(var(--size) * -1);
310 display: block;
311 bottom: 0px;
312 z-index: 1;
313 background-image: radial-gradient(
314 circle at 0 0,
315 rgba(0, 0, 0, 0) calc(var(--size) - 1px),
316 var(--bg-body-color) calc(var(--size) + 0px)
317 );
318 }
319
320 &::after {
321 bottom: initial;
322 left: initial;
323 top: calc(var(--size) * -1);
324 right: 0;
325 }
326 }
327 }
328
329 .agents-list {
330 width: 100%;
331
332 .item-appear {
333 &:last-child {
334 margin-bottom: 50px;
335 }
336 }
337 }
338 }
339 }
340 </style>