[Feature] Threshold Warning + Cosmetics (#277)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#277
This commit is contained in:
2025-12-20 02:11:47 +13:00
parent a4a4179261
commit 7c60a28801
10 changed files with 304 additions and 165 deletions

View File

@ -1,11 +1,10 @@
{% load static %}
<dialog
id="search_modal"
class="modal @max-sm:modal-top justify-center"
class="modal items-start sm:items-center"
x-data="searchModal()"
@close="reset()"
>
<div class="modal-box h-full w-lvw p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl">
<div class="modal-box mt-4 sm:mt-0 p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl">
<!-- Search Input and Mode Selector -->
<div class="form-control mb-4 w-full shrink-0">
<div class="join m-0 w-full items-center p-3">
@ -43,7 +42,7 @@
type="button"
tabindex="0"
popovertarget="search_dropdown_menu"
style="anchor-name: --1"
style="anchor-name: --anchor-mode"
class="btn join-item btn-ghost"
>
<span x-text="searchModeLabel"></span>
@ -67,7 +66,7 @@
popover
x-ref="modeDropdown"
id="search_dropdown_menu"
style="position-anchor: --anchor-2"
style="position-anchor: --anchor-mode"
>
<li class="menu-title">Text</li>
<li>
@ -495,8 +494,7 @@
</div>
</div>
<!-- Backdrop to close -->
<form method="dialog" class="modal-backdrop">
<button>close</button>
<button aria-label="close"></button>
</form>
</dialog>

View File

@ -181,6 +181,55 @@
</div>
{% endif %}
{% if compound.half_lifes %}
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">Half-lives</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Scenario</th>
<th>Values</th>
</tr>
</thead>
<tbody>
{% for scenario, half_lifes in compound.half_lifes.items %}
<tr>
<td>
<a href="{{ scenario.url }}" class="hover:bg-base-200"
>{{ scenario.name }}
<i>({{ scenario.package.name }})</i></a
>
</td>
<td>
<table class="table-zebra table">
<tbody>
<tr>
<td>Scenario Type</td>
<td>{{ scenario.scenario_type }}</td>
</tr>
<tr>
<td>Half-life (days)</td>
<td>{{ half_lifes.0.dt50 }}</td>
</tr>
<tr>
<td>Model</td>
<td>{{ half_lifes.0.model }}</td>
</tr>
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- External Identifiers -->
{% if compound.get_external_identifiers %}
<div class="collapse-arrow bg-base-200 collapse">

View File

@ -216,59 +216,62 @@
x-data="pathwayViewer({
status: '{{ pathway.status }}',
modified: '{{ pathway.modified|date:"Y-m-d H:i:s" }}',
statusUrl: '{{ pathway.url }}?status=true'
statusUrl: '{{ pathway.url }}?status=true',
emptyDueToThreshold: '{{ pathway.empty_due_to_threshold }}'
})"
x-init="init()"
>
<!-- Status Display -->
<div class="tooltip tooltip-left absolute top-4 right-4 z-10">
<div class="tooltip-content" x-text="statusTooltip"></div>
<div id="status" class="flex items-center">
<!-- Completed icon -->
<template x-if="status === 'completed'">
<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"
{% if pathway.predicted %}
<!-- Status Display -->
<div class="tooltip tooltip-left absolute top-4 right-4 z-10">
<div class="tooltip-content" x-text="statusTooltip"></div>
<div id="status" class="flex items-center">
<!-- Completed icon -->
<template x-if="status === 'completed'">
<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>
</template>
<!-- Failed icon -->
<template x-if="status === 'failed'">
<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>
</template>
<!-- Loading spinner -->
<div
x-show="status === 'running'"
style="width: 20px; height: 20px;"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</template>
<!-- Failed icon -->
<template x-if="status === 'failed'">
<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>
</template>
<!-- Loading spinner -->
<div
x-show="status === 'running'"
style="width: 20px; height: 20px;"
>
{% include "components/loading-spinner.html" %}
{% include "components/loading-spinner.html" %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Update Notice -->
<div
x-show="showUpdateNotice"
@ -280,6 +283,15 @@
Reload page
</button>
</div>
<!-- Empty due to Threshold notice -->
<div
x-show="showEmptyDueToThresholdNotice"
x-cloak
class="alert alert-info absolute right-4 bottom-4 left-4 z-10"
>
<span x-html="emptyDueToThresholdMessage"></span>
</div>
<svg id="pwsvg">
<defs>
<marker
@ -455,95 +467,110 @@
</div>
{% endif %}
</div>
{# prettier-ignore-start #}
{# FIXME: This is a hack to get the pathway data into the JavaScript code. #}
{{ pathway.d3_json|json_script:"pathway" }}
<script>
// Global switch for app domain view
var appDomainViewEnabled = false;
<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 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>');
}
function transformReferences(text) {
return text.replace(
/\[\s*(http[^\]|]+)\s*\|\s*([^\]]+)\s*\]/g,
'<a target="parent" href="$1">$2</a>',
);
}
var pathway = JSON.parse(document.getElementById("pathway").textContent);
var pathway = {{ pathway.d3_json | safe }};
document.addEventListener("DOMContentLoaded", function () {
draw(pathway, "vizdiv");
document.addEventListener('DOMContentLoaded', function() {
draw(pathway, 'vizdiv');
// Transform references in description
const descContent = document.getElementById("DescriptionContent");
if (descContent) {
const newDesc = transformReferences(descContent.innerText);
descContent.innerHTML = newDesc;
}
// 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");
// 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"/>';
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) => {
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");
}
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);
});
}
});
}
</script>
{# prettier-ignore-end #}
// 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>
{% endblock content %}