Implement emojis instead of colors (#1636)

This commit is contained in:
Alexandre de Souza 2023-01-12 17:37:12 -03:00 committed by GitHub
parent 57e1df74f3
commit ae3a5661a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 409 additions and 228 deletions

View file

@ -0,0 +1,33 @@
import { createPicker } from "picmo";
/**
* A hook for the emoji picker input.
*/
const EmojiPicker = {
mounted() {
const rootElement = this.el.querySelector("[data-emoji-container]");
const preview = this.el.querySelector("[data-emoji-preview]");
const input = this.el.querySelector("[data-emoji-input]");
const button = this.el.querySelector("[data-emoji-button]");
const pickerOptions = {
rootElement,
showSearch: false,
showPreview: false,
};
const picker = createPicker(pickerOptions);
picker.addEventListener("emoji:select", ({ emoji }) => {
preview.innerHTML = emoji;
input.value = emoji;
rootElement.classList.toggle("hidden");
});
button.addEventListener("click", (_) => {
rootElement.classList.toggle("hidden");
});
},
};
export default EmojiPicker;

View file

@ -4,6 +4,7 @@ import CellEditor from "./cell_editor";
import ConfirmModal from "./confirm_modal";
import Dropzone from "./dropzone";
import EditorSettings from "./editor_settings";
import EmojiPicker from "./emoji_picker";
import FocusOnUpdate from "./focus_on_update";
import Headline from "./headline";
import Highlight from "./highlight";
@ -26,6 +27,7 @@ export default {
ConfirmModal,
Dropzone,
EditorSettings,
EmojiPicker,
FocusOnUpdate,
Headline,
Highlight,

View file

@ -21,6 +21,7 @@
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"picmo": "^5.7.2",
"postcss-import": "^15.0.0",
"postcss-loader": "^7.0.1",
"rehype-katex": "^6.0.0",
@ -4505,6 +4506,15 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/emojibase": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/emojibase/-/emojibase-6.1.0.tgz",
"integrity": "sha512-1GkKJPXP6tVkYJHOBSJHoGOr/6uaDxZ9xJ6H7m6PfdGXTmQgbALHLWaVRY4Gi/qf5x/gT/NUXLPuSHYLqtLtrQ==",
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/milesjohnson"
}
},
"node_modules/emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
@ -8544,6 +8554,17 @@
"resolved": "../deps/phoenix_live_view",
"link": true
},
"node_modules/picmo": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/picmo/-/picmo-5.7.2.tgz",
"integrity": "sha512-A7c5O8x1Xwq11KBYFY93+GIbHnw9PVz35HaWWHn/dgT08GA67M6cXKjjwzLnEAyXSdxXKrEk8/gPyTs+ibzWfQ==",
"dependencies": {
"emojibase": "^6.1.0"
},
"funding": {
"url": "https://github.com/sponsors/joeattardi"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -14080,6 +14101,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"emojibase": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/emojibase/-/emojibase-6.1.0.tgz",
"integrity": "sha512-1GkKJPXP6tVkYJHOBSJHoGOr/6uaDxZ9xJ6H7m6PfdGXTmQgbALHLWaVRY4Gi/qf5x/gT/NUXLPuSHYLqtLtrQ=="
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
@ -16897,6 +16923,14 @@
"phoenix_live_view": {
"version": "file:../deps/phoenix_live_view"
},
"picmo": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/picmo/-/picmo-5.7.2.tgz",
"integrity": "sha512-A7c5O8x1Xwq11KBYFY93+GIbHnw9PVz35HaWWHn/dgT08GA67M6cXKjjwzLnEAyXSdxXKrEk8/gPyTs+ibzWfQ==",
"requires": {
"emojibase": "^6.1.0"
}
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",

View file

@ -25,6 +25,7 @@
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"picmo": "^5.7.2",
"postcss-import": "^15.0.0",
"postcss-loader": "^7.0.1",
"rehype-katex": "^6.0.0",

View file

@ -197,13 +197,11 @@ defmodule Livebook.Application do
if Livebook.Config.feature_flag_enabled?(:localhost_hub) do
defp insert_development_hub do
unless Livebook.Hubs.hub_exists?("local-host") do
Livebook.Hubs.save_hub(%Livebook.Hubs.Local{
id: "local-host",
hub_name: "Localhost",
hub_color: Livebook.EctoTypes.HexColor.random()
})
end
Livebook.Hubs.save_hub(%Livebook.Hubs.Local{
id: "local-host",
hub_name: "Localhost",
hub_emoji: "🏠"
})
end
else
defp insert_development_hub, do: :ok

View file

@ -2,7 +2,7 @@ defmodule Livebook.Hubs do
@moduledoc false
alias Livebook.Storage
alias Livebook.Hubs.{Enterprise, Fly, Local, Metadata, Provider}
alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Local, Metadata, Provider}
@namespace :hubs
@ -28,7 +28,7 @@ defmodule Livebook.Hubs do
@spec get_metadatas() :: list(Metadata.t())
def get_metadatas do
for hub <- get_hubs() do
Provider.normalize(hub)
%{Provider.normalize(hub) | connected?: Provider.connected?(hub)}
end
end
@ -71,7 +71,7 @@ defmodule Livebook.Hubs do
attributes = struct |> Map.from_struct() |> Map.to_list()
:ok = Storage.insert(@namespace, struct.id, attributes)
:ok = connect_hub(struct)
:ok = broadcast_hubs_change()
:ok = Broadcasts.hubs_metadata_changed()
struct
end
@ -84,7 +84,7 @@ defmodule Livebook.Hubs do
end
:ok = Storage.delete(@namespace, id)
:ok = broadcast_hubs_change()
:ok = Broadcasts.hubs_metadata_changed()
end
:ok
@ -98,30 +98,50 @@ defmodule Livebook.Hubs do
end
@doc """
Subscribes to updates in hubs information.
Subscribes to one or more subtopics in `"hubs"`.
## Messages
* `{:hubs_metadata_changed, hubs}`
Topic `hubs:crud`:
* `:hubs_metadata_changed`
Topic `hubs:connection`:
* `:hub_connected`
* `:hub_disconnected`
* `{:connection_error, reason}`
* `{:disconnection_error, reason}`
Topic `hubs:secrets`:
* `{:secret_created, %Secret{}}`
* `{:secret_updated, %Secret{}}`
"""
@spec subscribe() :: :ok | {:error, term()}
def subscribe do
Phoenix.PubSub.subscribe(Livebook.PubSub, "hubs")
@spec subscribe(atom() | list(atom())) :: :ok | {:error, term()}
def subscribe(topics) when is_list(topics) do
for topic <- topics, do: subscribe(topic)
:ok
end
def subscribe(topic) do
Phoenix.PubSub.subscribe(Livebook.PubSub, "hubs:#{topic}")
end
@doc """
Unsubscribes from `subscribe/0`.
"""
@spec unsubscribe() :: :ok
def unsubscribe do
Phoenix.PubSub.unsubscribe(Livebook.PubSub, "hubs")
@spec unsubscribe(atom() | list(atom())) :: :ok
def unsubscribe(topics) when is_list(topics) do
for topic <- topics, do: unsubscribe(topic)
:ok
end
# Notifies interested processes about hubs data change.
# Broadcasts `{:hubs_metadata_changed, hubs}` message under the `"hubs"` topic.
defp broadcast_hubs_change do
Phoenix.PubSub.broadcast(Livebook.PubSub, "hubs", {:hubs_metadata_changed, get_metadatas()})
def unsubscribe(topic) do
Phoenix.PubSub.unsubscribe(Livebook.PubSub, "hubs:#{topic}")
end
defp to_struct(%{id: "fly-" <> _} = fields) do

View file

@ -0,0 +1,71 @@
defmodule Livebook.Hubs.Broadcasts do
@moduledoc false
alias Livebook.Secrets.Secret
@type broadcast :: :ok | {:error, term()}
@crud_topic "hubs:crud"
@connection_topic "hubs:connection"
@secrets_topic "hubs:secrets"
@doc """
Broadcasts when hubs changed under `hubs:crud` topic
"""
@spec hubs_metadata_changed() :: broadcast()
def hubs_metadata_changed do
broadcast(@crud_topic, :hubs_metadata_changed)
end
@doc """
Broadcasts when hub connected under `hubs:connection` topic
"""
@spec hub_connected() :: broadcast()
def hub_connected do
broadcast(@connection_topic, :hub_connected)
end
@doc """
Broadcasts when hub disconnected under `hubs:connection` topic
"""
@spec hub_disconnected() :: broadcast()
def hub_disconnected do
broadcast(@connection_topic, :hub_disconnected)
end
@doc """
Broadcasts when hub had an error when connecting under `hubs:connection` topic
"""
@spec hub_connection_failed(String.t()) :: broadcast()
def hub_connection_failed(reason) when is_binary(reason) do
broadcast(@connection_topic, {:connection_error, reason})
end
@doc """
Broadcasts when hub had an error when disconnecting under `hubs:connection` topic
"""
@spec hub_disconnection_failed(String.t()) :: broadcast()
def hub_disconnection_failed(reason) when is_binary(reason) do
broadcast(@connection_topic, {:disconnection_error, reason})
end
@doc """
Broadcasts when hub received a new secret under `hubs:secrets` topic
"""
@spec secret_created(Secret.t()) :: broadcast()
def secret_created(%Secret{} = secret) do
broadcast(@secrets_topic, {:secret_created, secret})
end
@doc """
Broadcasts when hub received an updated secret under `hubs:secrets` topic
"""
@spec secret_updated(Secret.t()) :: broadcast()
def secret_updated(%Secret{} = secret) do
broadcast(@secrets_topic, {:secret_updated, secret})
end
defp broadcast(topic, message) do
Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message)
end
end

View file

@ -12,7 +12,7 @@ defmodule Livebook.Hubs.Enterprise do
token: String.t() | nil,
external_id: String.t() | nil,
hub_name: String.t() | nil,
hub_color: String.t() | nil
hub_emoji: String.t() | nil
}
embedded_schema do
@ -20,7 +20,7 @@ defmodule Livebook.Hubs.Enterprise do
field :token, :string
field :external_id, :string
field :hub_name, :string
field :hub_color, Livebook.EctoTypes.HexColor
field :hub_emoji, :string
end
@fields ~w(
@ -28,7 +28,7 @@ defmodule Livebook.Hubs.Enterprise do
token
external_id
hub_name
hub_color
hub_emoji
)a
@doc """
@ -113,7 +113,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
token: fields.token,
external_id: fields.external_id,
hub_name: fields.hub_name,
hub_color: fields.hub_color
hub_emoji: fields.hub_emoji
}
end
@ -122,7 +122,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
id: enterprise.id,
name: enterprise.hub_name,
provider: enterprise,
color: enterprise.hub_color
emoji: enterprise.hub_emoji
}
end
@ -130,4 +130,8 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
def connect(%Livebook.Hubs.Enterprise{} = enterprise),
do: {Livebook.Hubs.EnterpriseClient, enterprise}
def connected?(%Livebook.Hubs.Enterprise{id: id}) do
Livebook.Hubs.EnterpriseClient.connected?(id)
end
end

View file

@ -2,21 +2,31 @@ defmodule Livebook.Hubs.EnterpriseClient do
@moduledoc false
use GenServer
alias Livebook.Hubs.Broadcasts
alias Livebook.Hubs.Enterprise
alias Livebook.Secrets.Secret
alias Livebook.WebSocket.Server
@pubsub_topic "enterprise"
@registry Livebook.HubsRegistry
defstruct [:server, :hub, secrets: []]
defstruct [:server, :hub, connected?: false, secrets: []]
@doc """
Connects the Enterprise client with WebSocket server.
"""
@spec start_link(Enterprise.t()) :: GenServer.on_start()
def start_link(%Enterprise{} = enterprise) do
GenServer.start_link(__MODULE__, enterprise, name: registry_name(enterprise))
GenServer.start_link(__MODULE__, enterprise, name: registry_name(enterprise.id))
end
@doc """
Stops the WebSocket server.
"""
@spec stop(pid()) :: :ok
def stop(pid) do
pid |> GenServer.call(:get_server) |> GenServer.stop()
:ok
end
@doc """
@ -36,27 +46,15 @@ defmodule Livebook.Hubs.EnterpriseClient do
end
@doc """
Subscribe to WebSocket Server events.
## Messages
* `{:connect, :ok, :connected}`
* `{:connect, :error, reason}`
* `{:secret_created, %Secret{}}`
* `{:secret_updated, %Secret{}}`
Returns if the given enterprise is connected.
"""
@spec subscribe() :: :ok | {:error, {:already_registered, pid()}}
def subscribe do
Phoenix.PubSub.subscribe(Livebook.PubSub, @pubsub_topic)
end
@doc """
Unsubscribes from `subscribe/0`.
"""
@spec unsubscribe() :: :ok
def unsubscribe do
Phoenix.PubSub.unsubscribe(Livebook.PubSub, @pubsub_topic)
@spec connected?(String.t()) :: boolean()
def connected?(id) do
try do
GenServer.call(registry_name(id), :connected?)
catch
:exit, _ -> false
end
end
## GenServer callbacks
@ -78,44 +76,48 @@ defmodule Livebook.Hubs.EnterpriseClient do
{:reply, state.secrets, state}
end
@impl true
def handle_info({:connect, _, _} = message, state) do
broadcast_message(message)
{:noreply, state}
def handle_call(:connected?, _caller, state) do
{:reply, state.connected?, state}
end
def handle_info({:disconnect, :error, _} = message, state) do
broadcast_message(message)
{:noreply, state}
@impl true
def handle_info({:connect, :ok, _}, state) do
Broadcasts.hub_connected()
{:noreply, %{state | connected?: true}}
end
def handle_info({:connect, :error, reason}, state) do
Broadcasts.hub_connection_failed(reason)
{:noreply, %{state | connected?: false}}
end
def handle_info({:disconnect, :error, reason}, state) do
Broadcasts.hub_disconnection_failed(reason)
{:noreply, %{state | connected?: false}}
end
def handle_info({:event, :secret_created, %{name: name, value: value}}, state) do
secret = %Secret{name: name, value: value}
broadcast_message({:secret_created, secret})
Broadcasts.secret_created(secret)
{:noreply, put_secret(state, secret)}
end
def handle_info({:event, :secret_updated, %{name: name, value: value}}, state) do
secret = %Secret{name: name, value: value}
broadcast_message({:secret_updated, secret})
Broadcasts.secret_updated(secret)
{:noreply, put_secret(state, secret)}
end
def handle_info({:disconnect, :ok, :disconnected}, state) do
Broadcasts.hub_disconnected()
{:stop, :normal, state}
end
# Private
# Notifies interested processes about WebSocket Server messages.
# Broadcasts the given message under the `"enterprise"` topic.
defp broadcast_message(message) do
Phoenix.PubSub.broadcast(Livebook.PubSub, @pubsub_topic, message)
end
defp registry_name(%Enterprise{id: id}) do
defp registry_name(id) do
{:via, Registry, {@registry, id}}
end

View file

@ -10,7 +10,7 @@ defmodule Livebook.Hubs.Fly do
id: String.t() | nil,
access_token: String.t() | nil,
hub_name: String.t() | nil,
hub_color: String.t() | nil,
hub_emoji: String.t() | nil,
organization_id: String.t() | nil,
organization_type: String.t() | nil,
organization_name: String.t() | nil,
@ -20,7 +20,7 @@ defmodule Livebook.Hubs.Fly do
embedded_schema do
field :access_token, :string
field :hub_name, :string
field :hub_color, Livebook.EctoTypes.HexColor
field :hub_emoji, :string
field :organization_id, :string
field :organization_type, :string
field :organization_name, :string
@ -30,7 +30,7 @@ defmodule Livebook.Hubs.Fly do
@fields ~w(
access_token
hub_name
hub_color
hub_emoji
organization_id
organization_name
organization_type
@ -116,7 +116,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do
| id: fields.id,
access_token: fields.access_token,
hub_name: fields.hub_name,
hub_color: fields.hub_color,
hub_emoji: fields.hub_emoji,
organization_id: fields.organization_id,
organization_type: fields.organization_type,
organization_name: fields.organization_name,
@ -129,11 +129,13 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do
id: fly.id,
name: fly.hub_name,
provider: fly,
color: fly.hub_color
emoji: fly.hub_emoji
}
end
def type(_fly), do: "fly"
def connect(_fly), do: nil
def connected?(_fly), do: false
end

View file

@ -1,12 +1,12 @@
defmodule Livebook.Hubs.Local do
@moduledoc false
defstruct [:id, :hub_name, :hub_color]
defstruct [:id, :hub_name, :hub_emoji]
end
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Local do
def load(%Livebook.Hubs.Local{} = local, fields) do
%{local | id: fields.id, hub_name: fields.hub_name, hub_color: fields.hub_color}
%{local | id: fields.id, hub_name: fields.hub_name, hub_emoji: fields.hub_emoji}
end
def normalize(%Livebook.Hubs.Local{} = local) do
@ -14,11 +14,13 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Local do
id: local.id,
name: local.hub_name,
provider: local,
color: local.hub_color
emoji: local.hub_emoji
}
end
def type(_local), do: "local"
def connect(_local), do: nil
def connected?(_local), do: false
end

View file

@ -1,12 +1,13 @@
defmodule Livebook.Hubs.Metadata do
@moduledoc false
defstruct [:id, :name, :provider, :color]
defstruct [:id, :name, :provider, :emoji, connected?: false]
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
provider: struct(),
color: String.t()
emoji: String.t(),
connected?: boolean()
}
end

View file

@ -24,4 +24,10 @@ defprotocol Livebook.Hubs.Provider do
"""
@spec connect(struct()) :: Supervisor.child_spec() | module() | {module(), any()} | nil
def connect(struct)
@doc """
Gets the connection status of the given struct.
"""
@spec connected?(struct()) :: boolean()
def connected?(struct)
end

View file

@ -2,7 +2,6 @@ defmodule LivebookWeb.FormHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.Component
import Phoenix.HTML.Form
@ -55,6 +54,38 @@ defmodule LivebookWeb.FormHelpers do
"""
end
@doc """
Emoji input.
"""
def emoji_input(assigns) do
~H"""
<div id={@id} class="flex border-[1px] bg-gray-50 rounded-lg space-x-4 items-center">
<div id={"#{@id}-picker"} class="grid grid-cols-1 md:grid-cols-3 w-full" phx-hook="EmojiPicker">
<div class="place-content-start">
<div class="p-1 pl-3">
<span id={"#{@id}-preview"} data-emoji-preview><%= input_value(@form, @field) %></span>
</div>
</div>
<div />
<div class="flex items-center place-content-end">
<button
id={"#{@id}-button"}
type="button"
data-emoji-button
class="p-1 pl-3 pr-3 rounded-tr-lg rounded-br-lg bg-gray-50 hover:bg-gray-100 active:bg-gray-200 border-l-[1px] bg-white flex justify-center items-center cursor-pointer"
>
<.remix_icon icon="emotion-line" class="text-xl" />
</button>
</div>
<div id={"#{@id}-container"} data-emoji-container class="absolute mt-10 hidden" />
<%= hidden_input(@form, @field, class: "hidden emoji-picker-input", "data-emoji-input": true) %>
</div>
</div>
"""
end
@doc """
Translates an error message.
"""

View file

@ -6,7 +6,7 @@ defmodule LivebookWeb.SidebarHook do
def on_mount(:default, _params, _session, socket) do
if connected?(socket) do
Livebook.Hubs.subscribe()
Livebook.Hubs.subscribe([:crud, :connection])
end
socket =
@ -18,8 +18,16 @@ defmodule LivebookWeb.SidebarHook do
{:cont, socket}
end
defp handle_info({:hubs_metadata_changed, hubs}, socket) do
{:halt, assign(socket, saved_hubs: hubs)}
@connection_events ~w(hub_connected hub_disconnected hubs_metadata_changed)a
defp handle_info(event, socket) when event in @connection_events do
{:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
end
@error_events ~w(connection_error disconnection_error)a
defp handle_info({event, _reason}, socket) when event in @error_events do
{:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
end
defp handle_info(_event, socket), do: {:cont, socket}

View file

@ -21,7 +21,7 @@ defmodule LivebookWeb.UserHook do
{:user_change, %{id: id} = user},
%{assigns: %{current_user: %{id: id}}} = socket
) do
{:cont, assign(socket, :current_user, user)}
{:halt, assign(socket, :current_user, user)}
end
defp info(_message, socket), do: {:cont, socket}

View file

@ -1,7 +1,6 @@
defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
use LivebookWeb, :live_component
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.Enterprise
@impl true
@ -35,13 +34,9 @@ defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
phx-debounce="blur"
>
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
<.input_wrapper form={f} field={:hub_emoji} class="flex flex-col space-y-1">
<div class="input-label">Emoji</div>
<.emoji_input id="enterprise-emoji-input" form={f} field={:hub_emoji} />
</.input_wrapper>
</div>
@ -58,10 +53,6 @@ defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
end
@impl true
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"enterprise" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"enterprise" => params}, socket) do
case Enterprise.update_hub(socket.assigns.hub, params) do
{:ok, hub} ->

View file

@ -1,7 +1,6 @@
defmodule LivebookWeb.Hub.Edit.FlyComponent do
use LivebookWeb, :live_component
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.{Fly, FlyClient}
@impl true
@ -76,13 +75,9 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
<%= text_input(f, :hub_name, class: "input") %>
</.input_wrapper>
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
<.input_wrapper form={f} field={:hub_emoji} class="flex flex-col space-y-1">
<div class="input-label">Emoji</div>
<.emoji_input id="fly-emoji-input" form={f} field={:hub_emoji} />
</.input_wrapper>
</div>
@ -134,10 +129,6 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
end
@impl true
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"fly" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"fly" => params}, socket) do
case Fly.update_hub(socket.assigns.hub, params) do
{:ok, hub} ->

View file

@ -3,13 +3,12 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do
import Ecto.Changeset, only: [get_field: 2]
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.{Enterprise, EnterpriseClient}
@impl true
def update(assigns, socket) do
if connected?(socket) do
EnterpriseClient.subscribe()
Livebook.Hubs.subscribe(:connection)
end
{:ok,
@ -82,13 +81,9 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do
<%= text_input(f, :hub_name, class: "input", readonly: true) %>
</.input_wrapper>
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
<.input_wrapper form={f} field={:hub_emoji} class="flex flex-col space-y-1">
<div class="input-label">Emoji</div>
<.emoji_input id="enterprise-emoji-input" form={f} field={:hub_emoji} />
</.input_wrapper>
</div>
@ -109,20 +104,26 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do
token = get_field(socket.assigns.changeset, :token)
base = %Enterprise{
id: "enterprise-placeholder",
token: token,
external_id: "placeholder",
url: url,
hub_name: "Enterprise",
hub_color: HexColor.random()
hub_emoji: "🏭"
}
{:ok, pid} = EnterpriseClient.start_link(base)
receive do
{:connect, :error, reason} ->
GenServer.stop(pid)
handle_error(reason, socket)
{:connection_error, reason} ->
EnterpriseClient.stop(pid)
{:connect, :ok, :connected} ->
{:noreply,
socket
|> put_flash(:error, "Failed to connect with Enterprise: " <> reason)
|> push_patch(to: Routes.hub_path(socket, :new))}
:hub_connected ->
session_request =
LivebookProto.SessionRequest.new!(app_version: Livebook.Config.app_version())
@ -134,16 +135,16 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do
{:noreply, assign(socket, pid: pid, changeset: changeset, base: base)}
{:error, reason} ->
GenServer.stop(pid)
handle_error(reason, socket)
EnterpriseClient.stop(pid)
{:noreply,
socket
|> put_flash(:error, "Failed to connect with Enterprise: " <> reason)
|> push_patch(to: Routes.hub_path(socket, :new))}
end
end
end
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"enterprise" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"enterprise" => params}, socket) do
if socket.assigns.changeset.valid? do
case Enterprise.create_hub(socket.assigns.base, params) do
@ -168,23 +169,4 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do
def handle_event("validate", %{"enterprise" => attrs}, socket) do
{:noreply, assign(socket, changeset: Enterprise.change_hub(socket.assigns.base, attrs))}
end
def handle_error(%{reason: :econnrefused}, socket) do
show_connect_error("Failed to connect with given URL", socket)
end
def handle_error(%{reason: _}, socket) do
show_connect_error("Failed to connect with Enterprise", socket)
end
def handle_error(reason, socket) do
show_connect_error(reason, socket)
end
defp show_connect_error(message, socket) do
{:noreply,
socket
|> put_flash(:error, message)
|> push_patch(to: Routes.hub_path(socket, :new))}
end
end

View file

@ -3,7 +3,6 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
import Ecto.Changeset, only: [get_field: 2, add_error: 3]
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.{Fly, FlyClient}
@impl true
@ -60,13 +59,9 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
<%= text_input(f, :hub_name, class: "input") %>
</.input_wrapper>
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
<.input_wrapper form={f} field={:hub_emoji} class="flex flex-col space-y-1">
<div class="input-label">Emoji</div>
<.emoji_input id="fly-emoji-input" form={f} field={:hub_emoji} />
</.input_wrapper>
</div>
@ -86,7 +81,7 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
case FlyClient.fetch_apps(token) do
{:ok, apps} ->
opts = select_options(apps)
base = %Fly{access_token: token, hub_color: HexColor.random()}
base = %Fly{access_token: token, hub_emoji: "🚀"}
changeset = Fly.change_hub(base)
{:noreply,
@ -103,10 +98,6 @@ defmodule LivebookWeb.Hub.New.FlyComponent do
end
end
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"fly" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"fly" => params}, socket) do
if socket.assigns.changeset.valid? do
case Fly.create_hub(socket.assigns.selected_app, params) do

View file

@ -5,6 +5,7 @@ defmodule LivebookWeb.LayoutHelpers do
import LivebookWeb.UserHelpers
alias Phoenix.LiveView.JS
alias Livebook.Hubs.Provider
alias LivebookWeb.Router.Helpers, as: Routes
@doc """
@ -165,15 +166,9 @@ defmodule LivebookWeb.LayoutHelpers do
end
defp sidebar_link(assigns) do
assigns = assign_new(assigns, :icon_style, fn -> nil end)
~H"""
<%= live_redirect to: @to, class: "h-7 flex items-center hover:text-white #{sidebar_link_text_color(@to, @current)} border-l-4 #{sidebar_link_border_color(@to, @current)} hover:border-white" do %>
<.remix_icon
icon={@icon}
class="text-lg leading-6 w-[56px] flex justify-center"
style={@icon_style}
/>
<.remix_icon icon={@icon} class="text-lg leading-6 w-[56px] flex justify-center" />
<span class="text-sm font-medium">
<%= @title %>
</span>
@ -181,6 +176,28 @@ defmodule LivebookWeb.LayoutHelpers do
"""
end
defp sidebar_hub_link(assigns) do
~H"""
<%= live_redirect to: @to, class: "h-7 flex items-center hover:text-white #{sidebar_link_text_color(@to, @current)} border-l-4 #{sidebar_link_border_color(@to, @current)} hover:border-white" do %>
<div class="text-lg leading-6 w-[56px] flex justify-center">
<span class="relative">
<%= @hub.emoji %>
<%= if Provider.connect(@hub.provider) do %>
<div class={[
"absolute w-[10px] h-[10px] border-gray-900 border-2 rounded-full right-0 bottom-0",
if(@hub.connected?, do: "bg-green-400", else: "bg-red-400")
]} />
<% end %>
</span>
</div>
<span class="text-sm font-medium">
<%= @hub.name %>
</span>
<% end %>
"""
end
defp hub_section(assigns) do
~H"""
<%= if Livebook.Config.feature_flag_enabled?(:hub) do %>
@ -191,10 +208,8 @@ defmodule LivebookWeb.LayoutHelpers do
</div>
<%= for hub <- @hubs do %>
<.sidebar_link
title={hub.name}
icon="checkbox-blank-circle-fill"
icon_style={"color: #{hub.color}"}
<.sidebar_hub_link
hub={hub}
to={Routes.hub_path(@socket, :edit, hub.id)}
current={@current_page}
/>

View file

@ -26,8 +26,7 @@ defmodule LivebookWeb.SessionLive do
Session.subscribe(session_id)
Secrets.subscribe()
# TODO: Move this to Hubs.subscribe([:secrets]) and rename all "enterprise" to "hubs"
EnterpriseClient.subscribe()
Hubs.subscribe(:secrets)
{data, client_id}
else
@ -64,7 +63,7 @@ defmodule LivebookWeb.SessionLive do
autofocus_cell_id: autofocus_cell_id(data.notebook),
page_title: get_page_title(data.notebook.name),
livebook_secrets: Secrets.fetch_secrets() |> Map.new(&{&1.name, &1.value}),
enterprise_secrets: fetch_enterprise_secrets(),
hub_secrets: get_hub_secrets(),
select_secret_ref: nil,
select_secret_options: nil
)
@ -186,7 +185,7 @@ defmodule LivebookWeb.SessionLive do
<.secrets_list
data_view={@data_view}
livebook_secrets={@livebook_secrets}
enterprise_secrets={@enterprise_secrets}
hub_secrets={@hub_secrets}
session={@session}
socket={@socket}
/>
@ -739,13 +738,13 @@ defmodule LivebookWeb.SessionLive do
<%= if Livebook.Config.feature_flag_enabled?(:hub) do %>
<div class="mt-16">
<h3 class="uppercase text-sm font-semibold text-gray-500">
Enterprise secrets
Hub secrets
</h3>
<span class="text-sm text-gray-500">Available in all sessions</span>
</div>
<div class="flex flex-col space-y-4 mt-6">
<%= for {secret_name, secret_value} <- Enum.sort(@enterprise_secrets) do %>
<%= for {secret_name, secret_value} <- Enum.sort(@hub_secrets) do %>
<div
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
id={"enterprise-secret-#{secret_name}-wrapper"}
@ -1450,14 +1449,14 @@ defmodule LivebookWeb.SessionLive do
def handle_info({:secret_created, %Secrets.Secret{}}, socket) do
{:noreply,
socket
|> assign(enterprise_secrets: fetch_enterprise_secrets())
|> assign(hub_secrets: get_hub_secrets())
|> put_flash(:info, "A new secret has been created on your Livebook Enterprise")}
end
def handle_info({:secret_updated, %Secrets.Secret{}}, socket) do
{:noreply,
socket
|> assign(enterprise_secrets: fetch_enterprise_secrets())
|> assign(hub_secrets: get_hub_secrets())
|> put_flash(:info, "An existing secret has been updated on your Livebook Enterprise")}
end
@ -2298,7 +2297,7 @@ defmodule LivebookWeb.SessionLive do
secret in secrets
end
defp fetch_enterprise_secrets do
defp get_hub_secrets do
for connected_hub <- Hubs.get_connected_hubs(),
secret <- EnterpriseClient.list_cached_secrets(connected_hub.pid),
into: %{},

View file

@ -6,7 +6,7 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
alias Livebook.Secrets.Secret
setup do
EnterpriseClient.subscribe()
Livebook.Hubs.subscribe([:connection, :secrets])
:ok
end
@ -15,21 +15,21 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
enterprise = build(:enterprise, url: url, token: token)
EnterpriseClient.start_link(enterprise)
assert_receive {:connect, :ok, :connected}
assert_receive :hub_connected
end
test "rejects the websocket with invalid address", %{token: token} do
enterprise = build(:enterprise, url: "http://localhost:9999", token: token)
EnterpriseClient.start_link(enterprise)
assert_receive {:connect, :error, "connection refused"}
assert_receive {:connection_error, "connection refused"}
end
test "rejects the web socket connection with invalid credentials", %{url: url} do
enterprise = build(:enterprise, url: url, token: "foo")
EnterpriseClient.start_link(enterprise)
assert_receive {:connect, :error, reason}
assert_receive {:connection_error, reason}
assert reason =~ "the given token is invalid"
end
end
@ -38,8 +38,7 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
setup %{url: url, token: token} do
enterprise = build(:enterprise, url: url, token: token)
EnterpriseClient.start_link(enterprise)
assert_receive {:connect, :ok, :connected}
assert_receive :hub_connected
:ok
end

View file

@ -10,7 +10,7 @@ defmodule Livebook.Hubs.ProviderTest do
assert Provider.normalize(fly) == %Metadata{
id: fly.id,
name: fly.hub_name,
color: fly.hub_color,
emoji: fly.hub_emoji,
provider: fly
}
end

View file

@ -23,7 +23,7 @@ defmodule Livebook.HubsTest do
assert Hubs.get_metadatas() == [
%Hubs.Metadata{
id: "fly-livebook",
color: fly.hub_color,
emoji: fly.hub_emoji,
name: fly.hub_name,
provider: fly
}
@ -60,9 +60,9 @@ defmodule Livebook.HubsTest do
test "save_hub/1 updates hub" do
fly = insert_hub(:fly, id: "fly-foo2")
Hubs.save_hub(%{fly | hub_color: "#FFFFFF"})
Hubs.save_hub(%{fly | hub_emoji: "🐈"})
refute Hubs.fetch_hub!("fly-foo2") == fly
assert Hubs.fetch_hub!("fly-foo2").hub_color == "#FFFFFF"
assert Hubs.fetch_hub!("fly-foo2").hub_emoji == "🐈"
end
end

View file

@ -40,7 +40,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
attrs = %{
"hub_name" => "Personal Hub",
"hub_color" => "#FF00FF"
"hub_emoji" => "🐈"
}
view
@ -59,7 +59,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
assert render(view) =~ "Hub updated successfully"
assert_hub(view, conn, %{hub | hub_color: attrs["hub_color"], hub_name: attrs["hub_name"]})
assert_hub(view, conn, %{hub | hub_emoji: attrs["hub_emoji"], hub_name: attrs["hub_name"]})
refute Hubs.fetch_hub!(hub.id) == hub
end
@ -195,7 +195,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
hub = insert_hub(:enterprise)
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
attrs = %{"hub_color" => "#FF00FF"}
attrs = %{"hub_emoji" => "🐈"}
view
|> element("#enterprise-form")
@ -213,7 +213,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
assert render(view) =~ "Hub updated successfully"
assert_hub(view, conn, %{hub | hub_color: attrs["hub_color"]})
assert_hub(view, conn, %{hub | hub_emoji: attrs["hub_emoji"]})
refute Hubs.fetch_hub!(hub.id) == hub
end
end
@ -221,7 +221,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
defp assert_hub(view, conn, hub) do
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #{hub.hub_color}"/
assert hubs_html =~ hub.hub_emoji
assert hubs_html =~ Routes.hub_path(conn, :edit, hub.id)
assert hubs_html =~ hub.hub_name
end

View file

@ -37,7 +37,7 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
"url" => url,
"token" => token,
"hub_name" => "Enterprise",
"hub_color" => "#FF00FF"
"hub_emoji" => "🐈"
}
view
@ -58,7 +58,7 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
assert render(view) =~ "Hub added successfully"
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #FF00FF"/
assert hubs_html =~ "🐈"
assert hubs_html =~ "/hub/enterprise-#{id}"
assert hubs_html =~ "Enterprise"
end
@ -94,8 +94,13 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
stop_new_instance(name)
end
test "fails to create existing hub", %{conn: conn, url: url, token: token} do
node = EnterpriseServer.get_node()
test "fails to create existing hub", %{test: name, conn: conn} do
start_new_instance(name)
node = EnterpriseServer.get_node(name)
url = EnterpriseServer.url(name)
token = EnterpriseServer.token(name)
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, [])
user = :erpc.call(node, Enterprise.Integration, :create_user, [])
@ -135,7 +140,7 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
"url" => url,
"token" => token,
"hub_name" => "Enterprise",
"hub_color" => "#FFFFFF"
"hub_emoji" => "🐈"
}
view
@ -151,11 +156,13 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
|> render_submit(%{"enterprise" => attrs}) =~ "already exists"
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #{hub.hub_color}"/
assert hubs_html =~ hub.hub_emoji
assert hubs_html =~ Routes.hub_path(conn, :edit, hub.id)
assert hubs_html =~ hub.hub_name
assert Hubs.fetch_hub!(hub.id) == hub
after
stop_new_instance(name)
end
end

View file

@ -36,7 +36,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do
"access_token" => "dummy access token",
"application_id" => "123456789",
"hub_name" => "My Foo Hub",
"hub_color" => "#FF00FF"
"hub_emoji" => "🐈"
}
view
@ -55,17 +55,11 @@ defmodule LivebookWeb.Hub.NewLiveTest do
assert render(view) =~ "Hub added successfully"
assert view
|> element("#hubs")
|> render() =~ ~s/style="color: #FF00FF"/
hubs_html = view |> element("#hubs") |> render()
assert view
|> element("#hubs")
|> render() =~ "/hub/fly-123456789"
assert view
|> element("#hubs")
|> render() =~ "My Foo Hub"
assert hubs_html =~ "🐈"
assert hubs_html =~ "/hub/fly-123456789"
assert hubs_html =~ "My Foo Hub"
end
test "fails to create existing hub", %{conn: conn} do
@ -87,7 +81,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do
"access_token" => "dummy access token",
"application_id" => "foo",
"hub_name" => "My Foo Hub",
"hub_color" => "#FF00FF"
"hub_emoji" => "🐈"
}
view
@ -102,18 +96,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|> element("#fly-form")
|> render_submit(%{"fly" => attrs}) =~ "already exists"
assert view
|> element("#hubs")
|> render() =~ ~s/style="color: #{hub.hub_color}"/
assert view
|> element("#hubs")
|> render() =~ Routes.hub_path(conn, :edit, hub.id)
assert view
|> element("#hubs")
|> render() =~ hub.hub_name
assert_hub(view, conn, hub)
assert Hubs.fetch_hub!(hub.id) == hub
end
end
@ -177,4 +160,12 @@ defmodule LivebookWeb.Hub.NewLiveTest do
%{"data" => %{"app" => app}}
end
defp assert_hub(view, conn, hub) do
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ hub.hub_emoji
assert hubs_html =~ Routes.hub_path(conn, :edit, hub.id)
assert hubs_html =~ hub.hub_name
end
end

View file

@ -9,7 +9,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
describe "enterprise" do
setup %{url: url, token: token} do
id = Livebook.Utils.random_id()
node = EnterpriseServer.get_node()
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, [])
Livebook.Hubs.delete_hub("enterprise-#{id}")
enterprise =
@ -21,8 +22,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
)
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
Livebook.Hubs.EnterpriseClient.subscribe()
Livebook.Hubs.connect_hubs()
Livebook.Hubs.subscribe(:secrets)
on_exit(fn ->
Session.close(session.pid)

View file

@ -17,7 +17,7 @@ defmodule Livebook.Factory do
%Livebook.Hubs.Fly{
id: "fly-foo-bar-baz",
hub_name: "My Personal Hub",
hub_color: "#FF00FF",
hub_emoji: "🚀",
access_token: Livebook.Utils.random_cookie(),
organization_id: Livebook.Utils.random_id(),
organization_type: "PERSONAL",
@ -36,7 +36,7 @@ defmodule Livebook.Factory do
%Livebook.Hubs.Enterprise{
id: "enterprise-#{id}",
hub_name: "Enterprise",
hub_color: "#FF0000",
hub_emoji: "🏭",
external_id: id,
token: Livebook.Utils.random_cookie(),
url: "http://localhost"