From bee7ecd15402a8b9dfa7c80cbc822c0a08d32eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Laurent?= <francois.laurent@posteo.net> Date: Mon, 24 Feb 2025 17:51:56 +0100 Subject: [PATCH] feat: session reset --- routes.jl | 2 +- src/GenieExtras.jl | 8 +++-- src/Storage.jl | 23 ++++++++---- src/apps/larvatagger/app.jl | 39 +++++++++++++++----- src/apps/muscles/Backbone.jl | 22 ++++++++++-- src/apps/muscles/app.jl | 69 +++++++++++++++++++++++++++--------- src/nyxui.css | 11 ++++++ 7 files changed, 136 insertions(+), 38 deletions(-) diff --git a/routes.jl b/routes.jl index fdcc47d..bca171d 100755 --- a/routes.jl +++ b/routes.jl @@ -31,7 +31,7 @@ if Genie.Configuration.isprod() end function modelview(model, view) - page(model, NyxUI.add_footer(view); title=nyxui_title) + page(model, view; title=nyxui_title) end Stipple.Layout.add_css(nyxui_css) diff --git a/src/GenieExtras.jl b/src/GenieExtras.jl index 3dc561c..40cf48a 100644 --- a/src/GenieExtras.jl +++ b/src/GenieExtras.jl @@ -2,7 +2,7 @@ module GenieExtras import Genie.Renderer.Html -export publish_css, add_footer +export publish_css, appinfo, add_footer const project_root = dirname(Base.current_project()) @@ -30,7 +30,7 @@ function publish_css(; clear=true) return css_dir end -function footer() +function appinfo() version = string(pkgversion(@__MODULE__)) if endswith(version, ".0") version = version[1:end-2] @@ -40,9 +40,11 @@ function footer() version = join((version, readchomp(versionfile)), "-") end tagurl = "https://gitlab.pasteur.fr/nyx/NyxUI/-/tags" - Html.footer("NyxUI.jl <a href=\"$tagurl\">v$version</a>") + return "NyxUI.jl <a href=\"$tagurl\">v$version</a>" end +footer() = Html.footer(appinfo()) + add_footer(ui::AbstractString, footer=footer) = "$ui$(footer())" add_footer(ui::Function, footer=footer) = () -> add_footer(ui(), footer) diff --git a/src/Storage.jl b/src/Storage.jl index 24c879e..d037cbb 100644 --- a/src/Storage.jl +++ b/src/Storage.jl @@ -3,7 +3,7 @@ module Storage using Dates using TOML -export getconfig, getbucket, clear_oldest +export getconfig, getbucket, clear_oldest, purge_appdata const __storage_location__ = get(ENV, "STORAGE", joinpath(dirname(Base.current_project()), "storage")) @@ -32,10 +32,12 @@ function getconfig(key, morekeys...; default=nothing) config isa Dict ? default : config end -getbucket(session_id) = joinpath(__storage_location__, "exports", session_id) +function getbucket(session_id, appname) + joinpath(__storage_location__, "exports", session_id, appname) +end -function getbucket(session_id, mode) - session_bucket = getbucket(session_id) +function getbucket(session_id, appname, mode) + session_bucket = getbucket(session_id, appname) if mode === :read session_bucket elseif mode === :write @@ -50,8 +52,10 @@ end function getbucket(; child_resource) parts = splitpath(child_resource) - session_id = parts[findfirst(==("exports"), parts) + 1] - return getbucket(session_id) + exports = findfirst(==("exports"), parts) + session_id = parts[exports + 1] + appname = parts[exports + 2] + return getbucket(session_id, appname) end function clear_oldest(session_bucket) @@ -61,4 +65,11 @@ function clear_oldest(session_bucket) rm(oldest; recursive=true) end +function purge_appdata(session_id, appname) + bucket = getbucket(session_id, appname, :read) + isdir(bucket) && rm(bucket; recursive=true) + exportdir = joinpath("public", session_id, appname) + isdir(exportdir) && rm(exportdir; recursive=true) +end + end diff --git a/src/apps/larvatagger/app.jl b/src/apps/larvatagger/app.jl index 4dddc29..dbdea62 100644 --- a/src/apps/larvatagger/app.jl +++ b/src/apps/larvatagger/app.jl @@ -4,7 +4,8 @@ using NyxUI, NyxUI.Storage, NyxUI.GenieExtras using NyxWidgets.Base: Cache import LarvaTagger as LT using GenieSession, Stipple -import Stipple: @app, @init, @private, @in, @onchange +import Stipple: @app, @init, @private, @in, @onchange, @onbutton, @click +import StippleUI: tooltip const sizefactors = Dict("1.0"=>1., "1.5"=>1.5, "2.0"=>2.) const sizefactors_str = Dict(v=>k for (k,v) in pairs(sizefactors)) @@ -19,9 +20,9 @@ end const bonito_models = Cache{String, Model}() -const bonito_app = NamedApp(:inherit, +const bonito_app = NamedApp("larvatagger", function (session) - bucket = joinpath(getbucket(session, :read), "larvatagger") + bucket = getbucket(session, "larvatagger", :read) mkpath(bucket) model = lock(bonito_models) do if haskey(bonito_models, session) @@ -31,7 +32,7 @@ const bonito_app = NamedApp(:inherit, bonito_models.cache[session] = Model(1.0, nothing, inputfile) end end - exportdir = joinpath("public", session) + exportdir = joinpath("public", session, "larvatagger") function prepare_download(srcfile) mkpath(exportdir) filename = basename(srcfile) @@ -43,7 +44,7 @@ const bonito_app = NamedApp(:inherit, end end end - return "/$session/$filename" + return "/$session/larvatagger/$filename" end isnothing(model.app) || close(model.app) model.app = app = LT.larvaeditor(model.appdata; @@ -58,9 +59,20 @@ const bonito_app = NamedApp(:inherit, end ) +function purgesession(session) + model = lock(bonito_models) do + pop!(bonito_models.cache, session) + end + isnothing(model.app) || close(model.app) + purge_appdata(session, "larvatagger") + BonitoServer.addsession(bonito_app, session; restart=true) +end + + @app begin @private bonito_session_id = "" @in sizefactor = "1.0" + @in reset_bonito_session = false @onchange isready begin genie_session = GenieSession.session() @@ -98,6 +110,15 @@ const bonito_app = NamedApp(:inherit, """) end end + + @onbutton reset_bonito_session begin + purgesession(bonito_session_id) + # reset UI to defaults + bonito_session_id[!] = "" + sizefactor[!] = "1.0" + # reload page + run(__model__, "location.reload(true);") + end end function view() @@ -113,14 +134,16 @@ function view() multiple=false, clearable=false, counter=false, usechips=false, dense=true, var"options-dense"=true)]; style="display:flex;flex-direction:horizontal;align-items:center;"), - Html.span(footer())]; + Html.span("Reset session", @click(:reset_bonito_session), + style="cursor:pointer;", + [tooltip("Useful when the app crashes. All data will be lost!")]), + Html.span(appinfo())]; id="footer", - style="display:flex;flex-direction:horizontal;justify-content:space-between;align-items:center;height:40px;max-width:100%;padding:0 0.5rem;", ), ] end -function footer() +function appinfo() version = string(pkgversion(LT)) if endswith(version, ".0") version = version[1:end-2] diff --git a/src/apps/muscles/Backbone.jl b/src/apps/muscles/Backbone.jl index eb4758a..5547f55 100644 --- a/src/apps/muscles/Backbone.jl +++ b/src/apps/muscles/Backbone.jl @@ -9,7 +9,7 @@ include("MuscleWidgets.jl") using .MuscleWidgets export hasmodel, getmodel, setmodel, newsequence, getsequence, withsequences, - exportsequence, loadsequence, deletesequence + exportsequence, loadsequence, deletesequence, purgesession # mutable struct Backbone{W} # widget::Union{Nothing, W} @@ -132,10 +132,10 @@ end ## persistent storage -function exportsequence(session_id) +function exportsequence(session_id; appname="muscles") sequence = getsequence(session_id) isempty(sequence.program_name) && return "" - dir = getbucket(session_id, :write) + dir = getbucket(session_id, appname, :write) filepath = joinpath(dir, sequence.program_name * ".json") MuscleActivities.to_json_file(filepath, sequence) return filepath @@ -166,4 +166,20 @@ function deletesequence(session_id) end end +function purgesession(session_id; appname="muscles") + model = lock(__backbone_cache__) do + if session_id in keys(__backbone_cache__.cache) + #pop!(__valid_sessions__, session_id) # not sure what to do with it + pop!(__backbone_cache__.cache, session_id) + end + end + lock(__sequences__) do + if session_id in keys(__sequences__.cache) + pop!(__sequences__.cache, session_id) + end + end + purge_appdata(session_id, appname) + return model +end + end diff --git a/src/apps/muscles/app.jl b/src/apps/muscles/app.jl index 30eb0d0..6140bd2 100644 --- a/src/apps/muscles/app.jl +++ b/src/apps/muscles/app.jl @@ -1,6 +1,6 @@ module MuscleApp -using NyxUI, NyxUI.Storage +using NyxUI, NyxUI.Storage, NyxUI.GenieExtras using Observables using Genie, GenieSession, Stipple, StippleUI import Stipple: @app, @init, @private, @in, @out, @onchange, @onbutton, @notify @@ -21,7 +21,7 @@ const maxlength = getconfig("muscle-activity", "maxlength") const bonito_app = NamedApp(:inherit, Backbone.app) @app begin - @private back_session_id = "" + @private bonito_session_id = "" @private sequence = MuscleActivity("") @in new_sequence_click = false @@ -51,6 +51,8 @@ const bonito_app = NamedApp(:inherit, Backbone.app) @in delete_sequence_click = false @out no_sequences_yet = true + @in reset_bonito_session = false + @onchange isready begin _, url = init_model(__model__) run(__model__, """ @@ -60,7 +62,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) loader.style.display = 'block'; iframe.src = '$url'; """) - session_id = back_session_id + session_id = bonito_session_id model = getmodel(session_id) if true # TODO: diagnose missing Stipple initialization # restore state after page reload (Ctrl+R) @@ -92,8 +94,33 @@ const bonito_app = NamedApp(:inherit, Backbone.app) end end + @onbutton reset_bonito_session begin + model = purgesession(bonito_session_id; appname=bonito_app.name) + # reset UI to defaults + #bonito_session_id[!] = "" + sequence[!] = MuscleActivity("") + new_sequence_type[!] = "Empty" + make_new_sequences_random[!] = false + sequence_names[!] = String[] + selected_sequence_name[!] = "" + selected_sequence_index[!] = 0 + sequence_name[!] = "" + export_sequence_link[!] = "" + colormap[!] = default_colormap + first_time_in_editmode[!] = true + editmode[!] = false + series_length[!] = 21 + time_interval[!] = 0.05 + start_time[!] = 0.0 + heatmap_view[!] = false + no_sequences_yet[!] = true + # reload page + setmodel(bonito_session_id, sequence) + run(__model__, "location.reload(true);") + end + @onchange start_time, time_interval, series_length begin - model = getmodel(back_session_id) + model = getmodel(bonito_session_id) seq = model.sequence[] # input validation valid = true @@ -138,7 +165,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) end @onbutton new_sequence_click begin - sequence = newsequence(back_session_id, new_sequence_type, sequence_names, + sequence = newsequence(bonito_session_id, new_sequence_type, sequence_names, start_time, time_interval, round(Int, series_length)) # propagate name = sequence.program_name @@ -167,7 +194,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) @onchange selected_sequence_name begin isempty(selected_sequence_name) && return - seq = getsequence(back_session_id, selected_sequence_name) + seq = getsequence(bonito_session_id, selected_sequence_name) if !isnothing(seq) ix = findfirst(==(selected_sequence_name), sequence_names) selected_sequence_index[!] = ix @@ -202,7 +229,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) break end end - model = getmodel(back_session_id) + model = getmodel(bonito_session_id) if valid model.sequence[].program_name = sequence[!].program_name = sequence_name sequence_names[!][selected_sequence_index] = sequence_name @@ -214,7 +241,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) end @onchange colormap begin - model = getmodel(back_session_id) + model = getmodel(bonito_session_id) model.colormap[] = colormap end @@ -224,7 +251,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) editmode = false return end - model = getmodel(back_session_id) + model = getmodel(bonito_session_id) model.editmode[] = editmode editmode_disable = !editmode if editmode && first_time_in_editmode && heatmap_view @@ -244,18 +271,19 @@ const bonito_app = NamedApp(:inherit, Backbone.app) end @onbutton export_sequence_click begin - filepath = exportsequence(back_session_id) + filepath = exportsequence(bonito_session_id; appname=bonito_app.name) if isempty(filepath) @notify "No available motor programs; create one first" :error else filename = basename(filepath) - mkpath("./public/$back_session_id") - open("./public/$back_session_id/$filename", "w") do o + appname = bonito_app.name + mkpath("./public/$bonito_session_id/$appname") + open("./public/$bonito_session_id/$appname/$filename", "w") do o open(filepath, "r") do i write(o, read(i)) end end - export_sequence_link = "/$back_session_id/$filename" + export_sequence_link = "/$bonito_session_id/$appname/$filename" end end @@ -275,7 +303,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) if !isempty(fileuploads) file = fileuploads["path"] try - sequence = loadsequence(back_session_id, file) + sequence = loadsequence(bonito_session_id, file) if !(sequence.program_name in sequence_names) push!(sequence_names, sequence.program_name) end @@ -302,7 +330,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) @onbutton delete_sequence_click begin isempty(selected_sequence_name) && return # update the model - deletesequence(back_session_id) + deletesequence(bonito_session_id) popat!(sequence_names, selected_sequence_index) if length(sequence_names) < selected_sequence_index selected_sequence_index[!] -= 1 @@ -311,7 +339,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) 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) + model = getmodel(bonito_session_id) muscles = model.overview sequence = MuscleActivity("", time_support, muscles) # update the view @@ -381,6 +409,12 @@ function muscle_view(bonito_url=""; channel=":channel") disable=:no_sequences_yet)), ]) + footer = Html.div([ + Html.span("Reset session", @click(:reset_bonito_session), style="cursor:pointer;", + [tooltip("Useful when the app crashes. All data will be lost!")]), + Html.span(appinfo()), + ]; id="footer", style="width:1354.617px;") + Html.div(style=col, [ topbar, Html.div(style=row, [ @@ -391,6 +425,7 @@ function muscle_view(bonito_url=""; channel=":channel") ]), sidepanel, ]), + footer, ]) end @@ -422,7 +457,7 @@ function init_model(muscle_model=nothing) setmodel(session_id, muscle_model.sequence[!]) end BonitoServer.addsession(bonito_app, session_id) - muscle_model.back_session_id[!] = session_id + muscle_model.bonito_session_id[!] = session_id url = bonito_url(bonito_app, session_id) return muscle_model, url end diff --git a/src/nyxui.css b/src/nyxui.css index 12eb979..6535400 100644 --- a/src/nyxui.css +++ b/src/nyxui.css @@ -54,3 +54,14 @@ footer { text-align: right; padding-right: 1rem; } + +#footer { + display: flex; + flex-direction: horizontal; + justify-content: space-between; + align-items: center; + height: 40px; + max-width: 100%; + padding: 0; + padding-left: 1rem; +} -- GitLab