<script setup>
// Список правил маршрутизации. Условия и правка — на странице правила,
// здесь обзорные колонки и быстрый переключатель вкл/выкл.
import { onMounted, ref } from "vue";
import {
GnBadge,
GnButton,
GnChip,
GnConfirmDialog,
GnIconButton,
GnLoader,
GnModal,
GnPageHeader,
GnStatusCard,
GnSwitch,
GnTable,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { api } from "../api.js";
import { th, truncate } from "../ui.js";
import RuleForm from "../components/RuleForm.vue";
const toast = useToast();
const rows = ref([]);
const sources = ref([]);
const targets = ref([]);
const loading = ref(true);
const error = ref("");
const WEIGHT_TIP =
"Вес определяет порядок применения правил в режиме «первое подошедшее» (ROUTING_MATCH_MODE=first): меньше важнее, срабатывает раньше. В режиме «все» на результат не влияет.";
const THROTTLE_TIP =
"Шумодав: не чаще одной живой доставки в эту цель за N секунд. Подавленное отмечается статусом skipped, critical проходит всегда.";
async function load() {
loading.value = true;
error.value = "";
try {
const [rules, src, tg] = await Promise.all([api.listRules(), api.listSources(), api.listTargets()]);
rows.value = rules;
sources.value = src;
targets.value = tg;
} catch (ex) {
error.value = ex.message;
} finally {
loading.value = false;
}
}
// --- создание: модаль с общей формой RuleForm ---
const showCreate = ref(false);
const busy = ref(false);
const createForm = ref(null);
async function createRule() {
const payload = createForm.value?.collect();
if (!payload) return;
busy.value = true;
try {
await api.createRule(payload);
showCreate.value = false;
toast.success("Правило создано");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
busy.value = false;
}
}
async function toggleRule(rule, enabled) {
try {
await api.patchRule(rule.id, { enabled });
toast.info(`Правило ${enabled ? "включено" : "выключено"}`);
} catch (ex) {
toast.error(ex.message);
await load();
}
}
const confirmDelete = ref(null);
async function doDelete() {
try {
await api.deleteRule(confirmDelete.value.id);
toast.success("Правило удалено");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
confirmDelete.value = null;
}
}
onMounted(load);
</script>
<template>
<main class="container page">
<GnPageHeader
kicker="Synapse"
title="Правила"
subtitle="Каждое событие прогоняется через правила: совпали условия — создаётся доставка в каналы действия. Шумодав режет повторы, critical проходит всегда"
>
<template #actions>
<GnButton variant="primary" icon="ph-plus" @click="showCreate = true">Правило</GnButton>
</template>
</GnPageHeader>
<GnLoader v-if="loading" />
<GnStatusCard v-else-if="error" variant="error" icon="ph-warning-circle" :text="error" />
<template v-else>
<GnTable
:rows="rows"
empty-text="Правил нет — событие, не попавшее ни в одно правило, только логируется историей"
:columns="[
{ key: 'name', label: 'Правило' },
{ key: 'weight', label: th('Вес', WEIGHT_TIP) },
{ key: 'throttle_seconds', label: th('Шумодав', THROTTLE_TIP) },
{ key: 'actionsList', label: th('Действия', 'Куда доставить: канал и цель. internal_log без цели пишет журнал Synapse. Условия — на странице правила') },
{ key: 'enabled', label: 'Вкл./выкл.' },
{ key: 'actions', label: '' },
]"
>
<template #cell-name="{ row }">
<RouterLink :to="`/rules/${row.id}`" class="rule-link">{{ row.name }}</RouterLink>
</template>
<template #cell-throttle_seconds="{ row }">
<GnBadge v-if="row.throttle_seconds" :variant="'warning'" outline>{{ row.throttle_seconds }} с</GnBadge>
<span v-else>—</span>
</template>
<template #cell-actionsList="{ row }">
<div class="chips">
<GnChip v-for="(a, i) in row.actions" :key="i" variant="accent">
{{ a.channel }}{{ a.target_name ? ":" + a.target_name : "" }}
</GnChip>
</div>
</template>
<template #cell-enabled="{ row }">
<GnSwitch :model-value="row.enabled" @update:model-value="(v) => toggleRule(row, v)" />
</template>
<template #cell-actions="{ row }">
<GnIconButton icon="ph-trash" size="sm" label="Удалить правило" @click="confirmDelete = row" />
</template>
</GnTable>
</template>
<!-- Создание правила -->
<GnModal v-model:open="showCreate" title="Новое правило">
<RuleForm ref="createForm" :sources="sources" :targets="targets" />
<template #actions>
<GnButton variant="secondary" @click="showCreate = false">Отмена</GnButton>
<GnButton variant="primary" :loading="busy" @click="createRule">Создать</GnButton>
</template>
</GnModal>
<!-- Удаление правила -->
<GnConfirmDialog
:open="confirmDelete !== null" @update:open="(v) => (v ? null : (confirmDelete = null))"
:key="confirmDelete?.id"
title="Удалить правило?"
:message="confirmDelete ? truncate(`Правило «${confirmDelete.name}» будет удалено вместе с действиями. История доставок останется.`, 200) : ''"
confirm-variant="danger"
confirm-text="Удалить"
cancel-text="Отмена"
@confirm="doDelete"
/>
</main>
</template>
<style scoped>
.page {
max-width: 1440px;
margin-inline: auto;
padding-block: var(--gn-space-6);
display: flex;
flex-direction: column;
gap: var(--gn-space-4);
}
.rule-link {
color: inherit;
text-decoration: none;
}
.rule-link:hover {
text-decoration: underline;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--gn-space-1);
}
</style>