/** Общие мелочи для разметки (админка и личный раздел): бейджи статусов, приоритетов,
* каналов; формат времени, сокращение, подсветка JSON. */
import { h } from "vue";
import { GnTooltip } from "gnexus-ui-kit/vue";
/** Разрешённые варианты кита: primary, secondary, accent, success, warning, danger, error, info. */
export const statusVariant = {
// delivery
delivered: "success",
pending: "info",
failed: "danger",
skipped: "warning",
// event
queued: "info",
processing: "info",
done: "success",
expired: "warning", // истёкшший ttl_seconds — не маршрутизировано, ретеншн удалит
};
export function badgeFor(status) {
return { label: status, variant: statusVariant[status] || "secondary" };
}
/** Вариант бейджа для канала: служебные internal_log и user — нейтральнее каналов доставки. */
export function badgeChannel(channel) {
return channel === "internal_log"
? "secondary"
: channel === "user"
? "info"
: "accent";
}
/** Вариант бейджа приоритета события (конверт: low/normal/high/critical). */
export const priorityVariantMap = { low: "secondary", normal: "info", high: "warning", critical: "danger" };
export function priorityVariant(priority) {
return priorityVariantMap[priority] || "secondary";
}
// Время первым, как просил владелец: «03:44:35, 3 октября» (год — только не текущий).
const MONTHS = [
"января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря",
];
export function fmtTime(iso) {
if (!iso) return "—";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return String(iso);
const pad = (n) => String(n).padStart(2, "0");
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
const date = `${d.getDate()} ${MONTHS[d.getMonth()]}`;
const year = d.getFullYear() === new Date().getFullYear() ? "" : ` ${d.getFullYear()}`;
return `${time}, ${date}${year}`;
}
const TOKEN_RE = /("(?:[^"\\]|\\.)*")(\s*:)?|(-?\d+(?:\.\d+)?)\b|(\btrue\b|\bfalse\b|\bnull\b)/g;
/**
* Подсветка JSON: массив сегментов {cls, text} для CodeBlock — ключи, строки,
* числа, булевы/null окрашиваются классами; пунктуация остаётся базовой.
*/
export function jsonSpans(value) {
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
const spans = [];
let last = 0;
for (const m of text.matchAll(TOKEN_RE)) {
const [full, str, colon, num, bool] = m;
const start = m.index;
if (start > last) spans.push({ cls: "", text: text.slice(last, start) });
if (str !== undefined) {
// строка с двоеточием после неё — ключ объекта
spans.push({ cls: colon ? "code-key" : "code-string", text: full });
} else if (num !== undefined) {
spans.push({ cls: "code-number", text: num });
} else if (bool !== undefined) {
spans.push({ cls: "code-boolean", text: bool });
}
last = start + full.length;
}
if (last < text.length) spans.push({ cls: "", text: text.slice(last) });
return spans;
}
/**
* Заголовок колонки GnTable с тултипом кита: GnTable рендерит column.label
* как есть, поэтому передаём VNode с GnTooltip. Класс th-tip переключает
* панель тултипа вниз — иначе её обрезает overflow у .table-wrapper
* (см. глобальный стиль в App.vue).
*/
export function th(label, text) {
return h(GnTooltip, { text, class: "th-tip" }, () => label);
}
export function truncate(text, n = 80) {
if (!text) return "";
return text.length > n ? `${text.slice(0, n - 1)}…` : text;
}
/** JSON в одну строку для ячейки таблицы. */
export function shortJson(value, n = 60) {
if (!value || (typeof value === "object" && !Object.keys(value).length)) return "—";
const text = JSON.stringify(value);
return truncate(text, n);
}
/** Разбор строки с запятыми/пробелами в массив непустых токенов. */
export function parseList(text) {
return (text || "")
.split(/[,\n]+/)
.map((s) => s.trim())
.filter(Boolean);
}