Files
enviPy-bayer/templates/objects/pathway.html
Tobias O 1a2c9bb543 [Feature] Modern UI roll out (#236)
This PR moves all the collection pages into the new UI in a rough push.
I did not put the same amount of care into these as into search, index, and predict.

## Major changes

- All modals are now migrated to a state based alpine.js implementation.
- jQuery is no longer present in the base layout; ajax is replace by native fetch api
- most of the pps.js is now obsolte (as I understand it; the code is not referenced any more @jebus  please double check)
- in-memory pagination for large result lists (set to 50; we can make that configurable later; performance degrades at around 1k) stukk a bit rough tracked in #235

## Minor things

- Sarch and index also use alpine now
- The loading spinner is now CSS animated (not sure if it currently gets correctly called)

## Not done

- Ihave not even cheked the admin pages. Not sure If these need migrations
- The temporary migration pages still use the old template. Not sure what is supposed to happen with those? @jebus

## What I did to test

- opend all pages in browse, and user ; plus all pages reachable from there.
- Interacted and tested the functionality of each modal superfically with exception of the API key modal (no functional test).

---
This PR is massive sorry for that; just did not want to push half-brokenn state.
@jebus @liambrydon I would be glad if you could click around and try to break it :)

Finally closes #133

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#236
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-26 23:16:44 +13:00

612 lines
23 KiB
HTML

{% extends "framework_modern.html" %}
{% load static %}
{% block content %}
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
#vizdiv {
width: 100%;
height: 600px;
background: white;
position: relative;
}
#pwsvg {
width: 100%;
height: 100%;
color: red;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
/* marker-end: url(#arrow); */
}
.link_no_arrow {
stroke: #999;
stroke-opacity: 0.6;
}
.node image {
cursor: pointer;
}
.node circle {
fill: lightblue;
stroke: steelblue;
stroke-width: 1.5px;
}
.inside_app_domain {
fill: green;
stroke: green;
stroke-width: 1.5px;
}
.outside_app_domain {
fill: red;
stroke: red;
stroke-width: 1.5px;
}
.passes_app_domain {
stroke: green;
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.fails_app_domain {
stroke: red;
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.highlighted {
stroke: red;
stroke-width: 3px;
}
</style>
<script src="{% static 'js/pw.js' %}"></script>
{% block action_modals %}
{% include "modals/objects/add_pathway_node_modal.html" %}
{% include "modals/objects/add_pathway_edge_modal.html" %}
{% include "modals/objects/download_pathway_csv_modal.html" %}
{% include "modals/objects/download_pathway_image_modal.html" %}
{% include "modals/objects/identify_missing_rules_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/edit_pathway_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/delete_pathway_node_modal.html" %}
{% include "modals/objects/delete_pathway_edge_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="space-y-2 p-4">
<!-- Header Section -->
<div class="card bg-base-100">
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-2xl">{{ pathway.name }}</h2>
<div id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-wrench"
>
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
/>
</svg>
Actions
</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
>
{% block actions %}
{% include "actions/objects/pathway.html" %}
{% endblock %}
</ul>
</div>
</div>
</div>
</div>
<!-- Graphical Representation -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
Graphical Representation
</div>
<div class="collapse-content">
<div class="bg-base-100 mb-2 rounded-lg p-2">
<div class="navbar bg-base-100 rounded-lg">
<div class="flex-1">
{% if meta.can_edit %}
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-edit"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</svg>
Edit
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
>
{% include "actions/objects/pathway.html" %}
</ul>
</div>
{% endif %}
{% if pathway.setting.model.app_domain %}
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
View
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
>
<li>
<a id="app-domain-toggle-button" class="cursor-pointer">
<svg
id="app-domain-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
App Domain View
</a>
</li>
</ul>
</div>
{% endif %}
</div>
<div class="flex-none gap-2">
<button
class="btn btn-ghost btn-sm"
onclick="goFullscreen('vizdiv')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-maximize"
>
<path
d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"
/>
</svg>
Fullscreen
</button>
</div>
</div>
</div>
<div id="vizdiv">
{% if pathway.completed %}
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
<div class="tooltip-content">Pathway prediction complete.</div>
<div id="status" class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-check"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</div>
</div>
{% elif pathway.failed %}
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
<div class="tooltip-content">Pathway prediction failed.</div>
<div id="status" class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-x"
>
<path d="M18 6 6 18" />
<path d="M6 6l12 12" />
</svg>
</div>
</div>
{% else %}
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
<div class="tooltip-content">Pathway prediction running.</div>
<div id="status" class="flex items-center">
<div
id="status-loading-spinner"
style="width: 20px; height: 20px;"
></div>
</div>
</div>
{% endif %}
<svg id="pwsvg">
<defs>
<marker
id="arrow"
viewBox="0 0 10 10"
refX="43"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999" />
</marker>
<marker
id="arrow_passes_app_domain"
viewBox="0 0 10 10"
refX="43"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="green" />
</marker>
<marker
id="arrow_fails_app_domain"
viewBox="0 0 10 10"
refX="43"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="red" />
</marker>
</defs>
<g id="zoomable"></g>
</svg>
<div id="tooltip" class="tooltip-content"></div>
</div>
</div>
</div>
<!-- Description -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Description</div>
<div class="collapse-content">
<div id="DescriptionContent">{{ pathway.description | safe }}</div>
</div>
</div>
{% if pathway.aliases %}
<!-- Aliases -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Aliases</div>
<div class="collapse-content">
<ul class="menu bg-base-100 rounded-box">
{% for alias in pathway.aliases %}
<li><a class="hover:bg-base-200">{{ alias }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if pathway.scenarios.all %}
<!-- Scenarios -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">Scenarios</div>
<div class="collapse-content">
<ul class="menu bg-base-100 rounded-box">
{% for s in pathway.scenarios.all %}
<li>
<a href="{{ s.url }}" class="hover:bg-base-200">
{{ s.name }}
<span class="text-sm opacity-70">({{ s.package.name }})</span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if pathway.setting %}
<!-- Setting -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">Setting</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% if pathway.setting.model %}
<tr>
<td>Model</td>
<td>
<div class="space-y-2">
<div>
<a
href="{{ pathway.setting.model.url }}"
class="link link-primary"
>
{{ pathway.setting.model.name }}
</a>
</div>
<div class="overflow-x-auto">
<table class="table-xs table">
<thead>
<tr>
<th>Model Parameter</th>
<th>Parameter Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Threshold</td>
<td>{{ pathway.setting.model_threshold }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
{% endif %}
{% if pathway.setting.rule_packages.all %}
<tr>
<td>Rule Packages</td>
<td>
<ul class="menu bg-base-100 rounded-box">
{% for p in pathway.setting.rule_packages.all %}
<li>
<a href="{{ p.url }}" class="hover:bg-base-200"
>{{ p.name }}</a
>
</li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
<tr>
<td>Max Nodes</td>
<td>{{ pathway.setting.max_nodes }}</td>
</tr>
<tr>
<td>Max Depth</td>
<td>{{ pathway.setting.max_depth }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
{# prettier-ignore-start #}
{# FIXME: This is a hack to get the pathway data into the JavaScript code. #}
<script>
// Global switch for app domain view
var appDomainViewEnabled = false;
function goFullscreen(id) {
var element = document.getElementById(id);
if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
}
}
function transformReferences(text) {
return text.replace(/\[\s*(http[^\]|]+)\s*\|\s*([^\]]+)\s*\]/g, '<a target="parent" href="$1">$2</a>');
}
var pathway = {{ pathway.d3_json | safe }};
document.addEventListener('DOMContentLoaded', function() {
// Initialize loading spinner if pathway is running
if (pathway.status === 'running') {
const spinnerContainer = document.getElementById('status-loading-spinner');
if (spinnerContainer) {
showLoadingSpinner(spinnerContainer);
}
}
// If prediction is still running, regularly check status
if (pathway.status === 'running') {
let last_modified = pathway.modified;
let pollInterval = setInterval(async () => {
try {
const response = await fetch("{{ pathway.url }}?status=true", {});
const data = await response.json();
if (data.modified > last_modified) {
var msg = 'Prediction ';
var btn = '<button type="button" onclick="location.reload()" class="btn btn-primary btn-sm mt-2" id="reloadBtn">Reload page</button>';
if (data.status === "running") {
msg += 'is still running. But the Pathway was updated.<br>' + btn;
} else if (data.status === "completed") {
msg += 'is completed. Reload the page to see the updated Pathway.<br>' + btn;
} else if (data.status === "failed") {
msg += 'failed. Reload the page to see the current shape.<br>' + btn;
}
showStatusPopover(msg);
}
if (data.status === "completed" || data.status === "failed") {
const statusBtn = document.getElementById('status');
const tooltipContent = statusBtn.parentElement.querySelector('.tooltip-content');
const spinner = statusBtn.querySelector('#status-loading-spinner');
if (spinner) spinner.remove();
if (data.status === "completed") {
statusBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>`;
tooltipContent.textContent = 'Pathway prediction complete.';
} else {
statusBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></svg>`;
tooltipContent.textContent = 'Pathway prediction failed.';
}
clearInterval(pollInterval);
}
} catch (err) {
console.error("Polling error:", err);
}
}, 5000);
}
draw(pathway, 'vizdiv');
// Transform references in description
const descContent = document.getElementById('DescriptionContent');
if (descContent) {
const newDesc = transformReferences(descContent.innerText);
descContent.innerHTML = newDesc;
}
// App domain toggle
const appDomainBtn = document.getElementById('app-domain-toggle-button');
if (appDomainBtn) {
appDomainBtn.addEventListener('click', function() {
appDomainViewEnabled = !appDomainViewEnabled;
const icon = document.getElementById('app-domain-icon');
if (appDomainViewEnabled) {
// Change to eye-off icon
icon.innerHTML = '<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
nodes.forEach((x) => {
if(x.app_domain) {
if (x.app_domain.inside_app_domain) {
d3.select(x.el).select("circle").classed("inside_app_domain", true);
} else {
d3.select(x.el).select("circle").classed("outside_app_domain", true);
}
}
});
links.forEach((x) => {
if(x.app_domain) {
if (x.app_domain.passes_app_domain) {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow_passes_app_domain)");
d3.select(x.el).classed("passes_app_domain", true);
} else {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow_fails_app_domain)");
d3.select(x.el).classed("fails_app_domain", true);
}
}
});
} else {
// Change back to eye icon
icon.innerHTML = '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
nodes.forEach((x) => {
d3.select(x.el).select("circle").classed("inside_app_domain", false);
d3.select(x.el).select("circle").classed("outside_app_domain", false);
});
links.forEach((x) => {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)");
d3.select(x.el).classed("passes_app_domain", false);
d3.select(x.el).classed("fails_app_domain", false);
});
}
});
}
// Show actions button if there are actions
const actionsButton = document.getElementById("actionsButton");
const actionsList = actionsButton?.querySelector("ul");
if (actionsList && actionsList.children.length > 0) {
actionsButton?.classList.remove("hidden");
}
});
</script>
{# prettier-ignore-end #}
{% endblock content %}