From 132559cb8e8d5eb48818eb33f8ebf4339d3926e4 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 12 Jun 2026 14:07:24 +0700 Subject: [PATCH] feat(web): enhance MaintenancePoliciesTab with preset management Added functionality for selecting and applying maintenance policy presets in the MaintenancePoliciesTab. Users can now create policies from selected presets, apply presets to the form, and receive notifications on the creation process. Updated UI components to support these features, improving user experience and efficiency in managing maintenance policies. --- .../monitoring/MaintenancePoliciesTab.svelte | 146 +++++++++++++++++- web/src/lib/maintenance/policy-presets.ts | 130 ++++++++++++++++ 2 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 web/src/lib/maintenance/policy-presets.ts diff --git a/web/src/lib/components/monitoring/MaintenancePoliciesTab.svelte b/web/src/lib/components/monitoring/MaintenancePoliciesTab.svelte index ca5518e..22ad4cb 100644 --- a/web/src/lib/components/monitoring/MaintenancePoliciesTab.svelte +++ b/web/src/lib/components/monitoring/MaintenancePoliciesTab.svelte @@ -18,6 +18,13 @@ vacuumStrategies, type MaintenancePolicyForm } from '$lib/maintenance/policy.schema.js'; + import { + filterAvailablePresets, + isPresetAlreadyApplied, + maintenancePolicyPresets, + presetForm, + type MaintenancePolicyPreset + } from '$lib/maintenance/policy-presets.js'; import { Button } from '$lib/ui/core/button/index.js'; import { Card, @@ -37,6 +44,7 @@ } from '$lib/ui/core/dialog/index.js'; import { Switch } from '$lib/ui/core/switch/index.js'; import { Label } from '$lib/ui/core/label/index.js'; + import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import FormField from '$lib/ui/patterns/form/form-field.svelte'; import AppInput from '$lib/ui/patterns/form/app-input.svelte'; import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; @@ -49,6 +57,7 @@ import Play from '@lucide/svelte/icons/play'; import FlaskConical from '@lucide/svelte/icons/flask-conical'; import Info from '@lucide/svelte/icons/info'; + import Layers from '@lucide/svelte/icons/layers'; type Props = { session: AuthSession | null; @@ -66,6 +75,50 @@ let saving = $state(false); let hints = $state(null); let hintsLoading = $state(false); + let selectedPresetIds = $state([]); + let applyingPresets = $state(false); + let activePresetId = $state(null); + + const creatablePresets = $derived(filterAvailablePresets(policies, selectedPresetIds)); + + function togglePresetSelection(id: string, checked: boolean) { + if (checked) { + if (!selectedPresetIds.includes(id)) { + selectedPresetIds = [...selectedPresetIds, id]; + } + } else { + selectedPresetIds = selectedPresetIds.filter((x) => x !== id); + } + } + + function applyPresetToForm(preset: MaintenancePolicyPreset) { + form = presetForm(preset); + activePresetId = preset.id; + } + + async function createSelectedPresets() { + const toCreate = creatablePresets; + if (toCreate.length === 0) { + notify.error('Выберите пресеты, которые ещё не созданы'); + return; + } + applyingPresets = true; + let created = 0; + try { + for (const preset of toCreate) { + await createMaintenancePolicy(formToPayload(preset.form)); + created++; + } + selectedPresetIds = selectedPresetIds.filter((id) => !toCreate.some((p) => p.id === id)); + notify.success(`Создано политик: ${created}`); + await loadPolicies(); + } catch (e) { + notifyApiError(e, created > 0 ? `Создано ${created} из ${toCreate.length}` : undefined); + if (created > 0) await loadPolicies(); + } finally { + applyingPresets = false; + } + } const isOperator = $derived(session?.role === 'operator'); @@ -98,6 +151,7 @@ editTarget = null; form = emptyMaintenancePolicyForm(); hints = null; + activePresetId = null; dialogOpen = true; } @@ -220,7 +274,74 @@ {/if} - + + {#if isOperator} +
+
+
+

+ + Пресеты стратегий +

+

+ Выберите шаблоны и создайте политики одним действием или примените шаблон в форме. +

+
+ +
+
+ {#each maintenancePolicyPresets as preset (preset.id)} + {@const applied = isPresetAlreadyApplied(preset, policies)} + {@const checked = selectedPresetIds.includes(preset.id)} + + {/each} +
+
+ {/if} + Загрузка подсказок pg_stat…

{/if} + {#if !editTarget} +
+

Шаблон (опционально)

+
+ {#each maintenancePolicyPresets as preset (preset.id)} + + {/each} +
+

+ Поля формы заполняются из шаблона; перед сохранением можно изменить любое значение. +

+
+ {/if} +
diff --git a/web/src/lib/maintenance/policy-presets.ts b/web/src/lib/maintenance/policy-presets.ts new file mode 100644 index 0000000..1c4de6d --- /dev/null +++ b/web/src/lib/maintenance/policy-presets.ts @@ -0,0 +1,130 @@ +import type { MaintenancePolicy } from '$lib/maintenance/policy-api.js'; +import type { MaintenancePolicyForm } from '$lib/maintenance/policy.schema.js'; + +const DAY_SEC = 86_400; + +/** Рекомендуемый шаблон политики (только UI; в БД не seed'ится). */ +export type MaintenancePolicyPreset = { + id: string; + label: string; + description: string; + /** Подсказка: таблица должна быть видна в pg_stat (не блокирует создание). */ + tableHint?: string; + form: MaintenancePolicyForm; +}; + +/** Базовые пресеты EvoBGP — оператор выбирает, какие создать. */ +export const maintenancePolicyPresets: MaintenancePolicyPreset[] = [ + { + id: 'job_audit_retention', + label: 'Job audit — retention 90d', + description: + 'Удаляет завершённые записи job_audit старше 90 дней; VACUUM ANALYZE после очистки. Расписание 03:00 UTC.', + tableHint: 'job_audit', + form: { + name: 'Job audit retention (90d)', + table_name: 'job_audit', + condition: "status IN ('succeeded', 'failed', 'cancelled')", + retention_period_sec: String(90 * DAY_SEC), + max_rows: '10000', + vacuum_strategy: 'vacuum_analyze', + schedule: '0 3 * * *', + enabled: true, + dry_run_enabled: true + } + }, + { + id: 'postgres_maintenance_audit_retention', + label: 'Maintenance audit — 30d', + description: 'Очищает postgres_maintenance_audit старше 30 дней без vacuum.', + tableHint: 'postgres_maintenance_audit', + form: { + name: 'Postgres maintenance audit (30d)', + table_name: 'postgres_maintenance_audit', + condition: 'true', + retention_period_sec: String(30 * DAY_SEC), + max_rows: '5000', + vacuum_strategy: 'none', + schedule: '0 4 * * *', + enabled: true, + dry_run_enabled: true + } + }, + { + id: 'job_audit_vacuum_weekly', + label: 'Job audit — VACUUM weekly', + description: 'Только VACUUM ANALYZE job_audit по воскресеньям, без удаления строк.', + tableHint: 'job_audit', + form: { + name: 'Job audit vacuum (weekly)', + table_name: 'job_audit', + condition: 'true', + retention_period_sec: '', + max_rows: '', + vacuum_strategy: 'vacuum_analyze', + schedule: '0 2 * * 0', + enabled: true, + dry_run_enabled: false + } + }, + { + id: 'postgres_monitor_snapshot', + label: 'PG monitor snapshots — 14d', + description: + 'Удаляет снимки postgres_monitor_snapshot старше 14 дней (фильтр по collected_at в condition).', + tableHint: 'postgres_monitor_snapshot', + form: { + name: 'Postgres monitor snapshots (14d)', + table_name: 'postgres_monitor_snapshot', + condition: "collected_at < NOW() - INTERVAL '14 days'", + retention_period_sec: '', + max_rows: '10000', + vacuum_strategy: 'none', + schedule: '0 5 * * *', + enabled: true, + dry_run_enabled: true + } + }, + { + id: 'config_revision_retention', + label: 'Config revisions — 180d', + description: + 'Долгое хранение старых config_revision (180d). Перед включением проверьте revision_retention в настройках.', + tableHint: 'config_revision', + form: { + name: 'Config revision retention (180d)', + table_name: 'config_revision', + condition: 'true', + retention_period_sec: String(180 * DAY_SEC), + max_rows: '5000', + vacuum_strategy: 'vacuum', + schedule: '0 6 * * 0', + enabled: false, + dry_run_enabled: true + } + } +]; + +export function presetForm(preset: MaintenancePolicyPreset): MaintenancePolicyForm { + return structuredClone(preset.form); +} + +/** Политика с тем же именем и таблицей считается уже созданной из пресета. */ +export function isPresetAlreadyApplied( + preset: MaintenancePolicyPreset, + policies: MaintenancePolicy[] +): boolean { + return policies.some( + (p) => p.name === preset.form.name.trim() && p.table_name === preset.form.table_name.trim() + ); +} + +export function filterAvailablePresets( + policies: MaintenancePolicy[], + selected: Iterable +): MaintenancePolicyPreset[] { + const ids = new Set(selected); + return maintenancePolicyPresets.filter( + (p) => ids.has(p.id) && !isPresetAlreadyApplied(p, policies) + ); +}