main
vue 312 lines 7.97 KB
Raw
1 <template>
2 <n-spin :show="loadingRule">
3 <div v-if="rule" class="flex flex-col gap-8">
4 <!-- Rule Info -->
5 <RuleHeader :rule-detail="rule" />
6
7 <!-- Graylog Query Preview -->
8 <div v-if="rule.graylog?.query" class="flex flex-col gap-2">
9 <div class="text-sm font-semibold">Graylog Query</div>
10 <CodeSource :code="rule.graylog.query" lang="sql" />
11 </div>
12
13 <!-- No Graylog Query Warning -->
14 <n-alert v-else type="warning" title="No Graylog Query">
15 This rule does not have a Graylog query defined. Only rules with Graylog queries can be provisioned as
16 Graylog alerts.
17 </n-alert>
18
19 <!-- Provision Form -->
20 <n-form
21 v-if="rule.graylog?.query"
22 ref="formRef"
23 :model="formValue"
24 :rules="formRules"
25 label-placement="left"
26 label-width="auto"
27 :disabled="provisioning"
28 >
29 <!-- Custom Title -->
30 <n-form-item label="Alert Title" path="custom_title">
31 <div class="flex w-full flex-col gap-2">
32 <n-input v-model:value="formValue.custom_title" :placeholder="defaultTitle" clearable />
33 <div class="text-secondary text-xs">
34 Leave empty to use the default title: "{{ defaultTitle }}"
35 </div>
36 </div>
37 </n-form-item>
38
39 <!-- Search Within -->
40 <n-form-item label="Search Window" path="search_within_seconds">
41 <div class="flex w-full flex-col gap-2">
42 <n-input-number
43 v-model:value="formValue.search_within_seconds"
44 :min="60"
45 :max="86400"
46 :step="60"
47 class="w-full"
48 >
49 <template #suffix>seconds</template>
50 </n-input-number>
51 <div class="text-secondary text-xs">
52 Time window to search within ({{ formatDuration(formValue.search_within_seconds) }})
53 </div>
54 </div>
55 </n-form-item>
56
57 <!-- Execute Every -->
58 <n-form-item label="Execute Every" path="execute_every_seconds">
59 <div class="flex w-full flex-col gap-2">
60 <n-input-number
61 v-model:value="formValue.execute_every_seconds"
62 :min="60"
63 :max="86400"
64 :step="60"
65 class="w-full"
66 >
67 <template #suffix>seconds</template>
68 </n-input-number>
69 <div class="text-secondary text-xs">
70 How often to run the search ({{ formatDuration(formValue.execute_every_seconds) }})
71 </div>
72 </div>
73 </n-form-item>
74
75 <!-- Priority -->
76 <n-form-item label="Priority" path="priority">
77 <n-select v-model:value="formValue.priority" :options="priorityOptions" class="w-full" />
78 </n-form-item>
79
80 <!-- Event Limit -->
81 <n-form-item label="Event Limit" path="event_limit">
82 <div class="flex w-full flex-col gap-2">
83 <n-input-number v-model:value="formValue.event_limit" :min="1" :max="10000" class="w-full" />
84 <div class="text-secondary text-xs">Maximum number of events to process per execution</div>
85 </div>
86 </n-form-item>
87
88 <!-- Streams (Optional) -->
89 <n-form-item label="Streams" path="streams">
90 <div class="flex w-full flex-col gap-2">
91 <n-dynamic-tags v-model:value="formValue.streams" />
92 <div class="text-secondary text-xs">Optional: Limit search to specific Graylog stream IDs</div>
93 </div>
94 </n-form-item>
95 </n-form>
96
97 <!-- Actions -->
98 <div class="flex justify-end gap-2">
99 <n-button
100 type="primary"
101 :loading="provisioning"
102 :disabled="!rule.graylog?.query"
103 @click="handleProvision"
104 >
105 <template #icon>
106 <Icon :name="ProvisionIcon" />
107 </template>
108 Provision Alert
109 </n-button>
110 </div>
111 </div>
112
113 <n-empty v-else-if="!loadingRule" description="Failed to load rule details" />
114 </n-spin>
115 </template>
116
117 <script setup lang="ts">
118 import type { FormInst, FormRules } from "naive-ui"
119 import type { ProvisionGraylogAlertRequest, RuleDetail } from "@/types/copilotSearches.d"
120 import {
121 NAlert,
122 NButton,
123 NDynamicTags,
124 NEmpty,
125 NForm,
126 NFormItem,
127 NInput,
128 NInputNumber,
129 NSelect,
130 NSpin,
131 useMessage
132 } from "naive-ui"
133 import { computed, onBeforeMount, ref } from "vue"
134 import Api from "@/api"
135 import CodeSource from "@/components/common/CodeSource.vue"
136 import Icon from "@/components/common/Icon.vue"
137 import RuleHeader from "./RuleHeader.vue"
138
139 const props = defineProps<{
140 ruleId?: string
141 ruleData?: RuleDetail
142 }>()
143
144 const emit = defineEmits<{
145 (e: "success"): void
146 (e: "close"): void
147 }>()
148
149 const message = useMessage()
150 const formRef = ref<FormInst | null>(null)
151 const loadingRule = ref(false)
152 const provisioning = ref(false)
153 const rule = ref<RuleDetail | null>(null)
154
155 const ProvisionIcon = "carbon:add-alt"
156
157 const formValue = ref<{
158 custom_title: string | null
159 search_within_seconds: number
160 execute_every_seconds: number
161 priority: 1 | 2 | 3
162 event_limit: number
163 streams: string[]
164 }>({
165 custom_title: null,
166 search_within_seconds: 300,
167 execute_every_seconds: 300,
168 priority: 2,
169 event_limit: 1000,
170 streams: []
171 })
172
173 const formRules: FormRules = {
174 search_within_seconds: {
175 required: true,
176 type: "number",
177 min: 60,
178 max: 86400,
179 message: "Must be between 60 and 86400 seconds",
180 trigger: ["blur", "change"]
181 },
182 execute_every_seconds: {
183 required: true,
184 type: "number",
185 min: 60,
186 max: 86400,
187 message: "Must be between 60 and 86400 seconds",
188 trigger: ["blur", "change"]
189 },
190 event_limit: {
191 required: true,
192 type: "number",
193 min: 1,
194 max: 10000,
195 message: "Must be between 1 and 10000",
196 trigger: ["blur", "change"]
197 }
198 }
199
200 const priorityOptions = [
201 { label: "Low (1)", value: 1 },
202 { label: "Normal (2)", value: 2 },
203 { label: "High (3)", value: 3 }
204 ]
205
206 const SPACE_REGEX = / /g
207
208 const defaultTitle = computed(() => {
209 if (!rule.value) return ""
210 return rule.value.name.toUpperCase().replace(SPACE_REGEX, " - ")
211 })
212
213 function formatDuration(seconds: number): string {
214 if (seconds < 60) return `${seconds} seconds`
215 if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`
216 if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`
217 return `${Math.floor(seconds / 86400)} days`
218 }
219
220 function getPriorityFromSeverity(severity: string): 1 | 2 | 3 {
221 switch (severity.toLowerCase()) {
222 case "low":
223 return 1
224 case "medium":
225 return 2
226 case "high":
227 case "critical":
228 return 3
229 default:
230 return 2
231 }
232 }
233
234 function prefillForm() {
235 if (rule.value?.response?.severity) {
236 formValue.value.priority = getPriorityFromSeverity(rule.value.response.severity)
237 }
238 }
239
240 async function loadRule(ruleId: string) {
241 loadingRule.value = true
242 try {
243 const res = await Api.copilotSearches.getRuleById(ruleId)
244 if (res.data.success) {
245 rule.value = res.data.rule
246
247 // Set default priority based on rule severity
248 prefillForm()
249 } else {
250 message.error(res.data?.message || "Failed to load rule details")
251 }
252 } catch (err: any) {
253 message.error(err.response?.data?.message || "Failed to load rule details")
254 } finally {
255 loadingRule.value = false
256 }
257 }
258
259 async function handleProvision() {
260 if (!formRef.value || !rule.value) return
261
262 try {
263 await formRef.value.validate()
264 } catch {
265 return
266 }
267
268 provisioning.value = true
269
270 try {
271 const request: ProvisionGraylogAlertRequest = {
272 rule_id: props.ruleId || rule.value?.id || "",
273 search_within_seconds: formValue.value.search_within_seconds,
274 execute_every_seconds: formValue.value.execute_every_seconds,
275 priority: formValue.value.priority,
276 event_limit: formValue.value.event_limit
277 }
278
279 if (formValue.value.custom_title) {
280 request.custom_title = formValue.value.custom_title
281 }
282
283 if (formValue.value.streams.length > 0) {
284 request.streams = formValue.value.streams
285 }
286
287 const res = await Api.copilotSearches.provisionGraylogAlert(request)
288
289 if (res.data.success) {
290 message.success(`Graylog alert "${res.data.alert_title}" created successfully!`)
291 emit("success")
292 } else {
293 message.error(res.data?.message || "Failed to provision alert")
294 }
295 } catch (err: any) {
296 message.error(err.response?.data?.message || "Failed to provision Graylog alert")
297 } finally {
298 provisioning.value = false
299 }
300 }
301
302 onBeforeMount(() => {
303 if (props.ruleData) {
304 rule.value = props.ruleData
305 prefillForm()
306 } else if (props.ruleId) {
307 loadRule(props.ruleId)
308 } else {
309 message.error("No rule data or rule ID provided")
310 }
311 })
312 </script>