mirror of
https://github.com/penpot/penpot.git
synced 2026-08-15 01:08:45 +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;">
|
<div style="flex: 0 0 440px; min-width: 360px;">
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Load graph in memory</legend>
|
<legend>Load graph from Penpot</legend>
|
||||||
<desc>
|
<desc>
|
||||||
Projects the Penpot file into an in-memory Ladybug database for this
|
Click file or paste UUID to load Penpot file into an in-memory
|
||||||
admin session. Loading a new file replaces the previous one.
|
Ladybug database. Loading a new file replaces the previous one.
|
||||||
</desc>
|
</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">
|
<form id="graph-load-form" method="post" action="/dbg/actions/graph-load">
|
||||||
<div class="row">
|
<div class="row" style="display: flex; gap: 8px;">
|
||||||
<input type="text" style="width:420px" name="file-id"
|
<input type="text" style="width:320px" name="file-id"
|
||||||
placeholder="file-id"
|
placeholder="file-id"
|
||||||
value="{% if session %}{{session.file-id}}{% endif %}" />
|
value="{% if session %}{{session.file-id}}{% endif %}" />
|
||||||
</div>
|
|
||||||
<div class="row" style="display: flex; gap: 8px;">
|
|
||||||
<input type="submit" value="Load" />
|
<input type="submit" value="Load" />
|
||||||
{% if session %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -44,15 +44,9 @@ Graph Console
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</fieldset>
|
</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 %}
|
{% if session %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Loaded session</legend>
|
<legend>Loaded session (<span id="graph-loaded-at">{{session.loaded-at}}</span>)</legend>
|
||||||
<desc>
|
<desc>
|
||||||
<p>
|
<p>
|
||||||
File: <b><a id="graph-penpot-link" data-file-id="{{session.file-id}}"
|
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 />
|
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 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 />
|
Graph size: <b id="graph-size">…</b><br />
|
||||||
Schema: <b>{{session.schema-version}}</b><br />
|
Schema: <b>{{session.schema-version}}</b>
|
||||||
Loaded at: <b id="graph-loaded-at">{{session.loaded-at}}</b>
|
|
||||||
</p>
|
</p>
|
||||||
<p id="graph-sync-status">
|
<p id="graph-sync-status">
|
||||||
Feed: <b id="graph-ws-status">connecting…</b>
|
Feed: <b id="graph-ws-status">connecting…</b>
|
||||||
@ -73,16 +66,11 @@ Graph Console
|
|||||||
</desc>
|
</desc>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset id="graph-changelog-box" style="display: none;">
|
||||||
<legend>File changes (live)</legend>
|
<legend>Live changes</legend>
|
||||||
<desc>
|
<desc>Latest changes applied to the backend graph.</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>
|
|
||||||
<table id="graph-changelog" border="1" cellpadding="4" cellspacing="0"
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>revn</th>
|
<th>revn</th>
|
||||||
@ -94,8 +82,8 @@ Graph Console
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>LadybugDB <a href="https://docs.ladybugdb.com/cypher/"
|
<legend>Query graph (<a href="https://docs.ladybugdb.com/cypher/"
|
||||||
target="_blank">Cypher</a></legend>
|
target="_blank">LadybugDB Cypher</a>)</legend>
|
||||||
<form id="graph-query-form" method="post" action="/dbg/actions/graph-query">
|
<form id="graph-query-form" method="post" action="/dbg/actions/graph-query">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<textarea name="query" rows="8" style="width:100%; font-family: monospace;"
|
<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;">
|
style="flex: 1 1 auto; min-width: 0;">
|
||||||
<fieldset id="graph-view-panel">
|
<fieldset id="graph-view-panel">
|
||||||
<legend>Graph view
|
<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
|
<input type="checkbox" id="graph-fold-toggle" /> fold containers
|
||||||
</label>
|
</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
|
<input type="checkbox" id="graph-animate-toggle" /> animate
|
||||||
</label>
|
</label>
|
||||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
<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"
|
fade: <input type="number" id="graph-diff-steps" min="0" max="20"
|
||||||
style="width: 3.5em;" /> steps
|
style="width: 3.5em;" /> steps
|
||||||
</label>
|
</label>
|
||||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
<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
|
<input type="checkbox" id="graph-diff-fold" /> fold unchanged
|
||||||
</label>
|
</label>
|
||||||
<label style="margin-left: 1em; font-size: 12px; font-weight: normal;"
|
<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"
|
fold ≥ depth: <input type="number" id="graph-depth-fold" min="0" max="99"
|
||||||
style="width: 3em;" />
|
style="width: 3em;" />
|
||||||
</label>
|
</label>
|
||||||
@ -175,9 +165,8 @@ Graph Console
|
|||||||
</label>
|
</label>
|
||||||
</legend>
|
</legend>
|
||||||
<desc>
|
<desc>
|
||||||
Live view of the in-memory Ladybug graph (AntV G6); the legend
|
Live view of the in-memory Ladybug graph (AntV G6). Double-click
|
||||||
shows what is displayed. Double-click folds containers when
|
folds containers when folding is on.
|
||||||
folding is on.
|
|
||||||
</desc>
|
</desc>
|
||||||
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
||||||
<div id="graph-canvas"
|
<div id="graph-canvas"
|
||||||
@ -339,9 +328,8 @@ Graph Console
|
|||||||
const wsStatus = document.getElementById("graph-ws-status");
|
const wsStatus = document.getElementById("graph-ws-status");
|
||||||
const syncRevnEl = document.getElementById("graph-sync-revn");
|
const syncRevnEl = document.getElementById("graph-sync-revn");
|
||||||
const syncErrorEl = document.getElementById("graph-sync-error");
|
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 changelogBody = document.getElementById("graph-changelog-body");
|
||||||
const changelogEmpty = document.getElementById("graph-changelog-empty");
|
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
|
|
||||||
@ -607,7 +595,7 @@ Graph Console
|
|||||||
const DEFAULT_LAYOUT = "tree";
|
const DEFAULT_LAYOUT = "tree";
|
||||||
// Always-on labels are the residual overlap driver on ring/stress
|
// Always-on labels are the residual overlap driver on ring/stress
|
||||||
// layouts (their collision handling ignores label extents), so these
|
// 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 = {
|
const DENSE_LABEL_LAYOUTS = {
|
||||||
"concentric": true, "radial": true, "d3-force": true
|
"concentric": true, "radial": true, "d3-force": true
|
||||||
};
|
};
|
||||||
@ -703,9 +691,10 @@ Graph Console
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Derived fold state (overrides manual double-click folds while on):
|
// Derived fold state (overrides manual double-click folds while on):
|
||||||
// overview mode collapses every combo at depth ≥ the limit (all of
|
// overview mode collapses every combo at depth ≥ the limit — except
|
||||||
// them when only diff-fold is on), then diff-fold re-opens the
|
// 0, which expands everything (saves hunting for the max depth) —
|
||||||
// ancestor paths of changed elements — combined, that reads as
|
// 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".
|
// "overview, with changes drilled open".
|
||||||
const depthLimit = depthFoldValue();
|
const depthLimit = depthFoldValue();
|
||||||
if (withCombos && (diffFold || depthLimit != null)) {
|
if (withCombos && (diffFold || depthLimit != null)) {
|
||||||
@ -718,6 +707,7 @@ Graph Console
|
|||||||
};
|
};
|
||||||
collapsedIds = new Set();
|
collapsedIds = new Set();
|
||||||
Object.keys(hasCombo).forEach(function (id) {
|
Object.keys(hasCombo).forEach(function (id) {
|
||||||
|
if (depthLimit === 0) return;
|
||||||
if (depthLimit == null || depth(id) >= depthLimit) {
|
if (depthLimit == null || depth(id) >= depthLimit) {
|
||||||
collapsedIds.add(comboIdFor(id));
|
collapsedIds.add(comboIdFor(id));
|
||||||
}
|
}
|
||||||
@ -928,17 +918,6 @@ Graph Console
|
|||||||
exportGraphPng();
|
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;
|
if (layoutCfg) opts.layout = layoutCfg;
|
||||||
@ -1091,22 +1070,19 @@ Graph Console
|
|||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
// Session-fieldset size line stays live even through skipped
|
// Session-fieldset size line stays live even through skipped
|
||||||
// repaints (unlike the status line under the canvas). The MiB
|
// repaints (unlike the status line under the canvas); hover
|
||||||
// figure is a resident-size estimate fitted to the measurements
|
// shows per-table counts.
|
||||||
// in graph_sizes.md: ≈1.1 MiB session floor + ≈5.4 KiB per node
|
|
||||||
// (full-schema projection); hover shows per-table counts.
|
|
||||||
const sizeEl = document.getElementById("graph-size");
|
const sizeEl = document.getElementById("graph-size");
|
||||||
if (sizeEl) {
|
if (sizeEl) {
|
||||||
const mib = 1.1 + data.nodes.length * 5.4 / 1024;
|
|
||||||
sizeEl.textContent = data.nodes.length + " nodes, "
|
sizeEl.textContent = data.nodes.length + " nodes, "
|
||||||
+ data.edges.length + " edges (≈" + mib.toFixed(1) + " MiB)";
|
+ data.edges.length + " edges";
|
||||||
const counts = {};
|
const counts = {};
|
||||||
data.nodes.forEach(function (n) {
|
data.nodes.forEach(function (n) {
|
||||||
counts[n.table] = (counts[n.table] || 0) + 1;
|
counts[n.table] = (counts[n.table] || 0) + 1;
|
||||||
});
|
});
|
||||||
sizeEl.title = Object.keys(counts).sort().map(function (t) {
|
sizeEl.title = Object.keys(counts).sort().map(function (t) {
|
||||||
return t + "=" + counts[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
|
// Skip the repaint when the display projection is unchanged: a
|
||||||
// change burst that only touches non-projected attrs (e.g. moving
|
// change burst that only touches non-projected attrs (e.g. moving
|
||||||
@ -1174,8 +1150,9 @@ Graph Console
|
|||||||
} else {
|
} else {
|
||||||
const columns = result.columns || [];
|
const columns = result.columns || [];
|
||||||
let hidden = 0;
|
let hidden = 0;
|
||||||
html += '<table border="1" cellpadding="2" cellspacing="0"'
|
// Two-column flow; JSON-like values fold behind the same
|
||||||
+ ' style="border-collapse: collapse; margin-top: 4px;">';
|
// disclosure triangle the file tree uses.
|
||||||
|
html += '<div style="column-count: 2; column-gap: 16px; margin-top: 4px;">';
|
||||||
columns.forEach(function (col, i) {
|
columns.forEach(function (col, i) {
|
||||||
const val = rows[0][i];
|
const val = rows[0][i];
|
||||||
// Ladybug's value->clj fallback renders SQL nulls as "NULL".
|
// Ladybug's value->clj fallback renders SQL nulls as "NULL".
|
||||||
@ -1183,11 +1160,21 @@ Graph Console
|
|||||||
hidden += 1;
|
hidden += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
html += "<tr><td><code>" + escapeHtml(col.replace(/^n\./, ""))
|
const key = escapeHtml(col.replace(/^n\./, ""));
|
||||||
+ "</code></td><td><code>" + escapeHtml(String(val))
|
const sval = String(val);
|
||||||
+ "</code></td></tr>";
|
// 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) {
|
if (hidden) {
|
||||||
html += '<div style="color: #666;">' + hidden + " empty attrs hidden</div>";
|
html += '<div style="color: #666;">' + hidden + " empty attrs hidden</div>";
|
||||||
}
|
}
|
||||||
@ -1304,8 +1291,9 @@ Graph Console
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendChange(revn, summary) {
|
function appendChange(revn, summary) {
|
||||||
changelogEmpty.style.display = "none";
|
// The whole box stays hidden until the first change arrives; feed
|
||||||
changelog.style.display = "table";
|
// state lives in the session fieldset.
|
||||||
|
changelogBox.style.display = "block";
|
||||||
|
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
const revnCell = document.createElement("td");
|
const revnCell = document.createElement("td");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user