mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-27 09:19:02 +08:00
Show information when a new Livebook version is available (#1121)
* Show information when a new Livebook version is available * Wording * Use more precise version comparison * Update lib/livebook/application.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Up * Update notificaion styles * Update lib/livebook_web/live/home_live.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook_web/live/home_live.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Apply review comments * Use async_nolink Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
78b9a11d8c
commit
134fbe0589
8 changed files with 265 additions and 21 deletions
|
@ -196,6 +196,9 @@ The following environment variables configure Livebook:
|
|||
Enabled by default unless `LIVEBOOK_PASSWORD` is set. Set it to "false" to
|
||||
disable it.
|
||||
|
||||
* LIVEBOOK_UPDATE_INSTRUCTIONS_URL - sets the URL to direct the user to for
|
||||
updating Livebook when a new version becomes available.
|
||||
|
||||
<!-- Environment variables -->
|
||||
|
||||
If running Livebook as a Docker image or an Elixir release, [the environment
|
||||
|
|
|
@ -155,6 +155,11 @@ defmodule Livebook do
|
|||
:app_service_url,
|
||||
Livebook.Config.app_service_url!("LIVEBOOK_APP_SERVICE_URL")
|
||||
end
|
||||
|
||||
if update_instructions_url =
|
||||
Livebook.Config.update_instructions_url!("LIVEBOOK_UPDATE_INSTRUCTIONS_URL") do
|
||||
config :livebook, :update_instructions_url, update_instructions_url
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -18,16 +18,18 @@ defmodule Livebook.Application do
|
|||
LivebookWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Livebook.PubSub},
|
||||
# Start a supervisor for Livebook tasks
|
||||
{Task.Supervisor, name: Livebook.TaskSupervisor},
|
||||
# Start the storage module
|
||||
Livebook.Storage.current(),
|
||||
# Periodid measurement of system resources
|
||||
# Start the periodic version check
|
||||
Livebook.UpdateCheck,
|
||||
# Periodic measurement of system resources
|
||||
Livebook.SystemResources,
|
||||
# Start the tracker server on this node
|
||||
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
|
||||
# Start the supervisor dynamically managing sessions
|
||||
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
|
||||
# Start a supervisor for Livebook tasks
|
||||
{Task.Supervisor, name: Livebook.TaskSupervisor},
|
||||
# Start the server responsible for associating files with sessions
|
||||
Livebook.Session.FileGuard,
|
||||
# Start the Node Pool for managing node names
|
||||
|
|
|
@ -122,6 +122,14 @@ defmodule Livebook.Config do
|
|||
Application.fetch_env!(:livebook, :app_service_url)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the update check URL.
|
||||
"""
|
||||
@spec update_instructions_url() :: String.t() | nil
|
||||
def update_instructions_url() do
|
||||
Application.get_env(:livebook, :update_instructions_url)
|
||||
end
|
||||
|
||||
## Parsing
|
||||
|
||||
@doc """
|
||||
|
@ -254,6 +262,13 @@ defmodule Livebook.Config do
|
|||
System.get_env(env)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses update instructions url from env.
|
||||
"""
|
||||
def update_instructions_url!(env) do
|
||||
System.get_env(env)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses and validates default runtime from env.
|
||||
"""
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule Livebook.Settings do
|
|||
@doc """
|
||||
Returns the current autosave path.
|
||||
"""
|
||||
@spec autosave_path() :: String.t() | nil
|
||||
@spec autosave_path() :: String.t()
|
||||
def autosave_path() do
|
||||
case storage().fetch_key(:settings, "global", :autosave_path) do
|
||||
{:ok, value} -> value
|
||||
|
@ -98,4 +98,23 @@ defmodule Livebook.Settings do
|
|||
{:error, message} -> raise ArgumentError, "invalid S3 filesystem: #{message}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the update check is enabled.
|
||||
"""
|
||||
@spec update_check_enabled?() :: boolean()
|
||||
def update_check_enabled?() do
|
||||
case storage().fetch_key(:settings, "global", :update_check_enabled) do
|
||||
{:ok, value} -> value
|
||||
:error -> true
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets whether the update check is enabled.
|
||||
"""
|
||||
@spec set_update_check_enabled(boolean()) :: :ok
|
||||
def set_update_check_enabled(enabled) do
|
||||
storage().insert(:settings, "global", update_check_enabled: enabled)
|
||||
end
|
||||
end
|
||||
|
|
157
lib/livebook/update_check.ex
Normal file
157
lib/livebook/update_check.ex
Normal file
|
@ -0,0 +1,157 @@
|
|||
defmodule Livebook.UpdateCheck do
|
||||
@moduledoc false
|
||||
|
||||
# Periodically checks for available Livebook update.
|
||||
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
@name __MODULE__
|
||||
@timeout :infinity
|
||||
|
||||
@hour_in_ms 60 * 60 * 1000
|
||||
@day_in_ms 24 * @hour_in_ms
|
||||
|
||||
@doc false
|
||||
def start_link(_opts) do
|
||||
GenServer.start_link(__MODULE__, {}, name: @name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the latest Livebook version if it's more recent than the
|
||||
current one.
|
||||
"""
|
||||
@spec new_version() :: String.t() | nil
|
||||
def new_version() do
|
||||
GenServer.call(@name, :get_new_version, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the update check is enabled.
|
||||
"""
|
||||
@spec enabled?() :: boolean()
|
||||
def enabled?() do
|
||||
GenServer.call(@name, :get_enabled, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets whether the update check is enabled.
|
||||
"""
|
||||
@spec set_enabled(boolean()) :: :ok
|
||||
def set_enabled(enabled) do
|
||||
GenServer.cast(@name, {:set_enabled, enabled})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({}) do
|
||||
state = %{
|
||||
enabled: Livebook.Settings.update_check_enabled?(),
|
||||
new_version: nil,
|
||||
timer_ref: nil,
|
||||
request_ref: nil
|
||||
}
|
||||
|
||||
{:ok, schedule_check(state, 0)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:set_enabled, enabled}, state) do
|
||||
Livebook.Settings.set_update_check_enabled(enabled)
|
||||
state = %{state | enabled: enabled}
|
||||
state = state |> cancel_check() |> schedule_check(0)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_enabled, _from, state) do
|
||||
{:reply, state.enabled, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_new_version, _from, state) do
|
||||
new_version = if state.enabled, do: state.new_version
|
||||
{:reply, new_version, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check, state) do
|
||||
task = Task.Supervisor.async_nolink(Livebook.TaskSupervisor, &fetch_latest_version/0)
|
||||
{:noreply, %{state | request_ref: task.ref}}
|
||||
end
|
||||
|
||||
def handle_info({ref, response}, %{request_ref: ref} = state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
|
||||
state =
|
||||
case response do
|
||||
{:ok, version} ->
|
||||
new_version = if newer?(version), do: version
|
||||
state = %{state | new_version: new_version}
|
||||
schedule_check(state, @day_in_ms)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("version check failed, #{error}")
|
||||
schedule_check(state, @hour_in_ms)
|
||||
end
|
||||
|
||||
{:noreply, %{state | request_ref: nil}}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, reason}, %{request_ref: ref} = state) do
|
||||
Logger.error("version check failed, reason: #{inspect(reason)}")
|
||||
{:noreply, %{state | request_ref: nil} |> schedule_check(@hour_in_ms)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state), do: {:noreply, state}
|
||||
|
||||
defp schedule_check(%{enabled: false} = state, _time), do: state
|
||||
|
||||
defp schedule_check(state, time) do
|
||||
timer_ref = Process.send_after(self(), :check, time)
|
||||
%{state | timer_ref: timer_ref}
|
||||
end
|
||||
|
||||
defp cancel_check(%{timer_ref: nil} = state), do: state
|
||||
|
||||
defp cancel_check(state) do
|
||||
if Process.cancel_timer(state.timer_ref) == false do
|
||||
receive do
|
||||
:check -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
%{state | timer_ref: nil}
|
||||
end
|
||||
|
||||
defp fetch_latest_version() do
|
||||
url = "https://api.github.com/repos/livebook-dev/livebook/releases/latest"
|
||||
headers = [{"accept", "application/vnd.github.v3+json"}]
|
||||
|
||||
case Livebook.Utils.HTTP.request(:get, url, headers: headers) do
|
||||
{:ok, status, _headers, body} ->
|
||||
with 200 <- status,
|
||||
{:ok, data} <- Jason.decode(body),
|
||||
%{"tag_name" => "v" <> version} <- data do
|
||||
{:ok, version}
|
||||
else
|
||||
_ -> {:error, "unexpected response"}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "failed to make a request, reason: #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp newer?(version) do
|
||||
current_version = Application.spec(:livebook, :vsn) |> List.to_string()
|
||||
stable?(version) and Version.compare(current_version, version) == :lt
|
||||
end
|
||||
|
||||
defp stable?(version) do
|
||||
case Version.parse(version) do
|
||||
{:ok, %{pre: []}} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
sessions: sessions,
|
||||
notebook_infos: notebook_infos,
|
||||
page_title: "Livebook",
|
||||
new_version: Livebook.UpdateCheck.new_version(),
|
||||
update_instructions_url: Livebook.Config.update_instructions_url(),
|
||||
app_service_url: Livebook.Config.app_service_url(),
|
||||
memory: Livebook.SystemResources.memory()
|
||||
)}
|
||||
|
@ -39,9 +41,10 @@ defmodule LivebookWeb.HomeLive do
|
|||
<SidebarHelpers.sidebar>
|
||||
<SidebarHelpers.shared_home_footer socket={@socket} current_user={@current_user} />
|
||||
</SidebarHelpers.sidebar>
|
||||
<div class="grow px-6 py-8 overflow-y-auto">
|
||||
<div class="max-w-screen-lg w-full mx-auto px-4 pb-8 space-y-4">
|
||||
<.memory_notification memory={@memory} app_service_url={@app_service_url} />
|
||||
<div class="grow overflow-y-auto">
|
||||
<.update_notification version={@new_version} instructions_url={@update_instructions_url} />
|
||||
<.memory_notification memory={@memory} app_service_url={@app_service_url} />
|
||||
<div class="max-w-screen-lg w-full mx-auto px-8 pt-8 pb-32 space-y-4">
|
||||
<div class="flex flex-col space-y-2 items-center pb-4 border-b border-gray-200
|
||||
sm:flex-row sm:space-y-0 sm:justify-between">
|
||||
<div class="text-2xl text-gray-800 font-semibold">
|
||||
|
@ -157,18 +160,41 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
end
|
||||
|
||||
defp update_notification(%{version: nil} = assigns), do: ~H""
|
||||
|
||||
defp update_notification(assigns) do
|
||||
~H"""
|
||||
<div class="px-2 py-2 bg-blue-200 text-gray-900 text-sm text-center">
|
||||
<span>
|
||||
Livebook v<%= @version %> available!
|
||||
<%= if @instructions_url do %>
|
||||
Check out the news on
|
||||
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="https://livebook.dev/" target="_blank">
|
||||
livebook.dev
|
||||
</a>
|
||||
and follow the
|
||||
<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@instructions_url} target="_blank">
|
||||
update instructions
|
||||
</a>
|
||||
<% else %>
|
||||
Check out the news and installation steps on
|
||||
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="https://livebook.dev/" target="_blank">livebook.dev</a>
|
||||
<% end %>
|
||||
🚀
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp memory_notification(assigns) do
|
||||
~H"""
|
||||
<%= if @app_service_url && @memory.free < 30_000_000 do %>
|
||||
<div class="flex justify-between items-center border-b border-gray-200 pb-4 text-gray-700">
|
||||
<span class="flex items-end">
|
||||
<.remix_icon icon="alarm-warning-line" class="text-xl mr-2" />
|
||||
<span>
|
||||
Less than 30 MB of memory left, consider adding more resources to
|
||||
<a class="font-semibold" href={@app_service_url} target="_blank">the instance</a>
|
||||
or closing <a class="font-semibold" href="#running-sessions">running sessions</a>.
|
||||
</span>
|
||||
</span>
|
||||
<div class="px-2 py-2 bg-red-200 text-gray-900 text-sm text-center">
|
||||
<.remix_icon icon="alarm-warning-line" class="align-text-bottom mr-0.5" />
|
||||
Less than 30 MB of memory left, consider
|
||||
<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@app_service_url} target="_blank">adding more resources to the instance</a>
|
||||
or closing
|
||||
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="#running-sessions">running sessions</a>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
|
|
|
@ -7,17 +7,16 @@ defmodule LivebookWeb.SettingsLive do
|
|||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
file_systems = Livebook.Settings.file_systems()
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> SidebarHelpers.shared_home_handlers()
|
||||
|> assign(
|
||||
file_systems: file_systems,
|
||||
file_systems: Livebook.Settings.file_systems(),
|
||||
autosave_path_state: %{
|
||||
file: autosave_dir(),
|
||||
dialog_opened?: false
|
||||
},
|
||||
update_check_enabled: Livebook.UpdateCheck.enabled?(),
|
||||
page_title: "Livebook - Settings"
|
||||
)}
|
||||
end
|
||||
|
@ -45,9 +44,9 @@ defmodule LivebookWeb.SettingsLive do
|
|||
|
||||
<!-- System details -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h1 class="text-xl text-gray-800 font-semibold">
|
||||
<h2 class="text-xl text-gray-800 font-semibold">
|
||||
About
|
||||
</h1>
|
||||
</h2>
|
||||
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center space-x-12">
|
||||
<%= if app_name = Livebook.Config.app_service_name() do %>
|
||||
|
@ -76,6 +75,18 @@ defmodule LivebookWeb.SettingsLive do
|
|||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Preferences -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h2 class="text-xl text-gray-800 font-semibold">
|
||||
Preferences
|
||||
</h2>
|
||||
<form phx-change="save" onsubmit="return false;">
|
||||
<.switch_checkbox
|
||||
name="update_check_enabled"
|
||||
label="Show available Livebook updates"
|
||||
checked={@update_check_enabled} />
|
||||
</form>
|
||||
</div>
|
||||
<!-- Autosave path configuration -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
|
@ -253,6 +264,12 @@ defmodule LivebookWeb.SettingsLive do
|
|||
{:noreply, assign(socket, file_systems: file_systems)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"update_check_enabled" => enabled}, socket) do
|
||||
enabled = enabled == "true"
|
||||
Livebook.UpdateCheck.set_enabled(enabled)
|
||||
{:noreply, assign(socket, :update_check_enabled, enabled)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:file_systems_updated, file_systems}, socket) do
|
||||
{:noreply, assign(socket, file_systems: file_systems)}
|
||||
|
|
Loading…
Reference in a new issue