Newer
Older
gn-synapse / frontend / src / ui.js
/** Общие мелочи для разметки (админка и личный раздел): бейджи статусов, приоритетов,
 * каналов; формат времени, сокращение, подсветка 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);
}