<script setup>
// Список источников. Детали (описание, ключи, типы) — на странице источника,
// чтобы таблица оставалась обозримой.
import { onMounted, ref } from "vue";
import {
GnButton,
GnConfirmDialog,
GnInput,
GnLoader,
GnModal,
GnPageHeader,
GnStatusCard,
GnTable,
GnTextarea,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { api } from "../api.js";
import { fmtTime, th } from "../ui.js";
const toast = useToast();
const sources = ref([]);
const loading = ref(true);
const error = ref("");
async function load() {
loading.value = true;
error.value = "";
try {
sources.value = await api.listSources();
} catch (ex) {
error.value = ex.message;
} finally {
loading.value = false;
}
}
// --- создание источника ---
const showCreate = ref(false);
const form = ref({ name: "", label: "", description: "" });
const busy = ref(false);
async function createSource() {
if (!form.value.name) return;
busy.value = true;
try {
await api.createSource({ ...form.value, name: form.value.name.trim() });
showCreate.value = false;
form.value = { name: "", label: "", description: "" };
toast.success("Источник создан");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
busy.value = false;
}
}
// --- удаление источника ---
const confirmDelete = ref(null); // источник
async function doDelete() {
try {
await api.deleteSource(confirmDelete.value.id);
toast.success("Источник удалён");
await load();
} catch (ex) {
// 409: у источника есть события — удалить нельзя
toast.error(ex.message);
} finally {
confirmDelete.value = null;
}
}
onMounted(load);
</script>
<template>
<main class="container page">
<GnPageHeader
kicker="Synapse"
title="Источники"
subtitle="Сервисы, которые шлют события в хаб. Описание, ключи и типы — на странице источника"
>
<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="sources"
empty-text="Источников ещё нет — создайте первый"
:columns="[
{ key: 'name', label: th('Slug', 'Имя источника в событии (поле source конверта). Клик — страница источника') },
{ key: 'label', label: 'Название' },
{ key: 'created_at', label: 'Создан' },
{ key: 'actions', label: '' },
]"
>
<template #cell-name="{ row }">
<RouterLink :to="`/sources/${row.id}`" class="row-link">{{ row.name }}</RouterLink>
</template>
<template #cell-label="{ row }">
<RouterLink :to="`/sources/${row.id}`" class="row-link">{{ row.label || "—" }}</RouterLink>
</template>
<template #cell-created_at="{ value }">{{ fmtTime(value) }}</template>
<template #cell-actions="{ row }">
<GnIconButton icon="ph-trash" size="sm" label="Удалить источник" @click="confirmDelete = row" />
</template>
</GnTable>
</template>
<!-- Создание источника -->
<GnModal v-model:open="showCreate" title="Новый источник">
<div class="form">
<GnInput v-model="form.name" label="Slug (name)" icon="ph-hash" help="Например monitoring; только a-z 0-9 . _ -" />
<GnInput v-model="form.label" label="Название" icon="ph-tag" help="Человекочитаемое имя" />
<GnTextarea
v-model="form.description"
label="Описание"
help="Что это за сервис — это увидят человек и ИИ-агент в админке"
/>
</div>
<template #actions>
<GnButton variant="secondary" @click="showCreate = false">Отмена</GnButton>
<GnButton variant="primary" :loading="busy" :disabled="!form.name.trim()" @click="createSource">
Создать
</GnButton>
</template>
</GnModal>
<!-- Удаление источника -->
<GnConfirmDialog
:open="confirmDelete !== null" @update:open="(v) => (v ? null : (confirmDelete = null))"
:key="confirmDelete?.id"
title="Удалить источник?"
:message="confirmDelete ? `Источник «${confirmDelete.name}» будет удалён вместе с типами и ключами. Если у источника есть события, удаление будет отклонено.` : ''"
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);
}
.row-link {
color: inherit;
text-decoration: none;
}
.row-link:hover {
text-decoration: underline;
}
.form {
display: flex;
flex-direction: column;
gap: var(--gn-space-4);
}
</style>