main
vue 465 lines 11.9 KB
Raw
1 <template>
2 <n-spin :show="loading">
3 <n-form ref="formRef" :label-width="80" :model="form" :rules>
4 <div class="flex flex-col gap-2">
5 <div class="flex gap-4">
6 <n-form-item label="Priority" path="alert_priority" class="w-28">
7 <n-select
8 v-model:value="form.alert_priority"
9 :options="alertPriorityOptions"
10 placeholder="Select..."
11 clearable
12 />
13 </n-form-item>
14 <n-form-item label="Name" path="alert_name" class="grow">
15 <n-input
16 v-model:value.trim="form.alert_name"
17 placeholder="Please insert Alert Name"
18 clearable
19 />
20 </n-form-item>
21 </div>
22 <div class="flex flex-col gap-2">
23 <n-form-item label="Description" path="alert_description">
24 <n-input
25 v-model:value.trim="form.alert_description"
26 placeholder="Please insert Alert Description"
27 clearable
28 type="textarea"
29 :autosize="{
30 minRows: 3,
31 maxRows: 10
32 }"
33 />
34 </n-form-item>
35 <n-form-item label="Streams" path="streams">
36 <n-select
37 v-model:value="form.streams"
38 :options="availableStreamsOptions"
39 :loading="loadingStreams"
40 :placeholder="loadingStreams ? 'Loading Streams...' : 'Select Streams'"
41 multiple
42 clearable
43 to="body"
44 class="grow"
45 />
46 </n-form-item>
47 <n-form-item label="Search Query" path="search_query">
48 <n-input
49 v-model:value.trim="form.search_query"
50 placeholder="Please insert Search Query"
51 clearable
52 />
53 </n-form-item>
54 </div>
55 <div class="w-full">
56 <n-form-item label="Custom fields" path="custom_fields">
57 <div class="flex w-full flex-col gap-1">
58 <n-card v-for="(cf, index) of form.custom_fields" :key="cf.key" size="small" embedded>
59 <div class="flex w-full gap-2">
60 <n-form-item
61 label="Name"
62 class="grow"
63 size="small"
64 :path="`custom_fields[${index}].name`"
65 :rule="{
66 required: true,
67 message: `Field Name required`,
68 trigger: ['input', 'blur']
69 }"
70 >
71 <n-input
72 v-model:value.trim="cf.name"
73 placeholder="Custom field Name"
74 clearable
75 @update:value="validate()"
76 />
77 </n-form-item>
78 <n-form-item
79 label="Value"
80 class="grow"
81 size="small"
82 :path="`custom_fields[${index}].value`"
83 :rule="{
84 required: true,
85 message: `Field Value required`,
86 trigger: ['input', 'blur']
87 }"
88 >
89 <n-input
90 v-model:value.trim="cf.value"
91 placeholder="Custom field Value"
92 clearable
93 @update:value="validate()"
94 />
95 </n-form-item>
96 <n-form-item size="small">
97 <n-button type="error" secondary @click="removeCustomFiled(cf.key)">
98 <template #icon>
99 <Icon :name="RemoveIcon" :size="16" />
100 </template>
101 </n-button>
102 </n-form-item>
103 </div>
104 </n-card>
105 <div class="mt-3">
106 <n-button @click="addCustomFiled()">
107 <template #icon>
108 <Icon :name="AddIcon" />
109 </template>
110 Add Custom Field
111 </n-button>
112 </div>
113 </div>
114 </n-form-item>
115 </div>
116 <div class="flex gap-4">
117 <n-form-item label="Search within (seconds)" path="search_within_seconds" class="grow">
118 <n-input-number
119 v-model:value="form.search_within_seconds"
120 :min="1"
121 placeholder="Input time in seconds"
122 clearable
123 class="w-full"
124 />
125 </n-form-item>
126 <n-form-item label="Execute every (seconds)" path="execute_every_seconds" class="grow">
127 <n-input-number
128 v-model:value="form.execute_every_seconds"
129 :min="1"
130 placeholder="Input time in seconds"
131 clearable
132 class="w-full"
133 />
134 </n-form-item>
135 </div>
136 <div class="flex justify-between gap-4">
137 <div class="flex gap-4">
138 <slot name="additionalActions"></slot>
139 </div>
140 <div class="flex gap-4">
141 <n-button :disabled="loading" @click="reset()">Reset</n-button>
142 <n-button
143 type="primary"
144 :disabled="!isValid"
145 :loading="submittingCustomAlert"
146 @click="validate(() => submit())"
147 >
148 Submit
149 </n-button>
150 </div>
151 </div>
152 </div>
153 </n-form>
154 </n-spin>
155 </template>
156
157 <script setup lang="ts">
158 import type { FormInst, FormItemRule, FormRules, FormValidationError, MessageReactive } from "naive-ui"
159 import type { CustomProvisionPayload } from "@/api/endpoints/monitoringAlerts"
160 import type { Stream } from "@/types/graylog/stream.d"
161 import _get from "lodash/get"
162 import _toSafeInteger from "lodash/toSafeInteger"
163 import _trim from "lodash/trim"
164 import { NButton, NCard, NForm, NFormItem, NInput, NInputNumber, NSelect, NSpin, useMessage } from "naive-ui"
165 import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
166 import Api from "@/api"
167 import Icon from "@/components/common/Icon.vue"
168 import { CustomProvisionPriority } from "@/types/monitoringAlerts.d"
169
170 interface CustomProvisionForm {
171 alert_name: string
172 alert_description: string
173 alert_priority: null | CustomProvisionPriority
174 search_query: string
175 custom_fields: {
176 name: string
177 value: string
178 key: number
179 }[]
180 search_within_seconds: number
181 execute_every_seconds: number
182 streams: string[]
183 }
184
185 const emit = defineEmits<{
186 (e: "update:loading", value: boolean): void
187 (
188 e: "mounted",
189 value: {
190 reset: () => void
191 }
192 ): void
193 }>()
194
195 const RemoveIcon = "ph:trash"
196 const AddIcon = "carbon:add-alt"
197 const submittingCustomAlert = ref(false)
198 const loading = computed(() => submittingCustomAlert.value)
199 const loadingStreams = ref(false)
200 const message = useMessage()
201 const availableStreams = ref<Stream[]>([])
202 const form = ref<CustomProvisionForm>(getClearForm())
203 const formRef = ref<FormInst | null>(null)
204
205 const availableStreamsOptions = computed(() => availableStreams.value.map(o => ({ label: o.title, value: o.id })))
206
207 const areAllCustomerFieldsFilled = computed(() => {
208 const fieldsFilled = form.value.custom_fields.filter(o => !!o.name && !!o.value)
209
210 return fieldsFilled.length === form.value.custom_fields.length
211 })
212
213 const areAllCustomerFieldsUniques = computed(() => {
214 const fieldsFilled = form.value.custom_fields.filter(o => !!o.name).map(o => o.name)
215
216 const uniques: string[] = fieldsFilled.filter((value, index, self) => self.indexOf(value) === index)
217
218 return uniques.length === form.value.custom_fields.length
219 })
220
221 /** @deprecated */
222 /*
223 const isCustomerCodePresent = computed(() => {
224 const field = form.value.custom_fields.filter(o => o.name === "CUSTOMER_CODE" && !!o.value)
225
226 return !!field.length
227 })
228 */
229
230 const alertPriorityOptions: { label: string; value: CustomProvisionPriority }[] = [
231 { label: "Low", value: CustomProvisionPriority.LOW },
232 { label: "Medium", value: CustomProvisionPriority.MEDIUM },
233 { label: "High", value: CustomProvisionPriority.HIGH }
234 ]
235
236 const rules: FormRules = {
237 alert_priority: {
238 required: true,
239 validator: validatorNumber("Alert Priority", "Required"),
240 trigger: ["input", "blur"]
241 },
242 alert_name: {
243 required: true,
244 message: "Please input the Alert Name",
245 trigger: ["input", "blur"]
246 },
247 alert_description: {
248 required: true,
249 message: "Please input the Alert Description",
250 trigger: ["input", "blur"]
251 },
252 search_query: {
253 required: true,
254 message: "Please input the Search Query",
255 trigger: ["input", "blur"]
256 },
257 search_within_seconds: {
258 required: true,
259 // message: "Please input Search within",
260 validator: validatorNumber("Search within"),
261 trigger: ["input", "blur"]
262 },
263 execute_every_seconds: {
264 required: true,
265 // message: "Please input Execute every",
266 validator: validatorNumber("Execute every"),
267 trigger: ["input", "blur"]
268 },
269 custom_fields: {
270 required: false,
271
272 validator(_rule: FormItemRule, _value: string) {
273 if (!areAllCustomerFieldsFilled.value) {
274 return new Error(`Please fill all customer fields`)
275 }
276
277 if (!areAllCustomerFieldsUniques.value) {
278 return new Error(`There are duplicated fields`)
279 }
280
281 /** @deprecated */
282 /*
283 if (!value.length || !isCustomerCodePresent.value) {
284 return new Error(`At least one custom field with name CUSTOMER_CODE is required`)
285 }
286 */
287
288 return true
289 },
290 trigger: ["input", "blur"]
291 }
292 }
293
294 const isValid = computed(() => {
295 if (!areAllCustomerFieldsFilled.value) {
296 return false
297 }
298
299 /** @deprecated */
300 /*
301 if (!isCustomerCodePresent.value) {
302 return false
303 }
304 */
305
306 if (!areAllCustomerFieldsUniques.value) {
307 return false
308 }
309
310 let valid = true
311
312 for (const key in rules) {
313 const rule = rules[key] as FormRules
314
315 if (rule.required && !_trim(_get(form.value, key))) {
316 valid = false
317 }
318 }
319
320 return valid
321 })
322
323 const INTEGER_STRING_REGEX = /^\d*$/
324
325 function validatorNumber(fieldName: string, defaultMessage?: string) {
326 return (_rule: FormItemRule, value: string) => {
327 if (!value) {
328 return new Error(defaultMessage || `${fieldName} is required`)
329 } else if (!INTEGER_STRING_REGEX.test(value)) {
330 return new Error(`${fieldName} should be an integer`)
331 } else if (Number(value) < 1) {
332 return new Error(`${fieldName} should be above 1`)
333 }
334 return true
335 }
336 }
337
338 let validationMessage: MessageReactive | null = null
339
340 function validate(cb?: () => void) {
341 if (!formRef.value) return
342
343 formRef.value.validate((errors?: Array<FormValidationError>) => {
344 if (!errors) {
345 validationMessage?.destroy()
346 validationMessage = null
347 if (cb) cb()
348 } else {
349 if (!validationMessage) {
350 validationMessage = message.warning("You must fill in the required fields correctly.")
351 }
352 return false
353 }
354 })
355 }
356
357 function addCustomFiled() {
358 form.value.custom_fields.push({
359 name: "",
360 value: "",
361 key: Date.now()
362 })
363 }
364
365 function removeCustomFiled(key: number) {
366 form.value.custom_fields = form.value.custom_fields.filter(o => o.key !== key)
367 validate()
368 }
369
370 function getClearForm(): CustomProvisionForm {
371 return {
372 alert_name: "",
373 alert_description: "",
374 alert_priority: null,
375 search_query: "",
376 custom_fields: [],
377 search_within_seconds: 1,
378 execute_every_seconds: 1,
379 streams: []
380 }
381 }
382
383 function reset() {
384 if (!loading.value) {
385 resetForm()
386 formRef.value?.restoreValidation()
387 }
388 }
389
390 function resetForm() {
391 form.value = getClearForm()
392 }
393
394 function submit() {
395 submittingCustomAlert.value = true
396
397 const payload: CustomProvisionPayload = {
398 alert_name: _trim(form.value.alert_name),
399 alert_description: _trim(form.value.alert_description),
400 alert_priority: form.value.alert_priority as CustomProvisionPriority,
401 search_query: _trim(form.value.search_query),
402 custom_fields: form.value.custom_fields,
403 search_within_ms: _toSafeInteger(form.value.search_within_seconds) * 1000,
404 execute_every_ms: _toSafeInteger(form.value.execute_every_seconds) * 1000,
405 streams: form.value.streams || []
406 }
407
408 Api.monitoringAlerts
409 .customProvision(payload)
410 .then(res => {
411 if (res.data.success) {
412 message.success(
413 res.data?.message || `Monitoring alert "${payload.alert_name}" provisioned successfully`
414 )
415 resetForm()
416 } else {
417 message.warning(res.data?.message || "An error occurred. Please try again later.")
418 }
419 })
420 .catch(err => {
421 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
422 })
423 .finally(() => {
424 submittingCustomAlert.value = false
425 })
426 }
427
428 function getStreams() {
429 if (availableStreams.value.length) {
430 return
431 }
432
433 loadingStreams.value = true
434
435 Api.graylog
436 .getStreams()
437 .then(res => {
438 if (res.data.success) {
439 availableStreams.value = res.data.streams || []
440 } else {
441 message.warning(res.data?.message || "An error occurred. Please try again later.")
442 }
443 })
444 .catch(err => {
445 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
446 })
447 .finally(() => {
448 loadingStreams.value = false
449 })
450 }
451
452 watch(loading, val => {
453 emit("update:loading", val)
454 })
455
456 onBeforeMount(() => {
457 getStreams()
458 })
459
460 onMounted(() => {
461 emit("mounted", {
462 reset
463 })
464 })
465 </script>