defmodule LivebookWeb.SessionLive.PersistenceComponent do use LivebookWeb, :live_component alias Livebook.{Sessions, Session, LiveMarkdown, FileSystem} @impl true def mount(socket) do sessions = Sessions.list_sessions() running_files = Enum.map(sessions, & &1.file) {:ok, assign(socket, running_files: running_files)} end @impl true def update(%{event: {:set_file, file, _info}}, socket) do current_file = socket.assigns.draft_file autosave_interval_s = cond do FileSystem.File.same_file_system?(file, current_file) -> socket.assigns.new_attrs.autosave_interval_s FileSystem.File.local?(file) -> Livebook.Notebook.default_autosave_interval_s() true -> nil end {:ok, socket |> assign(draft_file: file) |> put_new_attr(:autosave_interval_s, autosave_interval_s)} end def update(%{event: :confirm_file}, socket) do {:ok, save(socket)} end def update(assigns, socket) do {file, assigns} = Map.pop!(assigns, :file) {persist_outputs, assigns} = Map.pop!(assigns, :persist_outputs) {autosave_interval_s, assigns} = Map.pop!(assigns, :autosave_interval_s) attrs = %{ persist_outputs: persist_outputs, autosave_interval_s: autosave_interval_s } socket = socket |> assign(assigns) |> assign_new(:attrs, fn -> attrs end) |> assign_new(:new_attrs, fn -> attrs end) |> assign_new(:draft_file, fn -> file || case assigns.session.origin do # If it's a forked notebook, default to the same folder {:file, file} -> FileSystem.File.containing_dir(file) _ -> Livebook.Settings.default_dir(assigns.hub) end end) |> assign_new(:saved_file, fn -> file end) {:ok, socket} end @impl true def render(assigns) do ~H"""

Save to file

<.live_component module={LivebookWeb.FileSelectComponent} id="persistence_file_select" file={@draft_file} hub={@hub} extnames={[LiveMarkdown.extension()]} running_files={@running_files} submit_event={:confirm_file} target={{__MODULE__, @id}} />
<.label>File
<%= normalize_file(@draft_file).path %>
<.select_field name="autosave_interval_s" label="Autosave" value={@new_attrs.autosave_interval_s || ""} options={[ {"every 5 seconds", "5"}, {"every 30 seconds", "30"}, {"every minute", "60"}, {"every 10 minutes", "600"}, {"never", ""} ]} /> <.checkbox_field name="persist_outputs" label="Persist outputs" value={@new_attrs.persist_outputs} />
<.link patch={~p"/sessions/#{@session.id}"} class="button-base button-outlined-gray"> Cancel
""" end @impl true def handle_event( "set_options", %{"persist_outputs" => persist_outputs, "autosave_interval_s" => autosave_interval_s}, socket ) do persist_outputs = persist_outputs == "true" autosave_interval_s = parse_optional_integer(autosave_interval_s) {:noreply, socket |> put_new_attr(:persist_outputs, persist_outputs) |> put_new_attr(:autosave_interval_s, autosave_interval_s)} end def handle_event("save", %{}, socket) do {:noreply, save(socket)} end def handle_event("stop_saving", %{}, socket) do Session.set_file(socket.assigns.session.pid, nil) {:noreply, push_patch(socket, to: ~p"/sessions/#{socket.assigns.session.id}")} end defp save(%{assigns: assigns} = socket) do %{new_attrs: new_attrs, attrs: attrs, draft_file: draft_file, saved_file: saved_file} = assigns draft_file = normalize_file(draft_file) if draft_file != saved_file do Session.set_file(assigns.session.pid, draft_file) end diff = map_diff(new_attrs, attrs) if diff != %{} do Session.set_notebook_attributes(assigns.session.pid, diff) end Session.save_sync(assigns.session.pid) # We can't do push_patch from update/2, so we ask the LV to do so send(self(), {:push_patch, ~p"/sessions/#{assigns.session.id}"}) socket end defp parse_optional_integer(string) do case Integer.parse(string) do {number, _} -> number :error -> nil end end defp put_new_attr(socket, key, value) do new_attrs = socket.assigns.new_attrs if new_attrs[key] == value do socket else new_attrs = put_in(new_attrs[key], value) assign(socket, :new_attrs, new_attrs) end end defp normalize_file(file) do FileSystem.File.ensure_extension(file, LiveMarkdown.extension()) end defp savable?(draft_file, saved_file, running_files) do file = normalize_file(draft_file) not FileSystem.File.dir?(draft_file) and (file not in running_files or file == saved_file) end defp map_diff(left, right) do Map.new(Map.to_list(left) -- Map.to_list(right)) end end