mirror of
https://github.com/penpot/penpot.git
synced 2026-08-09 06:18:47 +00:00
💄 Console UI polish round
Merge the load form and files tree into one "Load graph from Penpot" box (tree first, uuid + Load/Unload in a row); Loaded session carries HH:MM in its legend; the Live changes box stays hidden until the first change arrives; query fieldset reads "Query graph (LadybugDB Cypher)" with the link covering both terms. Drop the hover tooltips (distracting, useless zoomed out) and the resident-size estimate (per-table counts stay on hover); every toggle gets a "When set/checked ..." title. Depth fold: 0 now expands every container (no more hunting for max depth). Node inspector: two-column flow, structured or long values folded behind the file-tree disclosure triangle. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
parent
53420388d5
commit
e011dedccf
@ -21,21 +21,21 @@ Graph Console
|
||||
<div style="flex: 0 0 440px; min-width: 360px;">
|
||||
|
||||
<fieldset>
|
||||
<legend>Load graph in memory</legend>
|
||||
<legend>Load graph from Penpot</legend>
|
||||
<desc>
|
||||
Projects the Penpot file into an in-memory Ladybug database for this
|
||||
admin session. Loading a new file replaces the previous one.
|
||||
Click file or paste UUID to load Penpot file into an in-memory
|
||||
Ladybug database. Loading a new file replaces the previous one.
|
||||
</desc>
|
||||
<div id="graph-files-tree" style="font-size: 13px; margin-bottom: 6px;">Loading…</div>
|
||||
<form id="graph-load-form" method="post" action="/dbg/actions/graph-load">
|
||||
<div class="row">
|
||||
<input type="text" style="width:420px" name="file-id"
|
||||
<div class="row" style="display: flex; gap: 8px;">
|
||||
<input type="text" style="width:320px" name="file-id"
|
||||
placeholder="file-id"
|
||||
value="{% if session %}{{session.file-id}}{% endif %}" />
|
||||
</div>
|
||||
<div class="row" style="display: flex; gap: 8px;">
|
||||
<input type="submit" value="Load" />
|
||||
{% if session %}
|
||||
<input type="submit" value="Unload" form="graph-unload-form" />
|
||||
<input type="submit" value="Unload" form="graph-unload-form"
|
||||
title="Drop the in-memory session and free its memory" />
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
@ -44,15 +44,9 @@ Graph Console
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Files</legend>
|
||||
<desc>Teams → projects → files. Click a file to load it.</desc>
|
||||
<div id="graph-files-tree" style="font-size: 13px;">Loading…</div>
|
||||
</fieldset>
|
||||
|
||||
{% if session %}
|
||||
<fieldset>
|
||||
<legend>Loaded session</legend>
|
||||
<legend>Loaded session (<span id="graph-loaded-at">{{session.loaded-at}}</span>)</legend>
|
||||
<desc>
|
||||
<p>
|
||||
File: <b><a id="graph-penpot-link" data-file-id="{{session.file-id}}"
|
||||
@ -60,8 +54,7 @@ Graph Console
|
||||
Loaded at revision: <b>{{session.revn}}</b><br />
|
||||
Graph revision: <b id="graph-sync-revn">{% if session.graph-revn %}{{session.graph-revn}}{% else %}{{session.revn}}{% endif %}</b><br />
|
||||
Graph size: <b id="graph-size">…</b><br />
|
||||
Schema: <b>{{session.schema-version}}</b><br />
|
||||
Loaded at: <b id="graph-loaded-at">{{session.loaded-at}}</b>
|
||||
Schema: <b>{{session.schema-version}}</b>
|
||||
</p>
|
||||
<p id="graph-sync-status">
|
||||
Feed: <b id="graph-ws-status">connecting…</b>
|
||||
@ -73,16 +66,11 @@ Graph Console
|
||||
</desc>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>File changes (live)</legend>
|
||||
<desc>
|
||||
Subscribes to the workspace WebSocket feed for visibility. The backend
|
||||
applies supported changes incrementally to the in-memory Ladybug graph
|
||||
via msgbus (<code>:file-change</code>).
|
||||
</desc>
|
||||
<div id="graph-changelog-empty" style="color: #666;">Waiting for changes…</div>
|
||||
<fieldset id="graph-changelog-box" style="display: none;">
|
||||
<legend>Live changes</legend>
|
||||
<desc>Latest changes applied to the backend graph.</desc>
|
||||
<table id="graph-changelog" border="1" cellpadding="4" cellspacing="0"
|
||||
style="border-collapse: collapse; width: 100%; display: none;">
|
||||
style="border-collapse: collapse; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>revn</th>
|
||||
@ -94,8 +82,8 @@ Graph Console
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>LadybugDB <a href="https://docs.ladybugdb.com/cypher/"
|
||||
target="_blank">Cypher</a></legend>
|
||||
<legend>Query graph (<a href="https://docs.ladybugdb.com/cypher/"
|
||||
target="_blank">LadybugDB Cypher</a>)</legend>
|
||||
<form id="graph-query-form" method="post" action="/dbg/actions/graph-query">
|
||||
<div class="row">
|
||||
<textarea name="query" rows="8" style="width:100%; font-family: monospace;"
|
||||
@ -150,23 +138,25 @@ Graph Console
|
||||
style="flex: 1 1 auto; min-width: 0;">
|
||||
<fieldset id="graph-view-panel">
|
||||
<legend>Graph view
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;">
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
||||
title="When checked, containers render as foldable boxes (combos); double-click folds/unfolds. When unchecked, plain nodes only — the fold controls to the right go dormant.">
|
||||
<input type="checkbox" id="graph-fold-toggle" /> fold containers
|
||||
</label>
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;">
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
||||
title="When checked, graphs up to 100 nodes render with entrance animation.">
|
||||
<input type="checkbox" id="graph-animate-toggle" /> animate
|
||||
</label>
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
||||
title="added/removed marks fade out over this many display steps (0 = off)">
|
||||
title="When set, added/removed marks fade out over this many display steps (0 disables diff marks).">
|
||||
fade: <input type="number" id="graph-diff-steps" min="0" max="20"
|
||||
style="width: 3.5em;" /> steps
|
||||
</label>
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
||||
title="collapse every container that holds no changed elements">
|
||||
title="When checked, every container without changed elements collapses, so changes stand out (overrides manual folds).">
|
||||
<input type="checkbox" id="graph-diff-fold" /> fold unchanged
|
||||
</label>
|
||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
||||
title="overview mode: collapse every container at or beyond this depth from the root (root = 0; empty = off)">
|
||||
title="When set, overview mode: 0 expands every container, n ≥ 1 collapses every container at depth ≥ n from the root (overrides manual folds; empty = manual folding).">
|
||||
fold ≥ depth: <input type="number" id="graph-depth-fold" min="0" max="99"
|
||||
style="width: 3em;" />
|
||||
</label>
|
||||
@ -175,9 +165,8 @@ Graph Console
|
||||
</label>
|
||||
</legend>
|
||||
<desc>
|
||||
Live view of the in-memory Ladybug graph (AntV G6); the legend
|
||||
shows what is displayed. Double-click folds containers when
|
||||
folding is on.
|
||||
Live view of the in-memory Ladybug graph (AntV G6). Double-click
|
||||
folds containers when folding is on.
|
||||
</desc>
|
||||
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
||||
<div id="graph-canvas"
|
||||
@ -339,9 +328,8 @@ Graph Console
|
||||
const wsStatus = document.getElementById("graph-ws-status");
|
||||
const syncRevnEl = document.getElementById("graph-sync-revn");
|
||||
const syncErrorEl = document.getElementById("graph-sync-error");
|
||||
const changelog = document.getElementById("graph-changelog");
|
||||
const changelogBox = document.getElementById("graph-changelog-box");
|
||||
const changelogBody = document.getElementById("graph-changelog-body");
|
||||
const changelogEmpty = document.getElementById("graph-changelog-empty");
|
||||
|
||||
let ws = null;
|
||||
|
||||
@ -607,7 +595,7 @@ Graph Console
|
||||
const DEFAULT_LAYOUT = "tree";
|
||||
// Always-on labels are the residual overlap driver on ring/stress
|
||||
// layouts (their collision handling ignores label extents), so these
|
||||
// get smaller labels; the hover tooltip carries full identity anywhere.
|
||||
// get smaller labels; the node inspector carries full identity.
|
||||
const DENSE_LABEL_LAYOUTS = {
|
||||
"concentric": true, "radial": true, "d3-force": true
|
||||
};
|
||||
@ -703,9 +691,10 @@ Graph Console
|
||||
});
|
||||
}
|
||||
// Derived fold state (overrides manual double-click folds while on):
|
||||
// overview mode collapses every combo at depth ≥ the limit (all of
|
||||
// them when only diff-fold is on), then diff-fold re-opens the
|
||||
// ancestor paths of changed elements — combined, that reads as
|
||||
// overview mode collapses every combo at depth ≥ the limit — except
|
||||
// 0, which expands everything (saves hunting for the max depth) —
|
||||
// and all of them when only diff-fold is on; diff-fold then re-opens
|
||||
// the ancestor paths of changed elements — combined, that reads as
|
||||
// "overview, with changes drilled open".
|
||||
const depthLimit = depthFoldValue();
|
||||
if (withCombos && (diffFold || depthLimit != null)) {
|
||||
@ -718,6 +707,7 @@ Graph Console
|
||||
};
|
||||
collapsedIds = new Set();
|
||||
Object.keys(hasCombo).forEach(function (id) {
|
||||
if (depthLimit === 0) return;
|
||||
if (depthLimit == null || depth(id) >= depthLimit) {
|
||||
collapsedIds.add(comboIdFor(id));
|
||||
}
|
||||
@ -928,17 +918,6 @@ Graph Console
|
||||
exportGraphPng();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "tooltip",
|
||||
type: "tooltip",
|
||||
getContent: function (_evt, items) {
|
||||
return Promise.resolve(items.map(function (it) {
|
||||
const d = it.data || {};
|
||||
return "<div><b>" + escapeHtml(d.table || "") + "</b> "
|
||||
+ escapeHtml(d.label || "") + "<br/><code>"
|
||||
+ escapeHtml(String(it.id)) + "</code></div>";
|
||||
}).join(""));
|
||||
}
|
||||
}]
|
||||
};
|
||||
if (layoutCfg) opts.layout = layoutCfg;
|
||||
@ -1091,22 +1070,19 @@ Graph Console
|
||||
})
|
||||
.then(function (data) {
|
||||
// Session-fieldset size line stays live even through skipped
|
||||
// repaints (unlike the status line under the canvas). The MiB
|
||||
// figure is a resident-size estimate fitted to the measurements
|
||||
// in graph_sizes.md: ≈1.1 MiB session floor + ≈5.4 KiB per node
|
||||
// (full-schema projection); hover shows per-table counts.
|
||||
// repaints (unlike the status line under the canvas); hover
|
||||
// shows per-table counts.
|
||||
const sizeEl = document.getElementById("graph-size");
|
||||
if (sizeEl) {
|
||||
const mib = 1.1 + data.nodes.length * 5.4 / 1024;
|
||||
sizeEl.textContent = data.nodes.length + " nodes, "
|
||||
+ data.edges.length + " edges (≈" + mib.toFixed(1) + " MiB)";
|
||||
+ data.edges.length + " edges";
|
||||
const counts = {};
|
||||
data.nodes.forEach(function (n) {
|
||||
counts[n.table] = (counts[n.table] || 0) + 1;
|
||||
});
|
||||
sizeEl.title = Object.keys(counts).sort().map(function (t) {
|
||||
return t + "=" + counts[t];
|
||||
}).join(", ") + " — MiB ≈ 1.1 + 5.4 KiB × nodes (measured fit)";
|
||||
}).join(", ");
|
||||
}
|
||||
// Skip the repaint when the display projection is unchanged: a
|
||||
// change burst that only touches non-projected attrs (e.g. moving
|
||||
@ -1174,8 +1150,9 @@ Graph Console
|
||||
} else {
|
||||
const columns = result.columns || [];
|
||||
let hidden = 0;
|
||||
html += '<table border="1" cellpadding="2" cellspacing="0"'
|
||||
+ ' style="border-collapse: collapse; margin-top: 4px;">';
|
||||
// Two-column flow; JSON-like values fold behind the same
|
||||
// disclosure triangle the file tree uses.
|
||||
html += '<div style="column-count: 2; column-gap: 16px; margin-top: 4px;">';
|
||||
columns.forEach(function (col, i) {
|
||||
const val = rows[0][i];
|
||||
// Ladybug's value->clj fallback renders SQL nulls as "NULL".
|
||||
@ -1183,11 +1160,21 @@ Graph Console
|
||||
hidden += 1;
|
||||
return;
|
||||
}
|
||||
html += "<tr><td><code>" + escapeHtml(col.replace(/^n\./, ""))
|
||||
+ "</code></td><td><code>" + escapeHtml(String(val))
|
||||
+ "</code></td></tr>";
|
||||
const key = escapeHtml(col.replace(/^n\./, ""));
|
||||
const sval = String(val);
|
||||
// Fold structured values ({…}) and walls of text (long arrays
|
||||
// like migrations) behind a disclosure triangle.
|
||||
if (sval.indexOf("{") !== -1 || sval.length > 120) {
|
||||
html += '<details style="break-inside: avoid;">'
|
||||
+ '<summary style="cursor: pointer;"><code>' + key + '</code></summary>'
|
||||
+ '<pre style="white-space: pre-wrap; word-break: break-all; margin: 2px 0 4px 1.2em;">'
|
||||
+ escapeHtml(sval) + "</pre></details>";
|
||||
} else {
|
||||
html += '<div style="break-inside: avoid;"><code>' + key
|
||||
+ '</code>: <code>' + escapeHtml(sval) + "</code></div>";
|
||||
}
|
||||
});
|
||||
html += "</table>";
|
||||
html += "</div>";
|
||||
if (hidden) {
|
||||
html += '<div style="color: #666;">' + hidden + " empty attrs hidden</div>";
|
||||
}
|
||||
@ -1304,8 +1291,9 @@ Graph Console
|
||||
}
|
||||
|
||||
function appendChange(revn, summary) {
|
||||
changelogEmpty.style.display = "none";
|
||||
changelog.style.display = "table";
|
||||
// The whole box stays hidden until the first change arrives; feed
|
||||
// state lives in the session fieldset.
|
||||
changelogBox.style.display = "block";
|
||||
|
||||
const row = document.createElement("tr");
|
||||
const revnCell = document.createElement("td");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user