PageSourceSearch

https://smartcone.ai/src/shell/types/erdiagram/SchemaTree.js

js smartcone.ai collected 2026-10-05 15:07:36 UTC 6,844 bytes, 201 lines download raw bytes

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.