2021-08-14 03:17:43 +08:00
|
|
|
defmodule LivebookWeb.SessionLive.PersistenceLive do
|
|
|
|
# TODO: rewrite this live view as a component, once live_view
|
2021-10-31 14:14:35 +08:00
|
|
|
# has a unified way of sending events programmatically from a child
|
2021-08-14 03:17:43 +08:00
|
|
|
# component to parent live view or component. Currently we send an
|
|
|
|
# event to self() from FileSelectComponent and use handle_info in
|
|
|
|
# the parent live view.
|
|
|
|
use LivebookWeb, :live_view
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
alias Livebook.{Sessions, Session, LiveMarkdown, FileSystem}
|
2021-08-14 03:17:43 +08:00
|
|
|
|
|
|
|
@impl true
|
|
|
|
def mount(
|
|
|
|
_params,
|
|
|
|
%{
|
2021-09-05 01:16:01 +08:00
|
|
|
"session" => session,
|
2021-08-14 03:17:43 +08:00
|
|
|
"file" => file,
|
|
|
|
"persist_outputs" => persist_outputs,
|
|
|
|
"autosave_interval_s" => autosave_interval_s
|
|
|
|
},
|
|
|
|
socket
|
|
|
|
) do
|
2021-09-05 01:16:01 +08:00
|
|
|
sessions = Sessions.list_sessions()
|
|
|
|
running_files = Enum.map(sessions, & &1.file)
|
2021-08-14 03:17:43 +08:00
|
|
|
|
|
|
|
attrs = %{
|
|
|
|
file: file,
|
|
|
|
persist_outputs: persist_outputs,
|
|
|
|
autosave_interval_s: autosave_interval_s
|
|
|
|
}
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
assign(socket,
|
2021-09-05 01:16:01 +08:00
|
|
|
session: session,
|
2021-08-14 03:17:43 +08:00
|
|
|
running_files: running_files,
|
|
|
|
attrs: attrs,
|
2021-10-30 18:02:26 +08:00
|
|
|
new_attrs: attrs,
|
|
|
|
draft_file: nil,
|
|
|
|
saved_file: nil
|
2021-08-14 03:17:43 +08:00
|
|
|
)}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def render(assigns) do
|
|
|
|
~H"""
|
|
|
|
<div class="p-6 pb-4 flex flex-col space-y-8">
|
|
|
|
<h3 class="text-2xl font-semibold text-gray-800">
|
2021-10-30 18:02:26 +08:00
|
|
|
Save to file
|
2021-08-14 03:17:43 +08:00
|
|
|
</h3>
|
|
|
|
<div class="w-full flex-col space-y-8">
|
|
|
|
<div class="flex">
|
2021-12-04 23:29:14 +08:00
|
|
|
<form phx-change="set_options" onsubmit="return false;" class="flex flex-col space-y-4 items-start max-w-full">
|
2021-10-30 18:02:26 +08:00
|
|
|
<div class="flex flex-col space-y-4">
|
|
|
|
<.switch_checkbox
|
|
|
|
name="persist_outputs"
|
|
|
|
label="Persist outputs"
|
|
|
|
checked={@new_attrs.persist_outputs} />
|
|
|
|
<div class="flex space-x-2 items-center">
|
|
|
|
<span class="text-gray-700 whitespace-nowrap">Autosave</span>
|
|
|
|
<.select
|
|
|
|
name="autosave_interval_s"
|
|
|
|
selected={@new_attrs.autosave_interval_s}
|
|
|
|
options={[
|
|
|
|
{5, "every 5 seconds"},
|
|
|
|
{30, "every 30 seconds"},
|
|
|
|
{60, "every minute"},
|
|
|
|
{600, "every 10 minutes"},
|
|
|
|
{nil, "never"}
|
|
|
|
]} />
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-12-04 23:29:14 +08:00
|
|
|
<div class="flex space-x-2 items-center max-w-full">
|
2021-10-30 18:02:26 +08:00
|
|
|
<span class="text-gray-700 whitespace-nowrap">File:</span>
|
|
|
|
<%= if @new_attrs.file do %>
|
2021-11-02 01:20:56 +08:00
|
|
|
<span class="tooltip right" data-tooltip={file_system_label(@new_attrs.file.file_system)}>
|
2021-10-30 18:02:26 +08:00
|
|
|
<span class="flex items-center">
|
|
|
|
[<.file_system_icon file_system={@new_attrs.file.file_system} />]
|
|
|
|
</span>
|
|
|
|
</span>
|
2021-12-04 23:29:14 +08:00
|
|
|
<span class="text-gray-700 whitespace-no-wrap font-medium overflow-ellipsis overflow-hidden">
|
2021-10-30 18:02:26 +08:00
|
|
|
<%= @new_attrs.file.path %>
|
|
|
|
</span>
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-gray button-small"
|
2021-10-30 18:02:26 +08:00
|
|
|
phx-click="open_file_select">
|
|
|
|
Change file
|
|
|
|
</button>
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-gray button-small"
|
2021-10-30 18:02:26 +08:00
|
|
|
phx-click="clear_file">
|
|
|
|
Stop saving
|
|
|
|
</button>
|
|
|
|
<% else %>
|
|
|
|
<span class="text-gray-700 whitespace-no-wrap">
|
|
|
|
no file selected
|
|
|
|
</span>
|
2021-12-23 19:29:49 +08:00
|
|
|
<%= unless @draft_file do %>
|
|
|
|
<button class="button-base button-gray button-small"
|
|
|
|
phx-click="open_file_select">
|
|
|
|
Choose a file
|
|
|
|
</button>
|
|
|
|
<% end %>
|
2021-10-30 18:02:26 +08:00
|
|
|
<% end %>
|
2021-08-14 03:17:43 +08:00
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
2021-10-30 18:02:26 +08:00
|
|
|
<%= if @draft_file do %>
|
|
|
|
<div class="flex flex-col">
|
|
|
|
<div class="h-full h-52">
|
2021-11-02 02:33:43 +08:00
|
|
|
<.live_component module={LivebookWeb.FileSelectComponent}
|
|
|
|
id="persistence_file_select"
|
|
|
|
file={@draft_file}
|
|
|
|
extnames={[LiveMarkdown.extension()]}
|
|
|
|
running_files={@running_files}
|
|
|
|
submit_event={:confirm_file}>
|
2021-10-30 18:02:26 +08:00
|
|
|
<div class="flex justify-end space-x-2">
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-gray"
|
2021-10-30 18:02:26 +08:00
|
|
|
phx-click="close_file_select"
|
|
|
|
tabindex="-1">
|
|
|
|
Cancel
|
|
|
|
</button>
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-blue"
|
2021-10-30 18:02:26 +08:00
|
|
|
phx-click="confirm_file"
|
2021-12-25 06:53:48 +08:00
|
|
|
disabled={FileSystem.File.dir?(@draft_file)}
|
2021-10-30 18:02:26 +08:00
|
|
|
tabindex="-1">
|
|
|
|
Choose
|
|
|
|
</button>
|
|
|
|
</div>
|
2021-11-02 02:33:43 +08:00
|
|
|
</.live_component>
|
2021-10-30 18:02:26 +08:00
|
|
|
</div>
|
|
|
|
<div class="mt-6 text-gray-500 text-sm">
|
|
|
|
File: <%= normalize_file(@draft_file).path %>
|
|
|
|
</div>
|
2021-08-14 03:17:43 +08:00
|
|
|
</div>
|
|
|
|
<% end %>
|
2021-10-30 18:02:26 +08:00
|
|
|
<div class="flex">
|
2021-08-14 03:17:43 +08:00
|
|
|
<%= if @new_attrs.file do %>
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-blue"
|
2021-08-14 03:17:43 +08:00
|
|
|
phx-click="save"
|
2021-10-30 18:02:26 +08:00
|
|
|
disabled={
|
|
|
|
not savable?(@new_attrs, @attrs, @running_files, @draft_file) or
|
|
|
|
(same_attrs?(@new_attrs, @attrs) and @saved_file == @new_attrs.file)
|
|
|
|
}>
|
|
|
|
Save now
|
2021-08-14 03:17:43 +08:00
|
|
|
</button>
|
2021-10-30 18:02:26 +08:00
|
|
|
<% else %>
|
2021-12-04 04:57:21 +08:00
|
|
|
<button class="button-base button-blue"
|
2021-10-30 18:02:26 +08:00
|
|
|
phx-click="save"
|
|
|
|
disabled={not savable?(@new_attrs, @attrs, @running_files, @draft_file) or same_attrs?(@new_attrs, @attrs)}>
|
|
|
|
Apply
|
|
|
|
</button>
|
|
|
|
<% end %>
|
2021-08-14 03:17:43 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2021-10-30 18:02:26 +08:00
|
|
|
def handle_event("open_file_select", %{}, socket) do
|
|
|
|
file = socket.assigns.new_attrs.file || Livebook.Config.default_dir()
|
|
|
|
{:noreply, assign(socket, draft_file: file)}
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_event("close_file_select", %{}, socket) do
|
|
|
|
{:noreply, assign(socket, draft_file: nil)}
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_event("confirm_file", %{}, socket) do
|
|
|
|
handle_confirm_file(socket)
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
def handle_event("clear_file", %{}, socket) do
|
|
|
|
{:noreply, socket |> put_new_file(nil) |> assign(draft_file: nil)}
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
def handle_event("save", %{}, %{assigns: assigns} = socket) do
|
|
|
|
%{new_attrs: new_attrs, attrs: attrs} = assigns
|
|
|
|
new_attrs = Map.update!(new_attrs, :file, &normalize_file/1)
|
|
|
|
diff = map_diff(new_attrs, attrs)
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
if Map.has_key?(diff, :file) do
|
|
|
|
Session.set_file(assigns.session.pid, diff.file)
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
notebook_attrs_diff = Map.take(diff, [:autosave_interval_s, :persist_outputs])
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
if notebook_attrs_diff != %{} do
|
|
|
|
Session.set_notebook_attributes(assigns.session.pid, notebook_attrs_diff)
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
if new_attrs.file do
|
|
|
|
Session.save_sync(assigns.session.pid)
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
running_files =
|
|
|
|
[new_attrs.file | assigns.running_files]
|
|
|
|
|> List.delete(attrs.file)
|
|
|
|
|> Enum.reject(&is_nil/1)
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
{:noreply,
|
|
|
|
assign(socket,
|
|
|
|
running_files: running_files,
|
|
|
|
attrs: assigns.new_attrs,
|
|
|
|
saved_file: new_attrs.file
|
|
|
|
)}
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
@impl true
|
|
|
|
def handle_info({:set_file, file, _file_info}, socket) do
|
|
|
|
{:noreply, assign(socket, draft_file: file)}
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
def handle_info(:confirm_file, socket) do
|
|
|
|
handle_confirm_file(socket)
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
defp handle_confirm_file(socket) do
|
|
|
|
file = normalize_file(socket.assigns.draft_file)
|
|
|
|
{:noreply, socket |> put_new_file(file) |> assign(draft_file: nil)}
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
defp parse_optional_integer(string) do
|
|
|
|
case Integer.parse(string) do
|
|
|
|
{number, _} -> number
|
|
|
|
:error -> nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp put_new_file(socket, file) do
|
|
|
|
new_attrs = socket.assigns.new_attrs
|
|
|
|
current_file_system = new_attrs.file && new_attrs.file.file_system
|
|
|
|
new_file_system = file && file.file_system
|
|
|
|
|
|
|
|
autosave_interval_s =
|
|
|
|
case new_file_system do
|
|
|
|
^current_file_system ->
|
|
|
|
new_attrs.autosave_interval_s
|
|
|
|
|
|
|
|
nil ->
|
|
|
|
Livebook.Notebook.default_autosave_interval_s()
|
|
|
|
|
|
|
|
%FileSystem.Local{} ->
|
|
|
|
Livebook.Notebook.default_autosave_interval_s()
|
|
|
|
|
|
|
|
_other ->
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
socket
|
|
|
|
|> put_new_attr(:file, file)
|
|
|
|
|> put_new_attr(:autosave_interval_s, autosave_interval_s)
|
|
|
|
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(nil), do: nil
|
|
|
|
|
|
|
|
defp normalize_file(file) do
|
|
|
|
Map.update!(file, :path, fn path ->
|
|
|
|
if String.ends_with?(path, LiveMarkdown.extension()) do
|
|
|
|
path
|
|
|
|
else
|
|
|
|
path <> LiveMarkdown.extension()
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2021-10-30 18:02:26 +08:00
|
|
|
defp savable?(new_attrs, attrs, running_files, draft_file) do
|
|
|
|
new_attrs = Map.update!(new_attrs, :file, &normalize_file/1)
|
|
|
|
valid_file? = new_attrs.file == attrs.file or file_savable?(new_attrs.file, running_files)
|
|
|
|
valid_file? and draft_file == nil
|
|
|
|
end
|
|
|
|
|
|
|
|
defp same_attrs?(new_attrs, attrs) do
|
|
|
|
new_attrs = Map.update!(new_attrs, :file, &normalize_file/1)
|
|
|
|
new_attrs == attrs
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
defp file_savable?(nil, _running_files), do: true
|
|
|
|
|
|
|
|
defp file_savable?(file, running_files) do
|
|
|
|
not FileSystem.File.dir?(file) and file not in running_files
|
|
|
|
end
|
2021-10-30 18:02:26 +08:00
|
|
|
|
|
|
|
defp map_diff(left, right) do
|
|
|
|
Map.new(Map.to_list(left) -- Map.to_list(right))
|
|
|
|
end
|
2021-08-14 03:17:43 +08:00
|
|
|
end
|