main
vue 267 lines 6.74 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <div class="flex items-center justify-between">
4 <h2 class="text-lg font-semibold">Scheduled Snapshots</h2>
5 <div class="flex items-center gap-2">
6 <n-button :loading @click="fetchSchedules">
7 <template #icon>
8 <Icon :name="RefreshIcon" :size="16" />
9 </template>
10 Refresh
11 </n-button>
12 <n-button type="primary" @click="showCreateModal = true">
13 <template #icon>
14 <Icon :name="AddIcon" :size="16" />
15 </template>
16 Create Schedule
17 </n-button>
18 </div>
19 </div>
20
21 <n-spin :show="loading">
22 <n-card>
23 <n-data-table
24 :columns
25 :data="schedules"
26 :bordered="false"
27 :single-line="false"
28 size="small"
29 :row-key="(row: SnapshotScheduleResponse) => row.id"
30 />
31 </n-card>
32 </n-spin>
33
34 <n-empty v-if="!loading && schedules.length === 0" description="No snapshot schedules configured" />
35
36 <!-- Create Schedule Modal -->
37 <n-modal v-model:show="showCreateModal" preset="dialog" title="Create Snapshot Schedule" style="width: 600px">
38 <SnapshotScheduleForm @success="onScheduleCreated" @cancel="showCreateModal = false" />
39 </n-modal>
40
41 <!-- Edit Schedule Modal -->
42 <n-modal v-model:show="showEditModal" preset="dialog" title="Edit Snapshot Schedule" style="width: 600px">
43 <SnapshotScheduleForm
44 :schedule="selectedSchedule"
45 @success="onScheduleUpdated"
46 @cancel="showEditModal = false"
47 />
48 </n-modal>
49 </div>
50 </template>
51
52 <script setup lang="ts">
53 // TODO-FE: refactor
54 import type { DataTableColumns } from "naive-ui"
55 import type { SnapshotScheduleResponse } from "@/types/snapshots.d"
56 import { NButton, NCard, NDataTable, NEmpty, NModal, NPopconfirm, NSpin, NSwitch, NTag, useMessage } from "naive-ui"
57 import { h, onBeforeMount, ref } from "vue"
58 import Api from "@/api"
59 import Icon from "@/components/common/Icon.vue"
60 import SnapshotScheduleForm from "./SnapshotScheduleForm.vue"
61
62 const AddIcon = "carbon:add"
63 const RefreshIcon = "carbon:renew"
64
65 const message = useMessage()
66 const loading = ref(false)
67 const schedules = ref<SnapshotScheduleResponse[]>([])
68 const showCreateModal = ref(false)
69 const showEditModal = ref(false)
70 const selectedSchedule = ref<SnapshotScheduleResponse | null>(null)
71
72 const columns: DataTableColumns<SnapshotScheduleResponse> = [
73 {
74 title: "Name",
75 key: "name",
76 sorter: "default"
77 },
78 {
79 title: "Index Pattern",
80 key: "index_pattern",
81 render(row) {
82 return h("code", { class: "text-sm" }, row.index_pattern)
83 }
84 },
85 {
86 title: "Repository",
87 key: "repository"
88 },
89 {
90 title: "Enabled",
91 key: "enabled",
92 render(row) {
93 return h(NSwitch, {
94 value: row.enabled,
95 onUpdateValue: (value: boolean) => toggleEnabled(row, value)
96 })
97 }
98 },
99 {
100 title: "Retention",
101 key: "retention_days",
102 render(row) {
103 return row.retention_days ? `${row.retention_days} days` : "Forever"
104 }
105 },
106 {
107 title: "Schedule",
108 key: "scheduled_hour",
109 render(row) {
110 const hour = row.scheduled_hour
111 const minute = row.scheduled_minute
112 const interval = row.interval_days ?? 1
113 const tz = row.timezone || "UTC"
114 const dow = row.day_of_week
115 // Python convention: Monday=0 ... Sunday=6.
116 const weekdayNames = ["Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays", "Sundays"]
117
118 let cadence: string
119 if (dow != null) {
120 const dayLabel = weekdayNames[dow] ?? `Day ${dow}`
121 cadence = interval > 1 ? `${dayLabel} every ${interval} days` : dayLabel
122 } else {
123 cadence = interval === 1 ? "Daily" : `Every ${interval} days`
124 }
125
126 if (hour == null) {
127 return h("div", { class: "flex flex-col" }, [
128 h("span", {}, cadence),
129 h("span", { class: "text-xs text-gray-500" }, "Any hour")
130 ])
131 }
132
133 const hourStr = String(hour).padStart(2, "0")
134 const minStr = String(minute ?? 0).padStart(2, "0")
135
136 return h("div", { class: "flex flex-col" }, [
137 h("span", {}, `${cadence} at ${hourStr}:${minStr}`),
138 h("span", { class: "text-xs text-gray-500" }, tz)
139 ])
140 }
141 },
142 {
143 title: "Last Execution",
144 key: "last_execution_time",
145 render(row) {
146 if (!row.last_execution_time) return "-"
147 const status = row.last_execution_status || ""
148 const tagType = status.startsWith("SUCCESS")
149 ? "success"
150 : status.startsWith("SKIPPED") || status.startsWith("DEFERRED")
151 ? "warning"
152 : "error"
153 return h("div", { class: "flex flex-col" }, [
154 h("span", {}, new Date(row.last_execution_time).toLocaleString()),
155 h(
156 NTag,
157 {
158 type: tagType,
159 size: "small",
160 class: "mt-1"
161 },
162 () => status.split(":")[0] || "Unknown"
163 )
164 ])
165 }
166 },
167 {
168 title: "Last Snapshot",
169 key: "last_snapshot_name",
170 render(row) {
171 return row.last_snapshot_name || "-"
172 }
173 },
174 {
175 title: "Actions",
176 key: "actions",
177 width: 150,
178 render(row) {
179 return h("div", { class: "flex gap-2" }, [
180 h(
181 NButton,
182 {
183 size: "small",
184 onClick: () => openEditModal(row)
185 },
186 () => "Edit"
187 ),
188 h(
189 NPopconfirm,
190 {
191 onPositiveClick: () => deleteSchedule(row)
192 },
193 {
194 trigger: () => h(NButton, { size: "small", type: "error" }, () => "Delete"),
195 default: () => "Are you sure you want to delete this schedule?"
196 }
197 )
198 ])
199 }
200 }
201 ]
202
203 function openEditModal(schedule: SnapshotScheduleResponse) {
204 selectedSchedule.value = schedule
205 showEditModal.value = true
206 }
207
208 async function toggleEnabled(schedule: SnapshotScheduleResponse, enabled: boolean) {
209 try {
210 const response = await Api.snapshots.updateSchedule(schedule.id, { enabled })
211 if (response.data.success) {
212 message.success(`Schedule ${enabled ? "enabled" : "disabled"}`)
213 fetchSchedules()
214 } else {
215 message.error(response.data.message)
216 }
217 } catch (error: any) {
218 message.error(error.message || "Failed to update schedule")
219 }
220 }
221
222 async function deleteSchedule(schedule: SnapshotScheduleResponse) {
223 try {
224 const response = await Api.snapshots.deleteSchedule(schedule.id)
225 if (response.data.success) {
226 message.success("Schedule deleted")
227 fetchSchedules()
228 } else {
229 message.error(response.data.message)
230 }
231 } catch (error: any) {
232 message.error(error.message || "Failed to delete schedule")
233 }
234 }
235
236 async function fetchSchedules() {
237 loading.value = true
238 try {
239 const response = await Api.snapshots.getSchedules()
240 if (response.data.success) {
241 schedules.value = response.data.schedules
242 } else {
243 message.error(response.data.message)
244 }
245 } catch (error: any) {
246 message.error(error.message || "Failed to fetch schedules")
247 } finally {
248 loading.value = false
249 }
250 }
251
252 function onScheduleCreated() {
253 showCreateModal.value = false
254 fetchSchedules()
255 message.success("Schedule created successfully")
256 }
257
258 function onScheduleUpdated() {
259 showEditModal.value = false
260 fetchSchedules()
261 message.success("Schedule updated successfully")
262 }
263
264 onBeforeMount(() => {
265 fetchSchedules()
266 })
267 </script>