Add changed cell indicator (#105)

This commit is contained in:
Jonatan Kłosko 2021-03-22 15:43:31 +01:00 committed by GitHub
parent 5e20541774
commit 9e69e8f096
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 51 deletions

View file

@ -52,7 +52,8 @@ defmodule Livebook.Session.Data do
revision: cell_revision(), revision: cell_revision(),
deltas: list(Delta.t()), deltas: list(Delta.t()),
revision_by_client_pid: %{pid() => cell_revision()}, revision_by_client_pid: %{pid() => cell_revision()},
evaluated_at: DateTime.t() digest: String.t(),
evaluation_digest: String.t() | nil
} }
@type cell_revision :: non_neg_integer() @type cell_revision :: non_neg_integer()
@ -125,7 +126,7 @@ defmodule Livebook.Session.Data do
for section <- notebook.sections, for section <- notebook.sections,
cell <- section.cells, cell <- section.cells,
into: %{}, into: %{},
do: {cell.id, new_cell_info([])} do: {cell.id, new_cell_info(cell, [])}
end end
@doc """ @doc """
@ -415,7 +416,7 @@ defmodule Livebook.Session.Data do
data_actions data_actions
|> set!( |> set!(
notebook: Notebook.insert_cell(data.notebook, section_id, index, cell), notebook: Notebook.insert_cell(data.notebook, section_id, index, cell),
cell_infos: Map.put(data.cell_infos, cell.id, new_cell_info(data.client_pids)) cell_infos: Map.put(data.cell_infos, cell.id, new_cell_info(cell, data.client_pids))
) )
end end
@ -512,8 +513,7 @@ defmodule Livebook.Session.Data do
data_actions data_actions
|> set_cell_info!(cell.id, |> set_cell_info!(cell.id,
validity_status: :evaluated, validity_status: :evaluated,
evaluation_status: :ready, evaluation_status: :ready
evaluated_at: DateTime.utc_now()
) )
|> set_section_info!(section.id, evaluating_cell_id: nil) |> set_section_info!(section.id, evaluating_cell_id: nil)
end end
@ -553,7 +553,9 @@ defmodule Livebook.Session.Data do
data_actions data_actions
|> set!(notebook: Notebook.update_cell(data.notebook, id, &%{&1 | outputs: []})) |> set!(notebook: Notebook.update_cell(data.notebook, id, &%{&1 | outputs: []}))
|> set_cell_info!(id, evaluation_status: :evaluating) |> update_cell_info!(id, fn info ->
%{info | evaluation_status: :evaluating, evaluation_digest: info.digest}
end)
|> set_section_info!(section.id, evaluating_cell_id: id, evaluation_queue: ids) |> set_section_info!(section.id, evaluating_cell_id: id, evaluation_queue: ids)
|> add_action({:start_evaluation, cell, section}) |> add_action({:start_evaluation, cell, section})
@ -575,7 +577,11 @@ defmodule Livebook.Session.Data do
|> set_section_info!(section.id, evaluating_cell_id: nil, evaluation_queue: []) |> set_section_info!(section.id, evaluating_cell_id: nil, evaluation_queue: [])
|> reduce( |> reduce(
section.cells, section.cells,
&set_cell_info!(&1, &2.id, validity_status: :fresh, evaluation_status: :ready) &set_cell_info!(&1, &2.id,
validity_status: :fresh,
evaluation_status: :ready,
evaluation_digest: nil
)
) )
end end
@ -651,10 +657,18 @@ defmodule Livebook.Session.Data do
new_source = JSInterop.apply_delta_to_string(transformed_new_delta, cell.source) new_source = JSInterop.apply_delta_to_string(transformed_new_delta, cell.source)
new_digest = compute_digest(new_source)
data_actions data_actions
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source})) |> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source}))
|> update_cell_info!(cell.id, fn info -> |> update_cell_info!(cell.id, fn info ->
info = %{info | deltas: info.deltas ++ [transformed_new_delta], revision: info.revision + 1} info = %{
info
| deltas: info.deltas ++ [transformed_new_delta],
revision: info.revision + 1,
digest: new_digest
}
# Before receiving acknowledgement, the client receives all the other deltas, # Before receiving acknowledgement, the client receives all the other deltas,
# so we can assume they are in sync with the server and have the same revision. # so we can assume they are in sync with the server and have the same revision.
info = put_in(info.revision_by_client_pid[client_pid], info.revision) info = put_in(info.revision_by_client_pid[client_pid], info.revision)
@ -709,14 +723,15 @@ defmodule Livebook.Session.Data do
} }
end end
defp new_cell_info(client_pids) do defp new_cell_info(cell, client_pids) do
%{ %{
revision: 0, revision: 0,
deltas: [], deltas: [],
revision_by_client_pid: Map.new(client_pids, &{&1, 0}), revision_by_client_pid: Map.new(client_pids, &{&1, 0}),
validity_status: :fresh, validity_status: :fresh,
evaluation_status: :ready, evaluation_status: :ready,
evaluated_at: nil digest: compute_digest(cell.source),
evaluation_digest: nil
} }
end end
@ -770,6 +785,8 @@ defmodule Livebook.Session.Data do
set!(data_actions, dirty: dirty) set!(data_actions, dirty: dirty)
end end
defp compute_digest(source), do: :erlang.md5(source)
@doc """ @doc """
Finds the cell that's currently being evaluated in the given section. Finds the cell that's currently being evaluated in the given section.
""" """

View file

@ -249,55 +249,53 @@ defmodule LivebookWeb.CellComponent do
|> String.split("\n") |> String.split("\n")
end end
defp render_cell_status(%{evaluation_status: :evaluating}) do defp render_cell_status(%{evaluation_status: :evaluating} = info) do
assigns = %{} render_status_indicator(
"Evaluating",
~L""" "bg-blue-500",
<div class="flex items-center space-x-2"> "bg-blue-400",
<div class="text-xs text-gray-400">Evaluating</div> info.digest != info.evaluation_digest
<span class="flex relative h-3 w-3"> )
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
</div>
"""
end end
defp render_cell_status(%{evaluation_status: :queued}) do defp render_cell_status(%{evaluation_status: :queued}) do
assigns = %{} render_status_indicator("Queued", "bg-gray-500", "bg-gray-400", false)
end
defp render_cell_status(%{validity_status: :evaluated} = info) do
render_status_indicator(
"Evaluated",
"bg-green-400",
nil,
info.digest != info.evaluation_digest
)
end
defp render_cell_status(%{validity_status: :stale} = info) do
render_status_indicator("Stale", "bg-yellow-200", nil, info.digest != info.evaluation_digest)
end
defp render_cell_status(_), do: nil
defp render_status_indicator(text, circle_class, animated_circle_class, show_changed) do
assigns = %{
text: text,
circle_class: circle_class,
animated_circle_class: animated_circle_class,
show_changed: show_changed
}
~L""" ~L"""
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-1">
<div class="text-xs text-gray-400">Queued</div> <div class="flex text-xs text-gray-400">
<%= @text %>
<span class="<%= unless(@show_changed, do: "invisible") %>">*</span>
</div>
<span class="flex relative h-3 w-3"> <span class="flex relative h-3 w-3">
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-gray-400 opacity-75"></span> <span class="animate-ping absolute inline-flex h-3 w-3 rounded-full <%= @animated_circle_class %> opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-gray-500"></span> <span class="relative inline-flex rounded-full h-3 w-3 <%= @circle_class %>"></span>
</span> </span>
</div> </div>
""" """
end end
defp render_cell_status(%{validity_status: :evaluated}) do
assigns = %{}
~L"""
<div class="flex items-center space-x-2">
<div class="text-xs text-gray-400">Evaluated</div>
<div class="h-3 w-3 rounded-full bg-green-400"></div>
</div>
"""
end
defp render_cell_status(%{validity_status: :stale}) do
assigns = %{}
~L"""
<div class="flex items-center space-x-2">
<div class="text-xs text-gray-400">Stale</div>
<div class="h-3 w-3 rounded-full bg-yellow-200"></div>
</div>
"""
end
defp render_cell_status(_), do: nil
end end

View file

@ -665,6 +665,32 @@ defmodule Livebook.Session.DataTest do
}, []} = Data.apply_operation(data, operation) }, []} = Data.apply_operation(data, operation)
end end
test "updates cell evaluation digest" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:queue_cell_evaluation, self(), "c1"}
])
%{cell_infos: %{"c1" => %{digest: digest}}} = data
operation = {:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}}
assert {:ok,
%{
cell_infos: %{
"c1" => %{
validity_status: :evaluated,
evaluation_status: :ready,
evaluation_digest: ^digest
}
},
section_infos: %{"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}}
}, []} = Data.apply_operation(data, operation)
end
test "marks next queued cell in this section as evaluating if there is one" do test "marks next queued cell in this section as evaluating if there is one" do
data = data =
data_after_operations!([ data_after_operations!([
@ -1048,6 +1074,25 @@ defmodule Livebook.Session.DataTest do
}, _actions} = Data.apply_operation(data, operation) }, _actions} = Data.apply_operation(data, operation)
end end
test "updates cell digest based on the new content" do
data =
data_after_operations!([
{:client_join, self()},
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"}
])
%{cell_infos: %{"c1" => %{digest: digest}}} = data
delta = Delta.new() |> Delta.insert("cats")
operation = {:apply_cell_delta, self(), "c1", delta, 1}
assert {:ok, %{cell_infos: %{"c1" => %{digest: new_digest}}}, _actions} =
Data.apply_operation(data, operation)
assert digest != new_digest
end
test "transforms the delta if the revision is not the most recent" do test "transforms the delta if the revision is not the most recent" do
client1_pid = IEx.Helpers.pid(0, 0, 0) client1_pid = IEx.Helpers.pid(0, 0, 0)
client2_pid = IEx.Helpers.pid(0, 0, 1) client2_pid = IEx.Helpers.pid(0, 0, 1)