Skip to content
Snippets Groups Projects
Commit 611d1054 authored by François  LAURENT's avatar François LAURENT
Browse files

Merge branch 'pending' into 'main'

UX improvements

See merge request nyx/NyxUI.jl!2
parents 3c8186eb 8268e29a
No related branches found
No related tags found
1 merge request!2UX improvements
Pipeline #148151 passed
......@@ -119,3 +119,4 @@ deploy to dev.pasteur.cloud:
- if: ($CI_COMMIT_BRANCH == "dev" &&
$CI_PROJECT_ID == $GITLAB_PASTEUR_PROJECT_ID) # gitlab.pasteur.fr only
when: manual
......@@ -7,3 +7,25 @@ Web interface meant to be served at [nyx.pasteur.cloud](https://nyx.pasteur.clou
It features an app catalog, with currently a single available app:
* an editor for muscular activity programs.
## Local installation
For local execution, the installation instructions are as follows:
You will need [JuliaUp](https://github.com/JuliaLang/juliaup?tab=readme-ov-file#juliaup---julia-version-manager) and [git](https://git-scm.com/downloads).
Once these tools are installed, in a terminal (on Windows, preferably PowerShell), type:
```
git clone --branch dev https://gitlab.pasteur.fr/nyx/NyxUI.jl NyxUI
cd NyxUI
cp front/Manifest.toml .
juliaup add lts
juliaup default lts
julia --project=. -e 'using Pkg; Pkg.instantiate()'
julia --project=. routes.jl
```
You may be asked whether to authorize ports 9284 and 9285; please give the app permission.
From there, in a web browser, open http://localhost:9284/ to access the app.
......@@ -3,6 +3,7 @@ module MuscleActivities
using StructTypes
using NyxWidgets.Muscles
using Random
using Dates
using JSON3
using StructTypes
using OrderedCollections: OrderedDict
......@@ -217,8 +218,18 @@ from_json_file(::Type{T}, filepath) where {T} = JSON3.read(read(filepath, String
StructTypes.StructType(::Type{MuscleActivity}) = StructTypes.CustomStruct()
function StructTypes.lower(seq::MuscleActivity)
version = string(pkgversion(@__MODULE__))
if endswith(version, ".0")
version = version[1:end-2]
end
datetime = Dates.format(Dates.now(), "yyyymmdd_HHMMSS")
Dict = OrderedDict
Dict("name" => seq.program_name,
"metadata" =>
Dict("software" =>
Dict("name" => "NyxUI",
"version" => version),
"date_time" => datetime),
"time" =>
Dict("start" => seq.times[1],
"step" => step(seq.times),
......
......@@ -9,7 +9,7 @@ include("MuscleWidgets.jl")
using .MuscleWidgets
export hasmodel, getmodel, setmodel, newsequence, getsequence, withsequences,
exportsequence, loadsequence
exportsequence, loadsequence, deletesequence
# mutable struct Backbone{W}
# widget::Union{Nothing, W}
......@@ -150,4 +150,20 @@ function loadsequence(session_id, filepath)
return sequence
end
function deletesequence(session_id)
model = getmodel(session_id)
current = model.sequence[].program_name
sequences = __sequences__[session_id]
lock(sequences) do
for (ix, seq) in pairs(sequences)
if seq.program_name == current
current = ix
break
end
end
@assert current isa Int
pop!(sequences.cache, current)
end
end
end
module MuscleWidgets
using Base: has_nondefault_cmd_flags
using NyxWidgets.Players, NyxWidgets.Muscles, NyxWidgets.AnimatedLayers
import NyxWidgets.Base: lowerdom, dom_id, dom_id!, Div
using NyxPlots
......
......@@ -34,9 +34,9 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
@out export_sequence_link = ""
@in export_sequence_click = false
#@in delete_sequence_click = false
@in colormap = default_colormap
@out first_time_in_editmode = true # and in the heatmap view
@in editmode = false
@out editmode_disable = true
@in setvalue = 1.0
......@@ -44,9 +44,14 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
@in time_interval = 0.05
@in start_time = 0.0
@out heatmap_view = false
@out area_selected = false
@out new_clipboard_item = false
@in delete_sequence_click = false
@out no_sequences_yet = true
@onchange isready begin
_, url = init_model(__model__)
run(__model__, """
......@@ -71,6 +76,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
series_length = length(seq.times)
time_interval = step(seq.times)
start_time = first(seq.times)
no_sequences_yet = isempty(sequence_names)
end
on(MuscleWidgets.clipboard(model)) do _
new_clipboard_item[!] = false
......@@ -80,6 +86,12 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
area_selected[!] = false
area_selected = true
end
on(model.animation.switchlayer) do layers
if !isnothing(layers)
current = layers[1]
heatmap_view = 1 < current
end
end
end
@onchange start_time, time_interval, series_length begin
......@@ -96,24 +108,29 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
end
valid = false
end
previous_length = length(seq.times)
new_length = round(Int, series_length)
if series_length < 1
@notify "Time series length must be strictly positive" :error
series_length = length(seq.times)
series_length = previous_length
valid = false
elseif round(series_length) != series_length
elseif series_length != new_length
@notify "Time series length must be an integer" :error
series_length = round(series_length)
series_length = new_length
valid = false
elseif !isnothing(maxlength) && maxlength < series_length
elseif !isnothing(maxlength) && maxlength < new_length
@notify "Time series is too long" :error
series_length = length(seq.times)
series_length = previous_length
valid = false
elseif new_length < previous_length &&
startswith(string(previous_length), string(new_length))
@debug "Ignoring new size at the moment"
valid = false
end
#
if valid && !(start_time == seq.times[1] && time_interval == step(seq.times) &&
series_length == length(seq.times))
n = round(Int, series_length)
stop_time = start_time + (n-1) * time_interval
new_length == previous_length)
stop_time = start_time + (new_length - 1) * time_interval
ts = start_time:time_interval:stop_time
@info "Resampling" start_time time_interval series_length
MuscleActivities.resample!(seq, ts)
......@@ -140,6 +157,8 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
notify(start_time)
notify(time_interval)
notify(series_length)
# enable the download and delete buttons
no_sequences_yet = false
# toggle edit mode on
editmode = true
end
......@@ -149,6 +168,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
end
@onchange selected_sequence_name begin
isempty(selected_sequence_name) && return
seq = getsequence(back_session_id, selected_sequence_name)
if !isnothing(seq)
ix = findfirst(==(selected_sequence_name), sequence_names)
......@@ -204,6 +224,20 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
model = getmodel(back_session_id)
model.editmode[] = editmode
editmode_disable = !editmode
if editmode && first_time_in_editmode && heatmap_view
first_time_in_editmode = false
end
end
@onchange heatmap_view begin
if heatmap_view && editmode && first_time_in_editmode
first_time_in_editmode = false
end
end
@onchange first_time_in_editmode begin
@assert !first_time_in_editmode
@notify "Toggle the \"edit mode\" off to browse the sequence with the heatmap" :info
end
@onchange setvalue begin
......@@ -247,7 +281,10 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
if !(sequence.program_name in sequence_names)
push!(sequence_names, sequence.program_name)
end
notify(sequence_names)
selected_sequence_name = sequence.program_name
# enable the download and delete buttons
no_sequences_yet = false
catch exception
@error "Failed to load file" exception
@notify "Failed to load file" :error
......@@ -263,6 +300,37 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
@onchange area_selected begin
@notify "Click inside the selected area to assign the setvalue, or press Ctrl while clicking to copy the selection" :info
end
@onbutton delete_sequence_click begin
isempty(selected_sequence_name) && return
# update the model
deletesequence(back_session_id)
popat!(sequence_names, selected_sequence_index)
if length(sequence_names) < selected_sequence_index
selected_sequence_index[!] -= 1
end
if selected_sequence_index == 0
n = round(Int, series_length)
stop_time = start_time + (n-1) * time_interval
time_support = start_time:time_interval:stop_time
model = getmodel(back_session_id)
muscles = model.overview
sequence = MuscleActivity("", time_support, muscles)
# update the view
selected_sequence_name = ""
# update the view
sequence_name = ""
# disable the download and delete buttons (view)
no_sequences_yet = true
# update the model
model.sequence[] = sequence
else
# update both the view and model
selected_sequence_name = sequence_names[selected_sequence_index]
end
# update the view
notify(sequence_names)
end
end
function dropdown(key, options, label; margin=true, class=nothing)
......@@ -283,7 +351,7 @@ function muscle_view(bonito_url=""; channel=":channel")
label="Upload motor program(s)", autoupload=true, hideuploadbtn=true,
class="no-file-listing", id="uploader", url="/____/upload/$channel")),
item(btn("Download motor program", @click(:export_sequence_click), color="primary",
loading=:export_sequence_click,
loading=:export_sequence_click, disable=:no_sequences_yet,
[tooltip("Download the current motor program as a JSON file")])),
dropdown(:selected_sequence_name, :sequence_names, "Motor program",
class="sequence-list"),
......@@ -307,7 +375,8 @@ function muscle_view(bonito_url=""; channel=":channel")
textfield("Start time", :start_time, type="number", step="any",
disable=:editmode_disable),
]),
#item(btn("Delete motor program", @click(:delete_sequence_click), color="negative")),
item(btn("Delete motor program", @click(:delete_sequence_click), color="negative",
disable=:no_sequences_yet)),
])
Html.div(style=col, [
......
......@@ -12,3 +12,6 @@
margin-left: 1rem;
}
.q-notification__message {
font-size: 120%;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment