mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +08:00
Add autosave path configuration (#1019)
* Added autosave path configuration * Update settings.ex * Adjusted to review * Apply suggestions from code review Co-authored-by: José Valim <jose.valim@gmail.com> * Adjusted to review * Adjusted to review Co-authored-by: José Valim <jose.valim@gmail.com>
This commit is contained in:
parent
baccc964db
commit
4061aa150d
5 changed files with 179 additions and 13 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|||
<PageHelpers.title text="System settings" socket={@socket} />
|
||||
<p class="mt-4 text-gray-700">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -57,6 +61,18 @@ defmodule LivebookWeb.SettingsLive do
|
|||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Autosave path configuration -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xl text-gray-800 font-semibold">
|
||||
Autosave location
|
||||
</h2>
|
||||
<p class="mt-4 text-gray-700">
|
||||
A directory to temporarily keep notebooks until they are persisted.
|
||||
</p>
|
||||
</div>
|
||||
<.autosave_path_select state={@autosave_path_state} />
|
||||
</div>
|
||||
<!-- File systems configuration -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
|
@ -124,6 +140,50 @@ defmodule LivebookWeb.SettingsLive do
|
|||
"""
|
||||
end
|
||||
|
||||
defp autosave_path_select(%{state: %{dialog_opened?: true}} = assigns) do
|
||||
~H"""
|
||||
<div class="w-full h-52">
|
||||
<.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}
|
||||
>
|
||||
<button class="button-base button-gray"
|
||||
phx-click="cancel_autosave_path"
|
||||
tabindex="-1">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="button-base button-gray"
|
||||
phx-click="reset_autosave_path"
|
||||
tabindex="-1">
|
||||
Reset
|
||||
</button>
|
||||
<button class="button-base button-blue"
|
||||
phx-click="set_autosave_path"
|
||||
disabled={not Livebook.FileSystem.File.dir?(@state.file)}
|
||||
tabindex="-1">
|
||||
Save
|
||||
</button>
|
||||
</.live_component>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp autosave_path_select(assigns) do
|
||||
~H"""
|
||||
<div class="flex">
|
||||
<input class="input mr-2" readonly value={@state.file.path}/>
|
||||
<button class="button-base button-gray button-small"
|
||||
phx-click="open_autosave_path_select">
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue