forked from enviPath/enviPy
[Fix] UI Fixes (#266)
Rather than have a bunch of pull-requests that @jebus will have to merge shall we collect some of the fixes for the UI issues I found in here. - [x] #259 - [x] #260 - [x] #261 - [x] #262 - [x] #263 - [x] #264 - [x] #265 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Reviewed-on: enviPath/enviPy#266 Co-authored-by: Liam Brydon <lbry121@aucklanduni.ac.nz> Co-committed-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
This commit is contained in:
@ -2,7 +2,12 @@
|
||||
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
||||
|
||||
{# Loading state #}
|
||||
<div x-show="isLoading">{% include "components/loading-spinner.html" %}</div>
|
||||
<div
|
||||
x-show="isLoading"
|
||||
class="mx-auto flex h-32 w-32 items-center justify-center"
|
||||
>
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
|
||||
{# Error state #}
|
||||
<div
|
||||
|
||||
@ -45,43 +45,36 @@
|
||||
class="mt-6 w-full"
|
||||
x-data="{
|
||||
activeTab: 'reviewed',
|
||||
reviewedCount: 0,
|
||||
unreviewedCount: 0,
|
||||
reviewedLoaded: false,
|
||||
unreviewedLoaded: false,
|
||||
reviewedCount: null,
|
||||
unreviewedCount: null,
|
||||
get bothLoaded() { return this.reviewedCount !== null && this.unreviewedCount !== null },
|
||||
get isEmpty() { return this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount === 0 },
|
||||
updateTabSelection() {
|
||||
// Only auto-select unreviewed tab if both have loaded and there are no reviewed items
|
||||
if (this.reviewedLoaded && this.unreviewedLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
|
||||
if (this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
|
||||
this.activeTab = 'unreviewed';
|
||||
}
|
||||
}
|
||||
}"
|
||||
>
|
||||
{# No items found message #}
|
||||
<div
|
||||
x-show="reviewedCount === 0 && unreviewedCount === 0"
|
||||
class="text-base-content/70 py-8 text-center"
|
||||
>
|
||||
{# No items found message - only show after both tabs have loaded #}
|
||||
<div x-show="isEmpty" class="text-base-content/70 py-8 text-center">
|
||||
<p>No items found.</p>
|
||||
</div>
|
||||
|
||||
{# Tabs Navigation #}
|
||||
<div
|
||||
role="tablist"
|
||||
class="tabs tabs-border"
|
||||
x-show="reviewedCount > 0 || unreviewedCount > 0"
|
||||
>
|
||||
<div role="tablist" class="tabs tabs-border" x-show="!isEmpty">
|
||||
<button
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'reviewed' }"
|
||||
@click="activeTab = 'reviewed'"
|
||||
x-show="reviewedCount > 0"
|
||||
x-show="reviewedCount === null || reviewedCount > 0"
|
||||
>
|
||||
Reviewed
|
||||
<span
|
||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
||||
x-text="reviewedCount"
|
||||
:class="{ 'animate-pulse': reviewedCount === null }"
|
||||
x-text="reviewedCount ?? '…'"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
@ -89,12 +82,13 @@
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'unreviewed' }"
|
||||
@click="activeTab = 'unreviewed'"
|
||||
x-show="unreviewedCount > 0"
|
||||
x-show="unreviewedCount === null || unreviewedCount > 0"
|
||||
>
|
||||
Unreviewed
|
||||
<span
|
||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
||||
x-text="unreviewedCount"
|
||||
:class="{ 'animate-pulse': unreviewedCount === null }"
|
||||
x-text="unreviewedCount ?? '…'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@ -102,14 +96,14 @@
|
||||
{# Reviewed Tab Content #}
|
||||
<div
|
||||
class="mt-6"
|
||||
x-show="activeTab === 'reviewed' && (reviewedCount > 0 || unreviewedCount > 0)"
|
||||
x-show="activeTab === 'reviewed' && !isEmpty"
|
||||
x-data="remotePaginatedList({
|
||||
endpoint: '{{ api_endpoint }}?review_status=true',
|
||||
instanceId: '{{ entity_type }}_reviewed',
|
||||
isReviewed: true,
|
||||
perPage: {{ per_page|default:50 }}
|
||||
})"
|
||||
@items-loaded="reviewedCount = totalItems; reviewedLoaded = true; updateTabSelection()"
|
||||
@items-loaded="reviewedCount = totalItems; updateTabSelection()"
|
||||
>
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %}
|
||||
</div>
|
||||
@ -117,14 +111,14 @@
|
||||
{# Unreviewed Tab Content #}
|
||||
<div
|
||||
class="mt-6"
|
||||
x-show="activeTab === 'unreviewed' && (reviewedCount > 0 || unreviewedCount > 0)"
|
||||
x-show="activeTab === 'unreviewed' && !isEmpty"
|
||||
x-data="remotePaginatedList({
|
||||
endpoint: '{{ api_endpoint }}?review_status=false',
|
||||
instanceId: '{{ entity_type }}_unreviewed',
|
||||
isReviewed: false,
|
||||
perPage: {{ per_page|default:50 }}
|
||||
})"
|
||||
@items-loaded="unreviewedCount = totalItems; unreviewedLoaded = true; updateTabSelection()"
|
||||
@items-loaded="unreviewedCount = totalItems; updateTabSelection()"
|
||||
>
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %}
|
||||
</div>
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
<h6 class="footer-title">Services</h6>
|
||||
<a class="link link-hover" href="/predict">Predict</a>
|
||||
<a class="link link-hover" href="/package">Packages</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a class="link link-hover" href="/model">Your Collections</a>
|
||||
{% endif %}
|
||||
{# {% if user.is_authenticated %}#}
|
||||
{# <a class="link link-hover" href="/model">Your Collections</a>#}
|
||||
{# {% endif %}#}
|
||||
<a
|
||||
href="https://wiki.envipath.org/"
|
||||
target="_blank"
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
<div class="benzene-spinner">
|
||||
<svg viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
@keyframes spin-slow {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.spinner-slow svg {
|
||||
animation: spin-slow 3s linear infinite;
|
||||
}
|
||||
</style>
|
||||
<div class="spinner-slow flex h-full w-full items-center justify-center">
|
||||
<svg
|
||||
viewBox="0 0 1000 1000"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-full w-full"
|
||||
>
|
||||
<path
|
||||
class="hexagon"
|
||||
d="m 758.78924,684.71562 0.65313,-363.85 33.725,0.066 -0.65313,363.85001 z M 201.52187,362.53368 512.50834,173.66181 530.01077,202.48506 219.03091,391.35694 z M 510.83924,841.63056 199.3448,653.59653 216.77465,624.72049 528.2691,812.76111 z M 500,975 85.905556,742.30278 l 0,-474.94722 L 500,24.999998 914.09445,257.64444 l 0,475.00001 z M 124.90833,722.45834 500,936.15556 880.26389,713.69722 l 0,-436.15555 L 500,63.949998 124.90833,286.40833 z"
|
||||
|
||||
@ -118,7 +118,7 @@
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if meta.user.username == 'anonymous' or public_mode %}
|
||||
{% if meta.user.username == 'anonymous' %}
|
||||
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
|
||||
{% else %}
|
||||
<div class="dropdown dropdown-end">
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
<script src="{% static 'js/alpine/index.js' %}"></script>
|
||||
<script src="{% static 'js/alpine/search.js' %}"></script>
|
||||
<script src="{% static 'js/alpine/pagination.js' %}"></script>
|
||||
<script src="{% static 'js/alpine/pathway.js' %}"></script>
|
||||
|
||||
{# Font Awesome #}
|
||||
<link
|
||||
|
||||
@ -16,12 +16,12 @@
|
||||
>
|
||||
<div class="modal-box max-w-3xl">
|
||||
<!-- Header -->
|
||||
<h3 class="font-bold text-lg">New Scenario</h3>
|
||||
<h3 class="text-lg font-bold">New Scenario</h3>
|
||||
|
||||
<!-- Close button (X) -->
|
||||
<form method="dialog">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
✕
|
||||
@ -114,20 +114,37 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="scenario-type">
|
||||
<label class="label">
|
||||
<span class="label-text">Scenario Type</span>
|
||||
</label>
|
||||
<select
|
||||
<div role="tablist" class="tabs tabs-border">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': scenarioType === 'empty' }"
|
||||
@click="scenarioType = 'empty'"
|
||||
>
|
||||
Empty Scenario
|
||||
</button>
|
||||
{% for k, v in scenario_types.items %}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': scenarioType === '{{ v.name }}' }"
|
||||
@click="scenarioType = '{{ v.name }}'"
|
||||
>
|
||||
{{ k }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
id="scenario-type"
|
||||
name="scenario-type"
|
||||
class="select select-bordered w-full"
|
||||
x-model="scenarioType"
|
||||
>
|
||||
<option value="empty" selected>Empty Scenario</option>
|
||||
{% for k, v in scenario_types.items %}
|
||||
<option value="{{ v.name }}">{{ k }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{% for type in scenario_types.values %}
|
||||
|
||||
@ -18,7 +18,11 @@
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await fetch('{% url "package scenario list" meta.current_package.uuid %}');
|
||||
const response = await fetch('{% url "package scenario list" meta.current_package.uuid %}', {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
this.scenarios = data;
|
||||
this.loaded = true;
|
||||
@ -47,7 +51,13 @@
|
||||
}
|
||||
}"
|
||||
@close="reset()"
|
||||
x-init="$watch('$el.open', value => { if (value) loadScenarios(); })"
|
||||
x-init="
|
||||
new MutationObserver(() => {
|
||||
if ($el.hasAttribute('open')) {
|
||||
loadScenarios();
|
||||
}
|
||||
}).observe($el, { attributes: true });
|
||||
"
|
||||
>
|
||||
<div class="modal-box max-w-4xl">
|
||||
<!-- Header -->
|
||||
@ -102,7 +112,8 @@
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt"
|
||||
>Hold Ctrl/Cmd to select multiple scenarios</span
|
||||
>Hold Ctrl/Cmd to select multiple scenarios. Ctrl/Cmd + click one
|
||||
item to deselect it</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -136,7 +136,11 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictLoading" class="mt-2"></div>
|
||||
<div id="predictLoading" class="mt-2 flex hidden justify-center">
|
||||
<div class="h-8 w-8">
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="predictResultTable" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -167,7 +171,11 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="appDomainLoading" class="mt-2"></div>
|
||||
<div id="appDomainLoading" class="mt-2 flex hidden justify-center">
|
||||
<div class="h-8 w-8">
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="appDomainAssessmentResultTable" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -397,7 +405,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
smiles: smiles,
|
||||
@ -418,12 +427,12 @@
|
||||
})
|
||||
.then(data => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.innerHTML = "";
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
handlePredictionResponse(data);
|
||||
})
|
||||
.catch(error => {
|
||||
const loadingEl = document.getElementById("predictLoading");
|
||||
if (loadingEl) loadingEl.innerHTML = "";
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
const resultTable = document.getElementById("predictResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
@ -453,7 +462,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
smiles: smiles,
|
||||
@ -474,7 +484,7 @@
|
||||
})
|
||||
.then(data => {
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.innerHTML = "";
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
if (typeof handleAssessmentResponse === 'function') {
|
||||
handleAssessmentResponse("{% url 'depict' %}", data);
|
||||
}
|
||||
@ -482,7 +492,7 @@
|
||||
})
|
||||
.catch(error => {
|
||||
const loadingEl = document.getElementById("appDomainLoading");
|
||||
if (loadingEl) loadingEl.innerHTML = "";
|
||||
if (loadingEl) loadingEl.classList.add("hidden");
|
||||
const resultTable = document.getElementById("appDomainAssessmentResultTable");
|
||||
if (resultTable) {
|
||||
resultTable.classList.add("alert", "alert-error");
|
||||
|
||||
@ -211,11 +211,21 @@
|
||||
</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">
|
||||
<div
|
||||
id="vizdiv"
|
||||
x-data="pathwayViewer({
|
||||
status: '{{ pathway.status }}',
|
||||
modified: '{{ pathway.modified|date:"Y-m-d H:i:s" }}',
|
||||
statusUrl: '{{ pathway.url }}?status=true'
|
||||
})"
|
||||
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"
|
||||
@ -230,12 +240,9 @@
|
||||
>
|
||||
<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">
|
||||
</template>
|
||||
<!-- Failed icon -->
|
||||
<template x-if="status === 'failed'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@ -251,19 +258,28 @@
|
||||
<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" %}
|
||||
</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 %}
|
||||
</div>
|
||||
|
||||
<!-- Update Notice -->
|
||||
<div
|
||||
x-show="showUpdateNotice"
|
||||
x-cloak
|
||||
class="alert alert-info absolute right-4 bottom-4 left-4 z-10"
|
||||
>
|
||||
<span x-html="updateMessage"></span>
|
||||
<button @click="reloadPage()" class="btn btn-primary btn-sm mt-2">
|
||||
Reload page
|
||||
</button>
|
||||
</div>
|
||||
<svg id="pwsvg">
|
||||
<defs>
|
||||
<marker
|
||||
@ -463,60 +479,6 @@
|
||||
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
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
<a
|
||||
href="https://community.envipath.org/"
|
||||
target="_blank"
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-neutral"
|
||||
>Visit Forums</a
|
||||
>
|
||||
</div>
|
||||
@ -81,7 +81,7 @@
|
||||
<a
|
||||
href="https://wiki.envipath.org/"
|
||||
target="_blank"
|
||||
class="btn btn-accent"
|
||||
class="btn btn-neutral"
|
||||
>Read Docs</a
|
||||
>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user