main
vue 426 lines 13.4 KB
Raw
1 <template>
2 <div :id="`customer-${customer.customer_code}`">
3 <CardEntity :loading :highlighted="!!highlight" :hide-footer-extra="!!hideCardActions">
4 <template #default>
5 <div class="flex items-start gap-4">
6 <n-avatar
7 :src="customerInfo?.logo_file || fallbackAvatar"
8 :fallback-src="fallbackAvatar"
9 round
10 :size="40"
11 />
12
13 <div class="flex flex-col gap-1">
14 <div class="flex flex-wrap gap-2">
15 {{ customerInfo?.customer_name }}
16 <span class="text-secondary">#{{ customer.customer_code }}</span>
17 </div>
18 <p class="text-sm">
19 {{ customerInfo?.contact_first_name }} {{ customerInfo?.contact_last_name }}
20 </p>
21 </div>
22 </div>
23 </template>
24
25 <template #footerMain>
26 <div class="flex flex-wrap items-center gap-3">
27 <Badge type="splitted" color="primary">
28 <template #iconLeft>
29 <Icon :name="UserTypeIcon" :size="14" />
30 </template>
31 <template #label>Type</template>
32 <template #value>
33 {{ customerInfo?.customer_type || "-" }}
34 </template>
35 </Badge>
36
37 <n-popover trigger="hover" :disabled="addressLabel === '-'">
38 <template #trigger>
39 <Badge type="splitted" color="primary" :class="{ 'cursor-help': addressLabel !== '-' }">
40 <template #iconLeft>
41 <Icon :name="LocationIcon" :size="13" />
42 </template>
43 <template #value>
44 <div class="flex flex-wrap items-center gap-2">
45 {{ addressLabel }}
46 <Icon
47 v-if="addressLabel !== '-'"
48 :name="InfoIcon"
49 :size="13"
50 class="opacity-80!"
51 />
52 </div>
53 </template>
54 </Badge>
55 </template>
56
57 <div class="flex flex-col gap-1">
58 <div v-if="customerInfo?.address_line1" class="box">
59 address_line1:
60 <code>{{ customerInfo.address_line1 }}</code>
61 </div>
62 <div v-if="customerInfo?.address_line2" class="box">
63 address_line2:
64 <code>{{ customerInfo.address_line2 }}</code>
65 </div>
66 <div v-if="customerInfo?.postal_code" class="box">
67 postal_code:
68 <code>{{ customerInfo.postal_code }}</code>
69 </div>
70 <div v-if="customerInfo?.city" class="box">
71 city:
72 <code>{{ customerInfo.city }}</code>
73 </div>
74 <div v-if="customerInfo?.state" class="box">
75 state:
76 <code>{{ customerInfo.state }}</code>
77 </div>
78 <div v-if="customerInfo?.country" class="box">
79 country:
80 <code>{{ customerInfo.country }}</code>
81 </div>
82 </div>
83 </n-popover>
84
85 <Badge type="splitted" color="primary">
86 <template #iconLeft>
87 <Icon :name="PhoneIcon" :size="13" />
88 </template>
89 <template #value>
90 {{ customerInfo?.phone || "-" }}
91 </template>
92 </Badge>
93
94 <Badge v-if="customerInfo?.parent_customer_code" type="splitted" color="primary">
95 <template #iconLeft>
96 <Icon :name="ParentIcon" :size="13" />
97 </template>
98 <template #label>Parent</template>
99 <template #value>
100 {{ customerInfo?.parent_customer_code }}
101 </template>
102 </Badge>
103
104 <Badge type="splitted" :color="customer.is_provisioned ? 'success' : 'danger'" bright>
105 <template #iconLeft>
106 <Icon :name="ProvisionIcon" :size="13" />
107 </template>
108 <template #label>Status</template>
109 <template #value>
110 {{ customer.is_provisioned ? "Provisioned" : "Not Provisioned" }}
111 </template>
112 </Badge>
113
114 <Badge v-if="customer.is_provisioned && agentCount !== null" type="splitted" color="primary">
115 <template #iconLeft>
116 <Icon :name="AgentsIcon" :size="13" />
117 </template>
118 <template #label>Agents</template>
119 <template #value>
120 {{ agentCount }}
121 </template>
122 </Badge>
123 </div>
124 </template>
125
126 <template #footerExtra>
127 <n-button size="small" @click.stop="showDetails = true">
128 <template #icon>
129 <Icon :name="DetailsIcon" />
130 </template>
131 Details
132 </n-button>
133 </template>
134 </CardEntity>
135
136 <n-modal
137 v-model:show="showDetails"
138 preset="card"
139 content-class="p-0!"
140 :style="{ maxWidth: 'min(1100px, 90vw)', minHeight: 'min(470px, 90vh)', overflow: 'hidden' }"
141 :title="customerInfo?.customer_name"
142 :bordered="false"
143 segmented
144 >
145 <n-tabs type="line" animated :tabs-padding="24" class="h-full">
146 <n-tab-pane name="Customer" tab="Customer" display-directive="show" class="pt-0!">
147 <n-tabs
148 type="line"
149 animated
150 :tabs-padding="24"
151 class="[&_.n-tabs-nav]:bg-secondary z-20 h-full [&_.n-tabs-nav]:relative [&_.n-tabs-nav]:z-99"
152 placement="left"
153 :pane-style="{ minHeight: 'min(450px, 90vh)' }"
154 >
155 <n-tab-pane name="Info" tab="Info" display-directive="show:lazy" class="p-4!">
156 <CustomerInfo
157 v-if="customerInfo"
158 v-model:loading="loadingDelete"
159 :customer="customerInfo"
160 @delete="deletedItem()"
161 @submitted="customerInfo = $event"
162 />
163 </n-tab-pane>
164 <n-tab-pane name="Provision" tab="Provision" display-directive="show:lazy" class="p-4!">
165 <CustomerProvision
166 :customer-meta
167 :customer-code="customer.customer_code"
168 :customer-name="customer.customer_name"
169 @delete="customerMeta = null"
170 @submitted="customerMeta = $event"
171 />
172 </n-tab-pane>
173 <n-tab-pane
174 name="3rd Party Integrations"
175 tab="3rd Party Integrations"
176 display-directive="show:lazy"
177 class="p-4!"
178 >
179 <CustomerIntegrations
180 :customer-code="customer.customer_code"
181 :customer-name="customer.customer_name"
182 />
183 </n-tab-pane>
184 <n-tab-pane
185 name="Network Connectors"
186 tab="Network Connectors"
187 display-directive="show:lazy"
188 class="p-4!"
189 >
190 <CustomerNetworkConnectors
191 :customer-code="customer.customer_code"
192 :customer-name="customer.customer_name"
193 />
194 </n-tab-pane>
195 <n-tab-pane
196 name="Notification Workflows"
197 tab="Notification Workflows"
198 display-directive="show:lazy"
199 class="p-4!"
200 >
201 <CustomerNotificationsWorkflows :customer-code="customer.customer_code" />
202 </n-tab-pane>
203 <n-tab-pane name="AI Triggers" tab="AI Triggers" display-directive="show:lazy" class="p-4!">
204 <CustomerAITriggers :customer-code="customer.customer_code" />
205 </n-tab-pane>
206 <n-tab-pane
207 name="AI Notifications"
208 tab="AI Notifications"
209 display-directive="show:lazy"
210 class="overflow-hidden p-4!"
211 >
212 <CustomerAiNotifications :customer-code="customer.customer_code" />
213 </n-tab-pane>
214 <n-tab-pane name="Event Sources" tab="Event Sources" display-directive="show:lazy" class="p-4!">
215 <CustomerEventSources :customer-code="customer.customer_code" />
216 </n-tab-pane>
217
218 <template #suffix><div class="h-4 w-full"></div></template>
219 </n-tabs>
220 </n-tab-pane>
221
222 <n-tab-pane name="Agents" tab="Agents" display-directive="show" class="pt-0!">
223 <n-tabs
224 type="line"
225 animated
226 :tabs-padding="24"
227 class="[&_.n-tabs-nav]:bg-secondary z-20 h-full [&_.n-tabs-nav]:relative [&_.n-tabs-nav]:z-99"
228 placement="left"
229 :pane-style="{ minHeight: 'min(450px, 90vh)' }"
230 >
231 <n-tab-pane name="Agents" tab="Agents" display-directive="show" class="p-4! pr-0!">
232 <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
233 <CustomerAgents v-if="customerInfo" :customer="customerInfo" />
234 </n-scrollbar>
235 </n-tab-pane>
236 <n-tab-pane
237 name="Healthcheck Wazuh"
238 tab="Healthcheck Wazuh"
239 display-directive="show:lazy"
240 class="p-4! pr-0!"
241 >
242 <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
243 <CustomerHealthcheckList
244 v-model:filters="healthcheckFilters"
245 source="wazuh"
246 :customer-code="customer.customer_code"
247 />
248 </n-scrollbar>
249 </n-tab-pane>
250 <n-tab-pane
251 name="Healthcheck Velociraptor"
252 tab="Healthcheck Velociraptor"
253 display-directive="show:lazy"
254 class="p-4! pr-0!"
255 >
256 <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
257 <CustomerHealthcheckList
258 v-model:filters="healthcheckFilters"
259 source="velociraptor"
260 :customer-code="customer.customer_code"
261 />
262 </n-scrollbar>
263 </n-tab-pane>
264
265 <template #suffix><div class="h-4 w-full"></div></template>
266 </n-tabs>
267 </n-tab-pane>
268
269 <n-tab-pane
270 v-if="customerPortainerStackId !== null"
271 name="Wazuh Worker"
272 tab="Wazuh Worker"
273 display-directive="show"
274 class="p-4!"
275 >
276 <CustomerWazuhWorker :stack-id="customerPortainerStackId" />
277 </n-tab-pane>
278 </n-tabs>
279 </n-modal>
280 </div>
281 </template>
282
283 <script setup lang="ts">
284 // TODO-FE: refactor
285 import type { Customer, CustomerMeta } from "@/types/customers.d"
286 import { NAvatar, NButton, NModal, NPopover, NScrollbar, NTabPane, NTabs, useMessage } from "naive-ui"
287 import { computed, defineAsyncComponent, onBeforeMount, ref, toRefs, watch } from "vue"
288 import Api from "@/api"
289 import Badge from "@/components/common/Badge.vue"
290 import CardEntity from "@/components/common/cards/CardEntity.vue"
291 import Icon from "@/components/common/Icon.vue"
292 import { useCustomerHealthcheckFilters } from "@/composables/useCustomerHealthcheckFilters"
293 import { getAvatar, getNameInitials } from "@/utils"
294
295 const props = defineProps<{
296 customer: Customer
297 highlight?: boolean | null | undefined
298 hideCardActions?: boolean | null | undefined
299 }>()
300 const emit = defineEmits<{
301 (e: "delete"): void
302 }>()
303 const CustomerInfo = defineAsyncComponent(() => import("./CustomerInfo.vue"))
304 const CustomerAgents = defineAsyncComponent(() => import("./CustomerAgents.vue"))
305 const CustomerProvision = defineAsyncComponent(() => import("./provision/CustomerProvision.vue"))
306 const CustomerHealthcheckList = defineAsyncComponent(() => import("./healthcheck/CustomerHealthcheckList.vue"))
307 const CustomerIntegrations = defineAsyncComponent(() => import("./integrations/CustomerIntegrations.vue"))
308 const CustomerNetworkConnectors = defineAsyncComponent(
309 () => import("./networkConnectors/CustomerNetworkConnectors.vue")
310 )
311 const CustomerNotificationsWorkflows = defineAsyncComponent(
312 () => import("./notifications/CustomerNotificationsWorkflows.vue")
313 )
314 const CustomerAITriggers = defineAsyncComponent(() => import("./aiTriggers/CustomerAITriggers.vue"))
315 const CustomerAiNotifications = defineAsyncComponent(() => import("./aiNotifications/CustomerAiNotifications.vue"))
316 const CustomerEventSources = defineAsyncComponent(() => import("./eventSources/CustomerEventSources.vue"))
317 const CustomerWazuhWorker = defineAsyncComponent(() => import("./CustomerWazuhWorker.vue"))
318
319 const { customer, highlight, hideCardActions } = toRefs(props)
320
321 const DetailsIcon = "carbon:settings-adjust"
322 const UserTypeIcon = "solar:shield-user-linear"
323 const ParentIcon = "material-symbols-light:supervisor-account-outline-rounded"
324 const InfoIcon = "carbon:information"
325 const LocationIcon = "carbon:location"
326 const PhoneIcon = "carbon:phone"
327 const ProvisionIcon = "carbon:network-3"
328 const AgentsIcon = "carbon:devices"
329
330 const showDetails = ref(false)
331 const loadingFull = ref(false)
332 const loadingDelete = ref(false)
333 const message = useMessage()
334 const customerInfo = ref<Customer | null>(null)
335 const customerMeta = ref<CustomerMeta | null>(null)
336 const customerPortainerStackId = ref<number | null>(null)
337 const agentCount = ref<number | null>(null)
338 const { healthcheckFilters } = useCustomerHealthcheckFilters()
339
340 const loading = computed(() => loadingFull.value || loadingDelete.value)
341
342 const fallbackAvatar = computed(() => {
343 const initials = getNameInitials(customer.value.customer_name)
344 return getAvatar({ seed: initials, text: initials })
345 })
346
347 const addressLabel = computed(
348 () => [customerInfo.value?.city, customerInfo.value?.state].filter(o => !!o).join(", ") || "-"
349 )
350
351 function getFull() {
352 loadingFull.value = true
353
354 Api.customers
355 .getCustomerFull(customer.value.customer_code)
356 .then(res => {
357 if (res.data.success) {
358 customerInfo.value = res.data.customer
359 customerMeta.value = res.data.customer_meta || null
360 } else {
361 message.warning(res.data?.message || "An error occurred. Please try again later.")
362 }
363 })
364 .catch(err => {
365 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
366 })
367 .finally(() => {
368 loadingFull.value = false
369 })
370 }
371
372 function getPortainerStackId() {
373 Api.portainer.getCustomerStackId(customer.value.customer_name).then(res => {
374 if (res.data.success) {
375 customerPortainerStackId.value = res.data.stack_id || null
376 } else {
377 message.warning(res.data?.message || "An error occurred. Please try again later.")
378 }
379 })
380 }
381
382 function getAgentCount() {
383 Api.customers
384 .getCustomerAgents(customer.value.customer_code)
385 .then(res => {
386 if (res.data.success) {
387 agentCount.value = res.data.agents?.length || 0
388 }
389 })
390 .catch(err => {
391 console.error("Failed to fetch agent count:", err)
392 })
393 }
394
395 function deletedItem() {
396 showDetails.value = false
397 emit("delete")
398 }
399
400 watch(showDetails, val => {
401 if (val) {
402 if (
403 customer.value.customer_code &&
404 (!customer.value.customer_name || !customerMeta.value?.customer_meta_graylog_index)
405 ) {
406 getFull()
407 }
408
409 if (!customerPortainerStackId.value) {
410 getPortainerStackId()
411 }
412 }
413 })
414
415 onBeforeMount(() => {
416 customerInfo.value = customer.value
417
418 if (customer.value.customer_code && !customer.value.customer_name) {
419 getFull()
420 }
421
422 if (customer.value.is_provisioned) {
423 getAgentCount()
424 }
425 })
426 </script>