| 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> |