main
vue 179 lines 4.92 KB
Raw
1 <template>
2 <n-spin :show="loading">
3 <div ref="header" class="header flex items-center justify-end gap-2">
4 <div class="info flex grow gap-5">
5 <n-popover overlap placement="bottom-start">
6 <template #trigger>
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="cursor-help!">
9 <template #icon>
10 <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
14 </template>
15 <div class="flex flex-col gap-2">
16 <div class="box">
17 Total:
18 <code>{{ total }}</code>
19 </div>
20 <div class="box">
21 Passed:
22 <code class="text-success">{{ totalPassed }}</code>
23 </div>
24 <div class="box">
25 Not applicable:
26 <code class="text-warning">{{ totalNA }}</code>
27 </div>
28 <div class="box">
29 Failed:
30 <code class="text-error">{{ totalFailed }}</code>
31 </div>
32 </div>
33 </n-popover>
34 </div>
35 <n-pagination
36 v-model:page="currentPage"
37 v-model:page-size="pageSize"
38 :page-slot
39 :show-size-picker
40 :page-sizes
41 :item-count="itemsFiltered.length"
42 :simple="simpleMode"
43 />
44 <n-popover overlap placement="right" class="px-0!" to="body">
45 <template #trigger>
46 <div class="bg-default rounded-lg">
47 <n-button size="small">
48 <template #icon>
49 <Icon :name="FilterIcon" />
50 </template>
51 </n-button>
52 </div>
53 </template>
54 <div class="py-1">
55 <div class="px-3">
56 <div class="text-secondary mb-1 text-sm">Result:</div>
57 <n-select
58 v-model:value="resultFilter"
59 size="small"
60 :options="resultOptions"
61 clearable
62 placeholder="All"
63 class="w-40!"
64 />
65 </div>
66 </div>
67 </n-popover>
68 </div>
69 <div class="my-3 flex min-h-52 flex-col gap-2">
70 <template v-if="itemsPaginated.length">
71 <ScaResultItem v-for="item of itemsPaginated" :key="item.id" :data="item" embedded />
72 </template>
73 <template v-else>
74 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
75 </template>
76 </div>
77 <div class="footer flex justify-end">
78 <n-pagination
79 v-if="itemsPaginated.length > 3"
80 v-model:page="currentPage"
81 :page-size
82 :item-count="itemsFiltered.length"
83 :page-slot="6"
84 :simple="simpleMode"
85 />
86 </div>
87 </n-spin>
88 </template>
89
90 <script setup lang="ts">
91 // TODO-FE: refactor
92 import type { Agent, AgentSca, ScaPolicyResult } from "@/types/agents.d"
93 import { useResizeObserver } from "@vueuse/core"
94 import { NButton, NEmpty, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
95 import { computed, onBeforeMount, ref, watch } from "vue"
96 import Api from "@/api"
97 import Icon from "@/components/common/Icon.vue"
98 import ScaResultItem from "./ScaResultItem.vue"
99
100 const { sca, agent } = defineProps<{ sca: AgentSca; agent: Agent }>()
101
102 const FilterIcon = "carbon:filter-edit"
103 const InfoIcon = "carbon:information"
104
105 const message = useMessage()
106 const loading = ref(false)
107 const resultsList = ref<ScaPolicyResult[]>([])
108 const total = computed(() => resultsList.value.length)
109 const totalFailed = computed(() => resultsList.value.filter(o => o.result === "failed").length)
110 const totalNA = computed(() => resultsList.value.filter(o => o.result === "not applicable").length)
111 const totalPassed = computed(() => resultsList.value.filter(o => o.result === "passed").length)
112 const pageSize = ref(25)
113 const currentPage = ref(1)
114 const simpleMode = ref(false)
115 const showSizePicker = ref(true)
116 const pageSizes = [10, 25, 50, 100]
117 const header = ref()
118 const pageSlot = ref(8)
119 const resultFilter = ref<null | string>(null)
120 const resultOptions = [
121 { label: "Passed", value: "passed" },
122 { label: "Not applicable", value: "not applicable" },
123 { label: "Failed", value: "failed" }
124 ]
125
126 const itemsFiltered = computed(() =>
127 resultsList.value.filter(o => {
128 if (!resultFilter.value) {
129 return true
130 }
131 return resultFilter.value === o.result
132 })
133 )
134
135 const itemsPaginated = computed(() => {
136 const from = (currentPage.value - 1) * pageSize.value
137 const to = currentPage.value * pageSize.value
138
139 return itemsFiltered.value.slice(from, to)
140 })
141
142 watch(resultFilter, () => {
143 currentPage.value = 1
144 })
145
146 function getSCAResults(agentId: string, policyId: string) {
147 loading.value = true
148
149 Api.agents
150 .getSCAResults(agentId, policyId)
151 .then(res => {
152 if (res.data.success) {
153 resultsList.value = res.data.sca_policy_results || []
154 } else {
155 message.warning(res.data?.message || "An error occurred. Please try again later.")
156 }
157 })
158 .catch(err => {
159 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
160 })
161 .finally(() => {
162 loading.value = false
163 })
164 }
165
166 useResizeObserver(header, entries => {
167 const entry = entries[0]
168 if (!entry) return
169
170 const { width } = entry.contentRect
171
172 pageSlot.value = width < 650 ? 5 : 8
173 simpleMode.value = width < 450
174 })
175
176 onBeforeMount(() => {
177 if (agent?.agent_id && sca?.policy_id) getSCAResults(agent.agent_id, sca.policy_id)
178 })
179 </script>