diff --git a/lib/livebook/settings.ex b/lib/livebook/settings.ex index 078414024..11d8afa89 100644 --- a/lib/livebook/settings.ex +++ b/lib/livebook/settings.ex @@ -11,18 +11,40 @@ defmodule Livebook.Settings do @type file_system_id :: :local | String.t() @doc """ - Returns the autosave path. - - TODO: Make this configurable in the UI. + Returns the current autosave path. """ @spec autosave_path() :: String.t() | nil def autosave_path() do case storage().fetch_key(:settings, "global", :autosave_path) do {:ok, value} -> value - :error -> Path.join(Livebook.Config.data_path(), "autosaved") + :error -> default_autosave_path() end end + @doc """ + Returns the default autosave path. + """ + @spec default_autosave_path() :: String.t() + def default_autosave_path() do + Path.join(Livebook.Config.data_path(), "autosaved") + end + + @doc """ + Sets the current autosave path. + """ + @spec set_autosave_path(String.t()) :: :ok + def set_autosave_path(autosave_path) do + storage().insert(:settings, "global", autosave_path: autosave_path) + end + + @doc """ + Restores the default autosave path. + """ + @spec reset_autosave_path() :: :ok + def reset_autosave_path() do + storage().delete_key(:settings, "global", :autosave_path) + end + @doc """ Returns all known filesystems with their associated ids. diff --git a/lib/livebook/storage.ex b/lib/livebook/storage.ex index bd0cbb979..deaed9a8e 100644 --- a/lib/livebook/storage.ex +++ b/lib/livebook/storage.ex @@ -49,6 +49,11 @@ defmodule Livebook.Storage do """ @callback delete(namespace(), entity_id()) :: :ok + @doc """ + Deletes an attribute from given entity. + """ + @callback delete_key(namespace(), entity_id(), attribute()) :: :ok + @spec current() :: module() def current(), do: Application.fetch_env!(:livebook, :storage) end diff --git a/lib/livebook/storage/ets.ex b/lib/livebook/storage/ets.ex index d095b574f..cc77d9186 100644 --- a/lib/livebook/storage/ets.ex +++ b/lib/livebook/storage/ets.ex @@ -75,6 +75,11 @@ defmodule Livebook.Storage.Ets do GenServer.call(__MODULE__, {:delete, namespace, entity_id}) end + @impl Livebook.Storage + def delete_key(namespace, entity_id, key) do + GenServer.call(__MODULE__, {:delete_key, namespace, entity_id, key}) + end + @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) @@ -99,14 +104,9 @@ defmodule Livebook.Storage.Ets do @impl GenServer def handle_call({:insert, namespace, entity_id, attributes}, _from, %{table: table} = state) do - match_head = {{namespace, entity_id}, :"$1", :_, :_} + keys_to_delete = Enum.map(attributes, fn {key, _val} -> key end) - guards = - Enum.map(attributes, fn {key, _val} -> - {:==, :"$1", key} - end) - - :ets.select_delete(table, [{match_head, guards, [true]}]) + delete_keys(table, namespace, entity_id, keys_to_delete) timestamp = System.os_time(:millisecond) @@ -125,6 +125,12 @@ defmodule Livebook.Storage.Ets do {:reply, :ok, state, {:continue, :save_to_file}} end + @impl GenServer + def handle_call({:delete_key, namespace, entity_id, key}, _from, %{table: table} = state) do + delete_keys(table, namespace, entity_id, [key]) + {:reply, :ok, state, {:continue, :save_to_file}} + end + @impl GenServer def handle_continue(:save_to_file, %{table: table} = state) do file_path = String.to_charlist(config_file_path()) @@ -151,4 +157,12 @@ defmodule Livebook.Storage.Ets do :ets.new(__MODULE__, [:protected, :duplicate_bag]) end end + + defp delete_keys(table, namespace, entity_id, keys) do + match_head = {{namespace, entity_id}, :"$1", :_, :_} + + guards = Enum.map(keys, &{:==, :"$1", &1}) + + :ets.select_delete(table, [{match_head, guards, [true]}]) + end end diff --git a/lib/livebook_web/live/settings_live.ex b/lib/livebook_web/live/settings_live.ex index 90b5ae06b..522e15c8c 100644 --- a/lib/livebook_web/live/settings_live.ex +++ b/lib/livebook_web/live/settings_live.ex @@ -14,6 +14,10 @@ defmodule LivebookWeb.SettingsLive do |> SidebarHelpers.shared_home_handlers() |> assign( file_systems: file_systems, + autosave_path_state: %{ + file: autosave_dir(), + dialog_opened?: false + }, page_title: "Livebook - Settings" )} end @@ -34,8 +38,8 @@ defmodule LivebookWeb.SettingsLive do

Here you can change global Livebook configuration. Keep in mind - that this configuration is not persisted and gets discarded as - soon as you stop the application. + that this configuration gets persisted and will be restored on application + launch.

@@ -57,6 +61,18 @@ defmodule LivebookWeb.SettingsLive do <% end %> + +
+
+

+ Autosave location +

+

+ A directory to temporarily keep notebooks until they are persisted. +

+
+ <.autosave_path_select state={@autosave_path_state} /> +
@@ -124,6 +140,50 @@ defmodule LivebookWeb.SettingsLive do """ end + defp autosave_path_select(%{state: %{dialog_opened?: true}} = assigns) do + ~H""" +
+ <.live_component module={LivebookWeb.FileSelectComponent} + id="autosave-path-component" + file={@state.file} + extnames={[]} + running_files={[]} + submit_event={:set_autosave_path} + file_system_select_disabled={true} + > + + + + +
+ """ + end + + defp autosave_path_select(assigns) do + ~H""" +
+ + +
+ """ + end + @impl true def handle_params(%{"file_system_id" => file_system_id}, _url, socket) do {:noreply, assign(socket, file_system_id: file_system_id)} @@ -132,6 +192,42 @@ defmodule LivebookWeb.SettingsLive do def handle_params(_params, _url, socket), do: {:noreply, socket} @impl true + def handle_event("cancel_autosave_path", %{}, socket) do + {:noreply, + update( + socket, + :autosave_path_state, + &%{&1 | dialog_opened?: false, file: autosave_dir()} + )} + end + + def handle_event("set_autosave_path", %{}, socket) do + path = socket.assigns.autosave_path_state.file.path + + Livebook.Settings.set_autosave_path(path) + + {:noreply, + update( + socket, + :autosave_path_state, + &%{&1 | dialog_opened?: false, file: autosave_dir()} + )} + end + + @impl true + def handle_event("reset_autosave_path", %{}, socket) do + {:noreply, + update( + socket, + :autosave_path_state, + &%{&1 | file: default_autosave_dir()} + )} + end + + def handle_event("open_autosave_path_select", %{}, socket) do + {:noreply, update(socket, :autosave_path_state, &%{&1 | dialog_opened?: true})} + end + def handle_event("detach_file_system", %{"id" => file_system_id}, socket) do Livebook.Settings.remove_file_system(file_system_id) file_systems = Livebook.Settings.file_systems() @@ -143,5 +239,25 @@ defmodule LivebookWeb.SettingsLive do {:noreply, assign(socket, file_systems: file_systems)} end + def handle_info({:set_file, file, _info}, socket) do + {:noreply, update(socket, :autosave_path_state, &%{&1 | file: file})} + end + + def handle_info(:set_autosave_path, socket) do + handle_event("set_autosave_path", %{}, socket) + end + def handle_info(_message, socket), do: {:noreply, socket} + + defp autosave_dir() do + Livebook.Settings.autosave_path() + |> Livebook.FileSystem.Utils.ensure_dir_path() + |> Livebook.FileSystem.File.local() + end + + defp default_autosave_dir() do + Livebook.Settings.default_autosave_path() + |> Livebook.FileSystem.Utils.ensure_dir_path() + |> Livebook.FileSystem.File.local() + end end diff --git a/test/livebook/storage/ets_test.exs b/test/livebook/storage/ets_test.exs index 5bda55e38..9082c4d85 100644 --- a/test/livebook/storage/ets_test.exs +++ b/test/livebook/storage/ets_test.exs @@ -72,6 +72,15 @@ defmodule Livebook.Storage.EtsTest do assert :error = Ets.fetch(:delete, "test") end + test "delete_key/3" do + :ok = Ets.insert(:delete_key, "test", key1: "val1", key2: "val2") + + assert :ok = Ets.delete_key(:delete_key, "test", :key2) + + assert {:ok, "val1"} = Ets.fetch_key(:delete_key, "test", :key1) + assert :error = Ets.fetch_key(:delete_key, "test", :key2) + end + describe "all/1" do test "returns all inserted entities for given namespace" do :ok = Ets.insert(:all, "test1", key1: "val1")