main
vue 320 lines 7.89 KB
Raw
1 <template>
2 <div class="artifacts-list">
3 <div ref="header" class="header flex items-center justify-end gap-2">
4 <div class="info flex grow gap-2">
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>{{ totalArtifacts }}</code>
19 </div>
20 </div>
21 </n-popover>
22 </div>
23 <n-pagination
24 v-model:page="currentPage"
25 v-model:page-size="pageSize"
26 :page-slot
27 :show-size-picker
28 :page-sizes
29 :item-count="totalArtifacts"
30 :simple="simpleMode"
31 />
32 <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="px-0!">
33 <template #trigger>
34 <div class="bg-default rounded-lg">
35 <n-badge
36 :show="!!lastFilters.hostname || !!lastFilters.os"
37 dot
38 type="success"
39 :offset="[-4, 0]"
40 >
41 <n-button v-show="!isFilterPreselected" size="small" @click="showFilters = true">
42 <template #icon>
43 <Icon :name="FilterIcon" />
44 </template>
45 </n-button>
46 </n-badge>
47 </div>
48 </template>
49 <div class="flex flex-col gap-2 py-1">
50 <div class="px-3">
51 <n-input-group class="artifacts-list-filter-combo" :class="{ 'filters-active': filterType }">
52 <n-select
53 v-model:value="filterType"
54 class="artifacts-list-filter-type"
55 :options="[
56 {
57 label: 'Agent ',
58 value: 'agentHostname'
59 },
60 {
61 label: 'OS',
62 value: 'os'
63 }
64 ]"
65 placeholder="Filters..."
66 clearable
67 @update:value="
68 () => {
69 filters.hostname = undefined
70 filters.os = undefined
71 }
72 "
73 />
74
75 <n-select
76 v-if="filterType === 'agentHostname'"
77 v-model:value="filters.hostname"
78 :options="agentHostnameOptions"
79 placeholder="Select Agent"
80 clearable
81 filterable
82 :loading="loadingAgents"
83 />
84 <n-select
85 v-if="filterType === 'os'"
86 v-model:value="filters.os"
87 :options="osOptions"
88 clearable
89 placeholder="Select OS"
90 />
91 </n-input-group>
92 </div>
93 <div class="flex justify-end gap-2 px-3">
94 <n-button size="small" secondary @click="showFilters = false">Close</n-button>
95 <n-button size="small" type="primary" secondary @click="getData()">Submit</n-button>
96 </div>
97 </div>
98 </n-popover>
99 </div>
100 <n-spin :show="loading">
101 <div class="my-3 flex min-h-52 flex-col gap-2">
102 <template v-if="artifactsList.length">
103 <ArtifactItem
104 v-for="artifact of itemsPaginated"
105 :key="artifact.name"
106 :artifact
107 class="item-appear item-appear-bottom item-appear-005"
108 />
109 </template>
110 <template v-else>
111 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
112 </template>
113 </div>
114 </n-spin>
115 <div class="footer flex justify-end">
116 <n-pagination
117 v-if="itemsPaginated.length > 3"
118 v-model:page="currentPage"
119 :page-size
120 :item-count="totalArtifacts"
121 :page-slot="6"
122 />
123 </div>
124 </div>
125 </template>
126
127 <script setup lang="ts">
128 // TODO-FE: refactor
129 import type { ArtifactsQuery } from "@/api/endpoints/artifacts"
130 import type { Agent } from "@/types/agents.d"
131 import type { Artifact } from "@/types/artifacts.d"
132 import { useResizeObserver } from "@vueuse/core"
133 import _cloneDeep from "lodash/cloneDeep"
134 import { NBadge, NButton, NEmpty, NInputGroup, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
135 import { computed, nextTick, onBeforeMount, ref, toRefs, watch } from "vue"
136 import Api from "@/api"
137 import Icon from "@/components/common/Icon.vue"
138 import ArtifactItem from "./ArtifactItem.vue"
139
140 const props = defineProps<{ agentHostname?: string; agents?: Agent[]; artifacts?: Artifact[] }>()
141
142 const emit = defineEmits<{
143 (e: "loaded-agents", value: Agent[]): void
144 (e: "loaded-artifacts", value: Artifact[]): void
145 }>()
146
147 const { agentHostname, agents, artifacts } = toRefs(props)
148
149 const message = useMessage()
150 const loadingAgents = ref(false)
151 const loading = ref(false)
152 const showFilters = ref(false)
153 const agentsList = ref<Agent[]>([])
154 const artifactsList = ref<Artifact[]>([])
155
156 const pageSize = ref(25)
157 const currentPage = ref(1)
158 const simpleMode = ref(false)
159 const showSizePicker = ref(true)
160 const pageSizes = [10, 25, 50, 100]
161 const header = ref()
162 const pageSlot = ref(8)
163
164 const itemsPaginated = computed(() => {
165 const from = (currentPage.value - 1) * pageSize.value
166 const to = currentPage.value * pageSize.value
167
168 return artifactsList.value.slice(from, to)
169 })
170
171 const FilterIcon = "carbon:filter-edit"
172 const InfoIcon = "carbon:information"
173
174 const totalArtifacts = computed<number>(() => {
175 return artifactsList.value.length || 0
176 })
177
178 const filters = ref<ArtifactsQuery>({})
179 const lastFilters = ref<ArtifactsQuery>({})
180
181 const filterType = ref<string | null>(null)
182
183 const isFilterPreselected = computed(() => {
184 return !!agentHostname?.value
185 })
186
187 const agentHostnameOptions = computed(() => {
188 if (agentHostname?.value) {
189 return [{ value: agentHostname.value, label: agentHostname.value }]
190 }
191 return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
192 })
193
194 const osOptions = [
195 { label: "Windows", value: "windows" },
196 { label: "Linux", value: "linux" },
197 { label: "MacOS", value: "macos" }
198 ]
199
200 watch(showFilters, val => {
201 if (!val) {
202 filters.value = _cloneDeep(lastFilters.value)
203 }
204 })
205
206 function getData(cb?: (artifacts: Artifact[]) => void) {
207 showFilters.value = false
208 loading.value = true
209
210 lastFilters.value = _cloneDeep(filters.value)
211
212 Api.artifacts
213 .getAll(filters.value)
214 .then(res => {
215 if (res.data.success) {
216 artifactsList.value = res.data?.artifacts || []
217
218 if (cb && typeof cb === "function") {
219 cb(artifactsList.value)
220 }
221 } else {
222 message.warning(res.data?.message || "An error occurred. Please try again later.")
223 }
224 })
225 .catch(err => {
226 artifactsList.value = []
227
228 // MOCK
229 /*
230 artifactsList.value = artifact_list
231 */
232
233 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
234 })
235 .finally(() => {
236 loading.value = false
237 })
238 }
239
240 function getAgents(cb?: (agents: Agent[]) => void) {
241 loadingAgents.value = true
242
243 Api.agents
244 .getAgents()
245 .then(res => {
246 if (res.data.success) {
247 agentsList.value = res.data.agents || []
248
249 if (cb && typeof cb === "function") {
250 cb(agentsList.value)
251 }
252 } else {
253 message.error(res.data?.message || "An error occurred. Please try again later.")
254 }
255 })
256 .catch(err => {
257 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
258 })
259 .finally(() => {
260 loadingAgents.value = false
261 })
262 }
263
264 useResizeObserver(header, entries => {
265 const entry = entries[0]
266 if (!entry) return
267
268 const { width } = entry.contentRect
269
270 pageSlot.value = width < 650 ? 5 : 8
271 simpleMode.value = width < 450
272 })
273
274 onBeforeMount(() => {
275 if (agentHostname?.value) {
276 filters.value.hostname = agentHostname.value
277 }
278
279 if (agents?.value?.length) {
280 agentsList.value = agents.value
281 }
282
283 if (artifacts?.value?.length) {
284 artifactsList.value = artifacts.value
285 }
286
287 nextTick(() => {
288 if (!agentsList.value.length && !agentHostname?.value) {
289 getAgents((agents: Agent[]) => {
290 emit("loaded-agents", agents)
291 })
292 }
293 if (!artifactsList.value.length) {
294 getData((artifacts: Artifact[]) => {
295 emit("loaded-artifacts", artifacts)
296 })
297 }
298 })
299 })
300 </script>
301
302 <style lang="scss">
303 .artifacts-list-filter-combo {
304 .artifacts-list-filter-type {
305 min-width: 130px;
306 max-width: 130px;
307 }
308
309 &.filters-active {
310 min-width: 270px;
311 width: 50vw;
312 max-width: 400px;
313
314 .artifacts-list-filter-type {
315 min-width: 100px;
316 max-width: 100px;
317 }
318 }
319 }
320 </style>