1import { parseNDJSON } from "../../../streams/schema.js"; 2import { createStatusIcon, normalizeStatus } from "./StatusIcon.js"; 3 4export function buildTreeFromNDJSON(ndjsonText) { 5 const records = parseNDJSON(ndjsonText) || []; 6 const entities = records.filter((record) => record?.type === "entity"); 7 const relationships = records.filter((record) => record?.type === "relationship"); 8 const fields = records.filter((record) => record?.type === "field"); 9 10 return { 11 entities: entities.map((entity) => ({ 12 id: String(entity.id || entity.name), 13 name: String(entity.name || entity.id || "Entity"), 14 status: normalizeStatus(entity.status), 15 fields: fields 16 .filter((field) => String(field.entity || field.OwnerName || "") === String(entity.name || entity.id || "")) 17 .map((field) => ({ 18 name: String(field.name || field.FieldName || "Field"), 19 fieldType: String(field.fieldType || field.type || field.Type || "unknown"), 20 index: String(field.index || field.Index || ""), 21 })), 22 })), 23 relationships: relationships.map((rel) => ({ 24 id: String(rel.id || rel.name || `${rel.from || ""}:${rel.to || ""}`), 25 name: String(rel.name || rel.id || "Relationship"), 26 from: String(rel.from || ""), 27 to: String(rel.to || ""), 28 status: normalizeStatus(rel.status), 29 })), 30 }; 31} 32 33export function filterTreeData(tree, searchTerm) { 34 if (!searchTerm) return tree; 35 const needle = searchTerm.toLowerCase(); 36 return { 37 entities: tree.entities.filter((entity) => entity.name.toLowerCase().includes(needle)), 38 relationships: tree.relationships.filter((rel) => rel.name.toLowerCase().includes(needle)), 39 }; 40} 41 42async function loadSchema(schemaPath) { 43 const response = await fetch(schemaPath); 44 return response.text(); 45} 46 47function bindNodeContextMenu(nodeEl, node, onContextMenu) { 48 if (!onContextMenu) return; 49 nodeEl.addEventListener("contextmenu", (event) => { 50 event.preventDefault(); 51 onContextMenu(node, { x: event.clientX, y: event.clientY }); 52 }); 53} 54 55export function createSchemaTree({ onSelect, onContextMenu } = {}) { 56 const root = document.createElement("div"); 57 root.className = "schema-tree"; 58 59 const search = document.createElement("input"); 60 search.type = "text"; 61 search.placeholder = "Search..."; 62 root.appendChild(search); 63 64 const content = document.createElement("div"); 65 root.appendChild(content); 66 67 let treeData = { entities: [], relationships: [] }; 68 const expanded = new Set(); 69 70 function toggleExpand(id) { 71 if (expanded.has(id)) expanded.delete(id); 72 else expanded.add(id); 73 render(search.value); 74 } 75 76 function createSectionTitle(text) { 77 const heading = document.createElement("h3"); 78 heading.textContent = text; 79 return heading; 80 } 81 82 function render(term = "") { 83 content.innerHTML = ""; 84 const filtered = filterTreeData(treeData, term); 85 86 const entitiesSection = document.createElement("div"); 87 entitiesSection.className = "tree-section"; 88 entitiesSection.appendChild(createSectionTitle(`Entities (${filtered.entities.length})`)); 89 90 filtered.entities.forEach((entity) => { 91 const node = document.createElement("div"); 92 node.className = "tree-node"; 93 94 const header = document.createElement("div"); 95 header.className = "tree-node-header"; 96 header.addEventListener("click", () => { 97 toggleExpand(entity.id); 98 onSelect?.(entity); 99 }); 100 header.append(createStatusIcon(entity.status)); 101 102 const name = document.createElement("span"); 103 name.textContent = entity.name; 104 header.appendChild(name); 105 106 const icon = document.createElement("span"); 107 icon.className = "expand-icon"; 108 icon.textContent = expanded.has(entity.id) ? "â¼" : "â¶"; 109 header.appendChild(icon); 110 bindNodeContextMenu(header, { ...entity, type: "entity" }, onContextMenu); 111 112 node.appendChild(header); 113 114 if (expanded.has(entity.id)) { 115 const children = document.createElement("div"); 116 children.className = "tree-node-children"; 117 entity.fields.forEach((field) => { 118 const fieldNode = document.createElement("div"); 119 fieldNode.className = "field-node"; 120 121 if (String(field.index).toLowerCase() === "yes" || String(field.index).toLowerCase() === "pkey") {
122 const keyIcon = document.createElement("span"); 123 keyIcon.className = "index-icon"; 124 keyIcon.textContent = "ð"; 125 fieldNode.appendChild(keyIcon); 126 } 127 128 const fieldName = document.createElement("span"); 129 fieldName.textContent = field.name; 130 fieldNode.appendChild(fieldName); 131 132 const fieldType = document.createElement("span"); 133 fieldType.className = "field-type"; 134 fieldType.textContent = field.fieldType; 135 fieldNode.appendChild(fieldType); 136 137 children.appendChild(fieldNode); 138 }); 139 node.appendChild(children); 140 } 141 142 entitiesSection.appendChild(node); 143 }); 144 145 const relSection = document.createElement("div"); 146 relSection.className = "tree-section"; 147 relSection.appendChild(createSectionTitle(`Relationships (${filtered.relationships.length})`)); 148 149 filtered.relationships.forEach((rel) => { 150 const node = document.createElement("div"); 151 node.className = "tree-node tree-node-rel"; 152 node.addEventListener("click", () => onSelect?.(rel)); 153 bindNodeContextMenu(node, { ...rel, type: "relationship" }, onContextMenu); 154 node.append(createStatusIcon(rel.status)); 155 156 const name = document.createElement("span"); 157 name.textContent = rel.name; 158 node.appendChild(name); 159 160 const arrow = document.createElement("span"); 161 arrow.className = "rel-arrow"; 162 arrow.textContent = "â"; 163 node.appendChild(arrow); 164 165 const from = document.createElement("span"); 166 from.className = "rel-from"; 167 from.textContent = rel.from; 168 node.appendChild(from); 169 170 const to = document.createElement("span"); 171 to.className = "rel-to"; 172 to.textContent = rel.to; 173 node.appendChild(to); 174 175 relSection.appendChild(node); 176 }); 177 178 content.append(entitiesSection, relSection); 179 } 180 181 search.addEventListener("input", () => render(search.value)); 182 183 return { 184 el: root, 185 async setSchema(schemaPathOrText, { inline = false } = {}) { 186 if (!schemaPathOrText) { 187 treeData = { entities: [], relationships: [] }; 188 render(search.value); 189 return; 190 } 191 const ndjsonText = inline ? String(schemaPathOrText) : await loadSchema(schemaPathOrText); 192 treeData = buildTreeFromNDJSON(ndjsonText); 193 expanded.clear(); 194 render(search.value); 195 }, 196 setSearch(term) { 197 search.value = term || ""; 198 render(search.value); 199 }, 200 }; 201}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.