- Add type-to-trigger Spotlight with keyboard (any character) - Add canvas click to open Spotlight - Implement keyboard navigation (↑↓ Enter Esc) - Add keyboard shortcut handlers and spotlight store - Create useSpotlight hook with fuzzy search - Create mock Kubernetes resources for initial testing - Implement krate creation with collision detection - Add Quick Actions (all pods, services, deployments, namespaces) - Create Spotlight with filter chips and result rendering - Add Spotlight state management with setQuery, setFilter, setSel - Include design specs (Krates.dc.html, server.js, support.js)
882 lines
102 KiB
HTML
882 lines
102 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<script src="./support.js"></script>
|
||
</head>
|
||
<body>
|
||
<x-dc>
|
||
<helmet>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
*{box-sizing:border-box}
|
||
html,body{margin:0;height:100%;background:#0b0e13}
|
||
::selection{background:rgba(120,200,230,.28)}
|
||
@keyframes spotIn{from{opacity:0;transform:translate(-50%,-8px)}to{opacity:1;transform:translate(-50%,0)}}
|
||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||
@keyframes popIn{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
|
||
@keyframes blink{50%{opacity:0}}
|
||
@keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
|
||
input::placeholder{color:#5a6680}
|
||
pre::-webkit-scrollbar,div::-webkit-scrollbar{width:8px;height:8px}
|
||
pre::-webkit-scrollbar-thumb,div::-webkit-scrollbar-thumb{background:rgba(140,165,200,.18);border-radius:4px}
|
||
</style>
|
||
</helmet>
|
||
<div ref="{{ setRoot }}" style="position:fixed;inset:0;overflow:hidden;background:#0b0e13;font-family:'IBM Plex Sans',system-ui,sans-serif;color:#c7d2e0;cursor:{{ cursor }};user-select:none" onMouseDownCapture="{{ onRootDownCapture }}" onMouseDown="{{ onBgDown }}" onWheel="{{ onWheel }}">
|
||
|
||
<!-- ====== WORLD LAYER ====== -->
|
||
<div style="position:absolute;left:0;top:0;width:12000px;height:8000px;transform:{{ worldTransform }};transform-origin:0 0;transition:{{ worldTransition }};background-color:#0b0e13;background-image:linear-gradient(rgba(125,145,175,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(125,145,175,.04) 1px,transparent 1px),linear-gradient(rgba(125,145,175,.075) 1px,transparent 1px),linear-gradient(90deg,rgba(125,145,175,.075) 1px,transparent 1px);background-size:34px 34px,34px 34px,170px 170px,170px 170px">
|
||
|
||
<!-- krate frames + headers -->
|
||
<sc-for list="{{ frames }}" as="f" hint-placeholder-count="0">
|
||
<div style="position:absolute;left:{{ f.left }}px;top:{{ f.top }}px;width:{{ f.w }}px;height:{{ f.h }}px;border:1px dashed {{ f.border }};border-radius:18px;background:{{ f.fill }};pointer-events:none"></div>
|
||
<div style="position:absolute;left:{{ f.labelLeft }}px;top:{{ f.labelTop }}px;display:flex;align-items:center;gap:9px;padding:6px 12px;border:1px solid {{ f.border }};border-radius:9px;background:rgba(15,19,27,.94);font-family:'IBM Plex Mono',monospace;font-size:13px;letter-spacing:.02em;color:#dbe3ef;cursor:grab" onMouseDown="{{ f.onDrag }}" onDoubleClick="{{ f.onToggleCollapse }}">
|
||
<span style="width:8px;height:8px;border-radius:2px;background:{{ f.color }}"></span>{{ f.label }}
|
||
<span style="font-size:10px;color:{{ f.statusColor }};border:1px solid {{ f.statusColor }};border-radius:5px;padding:1px 6px">{{ f.status }}</span>
|
||
<span style="font-size:11px;color:#6b7890">{{ f.count }} views</span>
|
||
<button onClick="{{ f.onCollapse }}" onMouseDown="{{ stopDown }}" title="collapse krate" style="border:none;background:transparent;color:#6b7890;font-size:15px;cursor:pointer;padding:0 0 0 2px;line-height:1">–</button>
|
||
<button onClick="{{ f.onClose }}" onMouseDown="{{ stopDown }}" style="border:none;background:transparent;color:#6b7890;font-size:14px;cursor:pointer;padding:0 0 0 2px;line-height:1">×</button>
|
||
</div>
|
||
</sc-for>
|
||
|
||
<!-- collapsed krate bars -->
|
||
<sc-for list="{{ minis }}" as="m" hint-placeholder-count="0">
|
||
<div onDoubleClick="{{ m.onExpand }}" style="position:absolute;left:{{ m.x }}px;top:{{ m.y }}px;width:300px;display:flex;align-items:center;gap:10px;padding:11px 13px;border:1px solid {{ m.outline }};border-left:3px solid {{ m.color }};border-radius:11px;background:rgba(15,19,27,.97);box-shadow:0 14px 40px rgba(0,0,0,.5);cursor:grab" onMouseDown="{{ m.onDrag }}">
|
||
<span style="width:9px;height:9px;border-radius:2px;background:{{ m.color }};flex:none"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:13px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ m.label }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:{{ m.statusColor }};flex:none">{{ m.count }} views</span>
|
||
<button onClick="{{ m.onExpand }}" onMouseDown="{{ stopDown }}" title="expand" style="border:none;background:transparent;color:#9fb0c8;font-size:13px;cursor:pointer;padding:0 2px;line-height:1">⬚</button>
|
||
<button onClick="{{ m.onClose }}" onMouseDown="{{ stopDown }}" style="border:none;background:transparent;color:#6b7890;font-size:14px;cursor:pointer;padding:0;line-height:1">×</button>
|
||
</div>
|
||
</sc-for>
|
||
<sc-for list="{{ wins }}" as="w" hint-placeholder-count="0">
|
||
<div style="position:absolute;left:{{ w.x }}px;top:{{ w.y }}px;width:{{ w.w }}px;z-index:{{ w.z }};background:rgba(14,18,25,.98);border:1px solid {{ w.outline }};border-radius:11px;box-shadow:0 18px 50px rgba(0,0,0,.5);overflow:hidden;animation:popIn .16s ease" onMouseEnter="{{ w.onWinEnter }}" onMouseLeave="{{ w.onWinLeave }}" onMouseDown="{{ stopDown }}" onWheel="{{ winWheel }}" onDoubleClick="{{ stopDown }}">
|
||
<div style="display:flex;align-items:center;gap:9px;padding:9px 11px;border-bottom:1px solid rgba(140,165,200,.13);background:rgba(20,26,36,.7);cursor:grab" onMouseDown="{{ w.onDrag }}" onDoubleClick="{{ w.onFocus }}">
|
||
<span style="width:9px;height:9px;border-radius:2px;background:{{ w.color }};clip-path:{{ w.glyphClip }};flex:none"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:#dbe3ef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ w.title }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:{{ w.statusColor }};border:1px solid {{ w.statusColor }};border-radius:5px;padding:1px 6px;opacity:.9">{{ w.status }}</span>
|
||
<button onClick="{{ w.onFocus }}" onMouseDown="{{ stopDown }}" title="zoom to window (z)" style="border:none;background:transparent;color:#7e8aa2;font-size:12px;cursor:pointer;padding:0 2px;line-height:1">⤢</button>
|
||
<button onClick="{{ w.onRelated }}" onMouseDown="{{ stopDown }}" title="open related" style="border:none;background:transparent;color:#7e8aa2;font-size:13px;cursor:pointer;padding:0 2px;line-height:1;display:{{ w.relatedDisplay }}">⊕</button>
|
||
<button onClick="{{ w.onClose }}" onMouseDown="{{ stopDown }}" style="border:none;background:transparent;color:#7e8aa2;font-size:15px;cursor:pointer;padding:0 2px;line-height:1">×</button>
|
||
</div>
|
||
<sc-if value="{{ w.normal }}" hint-placeholder-val="{{ true }}">
|
||
<div style="display:flex;gap:2px;padding:6px 8px 0;border-bottom:1px solid rgba(140,165,200,.1)">
|
||
<sc-for list="{{ w.tabs }}" as="t" hint-placeholder-count="4">
|
||
<button onClick="{{ t.onClick }}" onMouseDown="{{ stopDown }}" style="border:none;background:{{ t.bg }};color:{{ t.fg }};font-family:'IBM Plex Mono',monospace;font-size:11.5px;padding:6px 10px;border-radius:7px 7px 0 0;cursor:{{ t.cursor }};opacity:{{ t.opacity }}">{{ t.label }}</button>
|
||
</sc-for>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ w.isText }}" hint-placeholder-val="{{ true }}">
|
||
<pre style="margin:0;padding:13px 15px;height:{{ w.bodyH }}px;overflow:auto;font-family:'IBM Plex Mono',monospace;font-size:12px;line-height:1.65;color:#b9c6d8;white-space:pre-wrap;word-break:break-word;user-select:text;cursor:text">{{ w.body }}</pre>
|
||
</sc-if>
|
||
<sc-if value="{{ w.isLogs }}" hint-placeholder-val="{{ false }}">
|
||
<div ref="{{ w.logRef }}" onScroll="{{ w.onLogScroll }}" style="height:{{ w.bodyH }}px;overflow:auto;padding:12px 14px;background:#090c11;font-family:'IBM Plex Mono',monospace;font-size:11.5px;line-height:1.6;user-select:text;cursor:text">
|
||
<sc-for list="{{ w.logLines }}" as="ln" hint-placeholder-count="6"><div style="color:{{ ln.color }};white-space:pre-wrap">{{ ln.text }}</div></sc-for>
|
||
<div style="display:flex;align-items:center;gap:7px;color:#5a6680;font-size:10px;margin-top:7px"><span style="width:6px;height:6px;border-radius:50%;background:#4ad07a;animation:blink 1.2s ease-in-out infinite"></span>streaming…</div>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ w.isShell }}" hint-placeholder-val="{{ false }}">
|
||
<div onMouseEnter="{{ w.onShEnter }}" onMouseLeave="{{ w.onShLeave }}" style="height:{{ w.bodyH }}px;display:flex;flex-direction:column;background:#090c11">
|
||
<div ref="{{ w.shRef }}" onScroll="{{ w.onShScroll }}" style="flex:1;overflow:auto;padding:12px 14px;font-family:'IBM Plex Mono',monospace;font-size:12px;line-height:1.6;user-select:text;cursor:text">
|
||
<sc-for list="{{ w.shellLines }}" as="ln" hint-placeholder-count="2"><div style="color:{{ ln.color }};white-space:pre-wrap;word-break:break-word">{{ ln.text }}</div></sc-for>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;padding:9px 13px;border-top:1px solid rgba(140,165,200,.12)">
|
||
<span style="color:{{ accent }};font-family:'IBM Plex Mono',monospace;font-size:11.5px;white-space:nowrap">{{ w.promptStr }}</span>
|
||
<input ref="{{ w.shInputRef }}" onKeyDown="{{ w.onShellKey }}" onMouseDown="{{ stopDown }}" placeholder="type a command…" spellcheck="false" autocomplete="off" style="flex:1;min-width:0;border:none;outline:none;background:transparent;color:#e6edf6;font-family:'IBM Plex Mono',monospace;font-size:12px"/>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ w.isCollection }}" hint-placeholder-val="{{ false }}">
|
||
<div onMouseEnter="{{ w.onCollEnter }}" onMouseLeave="{{ w.onCollLeave }}">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:8px 11px;border-bottom:1px solid rgba(140,165,200,.1)">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:{{ accent }};flex:none">⌕</span>
|
||
<input ref="{{ w.collInputRef }}" onChange="{{ w.onCollSearch }}" onKeyDown="{{ w.onCollKey }}" onMouseDown="{{ stopDown }}" value="{{ w.searchVal }}" placeholder="filter {{ w.total }} objects…" spellcheck="false" autocomplete="off" style="flex:1;min-width:0;border:none;outline:none;background:transparent;color:#e6edf6;font-family:'IBM Plex Mono',monospace;font-size:12px"/>
|
||
<span style="display:flex;align-items:center;gap:5px;padding:2px 8px;border-radius:6px;background:rgba(255,255,255,.03);border:1px solid rgba(140,165,200,.16);font-family:'IBM Plex Mono',monospace;font-size:9.5px;color:#7e8aa2;flex:none">↑↓ ⌥L/S/D/Y</span>
|
||
<div style="display:flex;gap:2px;background:rgba(8,11,16,.6);border:1px solid rgba(140,165,200,.16);border-radius:7px;padding:2px;flex:none">
|
||
<button onClick="{{ w.onCollList }}" onMouseDown="{{ stopDown }}" style="border:none;border-radius:5px;padding:3px 9px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:10.5px;background:{{ w.listBg }};color:{{ w.listFg }}">list</button>
|
||
<button onClick="{{ w.onCollTree }}" onMouseDown="{{ stopDown }}" style="border:none;border-radius:5px;padding:3px 9px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:10.5px;background:{{ w.treeBg }};color:{{ w.treeFg }}">tree</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:7px;padding:7px 12px;border-bottom:1px solid rgba(140,165,200,.08);font-family:'IBM Plex Mono',monospace;font-size:10px">
|
||
<span style="display:flex;align-items:center;gap:5px;color:#4ad07a"><span style="width:6px;height:6px;border-radius:50%;background:#4ad07a"></span>{{ w.summOk }} ok</span>
|
||
<span style="display:flex;align-items:center;gap:5px;color:#e8b54a"><span style="width:6px;height:6px;border-radius:50%;background:#e8b54a"></span>{{ w.summWarn }} pending</span>
|
||
<span style="display:flex;align-items:center;gap:5px;color:#ef6f6f"><span style="width:6px;height:6px;border-radius:50%;background:#ef6f6f"></span>{{ w.summBad }} degraded</span>
|
||
<span style="margin-left:auto;color:#6b7890">{{ w.count }} shown</span>
|
||
</div>
|
||
<div style="height:{{ w.bodyH }}px;overflow:auto;padding:6px">
|
||
<sc-for list="{{ w.rows }}" as="r" hint-placeholder-count="6">
|
||
<sc-if value="{{ r.isGroup }}" hint-placeholder-val="{{ false }}">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:9px 9px 4px;margin-top:2px">
|
||
<span style="width:9px;height:9px;background:{{ r.color }};clip-path:{{ r.clip }};border-radius:{{ r.radius }};flex:none"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:#9fb0c8;flex:1">{{ r.groupLabel }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:#6b7890;flex:none">{{ r.groupCount }}</span>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ r.row }}" hint-placeholder-val="{{ true }}">
|
||
<div onClick="{{ r.onOpen }}" onMouseEnter="{{ r.onHover }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:9px;padding:7px 9px 7px 7px;border-radius:8px;cursor:pointer;margin-left:{{ r.indentPx }}px;background:{{ r.selBg }};border-left:2px solid {{ r.selBar }}" style-hover="background:rgba(255,255,255,.04)">
|
||
<sc-if value="{{ r.child }}" hint-placeholder-val="{{ false }}"><span style="width:9px;height:9px;border-left:1px solid rgba(140,165,200,.35);border-bottom:1px solid rgba(140,165,200,.35);border-bottom-left-radius:3px;margin:0 -3px -3px 0;flex:none"></span></sc-if>
|
||
<span style="width:11px;height:11px;background:{{ r.color }};clip-path:{{ r.clip }};border-radius:{{ r.radius }};flex:none;filter:drop-shadow(0 0 3px {{ r.color }})"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ r.name }}</span>
|
||
<sc-if value="{{ r.hasRel }}" hint-placeholder-val="{{ false }}"><span style="font-family:'IBM Plex Mono',monospace;font-size:9px;color:#6b7890;flex:none">{{ r.rel }}</span></sc-if>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:9.5px;color:#7e8aa2;flex:none">{{ r.metric }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:9.5px;color:#8493ab;border:1px solid rgba(140,165,200,.2);border-radius:4px;padding:1px 5px;flex:none">{{ r.typeShort }}</span>
|
||
<div style="display:flex;gap:3px;flex:none">
|
||
<sc-for list="{{ r.views }}" as="v" hint-placeholder-count="0">
|
||
<button onClick="{{ v.onClick }}" onMouseDown="{{ stopDown }}" title="open" style="display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid rgba(140,165,200,.2);background:rgba(255,255,255,.03);border-radius:5px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:9px;color:#9fb0c8">{{ v.letter }}</button>
|
||
</sc-for>
|
||
</div>
|
||
<span style="width:7px;height:7px;border-radius:50%;background:{{ r.statusColor }};flex:none;box-shadow:0 0 6px {{ r.statusColor }}"></span>
|
||
</div>
|
||
</sc-if>
|
||
</sc-for>
|
||
<sc-if value="{{ w.empty }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:22px;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#6b7890">no objects match filter</div>
|
||
</sc-if>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
<div onMouseDown="{{ w.onResize }}" style="position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;border-right:2px solid rgba(140,165,200,.4);border-bottom:2px solid rgba(140,165,200,.4);border-bottom-right-radius:9px"></div>
|
||
</div>
|
||
</sc-for>
|
||
|
||
</div>
|
||
|
||
<!-- ====== COLLAPSED CARDS (screen space) ====== -->
|
||
<sc-for list="{{ cards }}" as="c" hint-placeholder-count="0">
|
||
<div style="position:absolute;left:{{ c.sx }}px;top:{{ c.sy }}px;width:230px;background:rgba(15,19,27,.97);border:1px solid {{ c.outline }};border-radius:12px;box-shadow:0 16px 44px rgba(0,0,0,.5);overflow:hidden;cursor:grab;z-index:{{ c.z }};transition:{{ c.trans }};animation:popIn .16s ease" onMouseDown="{{ c.onDown }}" onDoubleClick="{{ c.onExpand }}">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:11px 13px 9px">
|
||
<span style="width:9px;height:9px;border-radius:2px;background:{{ c.color }};flex:none"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:13px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ c.label }}</span>
|
||
<span style="width:7px;height:7px;border-radius:50%;background:{{ c.statusColor }};flex:none;box-shadow:0 0 7px {{ c.statusColor }}"></span>
|
||
<button onClick="{{ c.onClose }}" onMouseDown="{{ stopDown }}" style="border:none;background:transparent;color:#6b7890;font-size:14px;cursor:pointer;padding:0;line-height:1;flex:none">×</button>
|
||
</div>
|
||
<div style="display:flex;gap:5px;padding:0 13px 11px;flex-wrap:wrap">
|
||
<sc-for list="{{ c.badges }}" as="b" hint-placeholder-count="2">
|
||
<span style="display:flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:{{ c.tint }};border:1px solid {{ c.outline }};font-family:'IBM Plex Mono',monospace;font-size:11px;color:{{ c.color }}">{{ b }}</span>
|
||
</sc-for>
|
||
</div>
|
||
<div style="padding:8px 13px;border-top:1px solid rgba(140,165,200,.1);background:rgba(8,11,16,.5);font-family:'IBM Plex Mono',monospace;font-size:10px;color:#7e8aa2;display:flex;justify-content:space-between">
|
||
<span>{{ c.count }} windows · ns/{{ c.ns }}</span><span style="color:#5a6680">⤢ drag · dbl-click</span>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
|
||
<!-- ====== RELATED MENU ====== -->
|
||
<sc-if value="{{ relMenu }}" hint-placeholder-val="{{ false }}">
|
||
<div style="position:absolute;left:{{ relMenu.sx }}px;top:{{ relMenu.sy }}px;width:236px;background:rgba(16,20,28,.98);border:1px solid rgba(140,165,200,.24);border-radius:11px;box-shadow:0 22px 60px rgba(0,0,0,.6);overflow:hidden;z-index:55;animation:popIn .14s ease" onMouseDown="{{ stopDown }}" onWheel="{{ stopDown }}">
|
||
<div style="padding:9px 13px;font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:#6b7890;border-bottom:1px solid rgba(140,165,200,.1)">related to {{ relMenu.label }}</div>
|
||
<div style="max-height:300px;overflow:auto">
|
||
<sc-for list="{{ relMenu.items }}" as="it" hint-placeholder-count="3">
|
||
<div onClick="{{ it.onClick }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:11px;padding:9px 13px;cursor:pointer" style-hover="background:rgba(255,255,255,.045)">
|
||
<span style="width:13px;height:13px;background:{{ it.color }};clip-path:{{ it.clip }};border-radius:{{ it.radius }};flex:none;filter:drop-shadow(0 0 4px {{ it.color }})"></span>
|
||
<span style="flex:1;min-width:0;font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ it.name }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:#8493ab;border:1px solid rgba(140,165,200,.2);border-radius:5px;padding:1px 6px;flex:none">{{ it.typeShort }}</span>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
<sc-if value="{{ relMenu.empty }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:16px;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:11px;color:#6b7890">no related objects</div>
|
||
</sc-if>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ====== EMPTY STATE ====== -->
|
||
<sc-if value="{{ emptyCanvas }}" hint-placeholder-val="{{ true }}">
|
||
<div style="position:absolute;left:50%;top:47%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:20px;pointer-events:none;text-align:center;width:480px">
|
||
<div style="width:50px;height:50px;background:{{ accent }};clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);filter:drop-shadow(0 0 22px {{ accentGlow }});animation:floatY 4s ease-in-out infinite"></div>
|
||
<div style="font-family:'IBM Plex Sans',sans-serif;font-size:25px;color:#e8eef6;font-weight:500;letter-spacing:-.01em">Search your cluster</div>
|
||
<div style="font-family:'IBM Plex Mono',monospace;font-size:13px;color:#7e8aa2;line-height:1.75">Start typing or click anywhere. Pick a result, then <span style="color:{{ accent }}">⌥L</span> logs · <span style="color:{{ accent }}">⌥S</span> shell · <span style="color:{{ accent }}">⌥D</span> describe · <span style="color:{{ accent }}">⌥Y</span> yaml to open the views you want, grouped on the canvas.</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ====== TOP BAR ====== -->
|
||
<div style="position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:14px;padding:13px 18px;pointer-events:none">
|
||
<div style="display:flex;align-items:center;gap:10px;pointer-events:auto">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:7px 12px;background:rgba(14,18,25,.82);border:1px solid rgba(140,165,200,.18);border-radius:9px;backdrop-filter:blur(6px)">
|
||
<span style="width:8px;height:8px;background:{{ accent }};clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:13px;letter-spacing:.06em;color:#e3eaf4;font-weight:500">krates</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#6b7890">/ yard</span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:7px;padding:7px 12px;background:rgba(14,18,25,.82);border:1px solid rgba(140,165,200,.18);border-radius:9px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#9fb0c8">
|
||
<span style="width:6px;height:6px;border-radius:50%;background:#4ad07a;box-shadow:0 0 8px #4ad07a"></span>prod-eu-1
|
||
</div>
|
||
<sc-if value="{{ krateCount }}" hint-placeholder-val="{{ false }}">
|
||
<div style="display:flex;align-items:center;gap:7px;padding:7px 12px;background:rgba(14,18,25,.82);border:1px solid rgba(140,165,200,.18);border-radius:9px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#7e8aa2">{{ krateCount }} krates</div>
|
||
</sc-if>
|
||
</div>
|
||
<div style="flex:1"></div>
|
||
<div style="display:flex;align-items:center;gap:9px;pointer-events:auto">
|
||
<div style="display:flex;align-items:center;gap:6px;padding:6px 11px;background:rgba(14,18,25,.82);border:1px solid rgba(140,165,200,.18);border-radius:9px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11px;color:#7e8aa2">
|
||
<span style="width:6px;height:6px;border-radius:50%;background:{{ accent }};animation:blink 1.6s ease-in-out infinite"></span>synced
|
||
</div>
|
||
<button onClick="{{ onToggleAdmin }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:6px;padding:6px 11px;background:{{ adminBtnBg }};border:1px solid {{ adminBtnBorder }};border-radius:9px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11px;color:{{ adminBtnFg }};cursor:pointer">◉ admin</button>
|
||
<div style="display:flex;align-items:center">
|
||
<sc-for list="{{ roster }}" as="u" hint-placeholder-count="4">
|
||
<div title="{{ u.name }}" style="width:30px;height:30px;margin-left:-7px;border-radius:50%;background:{{ u.color }};border:2px solid #0b0e13;display:flex;align-items:center;justify-content:center;font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600;color:#0b0e13">{{ u.initial }}</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ====== BOTTOM HINT ====== -->
|
||
<div style="position:absolute;right:18px;bottom:18px;display:flex;align-items:center;gap:13px;padding:9px 14px;background:rgba(14,18,25,.8);border:1px solid rgba(140,165,200,.16);border-radius:10px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11px;color:#7e8aa2;pointer-events:none">
|
||
<span><span style="color:#aeb9cc">click / type</span> search</span>
|
||
<span style="opacity:.4">·</span>
|
||
<span><span style="color:#aeb9cc">scroll</span> pan</span>
|
||
<span style="opacity:.4">·</span>
|
||
<span><span style="color:#aeb9cc">⌘/ctrl + scroll</span> zoom</span>
|
||
<span style="opacity:.4">·</span>
|
||
<span><span style="color:#aeb9cc">space-drag</span> pan</span>
|
||
<span style="opacity:.4">·</span>
|
||
<span><span style="color:#aeb9cc">z</span> zoom window</span>
|
||
</div>
|
||
|
||
<!-- ====== ZOOM PILL ====== -->
|
||
<div style="position:absolute;left:18px;bottom:18px;display:flex;align-items:center;gap:9px;padding:8px 13px;background:rgba(14,18,25,.8);border:1px solid rgba(140,165,200,.16);border-radius:10px;backdrop-filter:blur(6px);font-family:'IBM Plex Mono',monospace;font-size:11px;color:#9fb0c8;pointer-events:none">
|
||
<span style="width:6px;height:6px;border-radius:50%;background:{{ zoomDotColor }}"></span>{{ zoomLabel }}
|
||
</div>
|
||
|
||
<!-- ====== MINIMAP ====== -->
|
||
<sc-if value="{{ minimap }}" hint-placeholder-val="{{ false }}">
|
||
<div ref="{{ minimap.setEl }}" onMouseDown="{{ minimap.onDown }}" style="position:absolute;right:18px;bottom:64px;width:{{ minimap.w }}px;height:{{ minimap.h }}px;background:rgba(11,14,19,.92);border:1px solid rgba(140,165,200,.2);border-radius:10px;backdrop-filter:blur(6px);overflow:hidden;cursor:pointer;box-shadow:0 12px 36px rgba(0,0,0,.5)">
|
||
<div style="position:absolute;left:6px;top:5px;font-family:'IBM Plex Mono',monospace;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:#5a6680;pointer-events:none">map</div>
|
||
<sc-for list="{{ minimap.rects }}" as="r" hint-placeholder-count="0">
|
||
<div style="position:absolute;left:{{ r.x }}px;top:{{ r.y }}px;width:{{ r.w }}px;height:{{ r.h }}px;background:{{ r.color }};opacity:.55;border-radius:2px;pointer-events:none"></div>
|
||
</sc-for>
|
||
<div style="position:absolute;left:{{ minimap.vx }}px;top:{{ minimap.vy }}px;width:{{ minimap.vw }}px;height:{{ minimap.vh }}px;border:1px solid rgba(230,237,246,.85);border-radius:2px;background:rgba(230,237,246,.06);pointer-events:none"></div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ====== ADMIN DRAWER ====== -->
|
||
<sc-if value="{{ admin }}" hint-placeholder-val="{{ false }}">
|
||
<div style="position:absolute;inset:0;background:rgba(7,9,13,.4);z-index:55;animation:fadeIn .12s ease" onMouseDown="{{ onToggleAdmin }}"></div>
|
||
<div style="position:absolute;top:0;right:0;bottom:0;width:380px;background:rgba(13,17,24,.98);border-left:1px solid rgba(140,165,200,.2);box-shadow:-20px 0 60px rgba(0,0,0,.5);z-index:56;display:flex;flex-direction:column;animation:fadeIn .16s ease" onMouseDown="{{ stopDown }}">
|
||
<div style="display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid rgba(140,165,200,.14)">
|
||
<span style="width:9px;height:9px;border-radius:50%;background:{{ accent }};box-shadow:0 0 8px {{ accent }}"></span>
|
||
<div style="flex:1"><div style="font-family:'IBM Plex Mono',monospace;font-size:14px;color:#e8eef6">Yard activity</div><div style="font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:#7e8aa2;margin-top:2px">{{ adminCount }} connected · prod-eu-1</div></div>
|
||
<button onClick="{{ onToggleAdmin }}" onMouseDown="{{ stopDown }}" style="border:none;background:transparent;color:#7e8aa2;font-size:17px;cursor:pointer;line-height:1">×</button>
|
||
</div>
|
||
<div style="flex:1;overflow:auto;padding:12px">
|
||
<sc-for list="{{ adminUsers }}" as="u" hint-placeholder-count="4">
|
||
<div style="margin-bottom:12px;border:1px solid {{ u.border }};border-radius:12px;background:rgba(255,255,255,.02);overflow:hidden">
|
||
<div style="display:flex;align-items:center;gap:10px;padding:11px 13px">
|
||
<div style="width:30px;height:30px;border-radius:50%;background:{{ u.color }};display:flex;align-items:center;justify-content:center;font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600;color:#0b0e13;flex:none">{{ u.initial }}</div>
|
||
<div style="flex:1;min-width:0"><div style="font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ u.name }}</div><div style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:{{ u.statusColor }};margin-top:1px">{{ u.status }}</div></div>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:#7e8aa2;flex:none">{{ u.krateCount }} krates</span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;padding:8px 13px;background:rgba(8,11,16,.5);border-top:1px solid rgba(140,165,200,.1)">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:{{ accent }};flex:none">⌕</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#aeb9cc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ u.query }}</span>
|
||
<sc-if value="{{ u.typing }}" hint-placeholder-val="{{ false }}"><span style="font-family:'IBM Plex Mono',monospace;font-size:9px;color:{{ accent }};flex:none">typing…</span></sc-if>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:1px;padding:4px 6px 7px">
|
||
<sc-for list="{{ u.krates }}" as="kr" hint-placeholder-count="2">
|
||
<div onClick="{{ kr.onSpectate }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:9px;padding:7px 8px;border-radius:8px;cursor:pointer" style-hover="background:rgba(255,255,255,.04)">
|
||
<span style="width:8px;height:8px;border-radius:2px;background:{{ kr.color }};flex:none"></span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#dbe3ef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">{{ kr.label }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:9px;color:#7e8aa2;border:1px solid rgba(140,165,200,.2);border-radius:4px;padding:1px 5px;flex:none">{{ kr.badges }}</span>
|
||
<span style="width:6px;height:6px;border-radius:50%;background:{{ kr.statusColor }};flex:none"></span>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ====== SPOTLIGHT ====== -->
|
||
<sc-if value="{{ spotOpen }}" hint-placeholder-val="{{ false }}">
|
||
<div style="position:absolute;inset:0;background:rgba(7,9,13,.34);z-index:60;animation:fadeIn .12s ease" onMouseDown="{{ onSpotBackdrop }}">
|
||
<div style="position:absolute;left:50%;top:14%;transform:translateX(-50%);width:min(660px,93vw);animation:spotIn .16s cubic-bezier(.2,.8,.3,1)" onMouseDown="{{ stopDown }}">
|
||
<div style="background:rgba(16,20,28,.97);border:1px solid rgba(140,165,200,.26);border-radius:14px;box-shadow:0 32px 90px rgba(0,0,0,.65),0 0 0 1px rgba(0,0,0,.5);overflow:hidden">
|
||
<div style="display:flex;align-items:center;gap:11px;padding:15px 17px">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:17px;color:{{ accent }};flex:none">⌕</span>
|
||
<sc-if value="{{ hasFilter }}" hint-placeholder-val="{{ false }}">
|
||
<span style="display:flex;align-items:center;gap:6px;padding:4px 9px;background:{{ accentDim }};border:1px solid {{ accent }};border-radius:7px;font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:{{ accent }};flex:none"><span style="width:8px;height:8px;background:{{ filterColor }};clip-path:{{ filterClip }};border-radius:{{ filterRadius }}"></span>{{ filterLabel }}<span style="color:#6b7890;font-size:11px">⌫</span></span>
|
||
</sc-if>
|
||
<input ref="{{ setInput }}" value="{{ query }}" onChange="{{ onSearchChange }}" onKeyDown="{{ onSearchKeydown }}" placeholder="{{ placeholder }}" style="flex:1;min-width:0;border:none;outline:none;background:transparent;color:#eef3fa;font-family:'IBM Plex Sans',sans-serif;font-size:18px"/>
|
||
<sc-if value="{{ ghost }}" hint-placeholder-val="{{ false }}">
|
||
<span style="display:flex;align-items:center;gap:7px;padding:4px 9px;border:1px dashed {{ accent }};border-radius:7px;font-family:'IBM Plex Mono',monospace;font-size:12px;color:{{ accent }};flex:none;white-space:nowrap">⇥ {{ ghost }}</span>
|
||
</sc-if>
|
||
<sc-if value="{{ sessionActive }}" hint-placeholder-val="{{ false }}">
|
||
<span style="display:flex;align-items:center;gap:6px;flex:none">
|
||
<sc-for list="{{ buildingViews }}" as="bv" hint-placeholder-count="1"><span style="display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:5px;background:{{ accent }};color:#0b0e13;font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:600">{{ bv }}</span></sc-for>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:#6b7890;white-space:nowrap">esc/idle places</span>
|
||
</span>
|
||
</sc-if>
|
||
</div>
|
||
<div style="display:flex;gap:6px;padding:0 16px 12px;flex-wrap:wrap;border-bottom:1px solid rgba(140,165,200,.1)">
|
||
<sc-for list="{{ chips }}" as="c" hint-placeholder-count="7">
|
||
<button onClick="{{ c.onClick }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:6px;border:1px solid {{ c.border }};background:{{ c.bg }};color:{{ c.fg }};font-family:'IBM Plex Mono',monospace;font-size:11px;padding:4px 9px;border-radius:7px;cursor:pointer">
|
||
<span style="width:8px;height:8px;background:{{ c.color }};clip-path:{{ c.clip }};border-radius:{{ c.radius }};flex:none"></span>{{ c.label }}
|
||
</button>
|
||
</sc-for>
|
||
</div>
|
||
<div style="max-height:48vh;overflow:auto;padding:8px">
|
||
<div style="font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.14em;color:#6b7890;text-transform:uppercase;padding:6px 9px 8px">{{ resultsHeader }}</div>
|
||
<sc-for list="{{ results }}" as="r" hint-placeholder-count="6">
|
||
<div onMouseEnter="{{ r.onHover }}" onMouseDown="{{ stopDown }}" onClick="{{ r.onClick }}" style="border-radius:9px;background:{{ r.bg }};margin-bottom:2px;cursor:pointer">
|
||
<div style="display:flex;align-items:center;gap:12px;padding:10px 11px">
|
||
<span style="width:16px;height:16px;background:{{ r.color }};clip-path:{{ r.clip }};border-radius:{{ r.radius }};flex:none;filter:drop-shadow(0 0 4px {{ r.glow }})"></span>
|
||
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:2px">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:13.5px;color:#e6edf6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ r.name }}</span>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:#7e8aa2">{{ r.sub }}</span>
|
||
</div>
|
||
<sc-if value="{{ r.isCrd }}" hint-placeholder-val="{{ false }}">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;color:#ffd479;border:1px solid rgba(255,212,121,.4);border-radius:5px;padding:2px 6px">CRD</span>
|
||
</sc-if>
|
||
<sc-if value="{{ r.showBadge }}" hint-placeholder-val="{{ false }}">
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;color:{{ accent }};border:1px solid {{ accentDim }};background:{{ accentDim }};border-radius:5px;padding:2px 6px;flex:none">{{ r.badgeLabel }}</span>
|
||
</sc-if>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:#8493ab;border:1px solid rgba(140,165,200,.2);border-radius:5px;padding:2px 7px;flex:none">{{ r.typeShort }}</span>
|
||
</div>
|
||
<sc-if value="{{ r.active }}" hint-placeholder-val="{{ false }}">
|
||
<div style="display:flex;align-items:center;gap:7px;padding:0 11px 11px 39px;flex-wrap:wrap">
|
||
<sc-if value="{{ r.enter }}" hint-placeholder-val="{{ false }}">
|
||
<span style="display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5px;background:{{ accent }};color:#0b0e13;font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600">⏎</span>
|
||
</sc-if>
|
||
<sc-for list="{{ r.views }}" as="v" hint-placeholder-count="4">
|
||
<button onClick="{{ v.onClick }}" onMouseDown="{{ stopDown }}" style="display:flex;align-items:center;gap:7px;border:1px solid {{ v.border }};background:{{ v.bg }};color:{{ v.fg }};font-family:'IBM Plex Mono',monospace;font-size:11.5px;padding:5px 10px 5px 5px;border-radius:8px;cursor:{{ v.cursor }};opacity:{{ v.opacity }}">
|
||
<span style="display:flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:5px;background:{{ v.kbdBg }};color:{{ v.kbdFg }};font-size:10px;font-weight:600">{{ v.letter }}</span>{{ v.label }}{{ v.mark }}
|
||
</button>
|
||
</sc-for>
|
||
<span style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:#5a6680;margin-left:2px">{{ r.viewHint }}</span>
|
||
</div>
|
||
</sc-if>
|
||
</div>
|
||
</sc-for>
|
||
<sc-if value="{{ noResults }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:24px 11px;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:12px;color:#6b7890">no objects match "{{ query }}"</div>
|
||
</sc-if>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:15px;padding:9px 16px;border-top:1px solid rgba(140,165,200,.1);background:rgba(10,13,19,.6);font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:#6b7890">
|
||
<span><span style="color:#9fb0c8">↑↓</span> pick</span>
|
||
<span><span style="color:#9fb0c8">{{ openKeysLabel }}</span> open views</span>
|
||
<span><span style="color:#9fb0c8">⏎</span> {{ enterLabel }}</span>
|
||
<span><span style="color:#9fb0c8">⇥</span> cycle type</span>
|
||
<span style="margin-left:auto"><span style="color:#9fb0c8">esc</span> done</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
</div>
|
||
</x-dc>
|
||
<script type="text/x-dc" data-dc-script data-props="{"accent":{"editor":"enum","options":["cyan","violet","amber"],"default":"cyan","tsType":"string"}}">
|
||
class Component extends DCLogic {
|
||
constructor(props){
|
||
super(props);
|
||
this.world = this.genWorld();
|
||
this.adj = {};
|
||
this.world.links.forEach(l=>{ (this.adj[l.from]=this.adj[l.from]||[]).push(l.to); (this.adj[l.to]=this.adj[l.to]||[]).push(l.from); });
|
||
this.byId = {}; this.world.nodes.forEach(n=>this.byId[n.id]=n);
|
||
this.state = { camX:140, camY:110, zoom:.92, flying:false, dragging:false, collapsed:false,
|
||
spotOpen:false, query:'', filterType:null, sel:0, navigated:false,
|
||
krates:[], krateSeq:0, session:null, logs:{}, sh:{}, relMenu:null, cardDrag:null, collSearch:{}, collView:{}, collSel:{}, collNav:{}, zoomedWin:null, admin:false };
|
||
this._logEls={}; this._stickLog={}; this._shEls={}; this._stickSh={}; this._shInputEls={}; this._hoverShWid=null; this._space=false;
|
||
this._collInputEls={}; this._hoverCollWid=null; this._typeUse={}; this._recentTypes=[]; this._hoverWinId=null; this._collCtx={}; this._collNavTimer={};
|
||
}
|
||
componentDidMount(){ this._onKey=(e)=>this.handleGlobalKey(e); this._onKeyUp=(e)=>{ if(e.key===' '){ this._space=false; if(this.state.spacePan) this.setState({spacePan:false}); } };
|
||
document.addEventListener('keydown', this._onKey); document.addEventListener('keyup', this._onKeyUp);
|
||
this._stream=setInterval(()=>this.appendLogs(), 1500); }
|
||
componentWillUnmount(){ document.removeEventListener('keydown', this._onKey); document.removeEventListener('keyup', this._onKeyUp); this.clearTimers(); clearInterval(this._stream); }
|
||
clearTimers(){ clearTimeout(this._arm); clearTimeout(this._idle); }
|
||
armSoon(){ clearTimeout(this._arm); this._arm=setTimeout(()=>{ if(this.state.spotOpen && !this.state.session && !this.state.navigated && this.buildResults(this.state.query,this.state.filterType).length){ this.setState({navigated:true}); } }, 460); }
|
||
armDismiss(){ clearTimeout(this._idle); this._idle=setTimeout(()=>{ if(this.state.spotOpen && this.state.session){ this.finishSpotlight(); } }, 520); }
|
||
componentDidUpdate(){ if(this._focus && this.inputEl){ this.inputEl.focus(); const v=this.inputEl.value; try{this.inputEl.setSelectionRange(v.length,v.length);}catch(e){} this._focus=false; }
|
||
Object.keys(this._logEls).forEach(wid=>{ const el=this._logEls[wid]; if(el && el.isConnected && this._stickLog[wid]!==false) el.scrollTop=el.scrollHeight; });
|
||
Object.keys(this._shEls).forEach(wid=>{ const el=this._shEls[wid]; if(el && el.isConnected && this._stickSh[wid]!==false) el.scrollTop=el.scrollHeight; });
|
||
if(this._focusCollWid){ const el=this._collInputEls[this._focusCollWid]; if(el && el.isConnected){ try{ el.focus(); }catch(e){} this._focusCollWid=null; } } }
|
||
|
||
// ---------- data ----------
|
||
genWorld(){
|
||
const nodes=[], links=[], namespaces=[]; let uid=0; const nid=(p)=>p+'-'+(uid++);
|
||
const defs=[
|
||
{ name:'payments', color:'#6fb1ff', primaries:[
|
||
{type:'deployment',name:'api-gateway',img:'krates/gateway:1.8.2',replicas:'3/3',children:[
|
||
{type:'pod',name:'api-gateway-7c4f-x2q'},{type:'pod',name:'api-gateway-7c4f-9kp'},{type:'pod',name:'api-gateway-7c4f-lph'},
|
||
{type:'configmap',name:'gateway-config'},{type:'secret',name:'gateway-tls',secret:{'tls.crt':'-----BEGIN CERTIFICATE-----','tls.key':'-----BEGIN PRIVATE KEY-----'}}]},
|
||
{type:'service',name:'api-gateway',relTo:'api-gateway',port:'443:8443'},
|
||
{type:'deployment',name:'ledger',img:'krates/ledger:2.3.0',replicas:'1/2',status:'Degraded',children:[
|
||
{type:'pod',name:'ledger-58d9-aa1'},{type:'pod',name:'ledger-58d9-bb2',status:'Pending'},
|
||
{type:'configmap',name:'ledger-config'},{type:'secret',name:'db-credentials',secret:{'username':'payments','password':'s3cr3t-pg!'}}]},
|
||
{type:'service',name:'ledger',relTo:'ledger',port:'80:8080'},
|
||
{type:'statefulset',name:'postgres',img:'postgres:16',replicas:'2/2',children:[
|
||
{type:'pod',name:'postgres-0'},{type:'pod',name:'postgres-1'},
|
||
{type:'pvc',name:'pgdata-postgres-0'},{type:'pvc',name:'pgdata-postgres-1'}]},
|
||
{type:'crd',name:'paymentroute/checkout',kindName:'PaymentRoute',relTo:'api-gateway'} ]},
|
||
{ name:'platform', color:'#5fd0c0', primaries:[
|
||
{type:'deployment',name:'auth',img:'krates/auth:4.1.1',replicas:'2/2',children:[
|
||
{type:'pod',name:'auth-9f7c-m1'},{type:'pod',name:'auth-9f7c-m2'},
|
||
{type:'configmap',name:'auth-config'},{type:'secret',name:'oauth-secret',secret:{'client_id':'krates-web','client_secret':'oa_7xK29fLp'}}]},
|
||
{type:'service',name:'auth',relTo:'auth',port:'80:9000'},
|
||
{type:'ingress',name:'platform-ingress',relTo:'auth'},
|
||
{type:'daemonset',name:'node-exporter',img:'prom/node-exporter:1.7',replicas:'5/5',children:[
|
||
{type:'pod',name:'node-exporter-tz4'},{type:'pod',name:'node-exporter-q8r'}]},
|
||
{type:'crd',name:'certificate/platform-tls',kindName:'Certificate',relTo:'platform-ingress'} ]},
|
||
{ name:'storefront', color:'#e58fb0', primaries:[
|
||
{type:'deployment',name:'web',img:'krates/web:7.0.4',replicas:'3/3',children:[
|
||
{type:'pod',name:'web-6b5c-aa'},{type:'pod',name:'web-6b5c-bb'},{type:'pod',name:'web-6b5c-cc'},
|
||
{type:'configmap',name:'web-config'}]},
|
||
{type:'service',name:'web',relTo:'web',port:'80:3000'},
|
||
{type:'deployment',name:'cart',img:'krates/cart:3.2.0',replicas:'2/2',children:[
|
||
{type:'pod',name:'cart-77d-aa'},{type:'pod',name:'cart-77d-bb'},
|
||
{type:'secret',name:'stripe-key',secret:{'STRIPE_SECRET':'sk_live_4eC39HqLy'}}]},
|
||
{type:'statefulset',name:'redis',img:'redis:7.2',replicas:'1/1',children:[
|
||
{type:'pod',name:'redis-0'},{type:'pvc',name:'redis-data-redis-0'}]} ]},
|
||
];
|
||
defs.forEach(d=>{ const nameMap={};
|
||
d.primaries.forEach(p=>{ const node={id:nid(p.type),type:p.type,name:p.name,ns:d.name,status:p.status||'Ready',data:p}; nodes.push(node); (nameMap[p.name]=nameMap[p.name]||[]).push(node); p._n=node;
|
||
(p.children||[]).forEach(c=>{ const cn={id:nid(c.type),type:c.type,name:c.name,ns:d.name,status:c.status||'Running',data:c}; nodes.push(cn); links.push({from:node.id,to:cn.id}); }); });
|
||
d.primaries.forEach(p=>{ if(!p.relTo) return; const tg=(nameMap[p.relTo]||[]).find(x=>x.id!==p._n.id); if(tg) links.push({from:p._n.id,to:tg.id}); });
|
||
namespaces.push({name:d.name,color:d.color});
|
||
});
|
||
return {nodes,links,namespaces};
|
||
}
|
||
meta(t){ const m={
|
||
deployment:{label:'Deployment',short:'deploy',color:'#6fb1ff',shape:'circle'},
|
||
statefulset:{label:'StatefulSet',short:'sts',color:'#b89cff',shape:'triangle'},
|
||
service:{label:'Service',short:'svc',color:'#5fd0c0',shape:'diamond'},
|
||
daemonset:{label:'DaemonSet',short:'ds',color:'#ffb27a',shape:'ring'},
|
||
ingress:{label:'Ingress',short:'ing',color:'#e58fb0',shape:'triangle'},
|
||
crd:{label:'Custom Resource',short:'crd',color:'#ffd479',shape:'hexagon',crd:true},
|
||
pod:{label:'Pod',short:'pod',color:'#8aa0bd',shape:'circle'},
|
||
configmap:{label:'ConfigMap',short:'cm',color:'#7fb39c',shape:'square'},
|
||
secret:{label:'Secret',short:'secret',color:'#c79bd6',shape:'hexagon'},
|
||
pvc:{label:'PVC',short:'pvc',color:'#9aa7c7',shape:'square'} };
|
||
return m[t]||{label:t,short:t,color:'#8aa0bd',shape:'circle'}; }
|
||
shapeCss(shape){ if(shape==='triangle') return {clip:'polygon(50% 0,100% 100%,0 100%)',radius:'0'};
|
||
if(shape==='diamond') return {clip:'polygon(50% 0,100% 50%,50% 100%,0 50%)',radius:'0'};
|
||
if(shape==='hexagon') return {clip:'polygon(25% 0,75% 0,100% 50%,75% 100%,25% 100%,0 50%)',radius:'0'};
|
||
if(shape==='square') return {clip:'none',radius:'3px'};
|
||
return {clip:'none',radius:'50%'}; }
|
||
statusColor(s){ return s==='Degraded'?'#ef6f6f':(s==='Pending'?'#e8b54a':(s==='Failed'?'#ef6f6f':'#4ad07a')); }
|
||
rgba(hex,a){ const h=hex.replace('#',''); const n=parseInt(h.length===3?h.split('').map(c=>c+c).join(''):h,16); return 'rgba('+((n>>16)&255)+','+((n>>8)&255)+','+(n&255)+','+a+')'; }
|
||
accentSet(){ const a=this.props.accent||'cyan'; const map={cyan:'#4dd6e8',violet:'#a98cff',amber:'#f5b454'}; const c=map[a]||map.cyan; return {accent:c,glow:this.rgba(c,.5),dim:this.rgba(c,.14)}; }
|
||
|
||
// ---------- views ----------
|
||
viewMeta(){ return {logs:{letter:'L',label:'logs'},shell:{letter:'S',label:'shell'},describe:{letter:'D',label:'describe'},yaml:{letter:'Y',label:'yaml'}}; }
|
||
viewOrder(){ return ['logs','shell','describe','yaml']; }
|
||
execable(type){ return ['pod','deployment','statefulset','daemonset'].includes(type); }
|
||
viewAvail(type,view){ if(view==='yaml'||view==='describe') return true; if(view==='logs'||view==='shell') return this.execable(type); return false; }
|
||
defaultView(type){ if(type==='pod') return 'logs'; if(['deployment','statefulset','daemonset'].includes(type)) return 'describe'; return 'yaml'; }
|
||
keyToView(k){ return ({l:'logs',s:'shell',d:'describe',y:'yaml'})[k]; }
|
||
codeLetter(e){ if(e.code && e.code.indexOf('Key')===0) return e.code.slice(3).toLowerCase(); const k=(e.key||''); return k.length===1?k.toLowerCase():''; }
|
||
|
||
// ---------- search ----------
|
||
aliases(){ return [
|
||
{k:['deploy','deployment','deployments','dep'],t:'deployment'},{k:['sts','statefulset','statefulsets'],t:'statefulset'},
|
||
{k:['svc','service','services'],t:'service'},{k:['ds','daemonset','daemonsets'],t:'daemonset'},
|
||
{k:['ing','ingress','ingresses'],t:'ingress'},{k:['crd','customresource','cr'],t:'crd'},
|
||
{k:['po','pod','pods'],t:'pod'},{k:['cm','configmap','configmaps','config'],t:'configmap'},
|
||
{k:['secret','secrets','sealed'],t:'secret'},{k:['pvc','pv','volume','volumes'],t:'pvc'} ]; }
|
||
suggestType(q,filter){ if(filter) return null; const s=q.trim().toLowerCase(); if(s.length<2) return null;
|
||
for(const a of this.aliases()){ for(const k of a.k){ if(k.startsWith(s)||s===k) return a.t; } } return null; }
|
||
fuzzy(q,text){ if(!q) return 0; q=q.toLowerCase(); const t=text.toLowerCase(); let qi=0,score=0,prev=-2;
|
||
for(let i=0;i<t.length && qi<q.length;i++){ if(t[i]===q[qi]){ let pts=1; if(i===prev+1) pts+=3; if(i===0||/[^a-z0-9]/.test(t[i-1])) pts+=5; score+=pts; prev=i; qi++; } }
|
||
return qi===q.length?score:-1; }
|
||
boost(t){ const m={crd:60,deployment:50,statefulset:48,service:46,daemonset:44,ingress:42,secret:24,configmap:22,pvc:18,pod:16}; return m[t]||10; }
|
||
// ---------- collections ----------
|
||
catMembers(type){ return this.world.nodes.filter(n=>n.type===type); }
|
||
nsMembers(ns){ return this.world.nodes.filter(n=>n.ns===ns); }
|
||
collMembers(scope){ return scope.kind==='namespace'? this.nsMembers(scope.value) : this.catMembers(scope.value); }
|
||
collTitle(scope){ return scope.kind==='namespace'? ('ns/'+scope.value) : ('All '+this.meta(scope.value).label+'s'); }
|
||
statusSummary(nodes){ const c={}; nodes.forEach(n=>{ c[n.status]=(c[n.status]||0)+1; }); const ok=(c.Ready||0)+(c.Running||0); const warn=(c.Pending||0); const bad=(c.Degraded||0)+(c.Failed||0); return {ok,warn,bad,total:nodes.length}; }
|
||
healthMetric(n){ const d=n.data||{}; if(d.replicas) return d.replicas+' ready'; if(n.type==='pod') return 'restarts '+(n.name.length%4); if(n.type==='pvc') return '20Gi · gp3'; if(n.type==='service') return d.port||'ClusterIP'; if(n.type==='secret') return 'Opaque · '+Object.keys(d.secret||{}).length+' keys'; if(n.type==='configmap') return '3 keys'; if(n.type==='ingress') return n.ns+'.krates.io'; return this.meta(n.type).label; }
|
||
collTreeByType(members){ const order=['deployment','statefulset','daemonset','service','ingress','crd','pod','configmap','secret','pvc']; const groups=[];
|
||
order.forEach(t=>{ const list=members.filter(n=>n.type===t); if(list.length) groups.push({type:t, label:this.meta(t).label+'s', nodes:list}); });
|
||
const seen=new Set(order); members.forEach(n=>{ if(!seen.has(n.type)){ seen.add(n.type); groups.push({type:n.type, label:this.meta(n.type).label+'s', nodes:members.filter(x=>x.type===n.type)}); } });
|
||
return groups; }
|
||
// k9s-xray style relationship tree: each object expands to the resources it references
|
||
xrayRel(a,b){ if(b.type==='pod') return 'pod'; if(b.type==='configmap') return 'configMap'; if(b.type==='secret') return 'secret'; if(b.type==='pvc') return 'volume'; if(a.type==='service') return 'selects'; if(a.type==='ingress') return 'routes'; if(a.type==='crd') return 'targets'; return 'ref'; }
|
||
buildXray(scope, members){ const isPrim=(n)=>['deployment','statefulset','daemonset','service','ingress','crd'].includes(n.type); const isCfg=(n)=>['configmap','secret','pvc'].includes(n.type);
|
||
const neigh=(id)=>(this.adj[id]||[]).map(x=>this.byId[x]); const workloadOf=(pod)=> neigh(pod.id).find(x=>isPrim(x)); const podsOf=(p)=> neigh(p.id).filter(x=>x.type==='pod'); const cfgOf=(p)=> neigh(p.id).filter(isCfg); const rows=[];
|
||
const push=(n,depth,rel)=>rows.push({node:n, depth:depth, rel:rel||''});
|
||
const expandWorkload=(p,d)=>{ push(p,d,''); podsOf(p).forEach(pod=>{ push(pod,d+1,'pod'); cfgOf(p).forEach(c=>push(c,d+2,this.xrayRel(p,c))); }); if(podsOf(p).length===0) cfgOf(p).forEach(c=>push(c,d+1,this.xrayRel(p,c))); };
|
||
if(scope.kind==='namespace'){ members.filter(isPrim).forEach(p=>{ if(p.type==='service'||p.type==='ingress'){ push(p,0,''); neigh(p.id).filter(isPrim).forEach(t=>push(t,1,this.xrayRel(p,t))); } else expandWorkload(p,0); }); }
|
||
else { const t=scope.value;
|
||
if(t==='pod'){ members.forEach(pod=>{ push(pod,0,''); const w=workloadOf(pod); if(w) cfgOf(w).forEach(c=>push(c,1,this.xrayRel(w,c))); }); }
|
||
else if(isPrim({type:t})){ members.forEach(p=>{ if(t==='service'||t==='ingress'){ push(p,0,''); neigh(p.id).filter(isPrim).forEach(tg=>push(tg,1,this.xrayRel(p,tg))); } else expandWorkload(p,0); }); }
|
||
else if(isCfg({type:t})){ members.forEach(c=>{ push(c,0,''); neigh(c.id).filter(isPrim).forEach(w=>{ push(w,1,'used by'); podsOf(w).forEach(pod=>push(pod,2,'pod')); }); }); }
|
||
else { members.forEach(n=>push(n,0,'')); } }
|
||
return rows; }
|
||
|
||
buildResults(query,filter){ const q=query.trim(); const items=[];
|
||
if(!filter){ this.state.krates.forEach(k=>{ let s; if(!q){ s=72; } else { const f=Math.max(this.fuzzy(q,k.label), this.fuzzy(q,'krate '+k.label)); if(f<0) return; s=f*10+66; } items.push({kind:'krate', id:k.id, krate:k, score:s}); }); }
|
||
if(filter){ const mem=this.catMembers(filter); items.push({kind:'category', id:'cat:'+filter, scope:{kind:'category',value:filter}, count:mem.length, score:1000}); }
|
||
if(!filter){ this.world.namespaces.forEach(ns=>{ const f=q? this.fuzzy(q, 'namespace '+ns.name): 30; const fn=q?this.fuzzy(q,ns.name):30; const sc=Math.max(f,fn); if(q && sc<0) return; items.push({kind:'namespace', id:'ns:'+ns.name, scope:{kind:'namespace',value:ns.name}, count:this.nsMembers(ns.name).length, color:ns.color, score:(q?sc*10+58:58)}); });
|
||
[['pod','pods'],['service','services'],['deployment','deployments'],['secret','secrets'],['statefulset','statefulsets'],['configmap','configmaps'],['ingress','ingresses'],['daemonset','daemonsets'],['pvc','pvcs'],['crd','crds']].forEach(([t,word])=>{ if(!q) return; const f=Math.max(this.fuzzy(q,'all '+word), this.fuzzy(q,word)); if(f<0) return; items.push({kind:'category', id:'cat:'+t, scope:{kind:'category',value:t}, count:this.catMembers(t).length, score:f*10+60}); }); }
|
||
let pool=this.world.nodes.slice(); if(filter) pool=pool.filter(n=>n.type===filter);
|
||
if(!q){ pool.forEach(n=>items.push({kind:'node', id:n.id, node:n, type:n.type, score:this.boost(n.type)})); }
|
||
else { pool.forEach(n=>{ const f=this.fuzzy(q,n.name); if(f>=0) items.push({kind:'node', id:n.id, node:n, type:n.type, score:f*10+this.boost(n.type)}); }); }
|
||
items.sort((a,b)=> b.score-a.score); return items.slice(0,8); }
|
||
|
||
// ---------- camera ----------
|
||
centerOn(wx,wy,z,fly){ const el=this.rootEl; const W=el?el.clientWidth:1280,H=el?el.clientHeight:760; const zoom=z||this.state.zoom;
|
||
this.setState({zoom, camX:W/2-wx*zoom, camY:H/2-wy*zoom, flying:!!fly, collapsed: zoom<0.48? this.state.collapsed : false});
|
||
if(fly){ clearTimeout(this._ft); this._ft=setTimeout(()=>this.setState({flying:false}),520); } }
|
||
onWheel=(e)=>{ e.preventDefault(); const el=this.rootEl; const rect=el.getBoundingClientRect(); const mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||
if(e.ctrlKey||e.metaKey){ const z0=this.state.zoom; let z1=z0*(1-e.deltaY*0.0042); z1=Math.max(.16,Math.min(2.2,z1));
|
||
const wx=(mx-this.state.camX)/z0, wy=(my-this.state.camY)/z0; let collapsed=this.state.collapsed;
|
||
if(z1<0.4) collapsed=true; else if(z1>0.5) collapsed=false;
|
||
this.setState({zoom:z1, camX:mx-wx*z1, camY:my-wy*z1, flying:false, collapsed}); return; }
|
||
this.setState(s=>({camX:s.camX - e.deltaX, camY:s.camY - e.deltaY, flying:false})); };
|
||
beginPan(e, canToggle){ const sx=e.clientX,sy=e.clientY,c0x=this.state.camX,c0y=this.state.camY; let moved=false;
|
||
const move=(ev)=>{ const dx=ev.clientX-sx,dy=ev.clientY-sy; if(Math.abs(dx)+Math.abs(dy)>4){ if(!moved){moved=true;this.setState({dragging:true});} this.setState({camX:c0x+dx,camY:c0y+dy,flying:false}); } };
|
||
const up=()=>{ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up); if(this.state.dragging) this.setState({dragging:false});
|
||
if(!moved && canToggle && !this._space){ if(!this.state.spotOpen) this.openSpot(''); else this.setState({spotOpen:false,session:null}); } };
|
||
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); }
|
||
onRootDownCapture=(e)=>{ if(this._space && e.button===0){ e.preventDefault(); e.stopPropagation(); if(this.state.relMenu) this.setState({relMenu:null}); this.beginPan(e, false); } };
|
||
onBgDown=(e)=>{ if(e.button!==0) return; if(this.state.relMenu){ this.setState({relMenu:null}); return; } this.beginPan(e, true); };
|
||
|
||
// ---------- spotlight ----------
|
||
handleGlobalKey(e){ const tag=(e.target&&e.target.tagName)||''; if(tag==='INPUT'||tag==='TEXTAREA') return;
|
||
const shEl=this._hoverShWid && this._shInputEls[this._hoverShWid];
|
||
if(shEl && !this.state.spotOpen && !e.metaKey && !e.ctrlKey && !e.altKey){ if(e.key!==' '){ if(e.key.length===1){ e.preventDefault(); shEl.focus(); shEl.value+=e.key; return; } if(e.key==='Backspace'){ shEl.focus(); return; } if(e.key==='Enter'){ return; } } }
|
||
const ciEl=this._hoverCollWid && this._collInputEls[this._hoverCollWid];
|
||
if(ciEl && !this.state.spotOpen && !e.metaKey && !e.ctrlKey){ if(e.key!==' '){ if(this.collKey(this._hoverCollWid, e)){ if(!e.altKey) ciEl.focus(); return; } } }
|
||
if(this._hoverWinId && !this._hoverShWid && !this._hoverCollWid && !this.state.spotOpen && !e.metaKey && !e.ctrlKey && !e.altKey && e.key.toLowerCase()==='z'){ e.preventDefault(); this.zoomToggleWindow(this._hoverWinId.kid, this._hoverWinId.wid); return; }
|
||
if(e.key===' '){ this._space=true; if(!this.state.spacePan) this.setState({spacePan:true}); if(!this.state.spotOpen) e.preventDefault(); return; }
|
||
if(this.state.spotOpen) return;
|
||
if(e.metaKey||e.ctrlKey||e.altKey) return; if(e.key==='Escape') return;
|
||
if(e.key==='/'){ e.preventDefault(); this.openSpot(''); } else if(e.key.length===1){ this.openSpot(e.key); } }
|
||
openSpot(initial){ this._focus=true; this._sessionKid=null; this.clearTimers(); this.setState({spotOpen:true, query:initial, sel:0, filterType:null, navigated:false, session:null}); this.armSoon(); }
|
||
filterTypeOrder(){ const all=['deployment','service','pod','secret','configmap','statefulset','daemonset','ingress','crd','pvc'];
|
||
const rec=this._recentTypes||[], use=this._typeUse||{};
|
||
return all.slice().sort((a,b)=>{ const ra=rec.indexOf(a), rb=rec.indexOf(b); const va=ra<0?999:ra, vb=rb<0?999:rb; if(va!==vb) return va-vb; return (use[b]||0)-(use[a]||0); }); }
|
||
recordType(t){ if(!t) return; this._typeUse=this._typeUse||{}; this._typeUse[t]=(this._typeUse[t]||0)+1; this._recentTypes=[t].concat((this._recentTypes||[]).filter(x=>x!==t)).slice(0,10); }
|
||
cycleFilter(dir){ const seq=[null].concat(this.filterTypeOrder()); const cur=this.state.filterType; let i=seq.indexOf(cur); if(i<0) i=0; const ni=(i+dir+seq.length)%seq.length; const nt=seq[ni]; this._focus=true; this.setState({filterType:nt, sel:0, navigated:false}); }
|
||
onSearchChange=(e)=>{ this.setState({query:e.target.value, sel:0, navigated:false}); this.armSoon(); };
|
||
onSearchKeydown=(e)=>{ const res=this.buildResults(this.state.query,this.state.filterType);
|
||
if(e.key==='Escape'){ e.preventDefault(); this.finishOrClose(); return; }
|
||
if(this.state.session) this.armDismiss();
|
||
if(e.key==='ArrowDown'){ e.preventDefault(); this.setState(s=>({sel:Math.min(res.length-1,s.sel+1),navigated:true})); return; }
|
||
if(e.key==='ArrowUp'){ e.preventDefault(); this.setState(s=>({sel:Math.max(0,s.sel-1),navigated:true})); return; }
|
||
if(e.key==='Tab'){ e.preventDefault(); const sug=this.suggestType(this.state.query,this.state.filterType);
|
||
if(sug && !this.state.filterType && !e.shiftKey){ this.recordType(sug); this._focus=true; this.setState({filterType:sug, query:'', sel:0, navigated:false}); }
|
||
else { this.cycleFilter(e.shiftKey?-1:1); } return; }
|
||
if(e.key==='Backspace' && this.state.query==='' && this.state.filterType){ e.preventDefault(); this.setState({filterType:null,sel:0}); return; }
|
||
if(e.key==='Enter'){ e.preventDefault(); if(this.state.session){ this.finishSpotlight(); } else { const r=res[this.state.sel]; if(r){ if(r.kind==='krate'){ this.jumpToKrate(r.id); } else if(r.kind==='category'||r.kind==='namespace'){ this.summonCollection(r.scope); } else { this.openView(r.id,this.defaultView(r.type),true); } } } return; }
|
||
if(e.altKey && !e.ctrlKey && !e.metaKey){ const v=this.keyToView(this.codeLetter(e)); if(v){ const r=res[this.state.sel]; if(r && r.kind==='node' && this.viewAvail(r.type,v)){ e.preventDefault(); this.openView(r.id,v,false); return; } } } };
|
||
onSpotBackdrop=()=>this.finishOrClose();
|
||
finishOrClose(){ this.clearTimers(); if(this.state.session) this.finishSpotlight(); else this.setState({spotOpen:false}); }
|
||
|
||
// ---------- krates / windows ----------
|
||
placeKrate(){ const ks=this.state.krates; if(!ks.length) return {cx:700, cy:480};
|
||
const last=ks[ks.length-1]; const lb=this.krateBox(last); return {cx:last.wx+lb.x+lb.w+360, cy:last.wy}; }
|
||
gridCols(n){ return n<=1?1:(n<=4?2:3); }
|
||
tileWindows(wins){ const gap=14; const N=wins.length; if(!N) return wins; const C=this.gridCols(N); const rows=Math.ceil(N/C);
|
||
const colW=[], rowH=[]; wins.forEach((w,i)=>{ const c=i%C, r=Math.floor(i/C); colW[c]=Math.max(colW[c]||300, w.w); rowH[r]=Math.max(rowH[r]||220, w.h); });
|
||
const colX=[]; let ax=0; for(let c=0;c<C;c++){ colX[c]=ax; ax+=colW[c]+gap; }
|
||
const rowY=[]; let ay=0; for(let r=0;r<rows;r++){ rowY[r]=ay; ay+=rowH[r]+gap; }
|
||
return wins.map((w,i)=>{ const c=i%C, r=Math.floor(i/C); return Object.assign({},w,{dx:colX[c], dy:rowY[r], w:colW[c], h:rowH[r], col:c, row:r}); }); }
|
||
fitBox(x,y,w,h,maxZ){ const el=this.rootEl; const W=el?el.clientWidth:1280,H=el?el.clientHeight:760; const pad=190; let z=Math.min((W-pad)/w,(H-pad)/h, maxZ||1.0); z=Math.max(0.22,Math.min(2.0,z)); this.centerOn(x+w/2, y+h/2, z, true); }
|
||
fitKrate(kid){ const k=this.state.krates.find(x=>x.id===kid); if(!k) return; const b=this.krateBox(k); this.fitBox(k.wx+b.x, k.wy+b.y-44, b.w, b.h+44, 1.0); }
|
||
krateBox(k){ if(!k.windows.length) return {x:0,y:0,w:380,h:360};
|
||
let a=1e9,b=1e9,c=-1e9,d=-1e9; k.windows.forEach(w=>{ a=Math.min(a,w.dx); b=Math.min(b,w.dy); c=Math.max(c,w.dx+w.w); d=Math.max(d,w.dy+w.h); });
|
||
return {x:a,y:b,w:c-a,h:d-b}; }
|
||
openView(objId, view, finish){
|
||
const s=this.state; let session = (s.session && s.session.objId===objId) ? s.session : null;
|
||
let krates=s.krates.map(k=>Object.assign({},k,{windows:k.windows.slice()})); let krateSeq=s.krateSeq;
|
||
let krate = session ? krates.find(k=>k.id===session.kid) : null;
|
||
if(!krate){ const p=this.placeKrate(); const n=this.byId[objId];
|
||
krate={id:'k'+krateSeq, objId, label:n.name, status:n.status, color:(this.world.namespaces.find(x=>x.name===n.ns)||{}).color||'#6fb1ff', wx:p.cx, wy:p.cy, windows:[], seq:0};
|
||
krateSeq++; krates.push(krate); session={kid:krate.id, objId, views:[]}; }
|
||
if(!krate.windows.find(w=>(w.objId||krate.objId)===objId && w.tab===view)){ let wl=krate.windows.slice();
|
||
wl.push({wid:krate.id+'-'+(krate.seq++), objId:objId, tab:view, dx:0, dy:0, w:380, h:360}); krate.windows=this.tileWindows(wl); }
|
||
session=Object.assign({},session,{views:Array.from(new Set(session.views.concat([view])))});
|
||
this._sessionKid=krate.id;
|
||
this.setState({krates, krateSeq, session}, ()=>{ if(finish){ this.finishSpotlight(); } else { this.armDismiss(); } });
|
||
}
|
||
finishSpotlight(){ this.clearTimers(); const kid=this._sessionKid; this.setState({spotOpen:false, session:null, navigated:false}, ()=>this.fitKrate(kid)); this._sessionKid=null; }
|
||
flyToKrate(kid){ this.fitKrate(kid); }
|
||
jumpToKrate(kid){ this.clearTimers(); this.setState({spotOpen:false, session:null, navigated:false}, ()=>this.fitKrate(kid)); }
|
||
cardLayout(){ const el=this.rootEl; const W=el?el.clientWidth:1280; const cardW=230, cardH=132, gap=20; const cols=Math.max(1, Math.floor((W-72+gap)/(cardW+gap)));
|
||
const n=this.state.krates.length; const span=Math.min(n||1,cols); const contentW=span*cardW+(span-1)*gap; const startX=Math.max(36,(W-contentW)/2); const startY=96; return {cardW,cardH,gap,cols,startX,startY}; }
|
||
cardSlotAt(x,y){ const L=this.cardLayout(); const col=Math.round((x-L.startX)/(L.cardW+L.gap)); const row=Math.round((y-L.startY)/(L.cardH+L.gap)); const cc=Math.max(0,Math.min(L.cols-1,col)); const idx=row*L.cols+cc; return Math.max(0,Math.min(this.state.krates.length-1,idx)); }
|
||
reorderKrate(kid,ti){ this.setState(s=>{ const arr=s.krates.slice(); const from=arr.findIndex(k=>k.id===kid); if(from<0) return null; ti=Math.max(0,Math.min(arr.length-1,ti)); if(from===ti) return null; const it=arr.splice(from,1)[0]; arr.splice(ti,0,it); return {krates:arr}; }); }
|
||
startCardDrag(kid,e){ e.stopPropagation(); if(e.button!==0) return; const sx=e.clientX,sy=e.clientY; let moved=false;
|
||
const move=(ev)=>{ const dx=ev.clientX-sx,dy=ev.clientY-sy; if(!moved && Math.abs(dx)+Math.abs(dy)>5) moved=true; if(moved){ const ti=this.cardSlotAt(ev.clientX,ev.clientY); this.reorderKrate(kid,ti); this.setState({cardDrag:{id:kid, mx:ev.clientX, my:ev.clientY}}); } };
|
||
const up=()=>{ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up); this.setState({cardDrag:null}); };
|
||
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); }
|
||
focusWindow(kid,wid){ const k=this.state.krates.find(x=>x.id===kid); const w=k&&k.windows.find(x=>x.wid===wid); if(!w) return; this.fitBox(k.wx+w.dx, k.wy+w.dy-44, w.w, w.h+44, 1.35); }
|
||
closeWindow(kid,wid){ this.setState(s=>{ let krates=s.krates.map(k=>{ if(k.id!==kid) return k; return Object.assign({},k,{windows:this.tileWindows(k.windows.filter(w=>w.wid!==wid))}); }); krates=krates.filter(k=>k.windows.length>0); return {krates}; }); }
|
||
startWinResize(kid,wid,e){ e.stopPropagation(); const sx=e.clientX,sy=e.clientY; const k=this.state.krates.find(x=>x.id===kid); const w=k&&k.windows.find(x=>x.wid===wid); if(!w) return; const w0=w.w,h0=w.h,z=this.state.zoom;
|
||
const move=(ev)=>{ const nw=Math.max(300, w0+(ev.clientX-sx)/z), nh=Math.max(220, h0+(ev.clientY-sy)/z); this.setState(s=>({krates:s.krates.map(kk=>kk.id!==kid?kk:Object.assign({},kk,{windows:this.tileWindows(kk.windows.map(ww=>ww.wid===wid?Object.assign({},ww,{w:nw,h:nh}):ww))}))})); };
|
||
const up=()=>{ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up); };
|
||
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); }
|
||
closeKrate(kid){ this.setState(s=>({krates:s.krates.filter(k=>k.id!==kid)})); }
|
||
setTab(kid,wid,tab){ this.setState(s=>({krates:s.krates.map(k=>k.id!==kid?k:Object.assign({},k,{windows:k.windows.map(w=>w.wid===wid?Object.assign({},w,{tab}):w)}))})); }
|
||
startWinDrag(kid,wid,e){ e.stopPropagation(); const sx=e.clientX,sy=e.clientY; const k=this.state.krates.find(x=>x.id===kid); const w=k&&k.windows.find(x=>x.wid===wid); if(!w) return; const x0=w.dx,y0=w.dy,z=this.state.zoom;
|
||
const move=(ev)=>{ const ndx=x0+(ev.clientX-sx)/z, ndy=y0+(ev.clientY-sy)/z; this.setState(s=>({krates:s.krates.map(kk=>kk.id!==kid?kk:Object.assign({},kk,{windows:kk.windows.map(ww=>ww.wid===wid?Object.assign({},ww,{dx:ndx,dy:ndy}):ww)}))})); };
|
||
const up=()=>{ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up); };
|
||
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); }
|
||
startKrateDrag(kid,e){ e.stopPropagation(); const sx=e.clientX,sy=e.clientY; const k=this.state.krates.find(x=>x.id===kid); if(!k) return; const x0=k.wx,y0=k.wy,z=this.state.zoom;
|
||
const move=(ev)=>{ const nx=x0+(ev.clientX-sx)/z, ny=y0+(ev.clientY-sy)/z; this.setState(s=>({krates:s.krates.map(kk=>kk.id!==kid?kk:Object.assign({},kk,{wx:nx,wy:ny}))})); };
|
||
const up=()=>{ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up); this.resolveKrateOverlaps(kid); };
|
||
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); }
|
||
krateFootprint(k){ if(k.collapsed){ return {x:k.wx, y:k.wy-44, w:300, h:104}; } const b=this.krateBox(k); return {x:k.wx+b.x-30, y:k.wy+b.y-72, w:b.w+60, h:b.h+102}; }
|
||
resolveKrateOverlaps(activeKid){ this.setState(s=>{ const ks=s.krates.map(k=>Object.assign({},k)); const exp=(k)=>{ const f=this.krateFootprint(k); return {x:f.x-14,y:f.y-14,w:f.w+28,h:f.h+28}; };
|
||
for(let it=0; it<80; it++){ let moved=false;
|
||
for(let i=0;i<ks.length;i++) for(let j=i+1;j<ks.length;j++){ const A=exp(ks[i]), B=exp(ks[j]);
|
||
const ox=Math.min(A.x+A.w,B.x+B.w)-Math.max(A.x,B.x); const oy=Math.min(A.y+A.h,B.y+B.h)-Math.max(A.y,B.y);
|
||
if(ox>0 && oy>0){ moved=true; if(ox<oy){ const dir=((A.x+A.w/2)<=(B.x+B.w/2))?1:-1; if(ks[i].id===activeKid){ ks[j].wx+=dir*ox; } else if(ks[j].id===activeKid){ ks[i].wx-=dir*ox; } else { ks[i].wx-=dir*ox/2; ks[j].wx+=dir*ox/2; } }
|
||
else { const dir=((A.y+A.h/2)<=(B.y+B.h/2))?1:-1; if(ks[i].id===activeKid){ ks[j].wy+=dir*oy; } else if(ks[j].id===activeKid){ ks[i].wy-=dir*oy; } else { ks[i].wy-=dir*oy/2; ks[j].wy+=dir*oy/2; } } } }
|
||
if(!moved) break; }
|
||
ks.forEach(k=>{ k.wx=Math.round(k.wx/20)*20; k.wy=Math.round(k.wy/20)*20; });
|
||
return {krates:ks}; }); }
|
||
toggleCollapseKrate(kid){ this.setState(s=>({krates:s.krates.map(k=>k.id===kid?Object.assign({},k,{collapsed:!k.collapsed}):k), zoomedWin:null}), ()=>this.resolveKrateOverlaps(kid)); }
|
||
zoomToggleWindow(kid,wid){ const k=this.state.krates.find(x=>x.id===kid); const w=k&&k.windows.find(x=>x.wid===wid); if(!w) return;
|
||
if(this.state.zoomedWin===wid){ const ps=this._prevSize&&this._prevSize[wid];
|
||
this.setState(s=>({zoomedWin:null, krates:s.krates.map(kk=>kk.id!==kid?kk:Object.assign({},kk,{windows:this.tileWindows(kk.windows.map(ww=>(ww.wid===wid&&ps)?Object.assign({},ww,{w:ps.w,h:ps.h}):ww))}))}), ()=>this.fitKrate(kid)); }
|
||
else { const el=this.rootEl; const W=el?el.clientWidth:1280,H=el?el.clientHeight:760; const sideM=22, topInset=68, botInset=66; const nw=Math.max(360,W-2*sideM), nh=Math.max(260,H-topInset-botInset);
|
||
this._prevSize=this._prevSize||{}; this._prevSize[wid]={w:w.w,h:w.h};
|
||
this.setState(s=>({zoomedWin:wid, krates:s.krates.map(kk=>kk.id!==kid?kk:Object.assign({},kk,{windows:kk.windows.map(ww=>ww.wid===wid?Object.assign({},ww,{w:nw,h:nh}):ww)}))}), ()=>{ const k2=this.state.krates.find(x=>x.id===kid); const w2=k2&&k2.windows.find(x=>x.wid===wid); if(w2){ const camX=sideM-(k2.wx+w2.dx), camY=topInset-(k2.wy+w2.dy); this.setState({zoom:1, camX, camY, flying:true}); clearTimeout(this._ft); this._ft=setTimeout(()=>this.setState({flying:false}),520); } }); } }
|
||
mmJump(e){ const r=this._mmEl?this._mmEl.getBoundingClientRect():null; if(!r||!this._mm) return; const mx=e.clientX-r.left, my=e.clientY-r.top; const wx=this._mm.bx+mx/this._mm.scale, wy=this._mm.by+my/this._mm.scale; this.centerOn(wx,wy,this.state.zoom,true); }
|
||
nodeYaml(n){ const d=n.data||{};
|
||
if(n.type==='secret'){ let s='apiVersion: v1\nkind: Secret\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\ntype: Opaque\ndata: # decoded\n'; const sec=d.secret||{}; Object.keys(sec).forEach(k=>{ s+=' '+k+': '+sec[k]+'\n'; }); return s; }
|
||
if(n.type==='configmap'){ return 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\ndata:\n LOG_LEVEL: info\n TIMEOUT: "30s"\n REGION: eu-west-1'; }
|
||
if(n.type==='pvc'){ return 'apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\nspec:\n accessModes: [ReadWriteOnce]\n resources:\n requests:\n storage: 20Gi\n storageClassName: gp3'; }
|
||
if(n.type==='pod'){ return 'apiVersion: v1\nkind: Pod\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\nspec:\n containers:\n - name: app\n image: '+(d.img||'krates/app:latest')+'\nstatus:\n phase: '+n.status; }
|
||
if(n.type==='service'){ return 'apiVersion: v1\nkind: Service\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\nspec:\n type: ClusterIP\n selector:\n app: '+(d.relTo||n.name)+'\n ports:\n - port: '+((d.port||'80:80').split(':')[0])+'\n targetPort: '+((d.port||'80:80').split(':')[1]); }
|
||
if(n.type==='ingress'){ return 'apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\nspec:\n rules:\n - host: '+n.ns+'.krates.io\n http:\n paths:\n - path: /\n backend:\n service:\n name: '+(d.relTo||'app'); }
|
||
if(n.type==='crd'){ return 'apiVersion: krates.io/v1\nkind: '+(d.kindName||'CustomResource')+'\nmetadata:\n name: '+n.name.split('/').pop()+'\n namespace: '+n.ns+'\nspec:\n target: '+(d.relTo||'')+'\nstatus:\n phase: Active'; }
|
||
const kind=n.type==='statefulset'?'StatefulSet':(n.type==='daemonset'?'DaemonSet':'Deployment');
|
||
return 'apiVersion: apps/v1\nkind: '+kind+'\nmetadata:\n name: '+n.name+'\n namespace: '+n.ns+'\nspec:\n replicas: '+((d.replicas||'1/1').split('/')[1])+'\n template:\n spec:\n containers:\n - name: '+n.name+'\n image: '+(d.img||'krates/app:latest')+'\nstatus:\n readyReplicas: '+((d.replicas||'1/1').split('/')[0]); }
|
||
nodeDescribe(n){ const m=this.meta(n.type); const d=n.data||{};
|
||
let s='Name: '+n.name+'\nNamespace: '+n.ns+'\nKind: '+m.label+'\nStatus: '+n.status+'\n';
|
||
if(d.img) s+='Image: '+d.img+'\n'; if(d.replicas) s+='Replicas: '+d.replicas+' ready\n'; if(d.port) s+='Port: '+d.port+'\n';
|
||
s+='Node: ip-10-0-'+(3+(n.name.length%6))+'-'+(12+(n.name.length%40))+'\nAge: '+(2+(n.name.length%18))+'d\nLabels: app='+n.name.split(/[-/]/)[0]+'\n';
|
||
const nb=(this.adj[n.id]||[]).map(id=>this.byId[id]).filter(Boolean); s+='\nRelated ('+nb.length+'):\n'+nb.map(x=>' → '+this.meta(x.type).short+'/'+x.name).join('\n'); return s; }
|
||
nodeLogs(n){ if(!this.execable(n.type)) return 'No logs for '+this.meta(n.type).label+' objects.';
|
||
const base=n.name.split(/[-/]/)[0]; const t=(i)=>'09:'+(12+i)+':'+((10+i*7)%60<10?'0':'')+((10+i*7)%60);
|
||
return [t(0)+' INFO '+base+' starting, build '+(n.data&&n.data.img||'dev'),t(1)+' INFO listening on :8080',t(2)+' INFO connected to upstream postgres.payments',t(3)+' WARN retry budget at 82%',t(4)+' INFO GET /healthz 200 1ms',t(5)+' INFO reconciled 14 routes',t(6)+' '+(n.status==='Degraded'?'ERROR liveness probe failed (3/3)':'INFO request 7f3a 200 23ms'),t(7)+' INFO GET /v1/orders 200 14ms'].join('\n'); }
|
||
nodeShell(n){ if(!this.execable(n.type)) return 'Exec not available for '+this.meta(n.type).label+'.';
|
||
return '$ kubectl exec -it '+n.name+' -n '+n.ns+' -- sh\n/ # whoami\nroot\n/ # ls /app\nbin config data server\n/ # cat /app/config/region\neu-west-1\n/ # \u2588'; }
|
||
winBody(n,tab){ if(tab==='describe') return this.nodeDescribe(n); if(tab==='yaml') return this.nodeYaml(n); return ''; }
|
||
|
||
// ---------- live logs ----------
|
||
seedLogs(n){ const base=n.name.split(/[-/]/)[0]; const T=(i)=>'09:'+(40+Math.floor(i/60))+':'+((i%60)<10?'0':'')+(i%60); let i=0;
|
||
return [T(i++)+' INFO '+base+' starting, build '+((n.data&&n.data.img)||'dev'), T(i+=7)+' INFO config loaded from /app/config', T(i+=4)+' INFO listening on :8080', T(i+=6)+' INFO connected to postgres.payments', T(i+=5)+' INFO GET /healthz 200 1ms', T(i+=9)+' INFO reconciled 14 routes', T(i+=3)+(n.status==='Degraded'?' ERROR liveness probe failed (3/3)':' INFO GET /v1/orders 200 14ms')]; }
|
||
logLine(n){ const base=n.name.split(/[-/]/)[0]; const now=new Date(); const ts=('0'+now.getHours()).slice(-2)+':'+('0'+now.getMinutes()).slice(-2)+':'+('0'+now.getSeconds()).slice(-2);
|
||
const paths=['/healthz','/v1/orders','/v1/cart','/metrics','/readyz','/v1/checkout']; const codes=['200','200','200','204','200','503','200','404'];
|
||
const c=codes[Math.floor(Math.random()*codes.length)]; const p=paths[Math.floor(Math.random()*paths.length)]; const ms=(1+Math.floor(Math.random()*60));
|
||
if(c==='503') return ts+' ERROR '+base+' upstream timeout on '+p+' (retry 2/3)';
|
||
if(c==='404') return ts+' WARN '+base+' '+p+' 404 not found';
|
||
return ts+' INFO '+base+' GET '+p+' '+c+' '+ms+'ms'; }
|
||
logColor(t){ return /ERROR/.test(t)?'#ef6f6f':(/WARN/.test(t)?'#e8b54a':'#aab8cc'); }
|
||
appendLogs(){ const S=this.state; if(!S.krates.length) return; let changed=false; const logs=Object.assign({},S.logs);
|
||
S.krates.forEach(k=>{ k.windows.forEach(w=>{ const n=this.byId[w.objId||k.objId]; if(w.tab!=='logs'||!this.execable(n.type)) return; const tk=w.wid+'#logs';
|
||
const buf=(logs[tk]?logs[tk].slice():this.seedLogs(n)); buf.push(this.logLine(n)); if(buf.length>160) buf.splice(0,buf.length-160); logs[tk]=buf; changed=true; }); });
|
||
if(changed) this.setState({logs}); }
|
||
|
||
// ---------- interactive shell ----------
|
||
seedShell(n){ return {cwd:'/', lines:[{t:'out',text:'krates shell · '+n.name+' · ns/'+n.ns},{t:'out',text:"connected. type 'help' for commands."}]}; }
|
||
prompt(n,cwd){ return 'root@'+n.name+':'+cwd+'#'; }
|
||
shellEval(n,sess,cmd){ const c=cmd.trim(); if(!c) return {out:[]}; const parts=c.split(/\s+/); const a=parts[0];
|
||
const fs={'/':['app','bin','etc','proc','var'],'/app':['bin','config','data','server','VERSION'],'/app/config':['region','log_level','feature_flags']};
|
||
if(a==='help') return {out:['available: ls cd pwd cat env ps whoami hostname date kubectl echo clear exit']};
|
||
if(a==='whoami') return {out:['root']};
|
||
if(a==='hostname') return {out:[n.name]};
|
||
if(a==='date') return {out:[new Date().toUTCString()]};
|
||
if(a==='pwd') return {out:[sess.cwd]};
|
||
if(a==='ls') return {out:[(fs[parts[1]||sess.cwd]||fs[sess.cwd]||[]).join(' ')]};
|
||
if(a==='env') return {out:['POD_NAME='+n.name,'NAMESPACE='+n.ns,'REGION=eu-west-1','LOG_LEVEL=info','PORT=8080','IMAGE='+((n.data&&n.data.img)||'krates/app')]};
|
||
if(a==='ps') return {out:[' PID USER COMMAND',' 1 root /app/server',' 18 root sh',' 42 root ps']};
|
||
if(a==='cat'){ const f=(parts[1]||''); if(/region/.test(f)) return {out:['eu-west-1']}; if(/VERSION/.test(f)) return {out:[((n.data&&n.data.img)||'1.0.0')]}; if(/log_level/.test(f)) return {out:['info']}; if(/feature_flags/.test(f)) return {out:['checkout_v2=true','dark_mode=false']}; return {out:['cat: '+f+': No such file or directory']}; }
|
||
if(a==='cd'){ let t=parts[1]||'/'; if(t==='..'){ const s=sess.cwd.split('/').filter(Boolean); s.pop(); t='/'+s.join('/'); } else if(t!=='/' && !t.startsWith('/')){ t=(sess.cwd==='/'?'':sess.cwd)+'/'+t; } t=t.replace(/\/+/g,'/'); if(t!=='/'&&t.length>1)t=t.replace(/\/$/,''); if(fs[t]||t==='/') return {out:[],cwd:t}; return {out:['cd: '+parts[1]+': Not a directory']}; }
|
||
if(a==='kubectl') return {out:['tip: explore visually in krates — or pipe real kubectl here']};
|
||
if(a==='echo') return {out:[parts.slice(1).join(' ')]};
|
||
if(a==='exit') return {out:['use × to close the window']};
|
||
return {out:[a+": command not found (try 'help')"]}; }
|
||
runShell(wid,n,cmd){ const tk=wid+'#shell'; const sh=Object.assign({},this.state.sh); const prev=sh[tk]||this.seedShell(n); const sess={cwd:prev.cwd,lines:prev.lines.slice()};
|
||
if(cmd.trim()==='clear'){ sess.lines=[]; sh[tk]=sess; this._stickSh[wid]=true; this.setState({sh}); return; }
|
||
sess.lines.push({t:'in',text:this.prompt(n,sess.cwd)+' '+cmd}); const r=this.shellEval(n,sess,cmd); (r.out||[]).forEach(o=>sess.lines.push({t:'out',text:o})); if(r.cwd!==undefined) sess.cwd=r.cwd;
|
||
if(sess.lines.length>200) sess.lines.splice(0,sess.lines.length-200); sh[tk]=sess; this._stickSh[wid]=true; this.setState({sh}); }
|
||
shellKey(kid,wid,n,e){ if(e.key!=='Enter') return; e.preventDefault(); e.stopPropagation(); const el=e.target; const cmd=el.value; el.value=''; this.runShell(wid,n,cmd); }
|
||
|
||
// ---------- open related ----------
|
||
openRelated(kid,relId){ const view=this.defaultView(this.byId[relId].type);
|
||
this.setState(s=>{ const krates=s.krates.map(k=>{ if(k.id!==kid) return k; let wl=k.windows.slice(); if(!wl.find(w=>(w.objId||k.objId)===relId && w.tab===view)){ wl=wl.concat([{wid:k.id+'-'+k.seq, objId:relId, tab:view, dx:0,dy:0,w:380,h:360}]); } return Object.assign({},k,{windows:this.tileWindows(wl), seq:k.seq+1}); }); return {krates, relMenu:null}; }, ()=>this.fitKrate(kid)); }
|
||
openInKrate(kid,objId,view){ this.setState(s=>{ const krates=s.krates.map(k=>{ if(k.id!==kid) return k; let wl=k.windows.slice(); if(!wl.find(w=>w.kind!=='collection' && (w.objId||k.objId)===objId && w.tab===view)){ wl=wl.concat([{wid:k.id+'-'+k.seq, objId, tab:view, dx:0,dy:0,w:380,h:360}]); } return Object.assign({},k,{windows:this.tileWindows(wl), seq:k.seq+1}); }); return {krates}; }, ()=>this.fitKrate(kid)); }
|
||
|
||
// ---------- collections ----------
|
||
summonCollection(scope){ this.clearTimers(); if(scope.kind==='category') this.recordType(scope.value); const existing=this.state.krates.find(k=>k.collScope && k.collScope.kind===scope.kind && k.collScope.value===scope.value);
|
||
if(existing){ this._focusCollWid=existing.windows[0].wid; this.setState({spotOpen:false, session:null, navigated:false}, ()=>this.fitKrate(existing.id)); return; }
|
||
const p=this.placeKrate(); const color = scope.kind==='namespace'? ((this.world.namespaces.find(x=>x.name===scope.value)||{}).color||'#6fb1ff') : this.meta(scope.value).color;
|
||
const wid0='k'+this.state.krateSeq+'-0'; const k={ id:'k'+this.state.krateSeq, objId:null, collScope:scope, label:this.collTitle(scope), status:'Ready', color, wx:p.cx, wy:p.cy, seq:1,
|
||
windows:[{wid:wid0, kind:'collection', scope, dx:0, dy:0, w:540, h:480}] };
|
||
this._focusCollWid=wid0;
|
||
this.setState(s=>({krates:s.krates.concat([k]), krateSeq:s.krateSeq+1, spotOpen:false, session:null, navigated:false}), ()=>this.fitKrate(k.id)); }
|
||
setCollSearch(wid,val){ this._collNavTimer=this._collNavTimer||{}; clearTimeout(this._collNavTimer[wid]); this.setState(s=>({collSearch:Object.assign({},s.collSearch,{[wid]:val}), collSel:Object.assign({},s.collSel,{[wid]:0}), collNav:Object.assign({},s.collNav,{[wid]:false})})); }
|
||
armCollNav(wid){ this._collNavTimer=this._collNavTimer||{}; clearTimeout(this._collNavTimer[wid]); this.setState(s=> s.collNav[wid]?null:({collNav:Object.assign({},s.collNav,{[wid]:true})}) );
|
||
this._collNavTimer[wid]=setTimeout(()=>{ this.setState(s=>({collNav:Object.assign({},s.collNav,{[wid]:false})})); }, 500); }
|
||
cancelCollNav(wid){ this._collNavTimer=this._collNavTimer||{}; clearTimeout(this._collNavTimer[wid]); this.setState(s=>({collNav:Object.assign({},s.collNav,{[wid]:false})})); }
|
||
// unified collection key handling — works whether the filter is focused OR the mouse is just hovering the box
|
||
collKey(wid, e){ const ctx=this._collCtx[wid]; if(!ctx) return false; const kid=ctx.kid, nodes=ctx.nodes;
|
||
if(e.key==='ArrowDown'){ e.preventDefault(); this.setState(s=>({collSel:Object.assign({},s.collSel,{[wid]:Math.min(nodes.length-1,(s.collSel[wid]||0)+1)})})); return true; }
|
||
if(e.key==='ArrowUp'){ e.preventDefault(); this.setState(s=>({collSel:Object.assign({},s.collSel,{[wid]:Math.max(0,(s.collSel[wid]||0)-1)})})); return true; }
|
||
const sel=Math.min(this.state.collSel[wid]||0, nodes.length-1); const node=nodes[sel];
|
||
if(e.altKey && !e.ctrlKey && !e.metaKey){ const v=this.keyToView(this.codeLetter(e)); if(v && node && this.viewAvail(node.type,v)){ e.preventDefault(); this.openInKrate(kid, node.id, v); return true; } return false; }
|
||
if(e.key==='Enter'){ e.preventDefault(); if(node) this.openInKrate(kid, node.id, this.defaultView(node.type)); return true; }
|
||
if(e.key==='Escape'){ if(e.target&&e.target.blur)e.target.blur(); return true; }
|
||
if(e.key===' ') return false; // reserve for pan
|
||
if(e.key.length===1 && !e.metaKey && !e.ctrlKey){ e.preventDefault(); this.setCollSearch(wid, (this.state.collSearch[wid]||'')+e.key); return true; }
|
||
if(e.key==='Backspace'){ e.preventDefault(); this.setCollSearch(wid, (this.state.collSearch[wid]||'').slice(0,-1)); return true; }
|
||
return false; }
|
||
handleCollKey(e, kid, wid, nodes){ this.collKey(wid, e); }
|
||
toggleCollView(wid){ this.setState(s=>({collView:Object.assign({},s.collView,{[wid]: (s.collView[wid]==='tree'?'list':'tree')})})); }
|
||
toggleAdmin=()=>{ this.setState(s=>({admin:!s.admin})); };
|
||
adminData(){ return [
|
||
{name:'Dana Okafor', color:'#6fb1ff', status:'active · payments', statusColor:'#4ad07a', query:'ledger', typing:true, krates:[['deploy/ledger','L Y','Degraded'],['All Secrets','▤','Ready']]},
|
||
{name:'Wen Li', color:'#5fd0c0', status:'active · platform', statusColor:'#4ad07a', query:'auth oauth-secret', typing:false, krates:[['deploy/auth','D S','Ready'],['ns/platform','▤','Ready']]},
|
||
{name:'Ravi Patel', color:'#e58fb0', status:'idle 4m · storefront', statusColor:'#7e8aa2', query:'redis', typing:false, krates:[['sts/redis','L Y','Ready']]},
|
||
{name:'You', color:null, status:'active', statusColor:'#4ad07a', query:'—', typing:false, krates:[]} ]; }
|
||
|
||
// ---------- refs ----------
|
||
setRoot=(el)=>{ this.rootEl=el; };
|
||
setInput=(el)=>{ this.inputEl=el; };
|
||
stopDown=(e)=>{ e.stopPropagation(); };
|
||
winWheel=(e)=>{ if(e.ctrlKey||e.metaKey) return; e.stopPropagation(); };
|
||
|
||
renderVals(){
|
||
const S=this.state; const A=this.accentSet(); const VM=this.viewMeta();
|
||
const W=this.rootEl?this.rootEl.clientWidth:1280, H=this.rootEl?this.rootEl.clientHeight:760;
|
||
const toScreen=(wx,wy)=>({x:S.camX+wx*S.zoom, y:S.camY+wy*S.zoom});
|
||
const frames=[]; const wins=[]; const cards=[]; const minis=[];
|
||
|
||
if(!S.collapsed){
|
||
S.krates.forEach(k=>{
|
||
if(k.collapsed){ minis.push({ x:k.wx, y:k.wy-44, color:k.color, outline:this.rgba(k.color,.3), label:k.label, status:k.status, statusColor:this.statusColor(k.status), count:k.windows.length,
|
||
onExpand:()=>this.toggleCollapseKrate(k.id), onDrag:(e)=>this.startKrateDrag(k.id,e), onClose:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.closeKrate(k.id); } }); return; }
|
||
const box=this.krateBox(k); const n=this.byId[k.objId];
|
||
frames.push({left:k.wx+box.x-30, top:k.wy+box.y-30, w:box.w+60, h:box.h+60, border:this.rgba(k.color,.3), fill:this.rgba(k.color,.04),
|
||
labelLeft:k.wx+box.x-30, labelTop:k.wy+box.y-72, color:k.color, label:k.label, status:k.status, statusColor:this.statusColor(k.status), count:k.windows.length,
|
||
onCollapse:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.toggleCollapseKrate(k.id); }, onToggleCollapse:()=>this.toggleCollapseKrate(k.id),
|
||
onDrag:(e)=>this.startKrateDrag(k.id,e), onClose:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.closeKrate(k.id); } });
|
||
k.windows.forEach(w=>{ if(w.kind==='collection'){ const scope=w.scope; const allMem=this.collMembers(scope); const qv=(S.collSearch[w.wid]||''); const q=qv.trim();
|
||
const matches=(nn)=> !q || this.fuzzy(q,nn.name)>=0 || this.fuzzy(q,this.meta(nn.type).short)>=0 || this.fuzzy(q,nn.ns)>=0;
|
||
const summ=this.statusSummary(allMem); const mode=S.collView[w.wid]||'list'; const csel=S.collSel[w.wid]||0;
|
||
const rowOf=(nn,depth,rel,oi)=>{ const mm=this.meta(nn.type); const ssc=this.shapeCss(mm.shape); const av=this.viewOrder().filter(v=>this.viewAvail(nn.type,v));
|
||
return { id:nn.id+':'+oi, name:nn.name, row:true, isGroup:false, indent:depth||0, indentPx:(depth||0)*20, child:(depth||0)>0, rel:rel||'', hasRel:!!rel, typeShort:mm.short, color:mm.color, clip:ssc.clip, radius:ssc.radius, statusColor:this.statusColor(nn.status), status:nn.status, metric:this.healthMetric(nn),
|
||
selected:(oi===csel), selBg:(oi===csel?A.dim:'transparent'), selBar:(oi===csel?A.accent:'transparent'),
|
||
views:av.map(v=>({letter:VM[v].letter, onClick:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.openInKrate(k.id, nn.id, v); }})),
|
||
onOpen:()=>this.openInKrate(k.id, nn.id, this.defaultView(nn.type)),
|
||
onHover:()=>this.setState(s=>({collSel:Object.assign({},s.collSel,{[w.wid]:oi})})),
|
||
onRelated:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.setState({relMenu:{kid:k.id, objId:nn.id, sx:Math.min(e.clientX,W-244), sy:Math.min(e.clientY,H-260)}}); } }; };
|
||
let rows=[]; let visNodes=[];
|
||
if(mode==='tree'){ let xr=this.buildXray(scope, allMem);
|
||
if(q){ const keep=new Array(xr.length).fill(false); for(let i=0;i<xr.length;i++){ if(matches(xr[i].node)){ keep[i]=true; let d=xr[i].depth; for(let j=i-1;j>=0&&d>0;j--){ if(xr[j].depth<d){ keep[j]=true; d=xr[j].depth; } } } } xr=xr.filter((r,i)=>keep[i]); }
|
||
visNodes=xr.map(r=>r.node); rows=xr.map((r,oi)=>rowOf(r.node, r.depth, r.rel, oi)); }
|
||
else { const rank=(nn)=>(nn.status==='Degraded'||nn.status==='Failed')?0:(nn.status==='Pending'?1:2); const list=allMem.filter(matches).slice().sort((a,b)=>rank(a)-rank(b)||a.name.localeCompare(b.name)); visNodes=list; rows=list.map((nn,oi)=>rowOf(nn,0,'',oi)); }
|
||
const cwd={ isCollection:true, normal:false, showRelated:false, relatedDisplay:'none', z:(S.zoomedWin===w.wid?60:1), isText:false, isLogs:false, isShell:false, x:k.wx+w.dx, y:k.wy+w.dy, w:w.w, bodyH:Math.max(150,w.h-150), outline:this.rgba(k.color,.22), color:k.color, glyphClip:'none', title:this.collTitle(scope), status:(summ.bad?'Degraded':(summ.warn?'Pending':'Ready')), statusColor:this.statusColor(summ.bad?'Degraded':(summ.warn?'Pending':'Ready')),
|
||
total:summ.total, summOk:summ.ok, summWarn:summ.warn, summBad:summ.bad, count:visNodes.length, navMode:!!S.collNav[w.wid], searchVal:qv, listBg:(mode!=='tree'?A.accent:'transparent'), listFg:(mode!=='tree'?'#0b0e13':'#9fb0c8'), treeBg:(mode==='tree'?A.accent:'transparent'), treeFg:(mode==='tree'?'#0b0e13':'#9fb0c8'), rows, empty:rows.length===0,
|
||
onCollSearch:(e)=>this.setCollSearch(w.wid, e.target.value), onCollKey:(e)=>this.handleCollKey(e, k.id, w.wid, visNodes), onCollList:()=>this.setState(s=>({collView:Object.assign({},s.collView,{[w.wid]:'list'}), collSel:Object.assign({},s.collSel,{[w.wid]:0})})), onCollTree:()=>this.setState(s=>({collView:Object.assign({},s.collView,{[w.wid]:'tree'}), collSel:Object.assign({},s.collSel,{[w.wid]:0})})),
|
||
collInputRef:(el)=>{ if(el) this._collInputEls[w.wid]=el; }, onCollEnter:()=>{ this._hoverCollWid=w.wid; }, onCollLeave:()=>{ if(this._hoverCollWid===w.wid) this._hoverCollWid=null; },
|
||
onClose:()=>this.closeWindow(k.id,w.wid), onDrag:(e)=>this.startKrateDrag(k.id,e), onFocus:()=>this.zoomToggleWindow(k.id,w.wid), onWinEnter:()=>{ this._hoverWinId={kid:k.id,wid:w.wid}; }, onWinLeave:()=>{ if(this._hoverWinId&&this._hoverWinId.wid===w.wid) this._hoverWinId=null; }, onResize:(e)=>this.startWinResize(k.id,w.wid,e) };
|
||
this._collCtx[w.wid]={kid:k.id, nodes:visNodes};
|
||
wins.push(cwd); return; }
|
||
const wn=this.byId[w.objId||k.objId]; const m=this.meta(wn.type); const sc=this.shapeCss(m.shape); const tk=w.wid+'#'+w.tab;
|
||
const tabs=this.viewOrder().map(v=>{ const dis=!this.viewAvail(wn.type,v); const act=w.tab===v; return {label:VM[v].label,bg:act?'rgba(30,38,52,.9)':'transparent',fg:act?'#e6edf6':(dis?'#54607a':'#7e8aa2'),cursor:dis?'default':'pointer',opacity:dis?.45:1,onClick:()=>dis?null:this.setTab(k.id,w.wid,v)}; });
|
||
const isShell=w.tab==='shell'&&this.execable(wn.type); const isLogs=w.tab==='logs'&&this.execable(wn.type); const isText=!isShell&&!isLogs;
|
||
const wd={ normal:true, isCollection:false, showRelated:true, relatedDisplay:'inline-block', z:(S.zoomedWin===w.wid?60:1), x:k.wx+w.dx, y:k.wy+w.dy, w:w.w, bodyH:Math.max(120,w.h-76), outline:this.rgba(k.color,.22), color:m.color, glyphClip:sc.clip, title:VM[w.tab].label+' · '+wn.name, status:wn.status, statusColor:this.statusColor(wn.status), tabs, isShell, isLogs, isText, body:isText?this.winBody(wn,w.tab):'',
|
||
onClose:()=>this.closeWindow(k.id,w.wid), onDrag:(e)=>this.startKrateDrag(k.id,e), onFocus:()=>this.zoomToggleWindow(k.id,w.wid), onWinEnter:()=>{ this._hoverWinId={kid:k.id,wid:w.wid}; }, onWinLeave:()=>{ if(this._hoverWinId&&this._hoverWinId.wid===w.wid) this._hoverWinId=null; }, onResize:(e)=>this.startWinResize(k.id,w.wid,e),
|
||
onRelated:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.setState({relMenu:{kid:k.id, objId:w.objId||k.objId, sx:Math.min(e.clientX, W-244), sy:Math.min(e.clientY, H-260)}}); } };
|
||
if(isLogs){ const wid=w.wid; const buf=(S.logs[tk]||this.seedLogs(wn)); wd.logLines=buf.map(t=>({text:t,color:this.logColor(t)})); wd.logRef=(el)=>{ if(el) this._logEls[wid]=el; }; wd.onLogScroll=(e)=>{ const el=e.target; this._stickLog[wid]= el.scrollTop+el.clientHeight >= el.scrollHeight-14; }; }
|
||
if(isShell){ const wid=w.wid; const sess=(S.sh[tk]||this.seedShell(wn)); wd.shellLines=sess.lines.map(L=>({text:L.text,color:L.t==='in'?A.accent:(/not found|No such|Not a directory|failed|timeout/i.test(L.text)?'#ef6f6f':'#aab8cc')})); wd.promptStr=this.prompt(wn,sess.cwd); wd.shRef=(el)=>{ if(el) this._shEls[wid]=el; }; wd.onShScroll=(e)=>{ const el=e.target; this._stickSh[wid]= el.scrollTop+el.clientHeight >= el.scrollHeight-14; }; wd.onShellKey=(e)=>this.shellKey(k.id,wid,wn,e); wd.shInputRef=(el)=>{ if(el) this._shInputEls[wid]=el; }; wd.onShEnter=()=>{ this._hoverShWid=wid; }; wd.onShLeave=()=>{ if(this._hoverShWid===wid) this._hoverShWid=null; }; }
|
||
wins.push(wd); });
|
||
});
|
||
} else {
|
||
const L=this.cardLayout(); const drag=S.cardDrag;
|
||
S.krates.forEach((k,i)=>{ const col=i%L.cols, row=Math.floor(i/L.cols); let sx=L.startX+col*(L.cardW+L.gap), sy=L.startY+row*(L.cardH+L.gap); let dragging=false;
|
||
if(drag && drag.id===k.id && drag.mx!=null){ sx=drag.mx-L.cardW/2; sy=drag.my-L.cardH/2; dragging=true; }
|
||
cards.push({sx, sy, dragging, z:dragging?60:5, trans:dragging?'none':'left .18s cubic-bezier(.2,.8,.3,1), top .18s cubic-bezier(.2,.8,.3,1)', label:k.label, color:k.color, statusColor:this.statusColor(k.status), outline:this.rgba(k.color,.3), tint:this.rgba(k.color,.12),
|
||
badges:k.windows.map(w=>w.kind==='collection'?'▤':VM[w.tab].letter), count:k.windows.length, ns:(k.objId?this.byId[k.objId].ns:(k.collScope&&k.collScope.kind==='namespace'?k.collScope.value:'cluster')), onExpand:()=>this.flyToKrate(k.id), onDown:(e)=>this.startCardDrag(k.id,e), onClose:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.closeKrate(k.id); } }); });
|
||
}
|
||
|
||
const users=[{name:'Dana Okafor',color:'#6fb1ff'},{name:'Wen Li',color:'#5fd0c0'},{name:'Ravi Patel',color:'#e58fb0'},{name:'You',color:A.accent}];
|
||
const roster=users.map(u=>({name:u.name,color:u.color,initial:u.name==='You'?'★':u.name.split(' ').map(p=>p[0]).join('').slice(0,2)}));
|
||
const initialOf=(n)=> n==='You'?'★':n.split(' ').map(p=>p[0]).join('').slice(0,2);
|
||
const adminUsers=this.adminData().map(u=>{ const isYou=u.name==='You'; const color=isYou?A.accent:u.color;
|
||
const krs = isYou ? S.krates.map(k=>({label:k.label, color:k.color, badges:k.windows.map(w=>w.kind==='collection'?'▤':VM[w.tab].letter).join(' '), statusColor:this.statusColor(k.status), onSpectate:()=>{ this.setState({admin:false}); this.fitKrate(k.id); }})) : u.krates.map(kr=>({label:kr[0], color:color, badges:kr[1], statusColor:this.statusColor(kr[2]), onSpectate:()=>this.setState({admin:false})}));
|
||
return { name:u.name, color, initial:initialOf(u.name), border:this.rgba(color,.3), status:u.status, statusColor:u.statusColor, query:(isYou?(S.query||'—'):u.query), typing:(isYou?S.spotOpen:u.typing), krateCount:krs.length, krates:krs }; });
|
||
|
||
// spotlight
|
||
const results=this.buildResults(S.query,S.filterType); const sug=this.suggestType(S.query,S.filterType);
|
||
const resDisplay=results.map((it,i)=>{ const active=i===S.sel;
|
||
if(it.kind==='krate'){ const k=it.krate; const vk=k.windows.map(w=>w.kind==='collection'?'▤':VM[w.tab].letter).join(' ');
|
||
return { isKrate:true, id:k.id, name:k.label, sub:k.windows.length+' window'+(k.windows.length===1?'':'s')+' · '+vk, color:k.color, clip:'none', radius:'3px', glow:this.rgba(k.color,.5),
|
||
typeShort:'krate', isCrd:false, active, bg:active?A.dim:'transparent', views:[], viewKeys:'', enter:true, showBadge:true, badgeLabel:'KRATE',
|
||
viewHint:'open working set', onHover:()=>this.setState({sel:i}), onClick:()=>this.jumpToKrate(k.id) }; }
|
||
if(it.kind==='category'||it.kind==='namespace'){ const isNs=it.kind==='namespace'; const col=isNs?(it.color||'#6fb1ff'):this.meta(it.scope.value).color; const sc=isNs?{clip:'none',radius:'4px'}:this.shapeCss(this.meta(it.scope.value).shape);
|
||
const summ=this.statusSummary(this.collMembers(it.scope)); const sub=it.count+' objects · '+summ.ok+' ok'+(summ.warn?' · '+summ.warn+' pending':'')+(summ.bad?' · '+summ.bad+' degraded':'');
|
||
return { isColl:true, id:it.id, name:this.collTitle(it.scope), sub, color:col, clip:sc.clip, radius:sc.radius, glow:this.rgba(col,.5),
|
||
typeShort:isNs?'namespace':'category', collBadge:true, active, bg:active?A.dim:'transparent', views:[], viewKeys:'', enter:true, showBadge:true, badgeLabel:isNs?'NS':'CATEGORY',
|
||
viewHint:'open status view', onHover:()=>this.setState({sel:i}), onClick:()=>this.summonCollection(it.scope) }; }
|
||
const n=it.node; const m=this.meta(n.type); const sc=this.shapeCss(m.shape);
|
||
const sessionViews = (S.session && S.session.objId===n.id) ? S.session.views : [];
|
||
const avViews=this.viewOrder().filter(v=>this.viewAvail(n.type,v));
|
||
const views=avViews.map(v=>{ const opened=sessionViews.indexOf(v)>=0;
|
||
return { letter:'⌥'+VM[v].letter, label:VM[v].label, mark: opened?' ✓':'', cursor:'pointer', opacity:1,
|
||
border: opened?A.accent:'rgba(140,165,200,.22)', bg: opened?A.dim:'rgba(255,255,255,.02)', fg: opened?A.accent:'#dbe3ef',
|
||
kbdBg: opened?A.accent:'rgba(140,165,200,.16)', kbdFg: opened?'#0b0e13':'#9fb0c8',
|
||
onClick:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.openView(n.id,v,false); } }; });
|
||
return { isKrate:false, id:n.id, name:n.name, sub:m.label+' · ns/'+n.ns, color:m.color, clip:sc.clip, radius:sc.radius, glow:this.rgba(m.color,.5),
|
||
typeShort:m.short, isCrd:!!m.crd, active, bg:active?A.dim:'transparent', views, viewKeys:avViews.map(v=>'⌥'+VM[v].letter).join(' '), enter:false, showBadge:false, badgeLabel:'',
|
||
viewHint: 'hover or ↓, then ⌥ + letter',
|
||
onHover:()=>this.setState({sel:i}), onClick:()=>this.openView(n.id,this.defaultView(n.type),true) }; });
|
||
const chipLabel={deployment:'deploy',service:'svc',pod:'pods',secret:'secrets',configmap:'config',statefulset:'sts',daemonset:'ds',ingress:'ing',crd:'crd',pvc:'pvc'};
|
||
const chipTypes=[['','all']].concat(this.filterTypeOrder().map(t=>[t, chipLabel[t]||this.meta(t).short]));
|
||
const chips=chipTypes.map(ct=>{ const t=ct[0]; const m=t?this.meta(t):{color:'#8aa0bd'}; const sc=t?this.shapeCss(m.shape):{clip:'none',radius:'50%'}; const active=(S.filterType||'')===t;
|
||
return {label:ct[1],color:m.color,clip:sc.clip,radius:sc.radius,bg:active?A.dim:'rgba(255,255,255,.02)',fg:active?A.accent:'#9fb0c8',border:active?A.accent:'rgba(140,165,200,.18)',onClick:()=>{ this.recordType(t); this._focus=true; this.setState({filterType:t||null, sel:0, navigated:false}); }}; });
|
||
const fm=S.filterType?this.meta(S.filterType):null; const fsc=fm?this.shapeCss(fm.shape):null;
|
||
const selRow=resDisplay[S.sel]; const openKeysLabel=(selRow && selRow.viewKeys) ? selRow.viewKeys : (selRow && selRow.enter ? '⏎ open' : '⌥L ⌥S ⌥D ⌥Y');
|
||
const sessionActive=!!S.session; const buildingViews= S.session? S.session.views.map(v=>VM[v].letter):[];
|
||
let relMenu=null; if(S.relMenu){ const o=this.byId[S.relMenu.objId]; const items=(this.adj[S.relMenu.objId]||[]).map(id=>{ const rn=this.byId[id]; const rm=this.meta(rn.type); const rsc=this.shapeCss(rm.shape);
|
||
return {name:rn.name, typeShort:rm.short, color:rm.color, clip:rsc.clip, radius:rsc.radius, onClick:(e)=>{ if(e&&e.stopPropagation)e.stopPropagation(); this.openRelated(S.relMenu.kid, id); }}; });
|
||
relMenu={sx:S.relMenu.sx, sy:S.relMenu.sy, label:o.name, items, empty:items.length===0}; }
|
||
|
||
const zoomPct=Math.round(S.zoom*100);
|
||
// minimap
|
||
let minimap=null;
|
||
if(S.krates.length){ const fps=S.krates.map(k=>this.krateFootprint(k));
|
||
let bx=1e9,by=1e9,bx2=-1e9,by2=-1e9; fps.forEach(f=>{ bx=Math.min(bx,f.x); by=Math.min(by,f.y); bx2=Math.max(bx2,f.x+f.w); by2=Math.max(by2,f.y+f.h); });
|
||
const vx1=(-S.camX)/S.zoom, vy1=(-S.camY)/S.zoom, vx2=(W-S.camX)/S.zoom, vy2=(H-S.camY)/S.zoom;
|
||
bx=Math.min(bx,vx1); by=Math.min(by,vy1); bx2=Math.max(bx2,vx2); by2=Math.max(by2,vy2);
|
||
const pad=140; bx-=pad; by-=pad; bx2+=pad; by2+=pad; const bw=bx2-bx, bh=by2-by;
|
||
const mmW=196, mmH=132; const scale=Math.min(mmW/bw, mmH/bh); const offX=(mmW-bw*scale)/2, offY=(mmH-bh*scale)/2;
|
||
this._mm={bx:bx-offX/scale, by:by-offY/scale, scale}; this._mmW=mmW; this._mmH=mmH;
|
||
const mapX=(x)=>(x-bx)*scale+offX, mapY=(y)=>(y-by)*scale+offY;
|
||
const rects=S.krates.map((k,i)=>{ const f=fps[i]; return {x:mapX(f.x), y:mapY(f.y), w:Math.max(6,f.w*scale), h:Math.max(5,f.h*scale), color:k.color, collapsed:!!k.collapsed}; });
|
||
minimap={ w:mmW, h:mmH, rects, vx:mapX(vx1), vy:mapY(vy1), vw:(vx2-vx1)*scale, vh:(vy2-vy1)*scale, onDown:(e)=>{ e.stopPropagation(); this.mmJump(e); }, setEl:(el)=>{ this._mmEl=el; } };
|
||
} else { this._mm=null; }
|
||
return {
|
||
cursor:S.dragging?'grabbing':(S.spacePan?'grab':'default'),
|
||
worldTransform:'translate('+S.camX+'px,'+S.camY+'px) scale('+S.zoom+')',
|
||
worldTransition:S.flying?'transform .52s cubic-bezier(.22,.8,.28,1)':'none',
|
||
accent:A.accent, accentGlow:A.glow, accentDim:A.dim,
|
||
frames, wins, cards, minis, minimap, roster, relMenu, krateCount:S.krates.length,
|
||
admin:S.admin, adminUsers, adminCount:adminUsers.length, onToggleAdmin:this.toggleAdmin,
|
||
adminBtnBg:S.admin?A.dim:'rgba(14,18,25,.82)', adminBtnBorder:S.admin?A.accent:'rgba(140,165,200,.18)', adminBtnFg:S.admin?A.accent:'#7e8aa2',
|
||
emptyCanvas: S.krates.length===0 && !S.spotOpen,
|
||
zoomLabel: (S.collapsed?'overview · ':'')+zoomPct+'%', zoomDotColor: S.collapsed?A.accent:'#4ad07a',
|
||
spotOpen:S.spotOpen, query:S.query, sel:S.sel,
|
||
hasFilter:!!S.filterType, filterLabel:fm?fm.label:'', filterColor:fm?fm.color:'#8aa0bd', filterClip:fsc?fsc.clip:'none', filterRadius:fsc?fsc.radius:'50%',
|
||
placeholder:S.filterType?('search '+fm.label.toLowerCase()+'s…'):'Search pods, services, secrets, CRDs…',
|
||
ghost: sug? this.meta(sug).label : '',
|
||
chips, results:resDisplay, noResults:S.query.trim()!=='' && resDisplay.length===0,
|
||
resultsHeader: S.query.trim()? 'results' : (S.filterType? (fm.label.toLowerCase()+'s') : 'top objects'),
|
||
enterLabel: S.session? 'place' : 'open default', openKeysLabel, sessionActive, buildingViews,
|
||
onSearchChange:this.onSearchChange, onSearchKeydown:this.onSearchKeydown, setInput:this.setInput, onSpotBackdrop:this.onSpotBackdrop,
|
||
onBgDown:this.onBgDown, onRootDownCapture:this.onRootDownCapture, onWheel:this.onWheel, setRoot:this.setRoot, stopDown:this.stopDown, winWheel:this.winWheel };
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|