UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

325 lines (296 loc) 13.5 kB
<!doctype html> <html> <head> <meta charset="utf-8"> <title>JOE MCP — Schemas Health</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px} h1{margin:8px 0} .small{font-size:12px;color:#666} table{width:100%;border-collapse:collapse;margin-top:10px} th,td{border:1px solid #e1e4e8;padding:8px;text-align:left;font-size:14px} th.sortable{cursor:pointer} th.sorted-asc, th.sorted-desc{background:#eef} th .sort-ind{font-size:12px;opacity:.6;margin-left:6px} .chips{display:flex;gap:6px;flex-wrap:wrap} .chip{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;font-size:12px} .chip.joe{background:#11bcd6;color:#fff;border-color:#0fa5bb} .icon-cell{width:72px} .icon64{width:64px;height:64px;display:flex;align-items:center;justify-content:center} .icon64 svg{width:64px;height:64px;display:block} .health-badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:12px;border:1px solid transparent} .health-good{background:#0a7d00;color:#fff;border-color:#0a7d00} .health-warn{background:#f0c040;color:#000;border-color:#e5b93b} .health-bad{background:#d93025;color:#fff;border-color:#d93025} .t-center{text-align:center} th{background:#f6f8fa} .status-ok{color:#0a7d00;font-size:22px} .status-bad{color:#b00020;font-size:22px} .badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:12px;background:#eef;border:1px solid #ccd} a[target]{text-decoration:none} </style> </head> <body> <div id="mcp-nav"></div> <script src="/JsonObjectEditor/_www/mcp-nav.js"></script> <h1>Schema Health</h1> <div class="small">Summary of all schemas available in this JOE instance.</div> <div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;"> <label for="base" style="margin:0">Base URL</label> <input id="base" value="" placeholder="http://localhost:2025" style="min-width:280px" /> <button id="refresh">Refresh</button> <span id="status" class="small"></span> </div> <div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:4px 0 10px;"> <label for="appFilter" style="margin:0">Filter by App</label> <select id="appFilter"><option value="">All</option></select> </div> <table> <thead> <tr> <th>Icon</th> <th class="sortable" data-key="name">Schema <span class="sort-ind"></span></th> <th class="sortable" data-key="count">Count <span class="sort-ind"></span></th> <th class="sortable" data-key="default">Default <span class="sort-ind"></span></th> <th class="sortable" data-key="summary">Summary <span class="sort-ind"></span></th> <th class="sortable" data-key="source">Source <span class="sort-ind"></span></th> <th class="sortable" data-key="fieldcheck">Field Check <span class="sort-ind"></span></th> <th>Outbound</th> <th>Apps</th> <th class="sortable" data-key="health">Synthesis Health <span class="sort-ind"></span></th> <th>API (summary)</th> </tr> </thead> <tbody id="rows"></tbody> </table> <script> (function(){ var $ = function(id){ return document.getElementById(id); }; var base = $('base'); var refresh = $('refresh'); var status = $('status'); var rows = $('rows'); var appFilterSel = null; base.value = base.value || location.origin; var defaultSchemasFallback = ['user','group','goal','initiative','event','report','tag','status','workflow','list','notification','note','include','instance','setting']; function setStatus(msg, ok){ status.textContent = msg||''; status.className = 'small ' + (ok===true?'status-ok': ok===false?'status-bad':''); } async function fetchJSON(url){ const res = await fetch(url); if(!res.ok){ throw new Error('HTTP '+res.status+' '+(await res.text())); } return res.json(); } function chunk(arr, size){ var out=[], i=0; for(i=0;i<arr.length;i+=size){ out.push(arr.slice(i,i+size)); } return out; } function check(mark){ return mark ? '<span class="status-ok">&#10003;</span>' : '<span class="status-bad">&#10007;</span>'; } async function load(){ try{ setStatus('Loading...', null); rows.innerHTML = ''; const baseUrl = base.value.replace(/\/$/,''); const list = await fetchJSON(baseUrl + '/API/list/schemas'); // Fetch full schemas in chunks (includes curated summary when present) var full = {}; var counts = {}; var chunks = chunk(list, 25); for (var i=0;i<chunks.length;i++){ var names = chunks[i].join(','); var got = await fetchJSON(baseUrl + '/API/schemas/' + encodeURIComponent(names)); var ds = await fetchJSON(baseUrl + '/API/datasets/' + encodeURIComponent(names)); Object.assign(full, got.schemas || {}); Object.assign(counts, (ds && ds.counts) || {}); } // Fetch apps via MCP listApps async function callMCP(method, params){ const url = baseUrl + '/mcp'; const body = { jsonrpc:'2.0', id:String(Date.now()), method:method, params:params||{} }; const resp = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); if(!resp.ok){ throw new Error('HTTP '+resp.status+' '+(await resp.text())); } const j = await resp.json(); return j && (j.result||j) || {}; } var appMap = {}; try{ var la = await callMCP('listApps', {}); appMap = (la && la.apps) || {}; }catch(_e){ appMap = {}; } var data = list.map(function(name){ var fs = full[name] || {}; var usedBy = []; for (var appName in appMap){ var a = appMap[appName] || {}; var cols = Array.isArray(a.collections) ? a.collections : []; if (cols.indexOf(name) !== -1){ usedBy.push(appName); } } // If schema is a default core schema, show only the JOE app chip to avoid noise if ((!!fs.default_schema)){ usedBy = ['joe']; } var outbound = []; try{ var rel = (fs.summary && fs.summary.relationships && fs.summary.relationships.outbound) || []; outbound = rel.filter(function(r){ return r && r.field && r.targetSchema; }) .map(function(r){ return { field: r.field, schema: r.targetSchema }; }); }catch(_e){ outbound = []; } var fieldCount = (fs.summary && typeof fs.summary.fieldCount === 'number') ? fs.summary.fieldCount : ((fs.summary && fs.summary.fields && fs.summary.fields.length) || 0); var fieldCountBase = (fs.summary && typeof fs.summary.fieldCountBase === 'number') ? fs.summary.fieldCountBase : fieldCount; var fieldCoverage = fieldCountBase ? Math.round((fieldCount/fieldCountBase)*100) : 0; return { name: name, count: counts[name] || 0, default: !!fs.default_schema || false, summary: !!fs.summary, source: (fs.summary && fs.summary.source) || (fs.__origin === 'core' ? 'core' : 'instance'), apiUrl: baseUrl + '/API/schema/' + encodeURIComponent(name) + '?summaryOnly=true', apps: usedBy.sort(), outbound: outbound, fieldcheck: fieldCoverage, fieldcheckText: fieldCount + '/' + fieldCountBase + ' ('+fieldCoverage+'%)', menuicon: fs.menuicon || '' }; }); function computeHealth(item){ var signals = 4; // Update when adding more health signals var step = 100/signals; var score = 0; if ((item.count||0) > 0) score += step; if (item.menuicon) score += step; if (!!item.summary) score += step; if ((item.apps||[]).length > 0) score += step; return Math.round(score); } // Read app parameter from URL var urlParams = new URLSearchParams(window.location.search); var urlApp = urlParams.get('app'); // Populate app filter options appFilterSel = document.getElementById('appFilter'); if (appFilterSel){ var appNames = Object.keys(appMap||{}).sort(); appNames.forEach(function(a){ var opt=document.createElement('option'); opt.value=a; opt.textContent=a; appFilterSel.appendChild(opt); }); // Set filter from URL parameter if present (before initial render) if (urlApp) { // Check if the app exists in the options for (var i = 0; i < appFilterSel.options.length; i++) { if (appFilterSel.options[i].value === urlApp) { appFilterSel.value = urlApp; break; } } } } function getFilteredData(){ var selected = appFilterSel && appFilterSel.value || ''; if (!selected) return data; return data.filter(function(d){ return (d.apps||[]).indexOf(selected) !== -1; }); } function renderTable(sorted){ rows.innerHTML = ''; (sorted||[]).forEach(function(item){ var health = computeHealth(item); var hClass = health === 100 ? 'health-good' : (health >= 75 ? 'health-warn' : 'health-bad'); var tr = document.createElement('tr'); function td(html, cls){ var c=document.createElement('td'); if(cls){c.className=cls;} c.innerHTML=html||''; return c; } function safeIcon(svg){ try{ if(!svg || svg.indexOf('<svg') === -1) return ''; var m = svg.match(/<path[^>]*d="([^"]+)"/i); if (m && m[1] && !/^[Mm]/.test(m[1])) return ''; return svg; }catch(_e){ return ''; } } var iconHTML = safeIcon(item.menuicon); tr.appendChild(td(iconHTML ? ('<div class="icon64">'+iconHTML+'</div>') : '', 'icon-cell')); tr.appendChild(td('<strong>'+item.name+'</strong>')); tr.appendChild(td(String(item.count), 't-center')); tr.appendChild(td(check(item.default), 't-center')); tr.appendChild(td(check(item.summary))); tr.appendChild(td(item.source === 'core' ? 'Core' : 'Instance', 't-center')); tr.appendChild(td(item.fieldcheckText||'', 't-center')); tr.appendChild(td('<div class="chips">'+ (item.outbound||[]).map(function(ln){ return '<span class="chip">'+ln.field+' → '+ln.schema+'</span>'; }).join(' ') +'</div>')); tr.appendChild(td('<div class="chips">'+ (item.apps||[]).map(function(app){ var cls = (app==='joe'?'chip joe':'chip'); var link='/JOE/'+app+'#'+item.name; return '<a class="'+cls+'" href="'+link+'" target="_app_'+app+'">'+app+'</a>'; }).join(' ') +'</div>')); tr.appendChild(td('<span class="health-badge '+hClass+'">'+health+'%</span>', 't-center')); tr.appendChild(td('<a href="'+item.apiUrl+'" target="_schemas_api_'+item.name+'">/API/schema/'+item.name+'?summaryOnly=true</a>')); rows.appendChild(tr); }); } var sortState = { key:null, dir:'asc' }; function sortData(key){ if (sortState.key === key){ sortState.dir = (sortState.dir === 'asc') ? 'desc' : 'asc'; } else { sortState.key = key; // For boolean/numeric columns, default to true-first or highest-first if (key === 'default' || key === 'summary' || key === 'count' || key === 'health') { sortState.dir = 'desc'; } else { sortState.dir = 'asc'; } } var dir = sortState.dir === 'asc' ? 1 : -1; var baseList = getFilteredData(); var sorted = baseList.slice().sort(function(a,b){ var av = a[key], bv = b[key]; if (key === 'health'){ av = computeHealth(a); bv = computeHealth(b); } // Normalize booleans for deterministic sort if (typeof av === 'boolean') { av = av ? 1 : 0; } if (typeof bv === 'boolean') { bv = bv ? 1 : 0; } if (typeof av === 'string' && typeof bv === 'string'){ av = av.toLowerCase(); bv = bv.toLowerCase(); } if (av > bv) return 1*dir; if (av < bv) return -1*dir; return 0; }); renderTable(sorted); updateHeaderIndicators(); } function updateHeaderIndicators(){ var ths = document.querySelectorAll('th.sortable'); Array.prototype.forEach.call(ths, function(th){ th.classList.remove('sorted-asc','sorted-desc'); var ind = th.querySelector('.sort-ind'); if (ind) ind.textContent = ''; if (th.getAttribute('data-key') === sortState.key){ th.classList.add(sortState.dir === 'asc' ? 'sorted-asc' : 'sorted-desc'); if (ind) ind.textContent = sortState.dir === 'asc' ? '▲' : '▼'; } }); } // Default sort by name asc (filter will be applied automatically if appFilterSel.value is set) sortData('name'); updateHeaderIndicators(); // Attach header sort handlers Array.prototype.forEach.call(document.querySelectorAll('th.sortable'), function(th){ th.onclick = function(){ sortData(th.getAttribute('data-key')); }; }); // Filter handler if (appFilterSel){ appFilterSel.onchange = function(){ // Update URL parameter var selectedApp = appFilterSel.value || ''; var url = new URL(window.location); if (selectedApp) { url.searchParams.set('app', selectedApp); } else { url.searchParams.delete('app'); } window.history.replaceState({}, '', url); // Apply filter sortData(sortState.key || 'name'); }; } setStatus('Loaded '+list.length+' schemas', true); }catch(e){ setStatus(e.message||String(e), false); } } refresh.onclick = load; setTimeout(load, 50); })(); </script> </body> </html>