main
vue 283 lines 8.38 KB
Raw
1 <template>
2 <n-form ref="formRef" :model="formData" :rules label-placement="left" label-width="160px">
3 <n-form-item label="Name" path="name">
4 <n-input v-model:value="formData.name" placeholder="Enter a friendly name for this schedule" />
5 </n-form-item>
6
7 <n-form-item label="Index Pattern" path="index_pattern">
8 <n-input v-model:value="formData.index_pattern" placeholder="e.g., wazuh_customer_*" />
9 <template #feedback>Use wildcards (*) to match multiple indices. Example: wazuh_customer_*</template>
10 </n-form-item>
11
12 <n-form-item label="Repository" path="repository">
13 <n-select
14 v-model:value="formData.repository"
15 :options="repositoryOptions"
16 placeholder="Select a repository"
17 :loading="loadingRepositories"
18 />
19 </n-form-item>
20
21 <n-form-item label="Snapshot Prefix" path="snapshot_prefix">
22 <n-input v-model:value="formData.snapshot_prefix" placeholder="e.g., scheduled" />
23 <template #feedback>
24 Prefix for generated snapshot names. Full name: {prefix}_{schedule_name}_{timestamp}
25 </template>
26 </n-form-item>
27
28 <n-form-item label="Enabled" path="enabled">
29 <n-switch v-model:value="formData.enabled" />
30 </n-form-item>
31
32 <n-form-item label="Skip Write Indices" path="skip_write_indices">
33 <n-switch v-model:value="formData.skip_write_indices" />
34 <span class="ml-2 text-sm text-gray-500">Skip indices currently being written to (recommended)</span>
35 </n-form-item>
36
37 <n-form-item label="Include Global State" path="include_global_state">
38 <n-switch v-model:value="formData.include_global_state" />
39 </n-form-item>
40
41 <n-form-item label="Retention (Days)" path="retention_days">
42 <n-input-number
43 v-model:value="formData.retention_days"
44 :min="1"
45 :max="365"
46 placeholder="Leave empty for no retention limit"
47 clearable
48 style="width: 100%"
49 />
50 <template #feedback>
51 Automatically delete snapshots older than this many days. Leave empty to keep forever.
52 </template>
53 </n-form-item>
54
55 <n-divider title-placement="left">Schedule Window</n-divider>
56
57 <n-form-item label="Day of Week" path="day_of_week">
58 <n-select v-model:value="formData.day_of_week" :options="weekdayOptions" placeholder="Any day" clearable />
59 <template #feedback>
60 Restrict execution to a single day of the week. Leave empty to allow any day. Combines with Interval
61 (Days) for patterns like "every other Sunday".
62 </template>
63 </n-form-item>
64
65 <n-form-item label="Scheduled Hour" path="scheduled_hour">
66 <n-input-number
67 v-model:value="formData.scheduled_hour"
68 :min="0"
69 :max="23"
70 placeholder="Leave empty for any hour"
71 clearable
72 style="width: 100%"
73 />
74 <template #feedback>
75 Hour of day (0-23) when this schedule is allowed to run. Leave empty to allow any hour (legacy behavior
76 — runs every poll).
77 </template>
78 </n-form-item>
79
80 <n-form-item label="Scheduled Minute" path="scheduled_minute">
81 <n-input-number
82 v-model:value="formData.scheduled_minute"
83 :min="0"
84 :max="59"
85 placeholder="Leave empty for any minute"
86 clearable
87 style="width: 100%"
88 :disabled="formData.scheduled_hour == null"
89 />
90 <template #feedback>
91 Minute of hour. The schedule runs within a 15-minute tolerance window starting at this minute. Requires
92 Scheduled Hour to be set.
93 </template>
94 </n-form-item>
95
96 <n-form-item label="Interval (Days)" path="interval_days">
97 <n-input-number v-model:value="formData.interval_days" :min="1" :max="365" style="width: 100%" />
98 <template #feedback>Minimum number of days between executions. Default 1 = at most once per day.</template>
99 </n-form-item>
100
101 <n-form-item label="Timezone" path="timezone">
102 <n-select
103 v-model:value="formData.timezone"
104 :options="timezoneOptions"
105 filterable
106 tag
107 placeholder="Select or type IANA timezone"
108 />
109 <template #feedback>
110 IANA timezone used to evaluate Scheduled Hour/Minute (e.g., UTC, America/New_York).
111 </template>
112 </n-form-item>
113
114 <div class="mt-4 flex justify-end gap-2">
115 <n-button @click="$emit('cancel')">Cancel</n-button>
116 <n-button type="primary" :loading @click="handleSubmit">
117 {{ isEditing ? "Update Schedule" : "Create Schedule" }}
118 </n-button>
119 </div>
120 </n-form>
121 </template>
122
123 <script setup lang="ts">
124 // TODO-FE: refactor
125 import type { FormInst, FormRules, SelectOption } from "naive-ui"
126 import type { SnapshotRepository, SnapshotScheduleCreate, SnapshotScheduleResponse } from "@/types/snapshots.d"
127 import { NButton, NDivider, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch, useMessage } from "naive-ui"
128 import { computed, onBeforeMount, ref, watch } from "vue"
129 import Api from "@/api"
130
131 const props = defineProps<{
132 schedule?: SnapshotScheduleResponse | null
133 }>()
134
135 const emit = defineEmits<{
136 (e: "success"): void
137 (e: "cancel"): void
138 }>()
139
140 const message = useMessage()
141 const formRef = ref<FormInst | null>(null)
142 const loading = ref(false)
143 const loadingRepositories = ref(false)
144 const repositories = ref<SnapshotRepository[]>([])
145
146 const isEditing = computed(() => !!props.schedule)
147
148 const formData = ref<SnapshotScheduleCreate>({
149 name: "",
150 index_pattern: "",
151 repository: "",
152 enabled: true,
153 snapshot_prefix: "scheduled",
154 include_global_state: false,
155 skip_write_indices: true,
156 retention_days: null,
157 scheduled_hour: null,
158 scheduled_minute: null,
159 interval_days: 1,
160 day_of_week: null,
161 timezone: "UTC"
162 })
163
164 const repositoryOptions = computed<SelectOption[]>(() =>
165 repositories.value.map(repo => ({
166 label: repo.name,
167 value: repo.name
168 }))
169 )
170
171 // Python convention: Monday=0 ... Sunday=6 (matches datetime.weekday()).
172 const weekdayOptions: SelectOption[] = [
173 { label: "Monday", value: 0 },
174 { label: "Tuesday", value: 1 },
175 { label: "Wednesday", value: 2 },
176 { label: "Thursday", value: 3 },
177 { label: "Friday", value: 4 },
178 { label: "Saturday", value: 5 },
179 { label: "Sunday", value: 6 }
180 ]
181
182 const timezoneOptions: SelectOption[] = [
183 { label: "UTC", value: "UTC" },
184 { label: "America/New_York", value: "America/New_York" },
185 { label: "America/Chicago", value: "America/Chicago" },
186 { label: "America/Denver", value: "America/Denver" },
187 { label: "America/Los_Angeles", value: "America/Los_Angeles" },
188 { label: "Europe/London", value: "Europe/London" },
189 { label: "Europe/Berlin", value: "Europe/Berlin" },
190 { label: "Europe/Paris", value: "Europe/Paris" },
191 { label: "Asia/Tokyo", value: "Asia/Tokyo" },
192 { label: "Asia/Singapore", value: "Asia/Singapore" },
193 { label: "Australia/Sydney", value: "Australia/Sydney" }
194 ]
195
196 const rules: FormRules = {
197 name: {
198 required: true,
199 message: "Name is required",
200 trigger: "blur"
201 },
202 index_pattern: {
203 required: true,
204 message: "Index pattern is required",
205 trigger: "blur"
206 },
207 repository: {
208 required: true,
209 message: "Repository is required",
210 trigger: "change"
211 }
212 }
213
214 watch(
215 () => props.schedule,
216 newSchedule => {
217 if (newSchedule) {
218 formData.value = {
219 name: newSchedule.name,
220 index_pattern: newSchedule.index_pattern,
221 repository: newSchedule.repository,
222 enabled: newSchedule.enabled,
223 snapshot_prefix: newSchedule.snapshot_prefix,
224 include_global_state: newSchedule.include_global_state,
225 skip_write_indices: newSchedule.skip_write_indices,
226 retention_days: newSchedule.retention_days,
227 scheduled_hour: newSchedule.scheduled_hour ?? null,
228 scheduled_minute: newSchedule.scheduled_minute ?? null,
229 interval_days: newSchedule.interval_days ?? 1,
230 day_of_week: newSchedule.day_of_week ?? null,
231 timezone: newSchedule.timezone ?? "UTC"
232 }
233 }
234 },
235 { immediate: true }
236 )
237
238 async function fetchRepositories() {
239 loadingRepositories.value = true
240 try {
241 const response = await Api.snapshots.getRepositories()
242 if (response.data.success) {
243 repositories.value = response.data.repositories
244 }
245 } catch (error: any) {
246 message.error(error.message || "Failed to fetch repositories")
247 } finally {
248 loadingRepositories.value = false
249 }
250 }
251
252 async function handleSubmit() {
253 try {
254 await formRef.value?.validate()
255 } catch {
256 return
257 }
258
259 loading.value = true
260 try {
261 let response
262 if (isEditing.value && props.schedule) {
263 response = await Api.snapshots.updateSchedule(props.schedule.id, formData.value)
264 } else {
265 response = await Api.snapshots.createSchedule(formData.value)
266 }
267
268 if (response.data.success) {
269 emit("success")
270 } else {
271 message.error(response.data.message)
272 }
273 } catch (error: any) {
274 message.error(error.message || "Failed to save schedule")
275 } finally {
276 loading.value = false
277 }
278 }
279
280 onBeforeMount(() => {
281 fetchRepositories()
282 })
283 </script>