mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 22:08:39 +00:00
✨ Split graph console in two columns; add file tree and fullscreen
Graph view moves to its own sticky right column (overrides .widget max-width). New /dbg/actions/graph-files endpoint lists teams -> projects -> files for the profile; the console renders it as a collapsible tree where clicking a file loads it. Maximize button fullscreens the graph panel and resizes G6 on fullscreenchange. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
parent
66376406c9
commit
208a65c8df
@ -11,16 +11,19 @@ Graph Console
|
||||
</div>
|
||||
</nav>
|
||||
<main class="dashboard">
|
||||
<section class="widget">
|
||||
<section class="widget" style="max-width: none;">
|
||||
<p><a href="/dbg">← Back to debug</a></p>
|
||||
|
||||
<div style="display: flex; gap: 16px; align-items: flex-start;">
|
||||
<div style="flex: 0 0 440px; min-width: 360px;">
|
||||
|
||||
<fieldset>
|
||||
<legend>Load graph in memory</legend>
|
||||
<desc>
|
||||
Projects the Penpot file into an in-memory Ladybug database for this
|
||||
admin session. Loading a new file replaces the previous one.
|
||||
</desc>
|
||||
<form method="post" action="/dbg/actions/graph-load">
|
||||
<form id="graph-load-form" method="post" action="/dbg/actions/graph-load">
|
||||
<div class="row">
|
||||
<input type="text" style="width:420px" name="file-id"
|
||||
placeholder="file-id"
|
||||
@ -39,6 +42,12 @@ 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>
|
||||
@ -88,19 +97,6 @@ Graph Console
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Graph view</legend>
|
||||
<desc>
|
||||
Renders the in-memory Ladybug graph with AntV G6 (CDN). Node color
|
||||
and glyph encode the node table; edges are <code>IsChildOf</code>
|
||||
(arrow points to parent). Redraws automatically after live changes.
|
||||
</desc>
|
||||
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
||||
<div id="graph-canvas"
|
||||
style="width: 100%; height: 600px; border: 1px solid #ccc; background: #fff;"></div>
|
||||
<div id="graph-view-status" style="color: #666; font-size: 12px;"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Cypher query</legend>
|
||||
<form id="graph-query-form" method="post" action="/dbg/actions/graph-query">
|
||||
@ -146,9 +142,111 @@ Graph Console
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div><!-- left column -->
|
||||
|
||||
{% if session %}
|
||||
<div id="graph-view-column"
|
||||
style="flex: 1 1 auto; min-width: 0; position: sticky; top: 8px;">
|
||||
<fieldset id="graph-view-panel">
|
||||
<legend>Graph view
|
||||
<button type="button" id="graph-maximize" style="margin-left: 1em;">Maximize</button>
|
||||
</legend>
|
||||
<desc>
|
||||
Renders the in-memory Ladybug graph with AntV G6 (CDN). Node color
|
||||
and glyph encode the node table; edges are <code>IsChildOf</code>
|
||||
(arrow points to parent). Redraws automatically after live changes.
|
||||
</desc>
|
||||
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
||||
<div id="graph-canvas"
|
||||
style="width: 100%; height: 600px; border: 1px solid #ccc; background: #fff;"></div>
|
||||
<div id="graph-view-status" style="color: #666; font-size: 12px;"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div><!-- flex row -->
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#graph-view-panel:fullscreen {
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
#graph-view-panel:fullscreen #graph-canvas {
|
||||
height: calc(100vh - 110px) !important;
|
||||
}
|
||||
#graph-files-tree summary { cursor: pointer; }
|
||||
#graph-files-tree ul { margin: 2px 0 4px 0; padding-left: 2em; }
|
||||
#graph-files-tree a { text-decoration: none; }
|
||||
#graph-files-tree a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tree = document.getElementById("graph-files-tree");
|
||||
const loadForm = document.getElementById("graph-load-form");
|
||||
const loadInput = loadForm ? loadForm.querySelector("input[name=file-id]") : null;
|
||||
if (!tree) return;
|
||||
|
||||
function fileLink(file) {
|
||||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
a.href = "#";
|
||||
a.textContent = file.name;
|
||||
a.title = file.id;
|
||||
a.addEventListener("click", function (ev) {
|
||||
ev.preventDefault();
|
||||
if (loadInput) {
|
||||
loadInput.value = file.id;
|
||||
loadForm.submit();
|
||||
}
|
||||
});
|
||||
li.appendChild(a);
|
||||
return li;
|
||||
}
|
||||
|
||||
fetch("/dbg/actions/graph-files")
|
||||
.then(function (resp) {
|
||||
if (!resp.ok) throw new Error("graph-files HTTP " + resp.status);
|
||||
return resp.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
tree.textContent = "";
|
||||
const teams = data.teams || [];
|
||||
if (!teams.length) {
|
||||
tree.textContent = "No files found.";
|
||||
return;
|
||||
}
|
||||
teams.forEach(function (team) {
|
||||
const teamEl = document.createElement("details");
|
||||
const teamSummary = document.createElement("summary");
|
||||
teamSummary.textContent = team.name;
|
||||
teamEl.appendChild(teamSummary);
|
||||
(team.projects || []).forEach(function (project) {
|
||||
const projEl = document.createElement("details");
|
||||
projEl.style.marginLeft = "1em";
|
||||
const projSummary = document.createElement("summary");
|
||||
projSummary.textContent = project.name;
|
||||
projEl.appendChild(projSummary);
|
||||
const list = document.createElement("ul");
|
||||
(project.files || []).forEach(function (file) {
|
||||
list.appendChild(fileLink(file));
|
||||
});
|
||||
projEl.appendChild(list);
|
||||
teamEl.appendChild(projEl);
|
||||
});
|
||||
tree.appendChild(teamEl);
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
tree.textContent = "Failed to load file tree: " + err;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% if session %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/@antv/g6@5/dist/g6.min.js"></script>
|
||||
<script>
|
||||
@ -218,6 +316,7 @@ Graph Console
|
||||
container: "graph-canvas",
|
||||
data: g6data,
|
||||
autoFit: "view",
|
||||
autoResize: true,
|
||||
padding: 20,
|
||||
layout: { type: "antv-dagre", rankdir: "BT", nodesep: 10, ranksep: 40 },
|
||||
node: {
|
||||
@ -506,6 +605,33 @@ Graph Console
|
||||
});
|
||||
}
|
||||
|
||||
const maximizeBtn = document.getElementById("graph-maximize");
|
||||
const graphPanel = document.getElementById("graph-view-panel");
|
||||
if (maximizeBtn && graphPanel) {
|
||||
maximizeBtn.addEventListener("click", function () {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
graphPanel.requestFullscreen().catch(function (err) {
|
||||
graphViewStatus.textContent = "fullscreen rejected: " + err;
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener("fullscreenchange", function () {
|
||||
maximizeBtn.textContent =
|
||||
document.fullscreenElement ? "Exit fullscreen" : "Maximize";
|
||||
setTimeout(function () {
|
||||
if (g6graph) {
|
||||
const canvas = document.getElementById("graph-canvas");
|
||||
try {
|
||||
g6graph.setSize(canvas.clientWidth, canvas.clientHeight);
|
||||
g6graph.fitView();
|
||||
} catch (_err) { /* autoResize covers most cases */ }
|
||||
}
|
||||
}, 120);
|
||||
});
|
||||
}
|
||||
|
||||
connect();
|
||||
refreshSyncStatus();
|
||||
renderGraphLegend();
|
||||
|
||||
@ -419,6 +419,49 @@
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode {:error "no-session"})}))
|
||||
|
||||
(def ^:private sql:graph-files
|
||||
"select t.id as team_id, t.name as team_name,
|
||||
p.id as project_id, p.name as project_name,
|
||||
f.id as file_id, f.name as file_name
|
||||
from team as t
|
||||
join team_profile_rel as tpr on (tpr.team_id = t.id)
|
||||
join project as p on (p.team_id = t.id)
|
||||
join file as f on (f.project_id = p.id)
|
||||
where tpr.profile_id = ?
|
||||
and t.deleted_at is null
|
||||
and p.deleted_at is null
|
||||
and f.deleted_at is null
|
||||
order by t.name, p.name, f.name
|
||||
limit 500")
|
||||
|
||||
(defn- graph-files-tree
|
||||
[rows]
|
||||
(->> (group-by (juxt :team-id :team-name) rows)
|
||||
(mapv (fn [[[team-id team-name] team-rows]]
|
||||
{:id (str team-id)
|
||||
:name team-name
|
||||
:projects
|
||||
(->> (group-by (juxt :project-id :project-name) team-rows)
|
||||
(mapv (fn [[[project-id project-name] project-rows]]
|
||||
{:id (str project-id)
|
||||
:name project-name
|
||||
:files (mapv (fn [{:keys [file-id file-name]}]
|
||||
{:id (str file-id) :name file-name})
|
||||
project-rows)}))
|
||||
(sort-by :name)
|
||||
(vec))}))
|
||||
(sort-by :name)
|
||||
(vec)))
|
||||
|
||||
(defn graph-files-handler
|
||||
"List teams -> projects -> files reachable by the current profile, as
|
||||
plain JSON for the graph console file tree."
|
||||
[{:keys [::db/pool]} {:keys [::session/profile-id]}]
|
||||
(let [rows (db/exec! pool [sql:graph-files profile-id])]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode {:teams (graph-files-tree rows)})}))
|
||||
|
||||
(defn- json-request?
|
||||
[request]
|
||||
(some-> request
|
||||
@ -710,6 +753,7 @@
|
||||
["/graph-reload" {:handler (partial graph-reload-handler cfg)}]
|
||||
["/graph-sync-status" {:handler (partial graph-sync-status-handler cfg)}]
|
||||
["/graph-data" {:handler (partial graph-data-handler cfg)}]
|
||||
["/graph-files" {:handler (partial graph-files-handler cfg)}]
|
||||
["/file-import" {:handler (partial import-handler cfg)}]
|
||||
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user