livebook/lib/livebook_web/live/hub/edit/team_component.ex

408 lines
14 KiB
Elixir

defmodule LivebookWeb.Hub.Edit.TeamComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Hubs.Provider
alias Livebook.Teams
alias LivebookWeb.LayoutComponents
alias LivebookWeb.NotFoundError
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
changeset = Teams.change_hub(assigns.hub)
show_key = assigns.params["show-key"]
secrets = Hubs.get_secrets(assigns.hub)
file_systems = Hubs.get_file_systems(assigns.hub, hub_only: true)
deployment_groups = Teams.get_deployment_groups(assigns.hub)
app_deployments = Teams.get_app_deployments(assigns.hub)
agents = Teams.get_agents(assigns.hub)
secret_name = assigns.params["secret_name"]
file_system_id = assigns.params["file_system_id"]
default? = default_hub?(assigns.hub)
secret_value =
if assigns.live_action == :edit_secret do
Enum.find_value(secrets, &(&1.name == secret_name and &1.value)) ||
raise(NotFoundError, "could not find secret matching #{inspect(secret_name)}")
end
file_system =
if assigns.live_action == :edit_file_system do
Enum.find_value(file_systems, &(&1.id == file_system_id && &1)) ||
raise(NotFoundError, "could not find file system matching #{inspect(file_system_id)}")
end
{:ok,
socket
|> assign(
secrets: secrets,
file_system: file_system,
file_system_id: file_system_id,
file_systems: file_systems,
deployment_groups: Enum.sort_by(deployment_groups, & &1.name),
app_deployments: Enum.frequencies_by(app_deployments, & &1.deployment_group_id),
agents: Enum.frequencies_by(agents, & &1.deployment_group_id),
show_key: show_key,
secret_name: secret_name,
secret_value: secret_value,
hub_metadata: Provider.to_metadata(assigns.hub),
default?: default?
)
|> assign_form(changeset)}
end
@impl true
def render(assigns) do
~H"""
<div>
<LayoutComponents.topbar :if={Provider.connection_status(@hub)} variant={:warning}>
<%= Provider.connection_status(@hub) %>
</LayoutComponents.topbar>
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
<div id={"#{@id}-component"}>
<div class="mb-8 flex flex-col space-y-2">
<LayoutComponents.title>
<div class="flex gap-2 items-center">
<div class="flex justify-center">
<span class="relative">
<%= @hub.hub_emoji %>
<div class={[
"absolute w-[10px] h-[10px] border-white border-2 rounded-full right-0 bottom-1",
if(@hub_metadata.connected?, do: "bg-green-400", else: "bg-red-400")
]} />
</span>
</div>
<%= @hub.hub_name %>
<span class="bg-green-100 text-green-800 text-xs px-2.5 py-0.5 rounded cursor-default">
Livebook Teams
</span>
<%= if @default? do %>
<span class="bg-blue-100 text-blue-800 text-xs px-2.5 py-0.5 rounded cursor-default">
Default
</span>
<% end %>
</div>
</LayoutComponents.title>
<p class="text-sm flex flex-row space-x-6 text-gray-700">
<a href={org_url(@hub, "/")} class="hover:text-blue-600">
<.remix_icon icon="mail-line" /> Invite users
</a>
<a href={org_url(@hub, "/")} class="hover:text-blue-600">
<.remix_icon icon="settings-line" /> Manage organization
</a>
<.link
patch={~p"/hub/#{@hub.id}?show-key=yes"}
class="hover:text-blue-600 cursor-pointer"
>
<.remix_icon icon="key-2-fill" /> Display Teams key
</.link>
<%= if @default? do %>
<button
phx-click="remove_as_default"
phx-target={@myself}
class="hover:text-blue-600 cursor-pointer"
>
<.remix_icon icon="star-fill" /> Remove as default
</button>
<% else %>
<button
phx-click="mark_as_default"
phx-target={@myself}
class="hover:text-blue-600 cursor-pointer"
>
<.remix_icon icon="star-line" /> Mark as default
</button>
<% end %>
</p>
</div>
<div class="mb-8 flex flex-col space-y-10">
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
General
</h2>
<.form
id={@id}
class="flex flex-col md:flex-row mt-4 space-y-4 md:space-x-2 md:space-y-0"
for={@form}
phx-submit="save"
phx-change="validate"
phx-target={@myself}
>
<div class="flex-auto">
<.text_field
field={@form[:hub_name]}
label="Name"
disabled
help="Name cannot be changed"
class="bg-gray-200/50 border-200/80 cursor-not-allowed"
/>
</div>
<div class="min-w-48">
<.emoji_field field={@form[:hub_emoji]} label="Emoji" />
</div>
<div class="!mt-6">
<.button type="submit" phx-disable-with="Updating...">
Save
</.button>
</div>
</.form>
</div>
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
Secrets
</h2>
<p class="text-gray-700">
Secrets are a safe way to allow notebooks to access
credentials and tokens.
</p>
<.live_component
module={LivebookWeb.Hub.SecretListComponent}
id="hub-secrets-list"
hub={@hub}
secrets={@secrets}
edit_path={"hub/#{@hub.id}/secrets/edit"}
return_to={~p"/hub/#{@hub.id}"}
/>
<div>
<.button patch={~p"/hub/#{@hub.id}/secrets/new"} id="add-secret">
Add secret
</.button>
</div>
</div>
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
File storages
</h2>
<p class="text-gray-700">
File storages are used to store notebooks and their files across your whole team.
</p>
<.live_component
module={LivebookWeb.Hub.FileSystemListComponent}
id="hub-file-systems-list"
hub_id={@hub.id}
file_systems={@file_systems}
target={@myself}
/>
</div>
<div
:if={Livebook.Config.feature_flag_enabled?(:deployment_groups)}
class="flex flex-col space-y-4"
>
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
Deployment groups
</h2>
<p class="text-gray-700">
Deployment groups allow you to deploy Livebook apps to self-hosted machines with the click of a button.
</p>
<.no_entries :if={@deployment_groups == []}>
No deployment groups here... yet!
</.no_entries>
<div :for={deployment_group <- @deployment_groups} class="flex flex-col space-y-4">
<.live_component
module={LivebookWeb.Hub.Teams.DeploymentGroupComponent}
id={"hub-deployment-group-#{deployment_group.id}"}
hub={@hub}
deployment_group={deployment_group}
app_deployments_count={Map.get(@app_deployments, deployment_group.id, 0)}
agents_count={Map.get(@agents, deployment_group.id, 0)}
live_action={@live_action}
params={@params}
/>
</div>
<div>
<.button patch={~p"/hub/#{@hub.id}/groups/new"} id="add-deployment-group">
Add deployment group
</.button>
</div>
</div>
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
Danger zone
</h2>
<div class="flex items-center justify-between gap-4 text-gray-700">
<div class="flex flex-col">
<h3 class="font-semibold">
Delete this workspace
</h3>
<p class="text-sm">
This only removes the workpsace from this machine. You must rejoin to access its features once again.
</p>
</div>
<.button
color="red"
outlined
id="delete-hub"
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
>
<span class="hidden sm:block">Delete workspace</span>
<.remix_icon icon="delete-bin-line" class="text-lg sm:hidden" />
</.button>
</div>
</div>
</div>
</div>
</div>
<.modal :if={@show_key} id="key-modal" show width={:medium} patch={~p"/hub/#{@hub.id}"}>
<.teams_key_modal
teams_key={@hub.teams_key}
confirm_url={if @show_key == "confirm", do: ~p"/hub/#{@hub.id}"}
/>
</.modal>
<.modal
:if={@live_action in [:new_secret, :edit_secret]}
id="secrets-modal"
show
width={:medium}
patch={~p"/hub/#{@hub.id}"}
>
<.live_component
module={LivebookWeb.Hub.SecretFormComponent}
id="secrets"
hub={@hub}
secret_name={@secret_name}
secret_value={@secret_value}
return_to={~p"/hub/#{@hub.id}"}
/>
</.modal>
<.modal
:if={@live_action in [:new_file_system, :edit_file_system]}
id="file-systems-modal"
show
width={:medium}
patch={~p"/hub/#{@hub.id}"}
>
<.live_component
module={LivebookWeb.Hub.FileSystemFormComponent}
id="file-systems"
hub={@hub}
file_system={@file_system}
file_system_id={@file_system_id}
return_to={~p"/hub/#{@hub.id}"}
/>
</.modal>
<.modal
:if={@live_action == :new_deployment_group}
id="deployment-group-modal"
show
width={:big}
patch={~p"/hub/#{@hub.id}"}
>
<.live_component
module={LivebookWeb.Hub.Teams.DeploymentGroupFormComponent}
id="deployment-group"
hub={@hub}
return_to={~p"/hub/#{@hub.id}"}
/>
</.modal>
</div>
"""
end
defp org_url(hub, path) do
Livebook.Config.teams_url() <> "/orgs/#{hub.org_id}" <> path
end
defp teams_key_modal(assigns) do
~H"""
<div class="flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
Teams key
</h3>
<div class="justify-center">
This is your <strong>Teams key</strong>. This key encrypts your
data before it is sent to Livebook Teams servers. This key is
required for you and invited users to join this organization.
We recommend storing it somewhere safe:
</div>
<div class="flex gap-2">
<div class="grow">
<.password_field id="teams-key" name="teams_key" value={@teams_key} readonly />
</div>
<span
data-tooltip="Copied to clipboard"
aria-label="copy to clipboard"
phx-click={
JS.dispatch("lb:clipcopy", to: "#teams-key")
|> JS.transition("tooltip left", time: 2000)
}
>
<.button color="gray" small type="button">
<.remix_icon icon="clipboard-line" class="text-xl leading-none py-1" />
</.button>
</span>
</div>
<.button :if={@confirm_url} patch={@confirm_url}>
<.remix_icon class="mr-1" icon="thumb-up-fill" /> I've saved my Teams key in a secure location
</.button>
</div>
"""
end
@impl true
def handle_event("save", %{"team" => params}, socket) do
case Teams.update_hub(socket.assigns.hub, params) do
{:ok, hub} ->
{:noreply,
socket
|> put_flash(:success, "Workspace updated successfully")
|> push_patch(to: ~p"/hub/#{hub.id}")}
{:error, changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
def handle_event("validate", %{"team" => attrs}, socket) do
changeset =
socket.assigns.hub
|> Teams.change_hub(attrs)
|> Map.replace!(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("mark_as_default", _, socket) do
Hubs.set_default_hub(socket.assigns.hub.id)
{:noreply, assign(socket, default?: true)}
end
def handle_event("remove_as_default", _, socket) do
Hubs.unset_default_hub()
{:noreply, assign(socket, default?: false)}
end
defp default_hub?(hub) do
Hubs.get_default_hub().id == hub.id
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, form: to_form(changeset))
end
end