mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 06:01:44 +08:00
Unavailable secret error (#1371)
* Show an Add Secret button for System.EnvError * Extracts render_formatted * Rewrite the env error message * Renames the env error type * Applying suggestions * Applying suggestions (live patch) * Test for unavailable secrets * Bump to rc.1 * Applying suggestions (error message) * New error message * Prefill the secret value * Applying suggestions (error message output) * Update test/livebook_web/live/session_live_test.exs Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com> * Update lib/livebook/runtime.ex Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com> Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
17d8774b73
commit
443ff8bf9f
10 changed files with 88 additions and 34 deletions
2
.github/workflows/deploy.yaml
vendored
2
.github/workflows/deploy.yaml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
env:
|
env:
|
||||||
otp: "25.0"
|
otp: "25.0"
|
||||||
elixir: "1.14.0-rc.0"
|
elixir: "1.14.0-rc.1"
|
||||||
jobs:
|
jobs:
|
||||||
assets:
|
assets:
|
||||||
outputs:
|
outputs:
|
||||||
|
|
|
||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
otp: "25.0"
|
otp: "25.0"
|
||||||
elixir: "1.14.0-rc.0"
|
elixir: "1.14.0-rc.1"
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Stage 1
|
# Stage 1
|
||||||
# Builds the Livebook release
|
# Builds the Livebook release
|
||||||
FROM hexpm/elixir:1.14.0-rc.0-erlang-24.3.4.2-debian-bullseye-20210902-slim AS build
|
FROM hexpm/elixir:1.14.0-rc.1-erlang-24.3.4.2-debian-bullseye-20210902-slim AS build
|
||||||
|
|
||||||
RUN apt-get update && apt-get upgrade -y && \
|
RUN apt-get update && apt-get upgrade -y && \
|
||||||
apt-get install --no-install-recommends -y \
|
apt-get install --no-install-recommends -y \
|
||||||
|
|
@ -38,7 +38,7 @@ RUN mix do compile, release livebook
|
||||||
# We use the same base image, because we need Erlang, Elixir and Mix
|
# We use the same base image, because we need Erlang, Elixir and Mix
|
||||||
# during runtime to spawn the Livebook standalone runtimes.
|
# during runtime to spawn the Livebook standalone runtimes.
|
||||||
# Consequently the release doesn't include ERTS as we have it anyway.
|
# Consequently the release doesn't include ERTS as we have it anyway.
|
||||||
FROM hexpm/elixir:1.14.0-rc.0-erlang-24.3.4.2-debian-bullseye-20210902-slim
|
FROM hexpm/elixir:1.14.0-rc.1-erlang-24.3.4.2-debian-bullseye-20210902-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get upgrade -y && \
|
RUN apt-get update && apt-get upgrade -y && \
|
||||||
apt-get install --no-install-recommends -y \
|
apt-get install --no-install-recommends -y \
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ defprotocol Livebook.Runtime do
|
||||||
# A control element
|
# A control element
|
||||||
| {:control, attrs :: map()}
|
| {:control, attrs :: map()}
|
||||||
# Internal output format for errors
|
# Internal output format for errors
|
||||||
| {:error, message :: binary()}
|
| {:error, message :: String.t(), type :: {:missing_secret, String.t()} | :other}
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
Additional information about a complted evaluation.
|
Additional information about a complted evaluation.
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ defmodule Livebook.Runtime.Evaluator.DefaultFormatter do
|
||||||
|
|
||||||
def format_result({:error, kind, error, stacktrace}) do
|
def format_result({:error, kind, error, stacktrace}) do
|
||||||
formatted = format_error(kind, error, stacktrace)
|
formatted = format_error(kind, error, stacktrace)
|
||||||
{:error, formatted}
|
{:error, formatted, error_type(error)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}}
|
@compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}}
|
||||||
|
|
@ -128,4 +128,7 @@ defmodule Livebook.Runtime.Evaluator.DefaultFormatter do
|
||||||
defp error_color(string) do
|
defp error_color(string) do
|
||||||
IO.ANSI.format([:red, string], true)
|
IO.ANSI.format([:red, string], true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp error_type(%System.EnvError{env: "LB_" <> secret}), do: {:missing_secret, secret}
|
||||||
|
defp error_type(_), do: :other
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ defmodule LivebookWeb.Output do
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
import LivebookWeb.Helpers
|
import LivebookWeb.Helpers
|
||||||
|
import LivebookWeb.LiveHelpers
|
||||||
|
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
alias LivebookWeb.Output
|
alias LivebookWeb.Output
|
||||||
|
alias LivebookWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a list of cell outputs.
|
Renders a list of cell outputs.
|
||||||
|
|
@ -32,7 +34,7 @@ defmodule LivebookWeb.Output do
|
||||||
|
|
||||||
defp border?({:stdout, _text}), do: true
|
defp border?({:stdout, _text}), do: true
|
||||||
defp border?({:text, _text}), do: true
|
defp border?({:text, _text}), do: true
|
||||||
defp border?({:error, _message}), do: true
|
defp border?({:error, _message, _type}), do: true
|
||||||
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
|
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
|
||||||
defp border?(_output), do: false
|
defp border?(_output), do: false
|
||||||
|
|
||||||
|
|
@ -233,19 +235,38 @@ defmodule LivebookWeb.Output do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_output({:error, formatted}, %{}) do
|
defp render_output({:error, formatted, {:missing_secret, secret}}, %{
|
||||||
assigns = %{message: formatted}
|
socket: socket,
|
||||||
|
session_id: session_id
|
||||||
|
}) do
|
||||||
|
assigns = %{message: formatted, secret: secret}
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div
|
<div class="-m-4 space-x-4 py-4">
|
||||||
class="whitespace-pre-wrap font-editor text-gray-500"
|
<div
|
||||||
role="complementary"
|
class="flex items-center justify-between font-editor border-b px-4 pb-4 mb-4"
|
||||||
aria-label="error"
|
style="color: var(--ansi-color-red);"
|
||||||
phx-no-format
|
>
|
||||||
><%= ansi_string_to_html(@message) %></div>
|
<div class="flex space-x-2">
|
||||||
|
<.remix_icon icon="close-circle-line" />
|
||||||
|
<span>Missing secret <%= inspect(@secret) %></span>
|
||||||
|
</div>
|
||||||
|
<%= live_patch to: Routes.session_path(socket, :secrets, session_id, secret: secret),
|
||||||
|
class: "button-base button-gray",
|
||||||
|
aria_label: "add secret",
|
||||||
|
role: "button" do %>
|
||||||
|
<span>Add secret</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render_formatted_error_message(@message) %>
|
||||||
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp render_output({:error, formatted, _type}, %{}) do
|
||||||
|
render_formatted_error_message(formatted)
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: remove on Livebook v0.7
|
# TODO: remove on Livebook v0.7
|
||||||
defp render_output(output, %{})
|
defp render_output(output, %{})
|
||||||
when elem(output, 0) in [
|
when elem(output, 0) in [
|
||||||
|
|
@ -279,4 +300,17 @@ defmodule LivebookWeb.Output do
|
||||||
><%= @message %></div>
|
><%= @message %></div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp render_formatted_error_message(formatted) do
|
||||||
|
assigns = %{message: formatted}
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div
|
||||||
|
class="whitespace-pre-wrap font-editor text-gray-500"
|
||||||
|
role="complementary"
|
||||||
|
aria-label="error"
|
||||||
|
phx-no-format
|
||||||
|
><%= ansi_string_to_html(@message) %></div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
module={LivebookWeb.SessionLive.SecretsComponent}
|
module={LivebookWeb.SessionLive.SecretsComponent}
|
||||||
id="secrets"
|
id="secrets"
|
||||||
session={@session}
|
session={@session}
|
||||||
|
secret={@secret}
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -745,6 +746,12 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, assign(socket, tab: tab)}
|
{:noreply, assign(socket, tab: tab)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_params(params, _url, socket)
|
||||||
|
when socket.assigns.live_action == :secrets do
|
||||||
|
label = Map.get(params, "secret", "")
|
||||||
|
{:noreply, assign(socket, secret: %{"label" => label, "value" => ""})}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_params(_params, _url, socket) do
|
def handle_params(_params, _url, socket) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
defmodule LivebookWeb.SessionLive.SecretsComponent do
|
defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
use LivebookWeb, :live_component
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(socket) do
|
|
||||||
{:ok, assign(socket, data: %{"label" => "", "value" => ""})}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
socket = assign(socket, assigns)
|
socket = assign(socket, assigns)
|
||||||
|
|
@ -25,7 +20,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
</p>
|
</p>
|
||||||
<.form
|
<.form
|
||||||
let={f}
|
let={f}
|
||||||
for={:data}
|
for={:secret}
|
||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
phx-change="validate"
|
phx-change="validate"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -37,7 +32,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
Label <span class="text-xs text-gray-500">(alphanumeric and underscore)</span>
|
Label <span class="text-xs text-gray-500">(alphanumeric and underscore)</span>
|
||||||
</div>
|
</div>
|
||||||
<%= text_input(f, :label,
|
<%= text_input(f, :label,
|
||||||
value: @data["label"],
|
value: @secret["label"],
|
||||||
class: "input",
|
class: "input",
|
||||||
placeholder: "secret label",
|
placeholder: "secret label",
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -48,7 +43,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
<div>
|
<div>
|
||||||
<div class="input-label">Value</div>
|
<div class="input-label">Value</div>
|
||||||
<%= text_input(f, :value,
|
<%= text_input(f, :value,
|
||||||
value: @data["value"],
|
value: @secret["value"],
|
||||||
class: "input",
|
class: "input",
|
||||||
placeholder: "secret value",
|
placeholder: "secret value",
|
||||||
aria_labelledby: "secret-value",
|
aria_labelledby: "secret-value",
|
||||||
|
|
@ -56,7 +51,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
) %>
|
) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="mt-5 button-base button-blue" type="submit" disabled={not data_valid?(@data)}>
|
<button class="mt-5 button-base button-blue" type="submit" disabled={not valid?(@secret)}>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</.form>
|
</.form>
|
||||||
|
|
@ -66,17 +61,17 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"data" => data}, socket) do
|
def handle_event("save", %{"secret" => secret}, socket) do
|
||||||
secret = %{label: String.upcase(data["label"]), value: data["value"]}
|
secret = %{label: String.upcase(secret["label"]), value: secret["value"]}
|
||||||
Livebook.Session.put_secret(socket.assigns.session.pid, secret)
|
Livebook.Session.put_secret(socket.assigns.session.pid, secret)
|
||||||
{:noreply, assign(socket, data: %{"label" => "", "value" => ""})}
|
{:noreply, assign(socket, secret: %{"label" => "", "value" => ""})}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("validate", %{"data" => data}, socket) do
|
def handle_event("validate", %{"secret" => secret}, socket) do
|
||||||
{:noreply, assign(socket, data: data)}
|
{:noreply, assign(socket, secret: secret)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp data_valid?(data) do
|
defp valid?(secret) do
|
||||||
String.match?(data["label"], ~r/^\w+$/) and data["value"] != ""
|
String.match?(secret["label"], ~r/^\w+$/) and secret["value"] != ""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
2
mix.exs
2
mix.exs
|
|
@ -5,7 +5,7 @@ defmodule Livebook.MixProject do
|
||||||
@version "0.6.3"
|
@version "0.6.3"
|
||||||
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
||||||
|
|
||||||
@app_elixir_version "1.14.0-rc.0"
|
@app_elixir_version "1.14.0-rc.1"
|
||||||
@app_rebar3_version "3.19.0"
|
@app_rebar3_version "3.19.0"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
|
|
|
||||||
|
|
@ -913,16 +913,31 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "add secret" do
|
describe "secrets" do
|
||||||
test "adds a secret from form", %{conn: conn, session: session} do
|
test "adds a secret from form", %{conn: conn, session: session} do
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element(~s{form[phx-submit="save"]})
|
|> element(~s{form[phx-submit="save"]})
|
||||||
|> render_submit(%{data: %{label: "foo", value: "123"}})
|
|> render_submit(%{secret: %{label: "foo", value: "123"}})
|
||||||
|
|
||||||
assert %{secrets: [%{label: "FOO", value: "123"}]} = Session.get_data(session.pid)
|
assert %{secrets: [%{label: "FOO", value: "123"}]} = Session.get_data(session.pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows the 'Add secret' button for unavailable secrets", %{conn: conn, session: session} do
|
||||||
|
Session.subscribe(session.id)
|
||||||
|
section_id = insert_section(session.pid)
|
||||||
|
cell_id = insert_text_cell(session.pid, section_id, :code, ~s{System.fetch_env!("LB_FOO")})
|
||||||
|
|
||||||
|
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||||
|
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> element("span", "Add secret")
|
||||||
|
|> has_element?()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue