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:
@ -34,23 +34,3 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "./daisyui-theme.css";
|
@import "./daisyui-theme.css";
|
||||||
|
|
||||||
/* Loading Spinner - Benzene Ring */
|
|
||||||
.benzene-spinner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benzene-spinner svg {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
animation: spin 3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
this.$dispatch('loading-start');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(this.endpoint, window.location.origin);
|
const url = new URL(this.endpoint, window.location.origin);
|
||||||
@ -74,6 +75,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.error = `Unable to load ${this.endpoint}. Please try again.`;
|
this.error = `Unable to load ${this.endpoint}. Please try again.`;
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.$dispatch('loading-end');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
88
static/js/alpine/pathway.js
Normal file
88
static/js/alpine/pathway.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Pathway Viewer Alpine.js Component
|
||||||
|
*
|
||||||
|
* Provides reactive status management and polling for pathway predictions.
|
||||||
|
* Handles status updates, change detection, and update notices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
/**
|
||||||
|
* Pathway Viewer Component
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <div x-data="pathwayViewer({
|
||||||
|
* status: 'running',
|
||||||
|
* modified: '2024-01-01T00:00:00Z',
|
||||||
|
* statusUrl: '/pathway/123?status=true'
|
||||||
|
* })" x-init="init()">
|
||||||
|
* ...
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
Alpine.data('pathwayViewer', (config) => ({
|
||||||
|
status: config.status,
|
||||||
|
modified: config.modified,
|
||||||
|
statusUrl: config.statusUrl,
|
||||||
|
showUpdateNotice: false,
|
||||||
|
updateMessage: '',
|
||||||
|
pollInterval: null,
|
||||||
|
|
||||||
|
get statusTooltip() {
|
||||||
|
const tooltips = {
|
||||||
|
'completed': 'Pathway prediction complete.',
|
||||||
|
'failed': 'Pathway prediction failed.',
|
||||||
|
'running': 'Pathway prediction running.'
|
||||||
|
};
|
||||||
|
return tooltips[this.status] || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.status === 'running') {
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
this.pollInterval = setInterval(() => this.checkStatus(), 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.statusUrl);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.modified > this.modified) {
|
||||||
|
this.showUpdateNotice = true;
|
||||||
|
this.updateMessage = this.getUpdateMessage(data.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status !== 'running') {
|
||||||
|
this.status = data.status;
|
||||||
|
if (this.pollInterval) {
|
||||||
|
clearInterval(this.pollInterval);
|
||||||
|
this.pollInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Polling error:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getUpdateMessage(status) {
|
||||||
|
let msg = 'Prediction ';
|
||||||
|
|
||||||
|
if (status === 'running') {
|
||||||
|
msg += 'is still running. But the Pathway was updated.';
|
||||||
|
} else if (status === 'completed') {
|
||||||
|
msg += 'is completed. Reload the page to see the updated Pathway.';
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
msg += 'failed. Reload the page to see the current shape.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadPage() {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
@ -2,7 +2,12 @@
|
|||||||
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
||||||
|
|
||||||
{# Loading state #}
|
{# 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 #}
|
{# Error state #}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -45,43 +45,36 @@
|
|||||||
class="mt-6 w-full"
|
class="mt-6 w-full"
|
||||||
x-data="{
|
x-data="{
|
||||||
activeTab: 'reviewed',
|
activeTab: 'reviewed',
|
||||||
reviewedCount: 0,
|
reviewedCount: null,
|
||||||
unreviewedCount: 0,
|
unreviewedCount: null,
|
||||||
reviewedLoaded: false,
|
get bothLoaded() { return this.reviewedCount !== null && this.unreviewedCount !== null },
|
||||||
unreviewedLoaded: false,
|
get isEmpty() { return this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount === 0 },
|
||||||
updateTabSelection() {
|
updateTabSelection() {
|
||||||
// Only auto-select unreviewed tab if both have loaded and there are no reviewed items
|
if (this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
|
||||||
if (this.reviewedLoaded && this.unreviewedLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
|
|
||||||
this.activeTab = 'unreviewed';
|
this.activeTab = 'unreviewed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{# No items found message #}
|
{# No items found message - only show after both tabs have loaded #}
|
||||||
<div
|
<div x-show="isEmpty" class="text-base-content/70 py-8 text-center">
|
||||||
x-show="reviewedCount === 0 && unreviewedCount === 0"
|
|
||||||
class="text-base-content/70 py-8 text-center"
|
|
||||||
>
|
|
||||||
<p>No items found.</p>
|
<p>No items found.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Tabs Navigation #}
|
{# Tabs Navigation #}
|
||||||
<div
|
<div role="tablist" class="tabs tabs-border" x-show="!isEmpty">
|
||||||
role="tablist"
|
|
||||||
class="tabs tabs-border"
|
|
||||||
x-show="reviewedCount > 0 || unreviewedCount > 0"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
class="tab"
|
class="tab"
|
||||||
:class="{ 'tab-active': activeTab === 'reviewed' }"
|
:class="{ 'tab-active': activeTab === 'reviewed' }"
|
||||||
@click="activeTab = 'reviewed'"
|
@click="activeTab = 'reviewed'"
|
||||||
x-show="reviewedCount > 0"
|
x-show="reviewedCount === null || reviewedCount > 0"
|
||||||
>
|
>
|
||||||
Reviewed
|
Reviewed
|
||||||
<span
|
<span
|
||||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
||||||
x-text="reviewedCount"
|
:class="{ 'animate-pulse': reviewedCount === null }"
|
||||||
|
x-text="reviewedCount ?? '…'"
|
||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -89,12 +82,13 @@
|
|||||||
class="tab"
|
class="tab"
|
||||||
:class="{ 'tab-active': activeTab === 'unreviewed' }"
|
:class="{ 'tab-active': activeTab === 'unreviewed' }"
|
||||||
@click="activeTab = 'unreviewed'"
|
@click="activeTab = 'unreviewed'"
|
||||||
x-show="unreviewedCount > 0"
|
x-show="unreviewedCount === null || unreviewedCount > 0"
|
||||||
>
|
>
|
||||||
Unreviewed
|
Unreviewed
|
||||||
<span
|
<span
|
||||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
||||||
x-text="unreviewedCount"
|
:class="{ 'animate-pulse': unreviewedCount === null }"
|
||||||
|
x-text="unreviewedCount ?? '…'"
|
||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -102,14 +96,14 @@
|
|||||||
{# Reviewed Tab Content #}
|
{# Reviewed Tab Content #}
|
||||||
<div
|
<div
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
x-show="activeTab === 'reviewed' && (reviewedCount > 0 || unreviewedCount > 0)"
|
x-show="activeTab === 'reviewed' && !isEmpty"
|
||||||
x-data="remotePaginatedList({
|
x-data="remotePaginatedList({
|
||||||
endpoint: '{{ api_endpoint }}?review_status=true',
|
endpoint: '{{ api_endpoint }}?review_status=true',
|
||||||
instanceId: '{{ entity_type }}_reviewed',
|
instanceId: '{{ entity_type }}_reviewed',
|
||||||
isReviewed: true,
|
isReviewed: true,
|
||||||
perPage: {{ per_page|default:50 }}
|
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 %}
|
{% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %}
|
||||||
</div>
|
</div>
|
||||||
@ -117,14 +111,14 @@
|
|||||||
{# Unreviewed Tab Content #}
|
{# Unreviewed Tab Content #}
|
||||||
<div
|
<div
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
x-show="activeTab === 'unreviewed' && (reviewedCount > 0 || unreviewedCount > 0)"
|
x-show="activeTab === 'unreviewed' && !isEmpty"
|
||||||
x-data="remotePaginatedList({
|
x-data="remotePaginatedList({
|
||||||
endpoint: '{{ api_endpoint }}?review_status=false',
|
endpoint: '{{ api_endpoint }}?review_status=false',
|
||||||
instanceId: '{{ entity_type }}_unreviewed',
|
instanceId: '{{ entity_type }}_unreviewed',
|
||||||
isReviewed: false,
|
isReviewed: false,
|
||||||
perPage: {{ per_page|default:50 }}
|
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" %}
|
{% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
<h6 class="footer-title">Services</h6>
|
<h6 class="footer-title">Services</h6>
|
||||||
<a class="link link-hover" href="/predict">Predict</a>
|
<a class="link link-hover" href="/predict">Predict</a>
|
||||||
<a class="link link-hover" href="/package">Packages</a>
|
<a class="link link-hover" href="/package">Packages</a>
|
||||||
{% if user.is_authenticated %}
|
{# {% if user.is_authenticated %}#}
|
||||||
<a class="link link-hover" href="/model">Your Collections</a>
|
{# <a class="link link-hover" href="/model">Your Collections</a>#}
|
||||||
{% endif %}
|
{# {% endif %}#}
|
||||||
<a
|
<a
|
||||||
href="https://wiki.envipath.org/"
|
href="https://wiki.envipath.org/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -1,5 +1,22 @@
|
|||||||
<div class="benzene-spinner">
|
<style>
|
||||||
<svg viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
@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
|
<path
|
||||||
class="hexagon"
|
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"
|
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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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>
|
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
<script src="{% static 'js/alpine/index.js' %}"></script>
|
<script src="{% static 'js/alpine/index.js' %}"></script>
|
||||||
<script src="{% static 'js/alpine/search.js' %}"></script>
|
<script src="{% static 'js/alpine/search.js' %}"></script>
|
||||||
<script src="{% static 'js/alpine/pagination.js' %}"></script>
|
<script src="{% static 'js/alpine/pagination.js' %}"></script>
|
||||||
|
<script src="{% static 'js/alpine/pathway.js' %}"></script>
|
||||||
|
|
||||||
{# Font Awesome #}
|
{# Font Awesome #}
|
||||||
<link
|
<link
|
||||||
|
|||||||
@ -16,12 +16,12 @@
|
|||||||
>
|
>
|
||||||
<div class="modal-box max-w-3xl">
|
<div class="modal-box max-w-3xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<h3 class="font-bold text-lg">New Scenario</h3>
|
<h3 class="text-lg font-bold">New Scenario</h3>
|
||||||
|
|
||||||
<!-- Close button (X) -->
|
<!-- Close button (X) -->
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button
|
<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"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
@ -114,20 +114,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control mb-3">
|
<div class="form-control mb-3">
|
||||||
<label class="label" for="scenario-type">
|
<label class="label">
|
||||||
<span class="label-text">Scenario Type</span>
|
<span class="label-text">Scenario Type</span>
|
||||||
</label>
|
</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"
|
id="scenario-type"
|
||||||
name="scenario-type"
|
name="scenario-type"
|
||||||
class="select select-bordered w-full"
|
|
||||||
x-model="scenarioType"
|
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>
|
</div>
|
||||||
|
|
||||||
{% for type in scenario_types.values %}
|
{% for type in scenario_types.values %}
|
||||||
|
|||||||
@ -18,7 +18,11 @@
|
|||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
this.scenarios = data;
|
this.scenarios = data;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
@ -47,7 +51,13 @@
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@close="reset()"
|
@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">
|
<div class="modal-box max-w-4xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@ -102,7 +112,8 @@
|
|||||||
</select>
|
</select>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt"
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -136,7 +136,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 id="predictResultTable" class="mt-4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,7 +171,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 id="appDomainAssessmentResultTable" class="mt-4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -397,7 +405,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
|
const loadingEl = document.getElementById("predictLoading");
|
||||||
|
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
smiles: smiles,
|
smiles: smiles,
|
||||||
@ -418,12 +427,12 @@
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const loadingEl = document.getElementById("predictLoading");
|
const loadingEl = document.getElementById("predictLoading");
|
||||||
if (loadingEl) loadingEl.innerHTML = "";
|
if (loadingEl) loadingEl.classList.add("hidden");
|
||||||
handlePredictionResponse(data);
|
handlePredictionResponse(data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
const loadingEl = document.getElementById("predictLoading");
|
const loadingEl = document.getElementById("predictLoading");
|
||||||
if (loadingEl) loadingEl.innerHTML = "";
|
if (loadingEl) loadingEl.classList.add("hidden");
|
||||||
const resultTable = document.getElementById("predictResultTable");
|
const resultTable = document.getElementById("predictResultTable");
|
||||||
if (resultTable) {
|
if (resultTable) {
|
||||||
resultTable.classList.add("alert", "alert-error");
|
resultTable.classList.add("alert", "alert-error");
|
||||||
@ -453,7 +462,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
|
const loadingEl = document.getElementById("appDomainLoading");
|
||||||
|
if (loadingEl) loadingEl.classList.remove("hidden");
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
smiles: smiles,
|
smiles: smiles,
|
||||||
@ -474,7 +484,7 @@
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const loadingEl = document.getElementById("appDomainLoading");
|
const loadingEl = document.getElementById("appDomainLoading");
|
||||||
if (loadingEl) loadingEl.innerHTML = "";
|
if (loadingEl) loadingEl.classList.add("hidden");
|
||||||
if (typeof handleAssessmentResponse === 'function') {
|
if (typeof handleAssessmentResponse === 'function') {
|
||||||
handleAssessmentResponse("{% url 'depict' %}", data);
|
handleAssessmentResponse("{% url 'depict' %}", data);
|
||||||
}
|
}
|
||||||
@ -482,7 +492,7 @@
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
const loadingEl = document.getElementById("appDomainLoading");
|
const loadingEl = document.getElementById("appDomainLoading");
|
||||||
if (loadingEl) loadingEl.innerHTML = "";
|
if (loadingEl) loadingEl.classList.add("hidden");
|
||||||
const resultTable = document.getElementById("appDomainAssessmentResultTable");
|
const resultTable = document.getElementById("appDomainAssessmentResultTable");
|
||||||
if (resultTable) {
|
if (resultTable) {
|
||||||
resultTable.classList.add("alert", "alert-error");
|
resultTable.classList.add("alert", "alert-error");
|
||||||
|
|||||||
@ -211,11 +211,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="vizdiv">
|
<div
|
||||||
{% if pathway.completed %}
|
id="vizdiv"
|
||||||
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
|
x-data="pathwayViewer({
|
||||||
<div class="tooltip-content">Pathway prediction complete.</div>
|
status: '{{ pathway.status }}',
|
||||||
<div id="status" class="flex items-center">
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
@ -230,12 +240,9 @@
|
|||||||
>
|
>
|
||||||
<path d="M20 6 9 17l-5-5" />
|
<path d="M20 6 9 17l-5-5" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<!-- Failed icon -->
|
||||||
{% elif pathway.failed %}
|
<template x-if="status === '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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
@ -251,19 +258,28 @@
|
|||||||
<path d="M18 6 6 18" />
|
<path d="M18 6 6 18" />
|
||||||
<path d="M6 6l12 12" />
|
<path d="M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</template>
|
||||||
|
<!-- Loading spinner -->
|
||||||
|
<div
|
||||||
|
x-show="status === 'running'"
|
||||||
|
style="width: 20px; height: 20px;"
|
||||||
|
>
|
||||||
|
{% include "components/loading-spinner.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
|
|
||||||
<div class="tooltip-content">Pathway prediction running.</div>
|
<!-- Update Notice -->
|
||||||
<div id="status" class="flex items-center">
|
<div
|
||||||
<div
|
x-show="showUpdateNotice"
|
||||||
id="status-loading-spinner"
|
x-cloak
|
||||||
style="width: 20px; height: 20px;"
|
class="alert alert-info absolute right-4 bottom-4 left-4 z-10"
|
||||||
></div>
|
>
|
||||||
</div>
|
<span x-html="updateMessage"></span>
|
||||||
</div>
|
<button @click="reloadPage()" class="btn btn-primary btn-sm mt-2">
|
||||||
{% endif %}
|
Reload page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<svg id="pwsvg">
|
<svg id="pwsvg">
|
||||||
<defs>
|
<defs>
|
||||||
<marker
|
<marker
|
||||||
@ -463,60 +479,6 @@
|
|||||||
var pathway = {{ pathway.d3_json | safe }};
|
var pathway = {{ pathway.d3_json | safe }};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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');
|
draw(pathway, 'vizdiv');
|
||||||
|
|
||||||
// Transform references in description
|
// Transform references in description
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
<a
|
<a
|
||||||
href="https://community.envipath.org/"
|
href="https://community.envipath.org/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-secondary"
|
class="btn btn-neutral"
|
||||||
>Visit Forums</a
|
>Visit Forums</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -81,7 +81,7 @@
|
|||||||
<a
|
<a
|
||||||
href="https://wiki.envipath.org/"
|
href="https://wiki.envipath.org/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-accent"
|
class="btn btn-neutral"
|
||||||
>Read Docs</a
|
>Read Docs</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -61,7 +61,7 @@ class HTMLGenerator:
|
|||||||
else:
|
else:
|
||||||
clz_name = additional_information.__class__.__name__
|
clz_name = additional_information.__class__.__name__
|
||||||
|
|
||||||
widget = f"<h4>{clz_name}</h4>"
|
widget = f'<h4 class="h4 font-semibold mt-2 mb-1">{clz_name}</h4>'
|
||||||
|
|
||||||
if hasattr(additional_information, "uuid"):
|
if hasattr(additional_information, "uuid"):
|
||||||
uuid = additional_information.uuid
|
uuid = additional_information.uuid
|
||||||
@ -89,15 +89,21 @@ class HTMLGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_interval_float:
|
if is_interval_float:
|
||||||
|
label_text_start = " ".join([x.capitalize() for x in name.split("_")]) + " Start"
|
||||||
|
label_text_end = " ".join([x.capitalize() for x in name.split("_")]) + " End"
|
||||||
widget += f"""
|
widget += f"""
|
||||||
<div class="form-group row">
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div class="col-md-6">
|
<div class="form-control">
|
||||||
<label for="{full_name}__start">{" ".join([x.capitalize() for x in name.split("_")])} Start</label>
|
<label class="label" for="{full_name}__start">
|
||||||
<input type="number" class="form-control" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ""}">
|
<span class="label-text">{label_text_start}</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" class="input input-bordered w-full" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ""}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="form-control">
|
||||||
<label for="{full_name}__end">{" ".join([x.capitalize() for x in name.split("_")])} End</label>
|
<label class="label" for="{full_name}__end">
|
||||||
<input type="number" class="form-control" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ""}">
|
<span class="label-text">{label_text_end}</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" class="input input-bordered w-full" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ""}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -106,11 +112,14 @@ class HTMLGenerator:
|
|||||||
for e in field_type:
|
for e in field_type:
|
||||||
options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>'
|
options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>'
|
||||||
|
|
||||||
|
label_text = " ".join([x.capitalize() for x in name.split("_")])
|
||||||
widget += f"""
|
widget += f"""
|
||||||
<div class="form-group">
|
<div class="form-control mb-4">
|
||||||
<label for="{full_name}">{" ".join([x.capitalize() for x in name.split("_")])}</label>
|
<label class="label" for="{full_name}">
|
||||||
<select class="form-control" id="{full_name}" name="{full_name}">
|
<span class="label-text">{label_text}</span>
|
||||||
<option value="" disabled selected>Select {" ".join([x.capitalize() for x in name.split("_")])}</option>
|
</label>
|
||||||
|
<select class="select select-bordered w-full" id="{full_name}" name="{full_name}">
|
||||||
|
<option value="" disabled selected>Select {label_text}</option>
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -126,15 +135,28 @@ class HTMLGenerator:
|
|||||||
raise ValueError(f"Could not parse field type {field_type} for {name}")
|
raise ValueError(f"Could not parse field type {field_type} for {name}")
|
||||||
|
|
||||||
value_to_use = value if value and field_type is not bool else ""
|
value_to_use = value if value and field_type is not bool else ""
|
||||||
|
label_text = " ".join([x.capitalize() for x in name.split("_")])
|
||||||
|
|
||||||
widget += f"""
|
if field_type is bool:
|
||||||
<div class="form-group">
|
widget += f"""
|
||||||
<label for="{full_name}">{" ".join([x.capitalize() for x in name.split("_")])}</label>
|
<div class="form-control mb-4">
|
||||||
<input type="{input_type}" class="form-control" id="{full_name}" name="{full_name}" value="{value_to_use}" {"checked" if value and field_type is bool else ""}>
|
<label class="label cursor-pointer">
|
||||||
</div>
|
<span class="label-text">{label_text}</span>
|
||||||
"""
|
<input type="checkbox" class="checkbox" id="{full_name}" name="{full_name}" {"checked" if value else ""}>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
widget += f"""
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label" for="{full_name}">
|
||||||
|
<span class="label-text">{label_text}</span>
|
||||||
|
</label>
|
||||||
|
<input type="{input_type}" class="input input-bordered w-full" id="{full_name}" name="{full_name}" value="{value_to_use}">
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
return widget + "<hr>"
|
return widget
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_models(params) -> Dict[str, List["EnviPyModel"]]:
|
def build_models(params) -> Dict[str, List["EnviPyModel"]]:
|
||||||
|
|||||||
Reference in New Issue
Block a user