diff --git a/web/src/routes/modules/+page.svelte b/web/src/routes/modules/+page.svelte index 313a8a8..5f26654 100644 --- a/web/src/routes/modules/+page.svelte +++ b/web/src/routes/modules/+page.svelte @@ -22,25 +22,11 @@ SelectTrigger, } from '$lib/components/ui/select/index.js'; import { Switch } from '$lib/components/ui/switch/index.js'; - import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle - } from '$lib/components/ui/alert-dialog/index.js'; - import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow - } from '$lib/components/ui/table/index.js'; - import { toast } from 'svelte-sonner'; + import { Checkbox } from '$lib/ui/core/checkbox/index.js'; + import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; + import PageHeader from '$lib/ui/app/page-header/page-header.svelte'; + import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; + import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { resolve } from '$app/paths'; import Plus from '@lucide/svelte/icons/plus'; import RefreshCw from '@lucide/svelte/icons/refresh-cw'; @@ -55,7 +41,6 @@ let saving = $state(false); let selectedModuleIds = $state(new Set()); let deletingBulkModules = $state(false); - let bulkDeleteModulesDialog = $state(false); const selectedModulesCount = $derived(selectedModuleIds.size); const allModulesSelected = $derived(rows.length > 0 && selectedModuleIds.size === rows.length); @@ -75,6 +60,17 @@ { value: 'IP_RANGES', label: 'IP Ranges' } ] as const; + const moduleColumns = [ + { id: 'select', label: '', class: 'w-10' }, + { id: 'name', label: 'Название', sortable: true, sortValue: (m: ModuleRow) => m.name }, + { id: 'type', label: 'Тип', sortable: true, sortValue: (m: ModuleRow) => m.type }, + { id: 'priority', label: 'Приоритет', sortable: true, sortValue: (m: ModuleRow) => m.priority ?? 0 }, + { id: 'interval', label: 'Интервал' }, + { id: 'refreshed', label: 'Последнее обновление', sortable: true, sortValue: (m: ModuleRow) => m.last_refreshed_at ?? '' }, + { id: 'status', label: 'Статус' }, + { id: 'actions', label: '', class: 'w-16' } + ] as const; + async function load() { loading = true; try { @@ -83,7 +79,7 @@ const validIds = new Set(rows.map((item) => item.id)); selectedModuleIds = new Set([...selectedModuleIds].filter((id) => validIds.has(id))); } catch (e) { - toast.error(e instanceof Error ? e.message : String(e)); + notifyApiError(e); } finally { loading = false; } @@ -93,18 +89,18 @@ async function create() { if (!form.name.trim()) { - toast.error('Укажите название модуля'); + notify.error('Укажите название модуля'); return; } saving = true; try { await apiMutate('/v1/modules', 'POST', form); - toast.success('Модуль создан'); + notify.success('Модуль создан'); dialogOpen = false; form = { type: 'AS_PREFIXES', name: '', enabled: true, priority: 0 }; await load(); } catch (e) { - toast.error(e instanceof Error ? e.message : String(e)); + notifyApiError(e); } finally { saving = false; } @@ -154,6 +150,17 @@ selectedModuleIds = checked ? new Set(rows.map((item) => item.id)) : new Set(); } + function requestBulkDelete() { + if (selectedModuleIds.size === 0) return; + void confirm({ + title: 'Удалить выбранные модули?', + description: `Будет удалено: ${selectedModulesCount}. Это действие необратимо.`, + confirmLabel: 'Удалить', + destructive: true, + onConfirm: bulkDeleteModules + }); + } + async function bulkDeleteModules() { if (selectedModuleIds.size === 0) return; deletingBulkModules = true; @@ -164,13 +171,12 @@ await apiMutate(`/v1/modules/${id}`, 'DELETE', undefined, { idempotent: false }); deleted += 1; } catch (e) { - toast.error(e instanceof Error ? e.message : String(e)); + notifyApiError(e); } } if (deleted > 0) { - toast.success(`Удалено модулей: ${deleted}`); + notify.success(`Удалено модулей: ${deleted}`); } - bulkDeleteModulesDialog = false; await load(); } finally { deletingBulkModules = false; @@ -178,21 +184,14 @@ } -
-
-
- -
-

Модули префиксов

-

Управление модулями — AS, CDN, домены, IP-диапазоны.

-
-
-
+
+ + {#snippet actions()} -
-
+ {/snippet} + @@ -214,7 +213,7 @@ variant="destructive" size="sm" disabled={deletingBulkModules} - onclick={() => (bulkDeleteModulesDialog = true)} + onclick={requestBulkDelete} > Удалить выбранные @@ -222,72 +221,45 @@
{/if} - - - - - - toggleAllModules((event.currentTarget as HTMLInputElement).checked)} - /> - - Название - Тип - Приоритет - Интервал - Последнее обновление - Статус - - - - - {#each rows as m (m.id)} - - - toggleModuleSelection(m.id)} - /> - - {m.name} - - {moduleTypeRu(m.type)} - - {m.priority} - - {moduleIntervalLabel(m)} - - - {formatDateTime(m.last_refreshed_at)} - - - {#if m.enabled} - вкл - {:else} - выкл - {/if} - - - - - - {:else} - - - {loading ? 'Загрузка…' : 'Нет модулей. Создайте первый.'} - - - {/each} - -
+ + m.id} + {loading} + emptyTitle="Нет модулей" + emptyDescription="Создайте первый модуль." + > + {#snippet cell({ row: m, column })} + {#if column.id === 'select'} + toggleModuleSelection(m.id)} + /> + {:else if column.id === 'name'} + {m.name} + {:else if column.id === 'type'} + {moduleTypeRu(m.type)} + {:else if column.id === 'priority'} + {m.priority} + {:else if column.id === 'interval'} + {moduleIntervalLabel(m)} + {:else if column.id === 'refreshed'} + {formatDateTime(m.last_refreshed_at)} + {:else if column.id === 'status'} + {#if m.enabled} + вкл + {:else} + выкл + {/if} + {:else if column.id === 'actions'} + + {/if} + {/snippet} +
@@ -353,19 +325,3 @@ - - - - Удалить выбранные модули? - - Будет удалено: {selectedModulesCount}. Это действие необратимо. - - - - Отмена - - {deletingBulkModules ? 'Удаление…' : 'Удалить'} - - - -