| 1 | <template> |
| 2 | <div class="mb-6 flex justify-end"> |
| 3 | <div> |
| 4 | <n-input-group> |
| 5 | <n-select |
| 6 | v-model:value="filterUnit" |
| 7 | :options="unitOptions" |
| 8 | placeholder="Time unit" |
| 9 | clearable |
| 10 | class="w-28!" |
| 11 | /> |
| 12 | <n-input-number v-model:value="filterTime" :min="1" clearable placeholder="Time" class="w-32!" /> |
| 13 | </n-input-group> |
| 14 | </div> |
| 15 | </div> |
| 16 | <n-spin :show="loading"> |
| 17 | <div class="flex min-h-52 flex-col gap-6"> |
| 18 | <n-collapse v-if="healthyList.length" :default-expanded-names="['healthy']"> |
| 19 | <n-collapse-item name="healthy"> |
| 20 | <template #header> |
| 21 | <div class="text-primary flex items-center gap-2"> |
| 22 | <Icon :name="CheckIcon" :size="16" /> |
| 23 | Healthy |
| 24 | <code>{{ healthyList.length }}</code> |
| 25 | </div> |
| 26 | </template> |
| 27 | <div class="flex flex-col gap-2"> |
| 28 | <CustomerHealthcheckItem |
| 29 | v-for="item of healthyList" |
| 30 | :key="item.id" |
| 31 | :health-data="item" |
| 32 | :source |
| 33 | type="healthy" |
| 34 | embedded |
| 35 | class="item-appear item-appear-bottom item-appear-005" |
| 36 | /> |
| 37 | </div> |
| 38 | </n-collapse-item> |
| 39 | </n-collapse> |
| 40 | <n-collapse v-if="unhealthyList.length" :default-expanded-names="['unhealthy']"> |
| 41 | <n-collapse-item name="unhealthy"> |
| 42 | <template #header> |
| 43 | <div class="text-warning flex items-center gap-2"> |
| 44 | <Icon :name="AlertIcon" :size="16" /> |
| 45 | Unhealthy |
| 46 | <code>{{ unhealthyList.length }}</code> |
| 47 | </div> |
| 48 | </template> |
| 49 | <div class="flex flex-col gap-2"> |
| 50 | <CustomerHealthcheckItem |
| 51 | v-for="item of unhealthyList" |
| 52 | :key="item.id" |
| 53 | :health-data="item" |
| 54 | :source |
| 55 | type="unhealthy" |
| 56 | embedded |
| 57 | class="item-appear item-appear-bottom item-appear-005" |
| 58 | /> |
| 59 | </div> |
| 60 | </n-collapse-item> |
| 61 | </n-collapse> |
| 62 | <n-empty v-if="!healthyList.length && !unhealthyList.length && !loading" class="h-48 justify-center" /> |
| 63 | </div> |
| 64 | </n-spin> |
| 65 | </template> |
| 66 | |
| 67 | <script setup lang="ts"> |
| 68 | // TODO-FE: refactor |
| 69 | import type { CustomerAgentsHealthcheckQuery } from "@/api/endpoints/customers" |
| 70 | import type { CustomerAgentHealth, CustomerHealthcheckSource } from "@/types/customers.d" |
| 71 | import { watchDebounced } from "@vueuse/core" |
| 72 | import _get from "lodash/get" |
| 73 | import { NCollapse, NCollapseItem, NEmpty, NInputGroup, NInputNumber, NSelect, NSpin, useMessage } from "naive-ui" |
| 74 | import { computed, onBeforeMount, ref, watch } from "vue" |
| 75 | import Api from "@/api" |
| 76 | import Icon from "@/components/common/Icon.vue" |
| 77 | import CustomerHealthcheckItem from "./CustomerHealthcheckItem.vue" |
| 78 | |
| 79 | const props = defineProps<{ |
| 80 | source: CustomerHealthcheckSource |
| 81 | customerCode: string |
| 82 | filters: Partial<{ time: number; unit: "minutes" | "hours" | "days" }> |
| 83 | }>() |
| 84 | |
| 85 | const emit = defineEmits<{ |
| 86 | (e: "update:filters", value: Partial<{ time: number; unit: "minutes" | "hours" | "days" }>): void |
| 87 | }>() |
| 88 | |
| 89 | const { source, customerCode } = props |
| 90 | |
| 91 | const CheckIcon = "carbon:checkmark-outline" |
| 92 | const AlertIcon = "mdi:alert-outline" |
| 93 | |
| 94 | const loading = ref(false) |
| 95 | const healthyList = ref<CustomerAgentHealth[]>([]) |
| 96 | const unhealthyList = ref<CustomerAgentHealth[]>([]) |
| 97 | const message = useMessage() |
| 98 | |
| 99 | const unitOptions = [ |
| 100 | { label: "Minutes", value: "minutes" }, |
| 101 | { label: "Hours", value: "hours" }, |
| 102 | { label: "Days", value: "days" } |
| 103 | ] |
| 104 | |
| 105 | const filters = computed({ |
| 106 | get: () => props.filters, |
| 107 | set: value => emit("update:filters", value) |
| 108 | }) |
| 109 | |
| 110 | const filterTime = computed({ |
| 111 | get: () => props.filters.time, |
| 112 | set: value => emit("update:filters", { ...props.filters, time: value }) |
| 113 | }) |
| 114 | |
| 115 | const filterUnit = computed({ |
| 116 | get: () => props.filters.unit, |
| 117 | set: value => emit("update:filters", { ...props.filters, unit: value }) |
| 118 | }) |
| 119 | |
| 120 | function getList() { |
| 121 | loading.value = true |
| 122 | |
| 123 | const params = { |
| 124 | method: "" as "getCustomerAgentsHealthcheckWazuh" | "getCustomerAgentsHealthcheckVelociraptor", |
| 125 | healthy: "", |
| 126 | unhealthy: "" |
| 127 | } |
| 128 | |
| 129 | switch (source) { |
| 130 | case "wazuh": |
| 131 | params.method = "getCustomerAgentsHealthcheckWazuh" |
| 132 | params.healthy = "healthy_wazuh_agents" |
| 133 | params.unhealthy = "unhealthy_wazuh_agents" |
| 134 | break |
| 135 | case "velociraptor": |
| 136 | params.method = "getCustomerAgentsHealthcheckVelociraptor" |
| 137 | params.healthy = "healthy_velociraptor_agents" |
| 138 | params.unhealthy = "unhealthy_velociraptor_agents" |
| 139 | break |
| 140 | } |
| 141 | |
| 142 | if (!params.method) { |
| 143 | return |
| 144 | } |
| 145 | |
| 146 | let query: CustomerAgentsHealthcheckQuery | undefined |
| 147 | if (filters.value.time && filters.value.unit) { |
| 148 | query = {} |
| 149 | query[filters.value.unit] = filters.value.time |
| 150 | } |
| 151 | |
| 152 | Api.customers[params.method](customerCode, query) |
| 153 | .then(res => { |
| 154 | if (res.data.success) { |
| 155 | healthyList.value = _get(res, `data.${params.healthy}`, []) |
| 156 | unhealthyList.value = _get(res, `data.${params.unhealthy}`, []) |
| 157 | } else { |
| 158 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 159 | } |
| 160 | }) |
| 161 | .catch(err => { |
| 162 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 163 | }) |
| 164 | .finally(() => { |
| 165 | loading.value = false |
| 166 | }) |
| 167 | } |
| 168 | |
| 169 | watchDebounced( |
| 170 | () => filters.value.time, |
| 171 | () => { |
| 172 | if ((filters.value.time && filters.value.unit) || (!filters.value.time && !filters.value.unit)) { |
| 173 | getList() |
| 174 | } |
| 175 | }, |
| 176 | { debounce: 500 } |
| 177 | ) |
| 178 | watch( |
| 179 | () => filters.value.unit, |
| 180 | () => { |
| 181 | if ((filters.value.time && filters.value.unit) || (!filters.value.time && !filters.value.unit)) { |
| 182 | getList() |
| 183 | } |
| 184 | } |
| 185 | ) |
| 186 | |
| 187 | onBeforeMount(() => { |
| 188 | getList() |
| 189 | }) |
| 190 | </script> |