inicontent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.github/pull_request.yml +18 -0
  2. package/CONTRIBUTING.md +67 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/app/app.vue +106 -0
  6. package/app/assets/main.css +974 -0
  7. package/app/components/Api/EndpointCard.vue +169 -0
  8. package/app/components/Api/SchemaDocsTable.vue +208 -0
  9. package/app/components/Asset/Card.vue +1363 -0
  10. package/app/components/Asset/Grid.vue +295 -0
  11. package/app/components/Asset/Icon.vue +38 -0
  12. package/app/components/Asset/Preview.vue +486 -0
  13. package/app/components/Asset/Scanner.vue +700 -0
  14. package/app/components/Asset/Thumb.vue +708 -0
  15. package/app/components/Auth/Reset.vue +99 -0
  16. package/app/components/Auth/index.vue +346 -0
  17. package/app/components/ChatInterface.vue +2326 -0
  18. package/app/components/ChatPagePreview.vue +34 -0
  19. package/app/components/Column/Array.vue +29 -0
  20. package/app/components/Column/Asset.vue +21 -0
  21. package/app/components/Column/Boolean.vue +12 -0
  22. package/app/components/Column/Color.vue +9 -0
  23. package/app/components/Column/Date.vue +57 -0
  24. package/app/components/Column/Edit.vue +215 -0
  25. package/app/components/Column/Email.vue +9 -0
  26. package/app/components/Column/Html.vue +27 -0
  27. package/app/components/Column/Id.vue +20 -0
  28. package/app/components/Column/Locale.vue +32 -0
  29. package/app/components/Column/Object.vue +18 -0
  30. package/app/components/Column/Password.vue +7 -0
  31. package/app/components/Column/Role.vue +15 -0
  32. package/app/components/Column/S.vue +14 -0
  33. package/app/components/Column/Table/Single.vue +49 -0
  34. package/app/components/Column/Table/index.vue +42 -0
  35. package/app/components/Column/Tags.vue +31 -0
  36. package/app/components/Column/Text.vue +9 -0
  37. package/app/components/Column/Url.vue +9 -0
  38. package/app/components/Column/index.vue +41 -0
  39. package/app/components/Dashboard/Editor.vue +221 -0
  40. package/app/components/Dashboard/Grid.vue +270 -0
  41. package/app/components/Dashboard/View.vue +131 -0
  42. package/app/components/Dashboard/Widget/BarChart.vue +73 -0
  43. package/app/components/Dashboard/Widget/Counter.vue +51 -0
  44. package/app/components/Dashboard/Widget/LineChart.vue +76 -0
  45. package/app/components/Dashboard/Widget/PieChart.vue +75 -0
  46. package/app/components/Dashboard/Widget/Table.vue +94 -0
  47. package/app/components/Dashboard/WidgetEditor.vue +274 -0
  48. package/app/components/Data/Array.vue +102 -0
  49. package/app/components/Data/Html.vue +9 -0
  50. package/app/components/Data/Icon.vue +9 -0
  51. package/app/components/Data/Object.vue +10 -0
  52. package/app/components/Data/S.vue +16 -0
  53. package/app/components/Data/Table.vue +53 -0
  54. package/app/components/Data/Value.vue +34 -0
  55. package/app/components/Data/index.vue +14 -0
  56. package/app/components/Field/Array.vue +324 -0
  57. package/app/components/Field/Asset/Actions.vue +97 -0
  58. package/app/components/Field/Asset/index.vue +639 -0
  59. package/app/components/Field/Boolean.vue +26 -0
  60. package/app/components/Field/Checkbox.vue +48 -0
  61. package/app/components/Field/Color.vue +27 -0
  62. package/app/components/Field/Date.vue +30 -0
  63. package/app/components/Field/Email.vue +51 -0
  64. package/app/components/Field/EmailTemplate.vue +420 -0
  65. package/app/components/Field/Html/ColorPicker.vue +127 -0
  66. package/app/components/Field/Html/Editor.vue +544 -0
  67. package/app/components/Field/Html/index.vue +25 -0
  68. package/app/components/Field/Icon.vue +114 -0
  69. package/app/components/Field/JSON.vue +113 -0
  70. package/app/components/Field/Mention.vue +36 -0
  71. package/app/components/Field/Number.vue +27 -0
  72. package/app/components/Field/Object.vue +92 -0
  73. package/app/components/Field/Password.vue +55 -0
  74. package/app/components/Field/Radio.vue +47 -0
  75. package/app/components/Field/S.vue +44 -0
  76. package/app/components/Field/Select.vue +49 -0
  77. package/app/components/Field/Table.vue +348 -0
  78. package/app/components/Field/Tags.vue +113 -0
  79. package/app/components/Field/Text.vue +31 -0
  80. package/app/components/Field/Textarea.vue +33 -0
  81. package/app/components/Field/Url.vue +40 -0
  82. package/app/components/Field/Wrapper.vue +128 -0
  83. package/app/components/Field/index.vue +68 -0
  84. package/app/components/FloatingChatbot.vue +73 -0
  85. package/app/components/Form/Card.vue +151 -0
  86. package/app/components/Form/Drawer.vue +195 -0
  87. package/app/components/Form/index.vue +621 -0
  88. package/app/components/Header.vue +328 -0
  89. package/app/components/Offline/ConflictModal.vue +200 -0
  90. package/app/components/Offline/SyncStatus.vue +249 -0
  91. package/app/components/PlatformFeatureLanding.vue +65 -0
  92. package/app/components/Table/Backups.vue +638 -0
  93. package/app/components/Table/Flows.vue +814 -0
  94. package/app/components/Table/Grid.vue +296 -0
  95. package/app/components/Table/Icon.vue +36 -0
  96. package/app/components/Table/Logs.vue +181 -0
  97. package/app/components/Table/Schedules.vue +722 -0
  98. package/app/components/Table/Search/Button.vue +256 -0
  99. package/app/components/Table/Search/Items.vue +286 -0
  100. package/app/components/Table/Search/index.vue +87 -0
  101. package/app/components/Table/Settings/Schema.vue +801 -0
  102. package/app/components/Table/Settings/index.vue +631 -0
  103. package/app/components/Table/TranslateDrawer.vue +526 -0
  104. package/app/components/Table/Views/Kanban.vue +289 -0
  105. package/app/components/Table/Views/Table.vue +1273 -0
  106. package/app/components/Table/index.vue +890 -0
  107. package/app/composables/Translation/define.ts +41 -0
  108. package/app/composables/Translation/fetch.ts +107 -0
  109. package/app/composables/Translation/languages.ts +1 -0
  110. package/app/composables/Translation/loadCoreTranslations.ts +38 -0
  111. package/app/composables/Translation/translate.ts +250 -0
  112. package/app/composables/databaseCookies.ts +79 -0
  113. package/app/composables/fieldValidator.ts +72 -0
  114. package/app/composables/fieldsList.ts +301 -0
  115. package/app/composables/formatDatabase.ts +101 -0
  116. package/app/composables/index.ts +444 -0
  117. package/app/composables/openDrawer.ts +118 -0
  118. package/app/composables/optimizeFile.ts +287 -0
  119. package/app/composables/renderLabel.ts +88 -0
  120. package/app/composables/schemaClipboard.ts +95 -0
  121. package/app/composables/search.ts +353 -0
  122. package/app/composables/setThemeConfig.ts +48 -0
  123. package/app/composables/translationValue.ts +169 -0
  124. package/app/composables/useAssetPreview.ts +84 -0
  125. package/app/composables/useAssetUploader.ts +56 -0
  126. package/app/composables/useDashboardData.ts +310 -0
  127. package/app/composables/useDocumentThumbnail.ts +351 -0
  128. package/app/composables/useEcharts.ts +33 -0
  129. package/app/composables/useLocalDatabaseConfig.ts +51 -0
  130. package/app/composables/useOcr.ts +300 -0
  131. package/app/composables/useOfflineFetch.ts +194 -0
  132. package/app/composables/useOfflineItem.ts +107 -0
  133. package/app/composables/useOfflineQueue.ts +302 -0
  134. package/app/composables/useOfflineSync.ts +235 -0
  135. package/app/composables/usePasskeyAuth.ts +156 -0
  136. package/app/composables/usePdfCompressor.ts +180 -0
  137. package/app/composables/usePlatformRedirect.ts +72 -0
  138. package/app/composables/useRealtimeSync.ts +233 -0
  139. package/app/composables/useReferencedItems.ts +180 -0
  140. package/app/composables/useScanner.ts +664 -0
  141. package/app/composables/useSubscription.ts +650 -0
  142. package/app/composables/useThumbnailCache.ts +136 -0
  143. package/app/composables/useVideoCompressor.ts +194 -0
  144. package/app/error.vue +112 -0
  145. package/app/layouts/api.vue +188 -0
  146. package/app/layouts/dashboard.vue +91 -0
  147. package/app/layouts/default.vue +228 -0
  148. package/app/layouts/table.vue +471 -0
  149. package/app/locales/ar.ts +996 -0
  150. package/app/locales/en.ts +655 -0
  151. package/app/locales/es.ts +996 -0
  152. package/app/locales/fr.ts +997 -0
  153. package/app/middleware/dashboard.ts +33 -0
  154. package/app/middleware/database.ts +83 -0
  155. package/app/middleware/global.ts +1 -0
  156. package/app/middleware/sid-ingest.global.ts +29 -0
  157. package/app/middleware/table.ts +54 -0
  158. package/app/middleware/user.ts +26 -0
  159. package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
  160. package/app/pages/[[database]]/admin/api/index.vue +18 -0
  161. package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
  162. package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
  163. package/app/pages/[[database]]/admin/billing/index.vue +16 -0
  164. package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
  165. package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
  166. package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
  167. package/app/pages/[[database]]/admin/index.vue +28 -0
  168. package/app/pages/[[database]]/admin/settings.vue +526 -0
  169. package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
  170. package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
  171. package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
  172. package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
  173. package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
  174. package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
  175. package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
  176. package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
  177. package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
  178. package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
  179. package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
  180. package/app/pages/[[database]]/admin/tables/index.vue +20 -0
  181. package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
  182. package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
  183. package/app/pages/[[database]]/api/index.vue +120 -0
  184. package/app/pages/[[database]]/auth/index.vue +12 -0
  185. package/app/pages/[[database]]/auth/reset.vue +10 -0
  186. package/app/pages/[[database]]/index.vue +10 -0
  187. package/app/plugins/offline-warmup.client.ts +53 -0
  188. package/app/plugins/template-globals.ts +9 -0
  189. package/app/spa-loading-template.html +93 -0
  190. package/biome.json +53 -0
  191. package/index.d.ts +380 -0
  192. package/modules/naiveui.ts +41 -0
  193. package/nuxt.config.ts +269 -0
  194. package/package.json +56 -0
  195. package/pnpm-workspace.yaml +5 -0
  196. package/public/apple-touch-icon.png +0 -0
  197. package/public/favicon-16x16.png +0 -0
  198. package/public/favicon-32x32.png +0 -0
  199. package/public/favicon.ico +0 -0
  200. package/public/pwa-192x192.png +0 -0
  201. package/public/pwa-512x512.png +0 -0
  202. package/public/pwa-maskable-192x192.png +0 -0
  203. package/public/pwa-maskable-512x512.png +0 -0
  204. package/public/vendor/opencv.js +48 -0
  205. package/tsconfig.json +9 -0
@@ -0,0 +1,638 @@
1
+ <template>
2
+ <div>
3
+ <NResult v-if="!isAdmin" status="403" :title="t('accessDenied')" :description="t('backupsAdminOnly')" />
4
+ <div v-else>
5
+ <NCard
6
+ :class="`table_${table.slug}`"
7
+ id="tableCard"
8
+ :title="t('backups')"
9
+ :header-style="{ paddingRight: 0, paddingLeft: 0 }"
10
+ content-style="padding: 0"
11
+ :bordered="false"
12
+ style="background-color: transparent;"
13
+ embedded
14
+ >
15
+ <template #header-extra>
16
+ <NButtonGroup>
17
+ <NTooltip :delay="1500">
18
+ <template #trigger>
19
+ <NButton secondary :loading="loading" @click="load()">
20
+ <template #icon>
21
+ <NIcon>
22
+ <Icon name="tabler:refresh" />
23
+ </NIcon>
24
+ </template>
25
+ </NButton>
26
+ </template>
27
+ {{ t("refresh") }}
28
+ </NTooltip>
29
+ <NButton type="primary" secondary round :loading="creating" @click="openCreateModal">
30
+ <template #icon>
31
+ <NIcon>
32
+ <Icon name="tabler:plus" />
33
+ </NIcon>
34
+ </template>
35
+ {{ t("createBackup") }}
36
+ </NButton>
37
+ </NButtonGroup>
38
+ </template>
39
+
40
+ <NDataTable
41
+ remote
42
+ :scroll-x="1600"
43
+ :columns="columns"
44
+ :data="items"
45
+ :loading="loading"
46
+ :pagination="tablePagination"
47
+ :single-line="false"
48
+ />
49
+ </NCard>
50
+
51
+ <!-- Create backup -->
52
+ <NModal v-model:show="showCreateModal" preset="card" :title="t('createBackup')"
53
+ style="max-width: 560px; width: calc(100vw - 24px);">
54
+ <NForm label-placement="top">
55
+ <NFormItem :label="t('backupName')">
56
+ <NInput v-model:value="createForm.name" :placeholder="t('backupNamePlaceholder')" />
57
+ <template #feedback>
58
+ <NText depth="3" style="display: block; margin: 0 0 16px;">{{ t("backupNameHelp") }}</NText>
59
+ </template>
60
+ </NFormItem>
61
+ <NFormItem :label="t('backupType')">
62
+ <NRadioGroup v-model:value="createForm.type">
63
+ <NFlex vertical>
64
+ <NRadio value="full">
65
+ <NFlex align="center">
66
+ <NIcon style="margin-right: 6px"><Icon name="tabler:database" /></NIcon>
67
+ <span>{{ t("fullBackup") }}</span>
68
+ </NFlex>
69
+ </NRadio>
70
+ <NRadio value="table">
71
+ <NFlex align="center">
72
+ <NIcon style="margin-right: 6px"><Icon name="tabler:table" /></NIcon>
73
+ <span>{{ t("tableBackup") }}</span>
74
+ </NFlex>
75
+ </NRadio>
76
+ </NFlex>
77
+ </NRadioGroup>
78
+ <template #feedback>
79
+ <NText depth="3" style="display: block; margin: 0 0 16px;">{{
80
+ createForm.type === "full" ? t("fullBackupHelp") : t("tableBackupHelp")
81
+ }}</NText>
82
+ </template>
83
+ </NFormItem>
84
+ <NFormItem v-if="createForm.type === 'table'" :label="t('table')">
85
+ <NSelect v-model:value="createForm.table" :options="tableOptions" filterable :placeholder="t('table')" />
86
+ </NFormItem>
87
+ </NForm>
88
+
89
+ <template #footer>
90
+ <NFlex justify="end">
91
+ <NButton @click="closeCreateModal">{{ t("cancel") }}</NButton>
92
+ <NButton
93
+ type="primary"
94
+ :loading="creating"
95
+ :disabled="createForm.type === 'table' && !createForm.table"
96
+ @click="createBackup"
97
+ >
98
+ {{ t("create") }}
99
+ </NButton>
100
+ </NFlex>
101
+ </template>
102
+ </NModal>
103
+
104
+ <!-- Restore -->
105
+ <NModal v-model:show="showRestoreModal" preset="card" :title="t('restoreBackup')"
106
+ style="max-width: 640px; width: calc(100vw - 24px);">
107
+ <NAlert type="warning" :show-icon="true" style="margin-bottom: 18px;">
108
+ {{ t("restoreWarning") }}
109
+ </NAlert>
110
+ <NForm label-placement="top">
111
+ <NFormItem :label="t('backupName')">
112
+ <NText>{{ restoreTarget?.name }}</NText>
113
+ </NFormItem>
114
+ <NFormItem :label="t('restoreScope')">
115
+ <NSelect v-model:value="restoreForm.scope" :options="scopeOptions" />
116
+ </NFormItem>
117
+ <NFormItem v-if="isTableRestoreScope" :label="t('table')">
118
+ <NSelect v-model:value="restoreForm.table" :options="tableOptions" filterable :placeholder="t('table')" />
119
+ </NFormItem>
120
+ <NFormItem :label="t('restoreConfirmation')">
121
+ <NCheckbox v-model:checked="restoreForm.confirm">{{ t("restoreConfirmationHelp") }}</NCheckbox>
122
+ </NFormItem>
123
+ </NForm>
124
+
125
+ <template #footer>
126
+ <NFlex justify="end">
127
+ <NButton @click="showRestoreModal = false">{{ t("cancel") }}</NButton>
128
+ <NButton
129
+ type="error"
130
+ :loading="restoring"
131
+ :disabled="!restoreForm.confirm || (isTableRestoreScope && !restoreForm.table)"
132
+ @click="restoreBackup"
133
+ >
134
+ {{ t("restore") }}
135
+ </NButton>
136
+ </NFlex>
137
+ </template>
138
+ </NModal>
139
+ </div>
140
+ </div>
141
+ </template>
142
+
143
+ <script lang="ts" setup>
144
+ import Inison from "inison";
145
+ import type { DataTableColumns, SelectOption } from "naive-ui";
146
+ import {
147
+ Icon,
148
+ NButton,
149
+ NButtonGroup,
150
+ NIcon,
151
+ NPopconfirm,
152
+ NTag,
153
+ } from "#components";
154
+
155
+ const database = useState<Database>("database");
156
+ const table = useState<Table>("table");
157
+ const config = useRuntimeConfig();
158
+ const Language = useScopedCookie<LanguagesType>(
159
+ "language",
160
+ database.value?.slug,
161
+ );
162
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
163
+ const user = useState<User>("user");
164
+ const route = useRoute();
165
+
166
+ const isAdmin = computed(() => user.value?.role === config.public.idOne);
167
+
168
+ useHead({
169
+ title: `${t(database.value?.slug)} | ${t("backups")}`,
170
+ link: [
171
+ { rel: "icon", href: database.value?.icon?.publicURL ?? "/favicon.ico" },
172
+ ],
173
+ });
174
+
175
+ const loading = ref(false);
176
+ const creating = ref(false);
177
+ const restoring = ref(false);
178
+ const showCreateModal = ref(false);
179
+ const showRestoreModal = ref(false);
180
+ const items = ref<Backup[]>([]);
181
+ const restoreTarget = ref<Backup | null>(null);
182
+ const pagination = reactive({
183
+ page: 1,
184
+ pageSize: 15,
185
+ itemCount: 0,
186
+ });
187
+
188
+ function isTableBackup(backup: Backup) {
189
+ // Formatted backup rows carry the MIME type in `type`; the backup kind
190
+ // (full vs table) is exposed through the `table` column being non-null.
191
+ return !!backup.table;
192
+ }
193
+
194
+ const INTERNAL_TABLES = new Set([
195
+ "backups",
196
+ "sessions",
197
+ "translations",
198
+ "assets",
199
+ "dashboards",
200
+ "passkey_credentials",
201
+ "passkey_challenges",
202
+ ]);
203
+
204
+ const createForm = reactive({
205
+ name: "",
206
+ type: "full" as "full" | "table",
207
+ table: "",
208
+ });
209
+
210
+ const restoreForm = reactive({
211
+ scope: "table",
212
+ table: "",
213
+ confirm: false,
214
+ });
215
+
216
+ const apiBase = computed(
217
+ () => `${config.public.apiBase}${database.value.slug}/backups`,
218
+ );
219
+
220
+ const tableOptions = computed<SelectOption[]>(() =>
221
+ (database.value.tables ?? [])
222
+ .filter(({ slug }) => !INTERNAL_TABLES.has(slug))
223
+ .map(({ slug }) => ({ label: t(slug), value: slug })),
224
+ );
225
+
226
+ const scopeOptions = computed<SelectOption[]>(() => {
227
+ const isFull = !isTableBackup(restoreTarget.value ?? ({} as Backup));
228
+ const options: SelectOption[] = [
229
+ { label: t("backupScopeTable"), value: "table" },
230
+ { label: t("backupScopeTableSchema"), value: "table-schema" },
231
+ { label: t("backupScopeTableData"), value: "table-data" },
232
+ ];
233
+ if (isFull) {
234
+ options.push(
235
+ { label: t("backupScopeDatabaseSchema"), value: "database-schema" },
236
+ { label: t("backupScopeDatabaseData"), value: "database-data" },
237
+ { label: t("backupScopeDatabaseFull"), value: "database-full" },
238
+ );
239
+ }
240
+ return options;
241
+ });
242
+
243
+ const isTableRestoreScope = computed(
244
+ () =>
245
+ typeof restoreForm.scope === "string" &&
246
+ restoreForm.scope.startsWith("table"),
247
+ );
248
+
249
+ function getRequestParams() {
250
+ return {
251
+ locale: Language.value,
252
+ [`${database.value.slug}_sid`]: sessionID.value,
253
+ };
254
+ }
255
+
256
+ function formatDate(value?: number) {
257
+ if (!value) return "--";
258
+ return new Date(value).toLocaleString();
259
+ }
260
+
261
+ async function load(silent = false) {
262
+ if (!isAdmin.value) return;
263
+ if (!silent) loading.value = true;
264
+ try {
265
+ const response = await $fetch<apiResponse<Backup[]>>(`${apiBase.value}`, {
266
+ params: {
267
+ ...getRequestParams(),
268
+ options: Inison.stringify({
269
+ page: pagination.page,
270
+ perPage: pagination.pageSize,
271
+ }),
272
+ },
273
+ credentials: "include",
274
+ });
275
+ items.value = response.result ?? [];
276
+ pagination.itemCount = Number(
277
+ response.options?.total ?? items.value.length,
278
+ );
279
+ } catch (error: unknown) {
280
+ const errorMessage =
281
+ typeof error === "object" && error !== null
282
+ ? (error as { data?: { message?: string }; message?: string })
283
+ : undefined;
284
+ window.$message.error(
285
+ errorMessage?.data?.message ?? errorMessage?.message ?? t("error"),
286
+ );
287
+ items.value = [];
288
+ pagination.itemCount = 0;
289
+ } finally {
290
+ if (!silent) loading.value = false;
291
+ }
292
+ }
293
+
294
+ const tablePagination = computed(() => ({
295
+ page: pagination.page,
296
+ pageSize: pagination.pageSize,
297
+ itemCount: pagination.itemCount,
298
+ showSizePicker: true,
299
+ pageSizes: [15, 30, 60, 100],
300
+ onChange: async (page: number) => {
301
+ pagination.page = page;
302
+ await load();
303
+ },
304
+ onUpdatePageSize: async (pageSize: number) => {
305
+ pagination.pageSize = pageSize;
306
+ pagination.page = 1;
307
+ await load();
308
+ },
309
+ prefix: ({ itemCount }: { itemCount?: number }) => itemCount,
310
+ }));
311
+
312
+ function openCreateModal() {
313
+ createForm.name = "";
314
+ createForm.type = "full";
315
+ createForm.table = "";
316
+ showCreateModal.value = true;
317
+ }
318
+
319
+ function closeCreateModal() {
320
+ showCreateModal.value = false;
321
+ }
322
+
323
+ function openRestoreModal(backup: Backup) {
324
+ restoreTarget.value = backup;
325
+ restoreForm.scope = isTableBackup(backup) ? "table" : "database-full";
326
+ restoreForm.table = backup.table ?? "";
327
+ restoreForm.confirm = false;
328
+ showRestoreModal.value = true;
329
+ }
330
+
331
+ async function createBackup() {
332
+ creating.value = true;
333
+ try {
334
+ const response = await $fetch<apiResponse<Backup>>(`${apiBase.value}`, {
335
+ method: "POST",
336
+ body: {
337
+ name: createForm.name || undefined,
338
+ type: createForm.type,
339
+ table: createForm.type === "table" ? createForm.table : undefined,
340
+ },
341
+ params: getRequestParams(),
342
+ credentials: "include",
343
+ });
344
+ if (response.result) {
345
+ window.$message.success(t("backupCreatedSuccessfully"));
346
+ showCreateModal.value = false;
347
+ createForm.name = "";
348
+ await load(true);
349
+ } else window.$message.error(response.message);
350
+ } catch (error: unknown) {
351
+ const errorMessage =
352
+ typeof error === "object" && error !== null
353
+ ? (error as { data?: { message?: string }; message?: string })
354
+ : undefined;
355
+ window.$message.error(
356
+ errorMessage?.data?.message ?? errorMessage?.message ?? t("error"),
357
+ );
358
+ } finally {
359
+ creating.value = false;
360
+ }
361
+ }
362
+
363
+ async function removeBackup(backup: Backup) {
364
+ loading.value = true;
365
+ try {
366
+ const response = await $fetch<apiResponse<boolean>>(
367
+ `${apiBase.value}/${backup.id}`,
368
+ {
369
+ method: "DELETE",
370
+ params: getRequestParams(),
371
+ credentials: "include",
372
+ },
373
+ );
374
+ const code =
375
+ typeof response === "object" && response !== null && "code" in response
376
+ ? (response as { code?: unknown }).code
377
+ : undefined;
378
+ if (response?.result || code === 204) {
379
+ window.$message.success(t("backupDeletedSuccessfully"));
380
+ await load(true);
381
+ } else window.$message.error(response?.message ?? t("error"));
382
+ } catch (error: unknown) {
383
+ const errorMessage =
384
+ typeof error === "object" && error !== null
385
+ ? (error as { data?: { message?: string }; message?: string })
386
+ : undefined;
387
+ window.$message.error(
388
+ errorMessage?.data?.message ?? errorMessage?.message ?? t("error"),
389
+ );
390
+ } finally {
391
+ loading.value = false;
392
+ }
393
+ }
394
+
395
+ async function restoreBackup() {
396
+ if (!restoreTarget.value) return;
397
+ restoring.value = true;
398
+ try {
399
+ const response = await $fetch<apiResponse<Backup>>(
400
+ `${apiBase.value}/${restoreTarget.value.id}/restore`,
401
+ {
402
+ method: "POST",
403
+ body: {
404
+ scope: restoreForm.scope,
405
+ table: isTableRestoreScope.value ? restoreForm.table : undefined,
406
+ confirm: true,
407
+ },
408
+ params: getRequestParams(),
409
+ credentials: "include",
410
+ },
411
+ );
412
+ if (response.result) {
413
+ window.$message.success(t("restoreStarted"));
414
+ showRestoreModal.value = false;
415
+ await load(true);
416
+ } else window.$message.error(response.message);
417
+ } catch (error: unknown) {
418
+ const errorMessage =
419
+ typeof error === "object" && error !== null
420
+ ? (error as { data?: { message?: string }; message?: string })
421
+ : undefined;
422
+ window.$message.error(
423
+ errorMessage?.data?.message ?? errorMessage?.message ?? t("error"),
424
+ );
425
+ } finally {
426
+ restoring.value = false;
427
+ }
428
+ }
429
+
430
+ const columns = computed<DataTableColumns<Backup>>(() => [
431
+ {
432
+ title: t("backupName"),
433
+ key: "name",
434
+ minWidth: 240,
435
+ render: (backup) =>
436
+ h(
437
+ "span",
438
+ { style: { display: "inline-flex", alignItems: "center", gap: "8px" } },
439
+ [
440
+ h(NIcon, { size: 18 }, () =>
441
+ h(Icon, {
442
+ name: "tabler:archive",
443
+ }),
444
+ ),
445
+ backup.name,
446
+ ],
447
+ ),
448
+ },
449
+ {
450
+ title: t("backupType"),
451
+ key: "type",
452
+ width: 150,
453
+ render: (backup) =>
454
+ h(
455
+ NTag,
456
+ {
457
+ bordered: false,
458
+ round: true,
459
+ size: "small",
460
+ type: isTableBackup(backup) ? "info" : "success",
461
+ },
462
+ {
463
+ default: () =>
464
+ isTableBackup(backup) ? t("tableBackup") : t("fullBackup"),
465
+ },
466
+ ),
467
+ },
468
+ {
469
+ title: t("table"),
470
+ key: "table",
471
+ width: 140,
472
+ render: (backup) => (backup.table ? t(backup.table) : "--"),
473
+ },
474
+ {
475
+ title: t("backupOrigin"),
476
+ key: "automatic",
477
+ width: 120,
478
+ render: (backup) =>
479
+ h(
480
+ NTag,
481
+ {
482
+ bordered: false,
483
+ round: true,
484
+ size: "small",
485
+ type: backup.automatic ? "warning" : "default",
486
+ },
487
+ {
488
+ default: () =>
489
+ backup.automatic ? t("automaticBackup") : t("manualBackup"),
490
+ },
491
+ ),
492
+ },
493
+ {
494
+ title: t("backupSize"),
495
+ key: "size",
496
+ width: 110,
497
+ render: (backup) => humanFileSize(backup.size),
498
+ },
499
+ {
500
+ title: t("backupState"),
501
+ key: "state",
502
+ width: 170,
503
+ render: (backup) => {
504
+ const tags = [];
505
+ const state = backup.state ?? "queued";
506
+ tags.push(
507
+ h(
508
+ NTag,
509
+ {
510
+ bordered: false,
511
+ round: true,
512
+ size: "small",
513
+ type:
514
+ state === "completed"
515
+ ? "success"
516
+ : state === "failed"
517
+ ? "error"
518
+ : state === "running"
519
+ ? "info"
520
+ : "default",
521
+ },
522
+ { default: () => t(state) },
523
+ ),
524
+ );
525
+ if (backup.restoreState === "running")
526
+ tags.push(
527
+ h(
528
+ NTag,
529
+ {
530
+ bordered: false,
531
+ round: true,
532
+ size: "small",
533
+ type: "warning",
534
+ },
535
+ { default: () => t("restoring") },
536
+ ),
537
+ );
538
+ return h(
539
+ "span",
540
+ { style: { display: "inline-flex", gap: "6px", flexWrap: "wrap" } },
541
+ tags,
542
+ );
543
+ },
544
+ },
545
+ {
546
+ title: t("createdAt"),
547
+ key: "createdAt",
548
+ width: 170,
549
+ render: (backup) => formatDate(backup.createdAt),
550
+ },
551
+ {
552
+ title: t("actions"),
553
+ key: "actions",
554
+ width: 170,
555
+ fixed: "right",
556
+ render: (backup) => {
557
+ const canRestore =
558
+ backup.state === "completed" &&
559
+ !!backup.publicURL &&
560
+ backup.restoreState !== "running";
561
+ return h(NButtonGroup, { size: "small" }, [
562
+ canRestore
563
+ ? h(
564
+ NButton,
565
+ {
566
+ type: "primary",
567
+ secondary: true,
568
+ onClick: () => openRestoreModal(backup),
569
+ },
570
+ {
571
+ default: () => t("restore"),
572
+ icon: () =>
573
+ h(NIcon, () => h(Icon, { name: "tabler:rotate-clockwise" })),
574
+ },
575
+ )
576
+ : null,
577
+ h(
578
+ NPopconfirm,
579
+ {
580
+ onPositiveClick: () => removeBackup(backup),
581
+ },
582
+ {
583
+ trigger: () =>
584
+ h(
585
+ NButton,
586
+ { type: "error", secondary: true },
587
+ {
588
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:trash" })),
589
+ },
590
+ ),
591
+ default: () => t("deleteBackupConfirm", { name: backup.name }),
592
+ },
593
+ ),
594
+ ]);
595
+ },
596
+ },
597
+ ]);
598
+
599
+ const hasActiveBackupJob = computed(() =>
600
+ items.value.some(
601
+ (backup) =>
602
+ backup.state === "queued" ||
603
+ backup.state === "running" ||
604
+ backup.restoreState === "running",
605
+ ),
606
+ );
607
+
608
+ let pollTimer: ReturnType<typeof setInterval> | null = null;
609
+
610
+ watch(
611
+ hasActiveBackupJob,
612
+ (active) => {
613
+ if (active) {
614
+ if (!pollTimer) pollTimer = setInterval(() => load(true), 4000);
615
+ } else if (pollTimer) {
616
+ clearInterval(pollTimer);
617
+ pollTimer = null;
618
+ }
619
+ },
620
+ { immediate: true },
621
+ );
622
+
623
+ watch(
624
+ () => route.fullPath,
625
+ () => {
626
+ pagination.page = 1;
627
+ void load();
628
+ },
629
+ );
630
+
631
+ onMounted(() => {
632
+ void load();
633
+ });
634
+
635
+ onBeforeUnmount(() => {
636
+ if (pollTimer) clearInterval(pollTimer);
637
+ });
638
+ </script>