Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ artefactory-*.tar

# Temporary files, for example, from tests.
/tmp/
/drafts
/.drafts
/.elixir_ls

.DS_Store
Expand Down
4 changes: 4 additions & 0 deletions artefact_kino/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ SPDX-License-Identifier: MIT

# Changelog

## 0.1.4 — 2026-05-05

- Inspector panel collapsible (matching the Export panel); both default collapsed to give the graph more room on bigger artefacts; selecting a node or relationship in the graph auto-expands the Inspector. Bumps `artefact` requirement to `~> 0.1.4` for convenience.

## 0.1.3 — 2026-04-30

- Compatible with `artefact ~> 0.1.3`
Expand Down
4 changes: 3 additions & 1 deletion artefact_kino/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ ArtefactKino is a viewer, not an editor. It renders three panels side by side:

- **Graph** (heartside) — an interactive vis-network graph. Nodes are colour-coded by label, with colours blended for multi-label nodes using circular hue averaging in linear RGB space. Layout strategies: Physics, Hierarchical, Radial.
- **Inspector** — tabbed Elixir view of the artefact struct, nodes table, and relationships table. Clicking a node or relationship in the graph navigates to and highlights the corresponding row.
- **Export** — CREATE Cypher, MERGE Cypher, and Arrows JSON. Click any panel to select all text for easy copying. The export panel is collapsible to give more space to the graph and inspector.
- **Export** — CREATE Cypher, MERGE Cypher, Arrows JSON, and Mermaid source. Click any panel to select all text for easy copying.

The Inspector and Export panels are both collapsible and start collapsed by default to give the graph room on bigger artefacts; selecting a node or relationship in the graph auto-expands the Inspector.

MERGE Cypher upserts nodes by uuid — safe to run repeatedly. CREATE always makes new nodes. See the [CreateMerge artefact](https://github.com/diffo-dev/artefactory) for a visual explanation of the difference.

Expand Down
116 changes: 72 additions & 44 deletions artefact_kino/lib/artefact_kino.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,35 @@ defmodule ArtefactKino do

defp build_data(artefact, default) do
%{
nodes: vis_nodes(artefact),
edges: vis_edges(artefact),
nodes: vis_nodes(artefact),
edges: vis_edges(artefact),
create_cypher: Artefact.Cypher.create(artefact),
merge_cypher: Artefact.Cypher.merge(artefact),
arrows_json: Artefact.Arrows.to_json(artefact),
mermaid: Artefact.Mermaid.export(artefact),
default: Atom.to_string(default),
title: artefact.title || artefact.base_label || "Artefact",
description: artefact.description,
merge_cypher: Artefact.Cypher.merge(artefact),
arrows_json: Artefact.Arrows.to_json(artefact),
mermaid: Artefact.Mermaid.export(artefact),
default: Atom.to_string(default),
title: artefact.title || artefact.base_label || "Artefact",
description: artefact.description,
artefact_rows: artefact_rows(artefact),
nodes_rows: nodes_rows(artefact),
rels_rows: rels_rows(artefact)
nodes_rows: nodes_rows(artefact),
rels_rows: rels_rows(artefact)
}
end

defp vis_nodes(%Artefact{graph: graph, base_label: base_label}) do
Enum.map(graph.nodes, fn node ->
all_labels = Enum.uniq(node.labels ++ if(base_label, do: [base_label], else: []))
all_labels = Enum.uniq(node.labels ++ if(base_label, do: [base_label], else: []))
semantic_labels = Enum.reject(node.labels, &(&1 == base_label))
name = Map.get(node.properties, "name", node.id)
label = if semantic_labels == [], do: name, else: "#{name}\n#{Enum.join(semantic_labels, " ")}"
tooltip = node.properties
name = Map.get(node.properties, "name", node.id)

label =
if semantic_labels == [], do: name, else: "#{name}\n#{Enum.join(semantic_labels, " ")}"

tooltip =
node.properties
|> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
|> Enum.join("\n")

%{id: node.id, label: label, labels: all_labels, title: "#{node.uuid}\n#{tooltip}"}
end)
end
Expand All @@ -69,21 +74,21 @@ defmodule ArtefactKino do

defp artefact_rows(%Artefact{} = a) do
[
%{key: "id", value: a.id},
%{key: "uuid", value: a.uuid},
%{key: "title", value: inspect(a.title)},
%{key: "id", value: a.id},
%{key: "uuid", value: a.uuid},
%{key: "title", value: inspect(a.title)},
%{key: "description", value: inspect(a.description)},
%{key: "base_label", value: inspect(a.base_label)},
%{key: "metadata", value: inspect(a.metadata, pretty: true)}
%{key: "base_label", value: inspect(a.base_label)},
%{key: "metadata", value: inspect(a.metadata, pretty: true)}
]
end

defp nodes_rows(%Artefact{graph: graph}) do
Enum.map(graph.nodes, fn n ->
%{
id: n.id,
uuid: n.uuid,
labels: Enum.join(n.labels, ", "),
id: n.id,
uuid: n.uuid,
labels: Enum.join(n.labels, ", "),
properties: inspect(n.properties)
}
end)
Expand All @@ -94,10 +99,10 @@ defmodule ArtefactKino do
|> Enum.with_index()
|> Enum.map(fn {r, idx} ->
%{
idx: idx,
from: r.from_id,
type: r.type,
to: r.to_id,
idx: idx,
from: r.from_id,
type: r.type,
to: r.to_id,
properties: inspect(r.properties)
}
end)
Expand Down Expand Up @@ -219,11 +224,12 @@ defmodule ArtefactKino do
</div>

<!-- elixir panel -->
<div style="flex:1;display:flex;flex-direction:column;border-right:1px solid #333;">
<div style="display:flex;gap:0;border-bottom:1px solid #333;">
<div id="inspector-panel" style="flex:1;display:flex;flex-direction:column;border-right:1px solid #333;">
<div style="display:flex;gap:4px;padding:4px;border-bottom:1px solid #333;align-items:center;justify-content:flex-end;">
<button class="tbtn" data-tab="artefact" style="flex:1;">Artefact</button>
<button class="tbtn" data-tab="nodes" style="flex:1;">Nodes</button>
<button class="tbtn" data-tab="rels" style="flex:1;">Relationships</button>
<button id="inspector-collapse-btn" title="Collapse">◀</button>
</div>
<div id="tab-content" style="flex:1;overflow:auto;"></div>
</div>
Expand All @@ -235,7 +241,7 @@ defmodule ArtefactKino do
<button class="cbtn" data-cypher="merge">MERGE</button>
<button class="cbtn" data-cypher="json">JSON</button>
<button class="cbtn" data-cypher="mermaid">MERMAID</button>
<button id="collapse-btn" title="Collapse" style="margin-left:auto;">◀</button>
<button id="export-collapse-btn" title="Collapse" style="margin-left:auto;">◀</button>
</div>
<pre id="cypher" style="flex:1;overflow:auto;margin:0;padding:10px;font-size:11px;line-height:1.6;color:#e0e0e0;white-space:pre-wrap;cursor:text;"></pre>
</div>
Expand Down Expand Up @@ -266,21 +272,41 @@ defmodule ArtefactKino do
cypherBtns.forEach(b => b.addEventListener("click", () => { currentCypher = b.dataset.cypher; renderCypher(); }));
renderCypher();

// -- collapse export panel --
const exportPanel = ctx.root.querySelector("#export-panel");
const collapseBtn = ctx.root.querySelector("#collapse-btn");
let exportCollapsed = false;

btnStyle(collapseBtn, false);
collapseBtn.addEventListener("click", () => {
exportCollapsed = !exportCollapsed;
exportPanel.style.flex = exportCollapsed ? "0 0 32px" : "1";
exportPanel.style.overflow = "hidden";
collapseBtn.textContent = exportCollapsed ? "▶" : "◀";
btnStyle(collapseBtn, exportCollapsed);
ctx.root.querySelector("#cypher").style.display = exportCollapsed ? "none" : "";
ctx.root.querySelectorAll(".cbtn").forEach(b => b.style.display = exportCollapsed ? "none" : "");
});
// -- collapse helper (shared by inspector and export) --
function setupCollapse(panel, button, hideSelectors, defaultCollapsed) {
let collapsed;
function setState(state) {
collapsed = state;
panel.style.flex = collapsed ? "0 0 32px" : "1";
panel.style.overflow = "hidden";
button.textContent = collapsed ? "▶" : "◀";
btnStyle(button, collapsed);
// btnStyle uses cssText which wipes inline styles — reapply margin-left:auto
// so the button stays pushed to the right of the header strip when it is
// the only visible child (the panel-is-collapsed case).
button.style.marginLeft = "auto";
hideSelectors.forEach(sel => {
ctx.root.querySelectorAll(sel).forEach(el => el.style.display = collapsed ? "none" : "");
});
}
setState(defaultCollapsed);
button.addEventListener("click", () => setState(!collapsed));
return { expand: () => { if (collapsed) setState(false); } };
}

const inspectorControl = setupCollapse(
ctx.root.querySelector("#inspector-panel"),
ctx.root.querySelector("#inspector-collapse-btn"),
["#tab-content", ".tbtn"],
true
);

const exportControl = setupCollapse(
ctx.root.querySelector("#export-panel"),
ctx.root.querySelector("#export-collapse-btn"),
["#cypher", ".cbtn"],
true
);

// -- click to select all in pre elements --
function selectAll(el) {
Expand Down Expand Up @@ -369,13 +395,15 @@ defmodule ArtefactKino do

network.on("selectNode", ({ nodes: selected }) => {
if (!selected.length) return;
inspectorControl.expand();
pendingHighlight = selected[0];
renderTab("nodes");
});

network.on("selectEdge", ({ edges: selected, nodes: selectedNodes }) => {
if (!selected.length) return;
if (selectedNodes && selectedNodes.length > 0) return;
inspectorControl.expand();
pendingHighlight = selected[0];
renderTab("rels");
});
Expand Down
16 changes: 11 additions & 5 deletions artefact_kino/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ArtefactKino.MixProject do
@moduledoc false
use Mix.Project

@version "0.1.3"
@version "0.1.4"
@github_url "https://github.com/diffo-dev/artefactory"

def project do
Expand All @@ -17,7 +17,8 @@ defmodule ArtefactKino.MixProject do
deps: deps(),
package: package(),
name: "ArtefactKino",
description: "Livebook Kino widget for rendering Artefactory knowledge graph fragments (Artefacts)",
description:
"Livebook Kino widget for rendering Artefactory knowledge graph fragments (Artefacts)",
source_url: @github_url,
docs: docs()
]
Expand All @@ -41,13 +42,13 @@ defmodule ArtefactKino.MixProject do
defp artefact_dep do
cond do
System.get_env("HEX_PUBLISH") == "1" ->
{:artefact, "~> 0.1.3"}
{:artefact, "~> 0.1.4"}

File.exists?(Path.join(__DIR__, "../artefact/mix.exs")) ->
{:artefact, path: "../artefact"}

true ->
{:artefact, "~> 0.1.3"}
{:artefact, "~> 0.1.4"}
end
end

Expand All @@ -64,7 +65,12 @@ defmodule ArtefactKino.MixProject do
main: "ArtefactKino",
source_url: @github_url,
source_ref: "v#{@version}",
extras: ["README.md", "CHANGELOG.md"]
extras: [
"README.md",
"CHANGELOG.md",
{"artefact_kino.livemd", title: "Livebook"},
{"LICENSES/MIT.txt", title: "License (MIT)"}
]
]
end
end
Loading