main
vue 239 lines 6.69 KB
Raw
1 <template>
2 <n-card class="agent-toolbar @container w-full max-w-full min-w-85 overflow-hidden" content-class="p-0!">
3 <div class="flex h-full flex-col gap-6 overflow-hidden px-4 py-3">
4 <div class="flex flex-col gap-2">
5 <div class="flex items-center justify-between gap-2">
6 <div class="text-secondary">
7 <strong v-if="agentsFilteredLength !== agentsLength">{{ agentsFilteredLength }}</strong>
8 <span v-if="agentsFilteredLength !== agentsLength">/</span>
9 <strong class="font-mono">{{ agentsLength }}</strong>
10 Agents
11 </div>
12
13 <n-dropdown
14 v-if="enableSyncVulnerabilitiesDropdown"
15 placement="bottom-start"
16 trigger="click"
17 :options="customersOptions"
18 @select="emit('run', $event)"
19 >
20 <n-button :loading="syncing" secondary size="small" @click="load()">Sync</n-button>
21 </n-dropdown>
22 <n-button v-else :loading="syncing" secondary size="small" @click="emit('run', 'sync-agents')">
23 Sync
24 </n-button>
25 </div>
26 <n-input v-model:value="textFilter" placeholder="Search for an agent" clearable>
27 <template #prefix>
28 <Icon :name="SearchIcon" />
29 </template>
30 </n-input>
31 </div>
32
33 <!-- Selection Mode & Bulk Delete Section -->
34 <n-card embedded content-class="flex flex-col gap-3" size="small" class="hidden! lg:flex!">
35 <div v-if="!hideSelectionSwitch" class="flex items-center gap-2">
36 <n-switch v-model:value="selectionMode" @update:value="emit('update:selection-mode', $event)">
37 <template #checked>Selection ON</template>
38 <template #unchecked>Selection OFF</template>
39 </n-switch>
40 </div>
41
42 <p v-if="selectionMode" class="text-secondary-color text-xs">
43 Click agents to select them for bulk operations. Or select "Bulk Delete" and apply a filter to
44 delete multiple agents at once.
45 </p>
46
47 <div class="flex items-center justify-between gap-2">
48 <n-button
49 type="error"
50 secondary
51 size="small"
52 :disabled="syncing || !selectedCount"
53 @click="emit('bulk-delete')"
54 >
55 <template #icon>
56 <Icon :name="DeleteIcon" />
57 </template>
58 {{ selectedCount ? `Delete ${selectedCount}` : "Bulk Delete" }}
59 </n-button>
60
61 <div v-if="selectedCount && selectedCount > 0">
62 <n-button size="small" quaternary @click="emit('clear-selection')">Clear</n-button>
63 </div>
64 </div>
65 </n-card>
66
67 <div class="hidden grow flex-col overflow-hidden lg:flex">
68 <n-scrollbar>
69 <div v-if="agentsCritical?.length" class="mb-5">
70 <div class="mb-2">
71 Critical Assets
72 <small class="text-secondary font-mono">({{ agentsCritical.length }})</small>
73 </div>
74 <div class="flex flex-col gap-2">
75 <n-tag
76 v-for="agent in agentsCritical"
77 :key="agent.agent_id"
78 type="error"
79 :bordered="false"
80 class="cursor-pointer!"
81 @click="emit('click', agent)"
82 >
83 {{ agent.hostname }}
84 </n-tag>
85 </div>
86 </div>
87 <div v-if="agentsOnline?.length">
88 <div class="mb-2">
89 Online Agents
90 <small class="text-secondary font-mono">({{ agentsOnline.length }})</small>
91 </div>
92 <div class="flex flex-col gap-2">
93 <n-tag
94 v-for="agent in agentsOnline"
95 :key="agent.agent_id"
96 type="success"
97 :bordered="false"
98 class="cursor-pointer!"
99 @click="emit('click', agent)"
100 >
101 {{ agent.hostname }}
102 </n-tag>
103 </div>
104 </div>
105 </n-scrollbar>
106 </div>
107 </div>
108 </n-card>
109 </template>
110
111 <script setup lang="ts">
112 // TODO-FE: refactor
113 import type { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface"
114 import type { Agent } from "@/types/agents.d"
115 import type { Customer } from "@/types/customers.d"
116 import { useWindowSize } from "@vueuse/core"
117 import { NButton, NCard, NDropdown, NInput, NScrollbar, NSwitch, NTag, useMessage } from "naive-ui"
118 import { computed, h, ref, toRefs } from "vue"
119 import Api from "@/api"
120 import Icon from "@/components/common/Icon.vue"
121
122 const props = defineProps<{
123 modelValue: string
124 syncing?: boolean
125 enableSyncVulnerabilitiesDropdown?: boolean
126 agentsLength?: number
127 agentsFilteredLength?: number
128 agentsCritical?: Agent[]
129 agentsOnline?: Agent[]
130 selectedCount?: number
131 hideSelectionSwitch?: boolean
132 }>()
133
134 const emit = defineEmits<{
135 (e: "run", value: "sync-agents" | `sync-vulnerabilities:${string}`): void
136 (e: "update:modelValue", value: string): void
137 (e: "click", value: Agent): void
138 (e: "bulk-delete"): void
139 (e: "update:selection-mode", value: boolean): void
140 (e: "clear-selection"): void
141 }>()
142
143 const {
144 modelValue,
145 syncing,
146 agentsLength,
147 agentsFilteredLength,
148 agentsCritical,
149 agentsOnline,
150 enableSyncVulnerabilitiesDropdown,
151 selectedCount,
152 hideSelectionSwitch
153 } = toRefs(props)
154
155 const SearchIcon = "carbon:search"
156 const DeleteIcon = "carbon:trash-can"
157 const message = useMessage()
158
159 const textFilter = computed<string>({
160 get() {
161 return modelValue.value
162 },
163 set(value) {
164 emit("update:modelValue", value)
165 }
166 })
167
168 const selectionMode = defineModel<boolean>("selectionMode", { default: true, required: false })
169
170 const loadingCustomersList = ref(false)
171 const customersList = ref<Customer[]>([])
172 const { width: winWidth } = useWindowSize()
173
174 const customersOptions = computed(() => {
175 const options: DropdownMixedOption[] = [
176 {
177 label: "Sync Agents",
178 key: "sync-agents"
179 }
180 ]
181
182 if (winWidth.value > 550) {
183 options.push({
184 label: `Sync Agent Vulnerabilities${loadingCustomersList.value ? "..." : ""}`,
185 key: "sync-vulnerabilities",
186 disabled: loadingCustomersList.value,
187 children: loadingCustomersList.value
188 ? undefined
189 : [
190 {
191 label: () => h("div", { class: "pl-2" }, "Select a Customer"),
192 type: "group",
193 children: customersList.value.map(o => ({
194 label: `#${o.customer_code} - ${o.customer_name}`,
195 key: `sync-vulnerabilities:${o.customer_code}`
196 }))
197 }
198 ]
199 })
200 } else {
201 options.push({
202 label: () => h("div", { class: "pl-2" }, "Select a Customer"),
203 type: "group",
204 children: customersList.value.map(o => ({
205 label: `#${o.customer_code} - ${o.customer_name}`,
206 key: `sync-vulnerabilities:${o.customer_code}`
207 }))
208 })
209 }
210
211 return options
212 })
213
214 function getCustomers() {
215 loadingCustomersList.value = true
216
217 Api.customers
218 .getCustomers()
219 .then(res => {
220 if (res.data.success) {
221 customersList.value = res.data?.customers || []
222 } else {
223 message.warning(res.data?.message || "An error occurred. Please try again later.")
224 }
225 })
226 .catch(err => {
227 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
228 })
229 .finally(() => {
230 loadingCustomersList.value = false
231 })
232 }
233
234 function load() {
235 if (!customersList.value.length) {
236 getCustomers()
237 }
238 }
239 </script>