main
vue 261 lines 7.05 KB
Raw
1 <template>
2 <div class="sigma-queries-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>{{ total }}</code>
19 </div>
20 <div class="box">
21 <span class="text-success">Active</span>
22 :
23 <code>{{ activeTotal }}</code>
24 </div>
25 <div class="box">
26 <span class="text-secondary">Inactive</span>
27 :
28 <code>{{ inactiveTotal }}</code>
29 </div>
30 </div>
31 </n-popover>
32
33 <n-button size="small" type="primary" secondary strong @click="showActionsView = !showActionsView">
34 <div class="flex items-center gap-2">
35 <Icon :name="ToolsIcon" :size="16" />
36 <span class="xs:block hidden">Actions</span>
37 <Icon
38 class="xs:block! hidden! transition-transform"
39 :class="{ 'rotate-90!': showActionsView }"
40 :name="ChevronIcon"
41 :size="16"
42 />
43 </div>
44 </n-button>
45 </div>
46 <n-pagination
47 v-model:page="currentPage"
48 v-model:page-size="pageSize"
49 :page-slot
50 :show-size-picker
51 :page-sizes
52 :item-count="total"
53 :simple="simpleMode"
54 />
55 <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="px-0!">
56 <template #trigger>
57 <div class="bg-default rounded-lg">
58 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
59 <n-button size="small" @click="showFilters = true">
60 <template #icon>
61 <Icon :name="FilterIcon" />
62 </template>
63 </n-button>
64 </n-badge>
65 </div>
66 </template>
67 <div class="flex flex-col gap-2 py-1">
68 <div class="px-3">
69 <small>Status:</small>
70 </div>
71 <div class="px-3">
72 <n-select
73 v-model:value="filters.active"
74 :options="activeOptions"
75 placeholder="Select a status"
76 clearable
77 class="w-56!"
78 />
79 </div>
80 <div class="flex justify-between gap-2 px-3">
81 <div class="flex justify-start gap-2">
82 <n-button size="small" quaternary @click="showFilters = false">Close</n-button>
83 </div>
84 <div class="flex justify-end gap-2">
85 <n-button size="small" secondary @click="resetFilters()">Reset</n-button>
86 <n-button size="small" type="primary" secondary :loading @click="getData()">
87 Submit
88 </n-button>
89 </div>
90 </div>
91 </div>
92 </n-popover>
93 </div>
94
95 <CollapseKeepAlive :show="showActionsView" embedded arrow="top-left" arrow-offset="54px">
96 <QueriesActions class="p-3" @updated="getData()" />
97 </CollapseKeepAlive>
98
99 <n-spin :show="loading">
100 <div class="my-3 flex min-h-52 flex-col gap-2">
101 <template v-if="queriesList.length">
102 <QueryItem
103 v-for="query of itemsPaginated"
104 :key="query.id"
105 :query
106 class="item-appear item-appear-bottom item-appear-005"
107 @deleted="deleteQueryItem"
108 @updated="updateQueryItem"
109 />
110 </template>
111 <template v-else>
112 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
113 </template>
114 </div>
115 </n-spin>
116
117 <div class="footer flex justify-end">
118 <n-pagination
119 v-if="itemsPaginated.length > 3"
120 v-model:page="currentPage"
121 :page-size
122 :item-count="total"
123 :page-slot="6"
124 />
125 </div>
126 </div>
127 </template>
128
129 <script setup lang="ts">
130 import type { SigmaQuery } from "@/types/sigma.d"
131 import { useResizeObserver, useStorage } from "@vueuse/core"
132 import _cloneDeep from "lodash/cloneDeep"
133 import _orderBy from "lodash/orderBy"
134 import { NBadge, NButton, NEmpty, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
135 import { computed, onBeforeMount, ref, watch } from "vue"
136 import Api from "@/api"
137 import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue"
138 import Icon from "@/components/common/Icon.vue"
139 import QueriesActions from "./QueriesActions.vue"
140 import QueryItem from "./QueryItem.vue"
141
142 interface QueriesFilter {
143 active: "active" | "inactive"
144 }
145
146 const FilterIcon = "carbon:filter-edit"
147 const InfoIcon = "carbon:information"
148 const ToolsIcon = "carbon:tools"
149 const ChevronIcon = "carbon:chevron-right"
150
151 const message = useMessage()
152 const loading = ref(false)
153 const showFilters = ref(false)
154 const showActionsView = useStorage<boolean>("sigma-queries-list-actions-view-state", false, localStorage)
155 const queriesList = ref<SigmaQuery[]>([])
156
157 const pageSizes = [10, 25, 50, 100]
158 const pageSize = ref(pageSizes[1])
159 const currentPage = ref(1)
160 const simpleMode = ref(false)
161 const showSizePicker = ref(true)
162 const header = ref()
163 const pageSlot = ref(8)
164
165 const itemsPaginated = computed(() => {
166 if (!pageSize.value) return []
167
168 const from = (currentPage.value - 1) * pageSize.value
169 const to = currentPage.value * pageSize.value
170
171 const list = _orderBy(queriesList.value, ["id"], ["desc"])
172
173 return list.slice(from, to)
174 })
175
176 const total = computed<number>(() => {
177 return queriesList.value.length || 0
178 })
179 const activeTotal = computed<number>(() => {
180 return queriesList.value.filter(o => o.active).length || 0
181 })
182 const inactiveTotal = computed<number>(() => {
183 return queriesList.value.filter(o => !o.active).length || 0
184 })
185
186 const filters = ref<Partial<QueriesFilter>>({})
187 const lastFilters = ref<Partial<QueriesFilter>>({})
188
189 const filtered = computed<boolean>(() => {
190 return !!filters.value.active
191 })
192
193 const activeOptions = [
194 { label: "Active", value: "active" },
195 { label: "Inactive", value: "inactive" }
196 ]
197
198 watch(showFilters, val => {
199 if (!val) {
200 filters.value = _cloneDeep(lastFilters.value)
201 }
202 })
203
204 function updateQueryItem(query: SigmaQuery) {
205 const index = queriesList.value.findIndex(o => o.id === query.id)
206 if (index !== -1) {
207 queriesList.value[index] = query
208 }
209 }
210
211 function deleteQueryItem(query: SigmaQuery) {
212 const index = queriesList.value.findIndex(o => o.id === query.id)
213 queriesList.value.splice(index, 1)
214 }
215
216 function resetFilters() {
217 filters.value.active = undefined
218 showFilters.value = false
219 getData()
220 }
221
222 function getData() {
223 showFilters.value = false
224 loading.value = true
225
226 lastFilters.value = _cloneDeep(filters.value)
227
228 const method = !filtered.value ? "getAvailable" : filters.value.active === "active" ? "getActive" : "getInactive"
229
230 Api.sigma[method]()
231 .then(res => {
232 if (res.data.success) {
233 queriesList.value = res.data?.sigma_queries || []
234 } else {
235 message.warning(res.data?.message || "An error occurred. Please try again later.")
236 }
237 })
238 .catch(err => {
239 queriesList.value = []
240
241 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
242 })
243 .finally(() => {
244 loading.value = false
245 })
246 }
247
248 useResizeObserver(header, entries => {
249 const entry = entries[0]
250 if (!entry) return
251
252 const { width } = entry.contentRect
253
254 pageSlot.value = width < 700 ? 5 : 8
255 simpleMode.value = width < 550
256 })
257
258 onBeforeMount(() => {
259 getData()
260 })
261 </script>