main
vue 179 lines 4.78 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <n-alert type="info">
4 CoPilot Actions leverages Velociraptor to run actions and Grafana to view results. See
5 <a href="https://github.com/socfortress/CoPilot-Action" target="_blank">
6 https://github.com/socfortress/CoPilot-Action
7 </a>
8 for details.
9 </n-alert>
10
11 <div class="flex flex-col">
12 <div class="flex flex-wrap items-center justify-end gap-2">
13 <div class="flex min-w-80 grow gap-2">
14 <n-popover overlap placement="bottom-start">
15 <template #trigger>
16 <div class="bg-default rounded-lg">
17 <n-button size="small" class="cursor-help!">
18 <template #icon>
19 <Icon :name="InfoIcon" />
20 </template>
21 </n-button>
22 </div>
23 </template>
24 <div class="flex flex-col gap-2">
25 <div class="box">
26 Total Actions:
27 <code>{{ pagination.total }}</code>
28 </div>
29 </div>
30 </n-popover>
31
32 <n-select
33 v-model:value="selectedTechnology"
34 :options="technologyOptions"
35 clearable
36 size="small"
37 placeholder="Technology"
38 :loading="loadingTechnologies"
39 class="max-w-30"
40 :consistent-menu-width="false"
41 />
42
43 <n-input
44 v-model:value="searchQuery"
45 size="small"
46 placeholder="Search actions..."
47 class="max-w-120!"
48 clearable
49 >
50 <template #prefix>
51 <Icon :name="SearchIcon" />
52 </template>
53 </n-input>
54 </div>
55
56 <n-pagination
57 v-model:page="pagination.current"
58 :page-size="pagination.size"
59 :item-count="pagination.total"
60 :page-slot="5"
61 />
62 </div>
63
64 <n-spin :show="loading">
65 <div class="my-3">
66 <div v-if="list.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
67 <ActionCard v-for="item of list" :key="item.copilot_action_name" :action="item" />
68 </div>
69
70 <template v-else>
71 <n-empty v-if="!loading" description="No actions found" class="h-48 justify-center" />
72 </template>
73 </div>
74 </n-spin>
75
76 <div class="flex justify-end">
77 <n-pagination
78 v-if="list.length > 3"
79 v-model:page="pagination.current"
80 :page-size="pagination.size"
81 :item-count="pagination.total"
82 :page-slot="6"
83 />
84 </div>
85 </div>
86 </div>
87 </template>
88
89 <script setup lang="ts">
90 import type { CopilotActionInventoryQuery } from "@/api/endpoints/copilotAction"
91 import type { CopilotAction } from "@/types/copilotAction.d"
92 import { watchDebounced } from "@vueuse/core"
93 import axios from "axios"
94 import { NAlert, NButton, NEmpty, NInput, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
95 import { onBeforeMount, ref } from "vue"
96 import Api from "@/api"
97 import Icon from "@/components/common/Icon.vue"
98 import ActionCard from "./ActionCard.vue"
99
100 const loading = ref(false)
101 const loadingTechnologies = ref(false)
102 const message = useMessage()
103 const list = ref<CopilotAction[]>([])
104 const pagination = ref({
105 current: 1,
106 size: 24,
107 total: 0
108 })
109 const selectedTechnology = ref<string | null>(null)
110 const technologyOptions = ref<{ label: string; value: string }[]>([])
111 const searchQuery = ref<string | null>(null)
112 const InfoIcon = "carbon:information"
113 const SearchIcon = "carbon:search"
114
115 let abortController: AbortController | null = null
116
117 function getList() {
118 abortController?.abort()
119 abortController = new AbortController()
120
121 loading.value = true
122
123 const query: CopilotActionInventoryQuery = {
124 offset: (pagination.value.current - 1) * pagination.value.size,
125 limit: pagination.value.size,
126 technology: selectedTechnology.value || undefined,
127 q: searchQuery.value || undefined
128 }
129
130 Api.copilotAction
131 .getInventory(query, abortController.signal)
132 .then(res => {
133 loading.value = false
134
135 if (res.data.success) {
136 list.value = res.data?.copilot_actions || []
137 pagination.value.total = res.data?.total || 0
138 } else {
139 message.warning(res.data?.message || "An error occurred. Please try again later.")
140 }
141 })
142 .catch(err => {
143 if (!axios.isCancel(err)) {
144 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
145 loading.value = false
146 }
147 })
148 }
149
150 function getTechnologies() {
151 loadingTechnologies.value = true
152
153 Api.copilotAction
154 .getTechnologies()
155 .then(res => {
156 if (res.data.success) {
157 technologyOptions.value = res.data.technologies.map(o => ({ label: o, value: o }))
158 } else {
159 message.warning(res.data?.message || "An error occurred. Please try again later.")
160 }
161 })
162 .catch(err => {
163 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
164 })
165 .finally(() => {
166 loadingTechnologies.value = false
167 })
168 }
169
170 watchDebounced([selectedTechnology, searchQuery, () => pagination.value.current], getList, {
171 deep: true,
172 debounce: 300,
173 immediate: true
174 })
175
176 onBeforeMount(() => {
177 getTechnologies()
178 })
179 </script>