@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
210 lines (209 loc) • 7.36 kB
JavaScript
/**
* Loss indicator object for `<ui9000-loss-indicator>`.
*
* One metric that already has a minimum, a maximum, and operating thresholds.
* The reading is its position on that scale. A health score beside other
* measures stays a dial. Several metrics stay a KPI card.
*/
const LABEL_KEYS = /^(key|name|label|metric)$/i;
const VALUE_KEYS = /^(value|reading)$/i;
const UNIT_KEYS = /^(unit|measure_unit|uom)$/i;
const MIN_KEYS = /^min$/i;
const MAX_KEYS = /^max$/i;
const TICK_KEYS = /^ticks$/i;
const DECIMALS_KEYS = /^decimals$/i;
const BAND_KEYS = /^(bands|thresholds)$/i;
const TREND_KEYS = /^trend$/i;
const RESERVED = /^(key|name|label|metric|value|reading|unit|measure_unit|uom|min|max|ticks|bands|thresholds|trend|decimals|okto|warningto|severeto|criticalto|ok_to|warning_to|severe_to|critical_to)$/i;
const LEVELS = new Set(['ok', 'warning', 'severe', 'critical']);
const LEVEL_COLOR = {
ok: '#3ad07c',
warning: '#f5c451',
severe: '#ff9a3c',
critical: '#ef3b4a',
};
export function lossIndicatorFits(payload) {
return readLoss(payload) !== null;
}
export function lossIndicatorPayload(_fields, rows) {
return readLoss(rows);
}
function readLoss(payload) {
if (!Array.isArray(payload) || payload.length !== 1)
return null;
const row = payload[0];
if (!row || typeof row !== 'object' || Array.isArray(row))
return null;
const record = row;
const min = num(cell(record, MIN_KEYS));
const max = num(cell(record, MAX_KEYS));
if (min === undefined || max === undefined || !(max > min))
return null;
const metrics = metricKeys(record);
const explicit = num(cell(record, VALUE_KEYS));
if (explicit !== undefined && metrics.length > 0)
return null;
if (explicit === undefined && metrics.length !== 1)
return null;
const value = explicit ?? num(record[metrics[0]]);
if (value === undefined)
return null;
const bands = bandsOf(record, min, max);
if (!bands || bands.length === 0)
return null;
const label = text(cell(record, LABEL_KEYS)) ||
(metrics[0] ? labelFromKey(metrics[0]) : '');
const unit = text(cell(record, UNIT_KEYS)) ?? '';
const ticks = ticksOf(cell(record, TICK_KEYS), min, max);
const trend = trendOf(cell(record, TREND_KEYS));
const decimals = decimalsOf(cell(record, DECIMALS_KEYS));
const field = explicit !== undefined ? 'value' : metrics[0];
const reading = decimals !== undefined ? value.toFixed(decimals) : value;
return {
chartType: 'lossIndicator',
name: label,
yAxe: [field],
data: [
{
[field]: reading,
...(ticks ? { ticks } : {}),
...(trend && trend !== 'flat' ? { trend } : {}),
},
],
axisDetails: {
[field]: {
label: label || field,
type: 'number',
subtype: unit === '%' ? 'percent' : 'float',
...(unit ? { measure_unit: unit } : {}),
},
},
limitsDomains: [[min, max]],
domainsLimits: bands.map((band) => ({
values: [band.from, band.to],
color: band.color ?? LEVEL_COLOR[band.level],
level: band.level,
orientation: 'horizontal',
})),
};
}
function bandsOf(row, min, max) {
const explicit = parseBands(cell(row, BAND_KEYS));
if (explicit.length > 0)
return explicit.filter((band) => band.to > band.from);
return bandsFromThresholds(row, min, max);
}
function parseBands(value) {
let raw = value;
if (typeof raw === 'string' && raw.trim()) {
try {
raw = JSON.parse(raw);
}
catch {
return [];
}
}
if (!Array.isArray(raw))
return [];
const bands = [];
for (const item of raw) {
if (!item || typeof item !== 'object' || Array.isArray(item))
continue;
const record = item;
const from = num(record.from);
const to = num(record.to);
const level = String(record.level ?? '').trim().toLowerCase();
if (from === undefined || to === undefined || !LEVELS.has(level))
continue;
const color = hex(record.color);
bands.push(color ? { from, to, level: level, color } : { from, to, level: level });
}
return bands.sort((a, b) => a.from - b.from);
}
function bandsFromThresholds(row, min, max) {
const stops = [];
const okTo = num(named(row, /^ok_?to$/i));
const warningTo = num(named(row, /^warning_?to$/i));
const severeTo = num(named(row, /^severe_?to$/i));
if (okTo !== undefined)
stops.push({ to: okTo, level: 'ok' });
if (warningTo !== undefined)
stops.push({ to: warningTo, level: 'warning' });
if (severeTo !== undefined)
stops.push({ to: severeTo, level: 'severe' });
const inside = stops
.filter((stop) => stop.to > min && stop.to < max)
.sort((a, b) => a.to - b.to);
if (inside.length === 0)
return null;
const bands = [];
let from = min;
for (const stop of inside) {
if (stop.to <= from)
continue;
bands.push({ from, to: stop.to, level: stop.level });
from = stop.to;
}
if (from < max)
bands.push({ from, to: max, level: 'critical' });
return bands.length > 0 ? bands : null;
}
function ticksOf(value, min, max) {
let raw = value;
if (typeof raw === 'string' && raw.trim())
raw = raw.split(/[, ]+/).filter(Boolean);
const ticks = Array.isArray(raw)
? raw.map((item) => num(item)).filter((item) => item !== undefined)
: [];
const usable = [...new Set(ticks.filter((tick) => tick >= min && tick <= max))].sort((a, b) => a - b);
return usable.length >= 2 ? usable : undefined;
}
function decimalsOf(value) {
const parsed = num(value);
if (parsed === undefined || !Number.isInteger(parsed) || parsed < 0 || parsed > 8)
return undefined;
return parsed;
}
function hex(value) {
const text = String(value ?? '').trim();
return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(text)
? text
: undefined;
}
function metricKeys(row) {
return Object.keys(row).filter((key) => !RESERVED.test(key) && num(row[key]) !== undefined);
}
function trendOf(value) {
const text = String(value ?? '').trim().toLowerCase();
if (text === 'up' || text === 'down' || text === 'flat')
return text;
return undefined;
}
function named(row, pattern) {
const key = Object.keys(row).find((name) => pattern.test(name));
return key ? row[key] : undefined;
}
function cell(row, pattern) {
return named(row, pattern);
}
function text(value) {
if (typeof value !== 'string')
return undefined;
const trimmed = value.trim();
return trimmed || undefined;
}
function num(value) {
if (typeof value === 'number' && Number.isFinite(value))
return value;
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
return Number(value);
}
return undefined;
}
function labelFromKey(key) {
return key
.replace(/[_-]+/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/\s+/g, ' ')
.trim();
}