Replace the layer-based (Infrastructure/Governance/Cognition) browsing tabs with a synthesized category taxonomy built from the ontology's finer-grained `domain` field, since layer lumped unrelated entity types (an LXC and a DNS record and a storage volume) into one bucket. Network and Fleet each span two domains, so the table view now fans out per-domain fetches and merges, while the graph view maps domain->category client-side. Also carries over several detail-panel polish items (Tasks-not-raw-executions, slug URL encoding, MultiSelectFilter) from earlier in this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
import { clsx, type ClassValue } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
|
|
// relativeTime renders a compact "Xs/Xm/Xh/Xd ago" label for freshness
|
|
// indicators (health checks, live event timestamps, etc).
|
|
export function relativeTime(iso: string | null | undefined): string {
|
|
if (!iso) return "never";
|
|
const ms = Date.now() - new Date(iso).getTime();
|
|
if (ms < 0) return "just now";
|
|
const s = Math.floor(ms / 1000);
|
|
if (s < 60) return `${s}s ago`;
|
|
const m = Math.floor(s / 60);
|
|
if (m < 60) return `${m}m ago`;
|
|
const h = Math.floor(m / 60);
|
|
if (h < 24) return `${h}h ago`;
|
|
const d = Math.floor(h / 24);
|
|
return `${d}d ago`;
|
|
}
|
|
|
|
// Truncates long slugs/names in the middle (keeping the "type:" prefix and
|
|
// the tail visible) rather than at the end — for slugs the distinguishing
|
|
// part is often at both ends, e.g. "investigation:nomos/dragonfly-memlock-…"
|
|
// vs "…rlimit-type-8-in-unprivileged-lxcs".
|
|
export function truncateMiddle(s: string, maxLen = 36): string {
|
|
if (s.length <= maxLen) return s;
|
|
const keep = Math.floor((maxLen - 1) / 2);
|
|
return `${s.slice(0, keep)}…${s.slice(-keep)}`;
|
|
}
|
|
|
|
// debounce wraps fn so rapid calls (e.g. keystrokes in a filter input)
|
|
// collapse into one invocation after `wait`ms of silence.
|
|
export function debounce<T extends (...args: never[]) => void>(fn: T, wait = 300): T {
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
return ((...args: Parameters<T>) => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), wait);
|
|
}) as T;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|