<script setup>
// Реестр типов: (source, subject, action) — что источники могут присылать.
import { onMounted, ref } from "vue";
import {
GnButton,
GnConfirmDialog,
GnInput,
GnLoader,
GnModal,
GnPageHeader,
GnSelect,
GnStatusCard,
GnTable,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { api } from "../api.js";
import { fmtTime } from "../ui.js";
const toast = useToast();
const rows = ref([]);
const sources = ref([]);
const loading = ref(true);
const error = ref("");
async function load() {
loading.value = true;
error.value = "";
try {
const [types, src] = await Promise.all([api.listTypes(), api.listSources()]);
rows.value = types;
sources.value = src;
} catch (ex) {
error.value = ex.message;
} finally {
loading.value = false;
}
}
const showCreate = ref(false);
const form = ref({ source_id: "", subject: "", action: "" });
const busy = ref(false);
const sourceOptions = [{ value: "", label: "— выберите источник —" }];
async function createType() {
if (!form.value.source_id || !form.value.subject || !form.value.action) return;
busy.value = true;
try {
await api.createType({
source_id: Number(form.value.source_id),
subject: form.value.subject.trim(),
action: form.value.action.trim(),
});
showCreate.value = false;
form.value = { source_id: "", subject: "", action: "" };
toast.success("Тип создан");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
busy.value = false;
}
}
const confirmDelete = ref(null);
async function doDelete() {
try {
await api.deleteType(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="Реестр (source, subject, action). Событие с незарегистрированной тройкой отклоняется с 422"
>
<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: 'source_name', label: 'Source' },
{ key: 'subject', label: 'Subject' },
{ key: 'action', label: 'Action' },
{ key: 'created_at', label: 'Создан' },
{ key: 'actions', label: '' },
]"
>
<template #cell-created_at="{ value }">{{ fmtTime(value) }}</template>
<template #cell-actions="{ row }">
<GnButton variant="danger" size="sm" icon="ph-trash" @click="confirmDelete = row" />
</template>
</GnTable>
</template>
<GnModal v-model:open="showCreate" title="Новый тип">
<div class="form">
<GnSelect
v-model="form.source_id"
label="Источник"
:options="sourceOptions.concat(sources.map((s) => ({ value: s.id, label: s.name })))"
/>
<GnInput v-model="form.subject" label="Subject" icon="ph-shapes" help="Сущность: container, issue, build…" />
<GnInput v-model="form.action" label="Action" icon="ph-lightning" help="Что случилось: down, created, resolved…" />
</div>
<template #actions>
<GnButton variant="secondary" @click="showCreate = false">Отмена</GnButton>
<GnButton
variant="primary"
:loading="busy"
:disabled="!form.source_id || !form.subject.trim() || !form.action.trim()"
@click="createType"
>
Создать
</GnButton>
</template>
</GnModal>
<GnConfirmDialog
:open="confirmDelete !== null" @update:open="(v) => (v ? null : (confirmDelete = null))"
:key="confirmDelete?.id"
title="Удалить тип?"
:message="confirmDelete ? `Тип (${confirmDelete.source_name}, ${confirmDelete.subject}, ${confirmDelete.action}) будет удалён — события такой тройки отклонятся с 422.` : ''"
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);
}
.form {
display: flex;
flex-direction: column;
gap: var(--gn-space-4);
}
</style>