main
vue 271 lines 6.55 KB
Raw
1 <template>
2 <div class="soc-cases-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>
21 </n-popover>
22
23 <n-button
24 v-if="casesList.length"
25 size="small"
26 type="error"
27 ghost
28 :loading="loadingPurge"
29 @click="handlePurge()"
30 >
31 <div class="flex items-center gap-2">
32 <Icon :name="TrashIcon" :size="16" />
33 <span class="xs:block hidden">Purge</span>
34 </div>
35 </n-button>
36 </div>
37 <n-pagination
38 v-model:page="currentPage"
39 v-model:page-size="pageSize"
40 :page-slot
41 :show-size-picker
42 :page-sizes
43 :item-count="total"
44 :simple="simpleMode"
45 />
46 <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="px-0!">
47 <template #trigger>
48 <div class="bg-default rounded-lg">
49 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
50 <n-button size="small" @click="showFilters = true">
51 <template #icon>
52 <Icon :name="FilterIcon" />
53 </template>
54 </n-button>
55 </n-badge>
56 </div>
57 </template>
58 <div class="flex flex-col gap-2 py-1">
59 <div class="px-3">
60 <small>Cases older then:</small>
61 </div>
62 <div class="px-3">
63 <n-input-group>
64 <n-select
65 v-model:value="filters.unit"
66 :options="unitOptions"
67 placeholder="Time unit"
68 clearable
69 class="w-28!"
70 />
71 <n-input-number
72 v-model:value="filters.olderThan"
73 clearable
74 placeholder="Time"
75 class="w-32!"
76 />
77 </n-input-group>
78 </div>
79 <div class="flex justify-end gap-2 px-3">
80 <n-button size="small" secondary @click="showFilters = false">Close</n-button>
81 <n-button size="small" type="primary" secondary @click="getData()">Submit</n-button>
82 </div>
83 </div>
84 </n-popover>
85 </div>
86 <n-spin :show="loading">
87 <div class="my-3 flex min-h-52 flex-col gap-2">
88 <template v-if="casesList.length">
89 <SocCaseItem
90 v-for="caseData of itemsPaginated"
91 :key="caseData.case_id"
92 :case-data
93 class="item-appear item-appear-bottom item-appear-005"
94 @deleted="getData()"
95 />
96 </template>
97 <template v-else>
98 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
99 </template>
100 </div>
101 </n-spin>
102 <div class="footer flex justify-end">
103 <n-pagination
104 v-if="itemsPaginated.length > 3"
105 v-model:page="currentPage"
106 :page-size
107 :item-count="total"
108 :page-slot="6"
109 />
110 </div>
111 </div>
112 </template>
113
114 <script setup lang="ts">
115 import type { CasesFilter } from "@/api/endpoints/soc"
116 import type { DateFormatted, SocCase } from "@/types/soc/case.d"
117 import { useResizeObserver } from "@vueuse/core"
118 import _cloneDeep from "lodash/cloneDeep"
119 import _orderBy from "lodash/orderBy"
120 import {
121 NBadge,
122 NButton,
123 NEmpty,
124 NInputGroup,
125 NInputNumber,
126 NPagination,
127 NPopover,
128 NSelect,
129 NSpin,
130 useDialog,
131 useMessage
132 } from "naive-ui"
133 import { computed, onBeforeMount, ref, watch } from "vue"
134 import Api from "@/api"
135 import Icon from "@/components/common/Icon.vue"
136 import dayjs from "@/utils/dayjs"
137 import SocCaseItem from "./SocCaseItem.vue"
138
139 const dialog = useDialog()
140 const message = useMessage()
141 const loadingPurge = ref(false)
142 const loading = ref(false)
143 const showFilters = ref(false)
144 const casesList = ref<SocCase[]>([])
145
146 const pageSize = ref(25)
147 const currentPage = ref(1)
148 const simpleMode = ref(false)
149 const showSizePicker = ref(true)
150 const pageSizes = [10, 25, 50, 100]
151 const header = ref()
152 const pageSlot = ref(8)
153
154 const itemsPaginated = computed(() => {
155 const from = (currentPage.value - 1) * pageSize.value
156 const to = currentPage.value * pageSize.value
157
158 const list = _orderBy(
159 casesList.value.map(o => {
160 o.case_open_date = dayjs(o.case_open_date).format("YYYY/MM/DD") as DateFormatted
161 return o
162 }),
163 ["case_open_date"],
164 ["desc"]
165 )
166
167 return list.slice(from, to)
168 })
169
170 const FilterIcon = "carbon:filter-edit"
171 const InfoIcon = "carbon:information"
172 const TrashIcon = "carbon:trash-can"
173
174 const total = computed<number>(() => {
175 return casesList.value.length || 0
176 })
177
178 const filters = ref<Partial<CasesFilter>>({})
179 const lastFilters = ref<Partial<CasesFilter>>({})
180
181 const filtered = computed<boolean>(() => {
182 return !!filters.value.unit && !!filters.value.olderThan
183 })
184
185 const unitOptions = [
186 { label: "Hours", value: "hours" },
187 { label: "Days", value: "days" },
188 { label: "Weeks", value: "weeks" }
189 ]
190
191 watch(showFilters, val => {
192 if (!val) {
193 filters.value = _cloneDeep(lastFilters.value)
194 }
195 })
196
197 function getData() {
198 showFilters.value = false
199 loading.value = true
200
201 lastFilters.value = _cloneDeep(filters.value)
202
203 Api.soc
204 .getCases(filtered?.value ? (lastFilters.value as CasesFilter) : undefined)
205 .then(res => {
206 if (res.data.success) {
207 casesList.value = res.data?.cases || res.data?.cases_breached || []
208 } else {
209 message.warning(res.data?.message || "An error occurred. Please try again later.")
210 }
211 })
212 .catch(err => {
213 casesList.value = []
214
215 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
216 })
217 .finally(() => {
218 loading.value = false
219 })
220 }
221
222 function handlePurge() {
223 dialog.warning({
224 title: "Confirm",
225 content: "This will remove ALL cases, are you sure you want to proceed?",
226 positiveText: "Yes I'm sure",
227 negativeText: "Cancel",
228 onPositiveClick: () => {
229 purge()
230 },
231 onNegativeClick: () => {
232 message.info("Purge canceled")
233 }
234 })
235 }
236
237 function purge() {
238 loadingPurge.value = true
239
240 Api.soc
241 .purgeAllCases()
242 .then(res => {
243 if (res.data.success) {
244 getData()
245 message.success(res.data?.message || "SOC Cases purged successfully")
246 } else {
247 message.warning(res.data?.message || "An error occurred. Please try again later.")
248 }
249 })
250 .catch(err => {
251 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
252 })
253 .finally(() => {
254 loadingPurge.value = false
255 })
256 }
257
258 useResizeObserver(header, entries => {
259 const entry = entries[0]
260 if (!entry) return
261
262 const { width } = entry.contentRect
263
264 pageSlot.value = width < 700 ? 5 : 8
265 simpleMode.value = width < 550
266 })
267
268 onBeforeMount(() => {
269 getData()
270 })
271 </script>