Newer
Older
gn-synapse / frontend / src / views / RulesView.vue
<script setup>
// Список правил маршрутизации. Условия и правка — на странице правила,
// здесь обзорные колонки и быстрый переключатель вкл/выкл.
import { computed, 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";
import { t } from "../i18n/index.js";

const toast = useToast();

const rows = ref([]);
const sources = ref([]);
const targets = ref([]);
const loading = ref(true);
const error = ref("");

const WEIGHT_TIP = computed(() => t("rules.weightTip"));
const THROTTLE_TIP = computed(() => t("rules.throttleTip"));

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(t("rules.toastCreated"));
    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 ? t("rules.toastEnabled") : t("rules.toastDisabled"));
  } catch (ex) {
    toast.error(ex.message);
    await load();
  }
}

const confirmDelete = ref(null);
async function doDelete() {
  try {
    await api.deleteRule(confirmDelete.value.id);
    toast.success(t("rules.toastDeleted"));
    await load();
  } catch (ex) {
    toast.error(ex.message);
  } finally {
    confirmDelete.value = null;
  }
}

onMounted(load);
</script>

<template>
  <main class="container page">
    <GnPageHeader
      kicker="Synapse"
      :title="t('rules.title')"
      :subtitle="t('rules.subtitle')"
    >
      <template #actions>
        <GnButton variant="primary" icon="ph-plus" @click="showCreate = true">{{ t("rules.createButton") }}</GnButton>
      </template>
    </GnPageHeader>

    <GnLoader v-if="loading" circle :label="t('rules.loading')" />
    <GnStatusCard v-else-if="error" variant="error" icon="ph-warning-circle" :text="error" />
    <template v-else>
      <GnTable
        :rows="rows"
        :empty-text="t('rules.emptyText')"
        :columns="[
          { key: 'name', label: t('rules.colName') },
          { key: 'weight', label: th(t('rules.colWeight'), WEIGHT_TIP) },
          { key: 'throttle_seconds', label: th(t('rules.colThrottle'), THROTTLE_TIP) },
          { key: 'actionsList', label: th(t('rules.colActions'), t('rules.actionsTip')) },
          { key: 'enabled', label: t('rules.colEnabled') },
          { 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 }} {{ t("rules.secondsShort") }}</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="t('rules.deleteRule')" @click="confirmDelete = row" />
        </template>
      </GnTable>
    </template>

    <!-- Создание правила -->
    <GnModal v-model:open="showCreate" :title="t('rules.newRuleTitle')">
      <RuleForm ref="createForm" :sources="sources" :targets="targets" />
      <template #actions>
        <GnButton variant="secondary" @click="showCreate = false">{{ t("rules.cancel") }}</GnButton>
        <GnButton variant="primary" :loading="busy" @click="createRule">{{ t("rules.create") }}</GnButton>
      </template>
    </GnModal>

    <!-- Удаление правила -->
    <GnConfirmDialog
      :open="confirmDelete !== null" @update:open="(v) => (v ? null : (confirmDelete = null))"
      :key="confirmDelete?.id"
      :title="t('rules.deleteTitle')"
      :message="confirmDelete ? truncate(t('rules.deleteMessage', { name: confirmDelete.name }), 200) : ''"
      confirm-variant="danger"
      :confirm-text="t('rules.delete')"
      :cancel-text="t('rules.cancel')"
      @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>