| 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> |