fix(portal): UX improvements (#7013)

This PR accumulates lots of small UX fixes from #6645.

---------

Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
Andrew Dryga
2024-10-14 11:32:44 -06:00
committed by GitHub
parent ce11f26fca
commit 1abfa10fb7
44 changed files with 419 additions and 250 deletions

View File

@@ -661,7 +661,7 @@ defmodule API.Gateway.ChannelTest do
}, otel_ctx}
)
assert_push "request_connection", %{}
assert_push "request_connection", %{}, 200
{:updated, resource} =
Domain.Resources.update_or_replace_resource(
@@ -670,7 +670,7 @@ defmodule API.Gateway.ChannelTest do
subject
)
assert_push "resource_updated", payload
assert_push "resource_updated", payload, 200
assert payload == %{
address: resource.address,

View File

@@ -194,7 +194,7 @@ defmodule Domain.Actors.Actor.Query do
title: "Status",
type: :string,
values: [
{"Enabled", "enabled"},
{"Active", "active"},
{"Disabled", "disabled"}
],
fun: &filter_by_status/2
@@ -234,7 +234,7 @@ defmodule Domain.Actors.Actor.Query do
}
]
def filter_by_status(queryable, "enabled") do
def filter_by_status(queryable, "active") do
{queryable, dynamic([actors: actors], is_nil(actors.disabled_at))}
end

View File

@@ -100,4 +100,27 @@ defmodule Domain.Flows.Flow.Query do
{:flows, :desc, :inserted_at},
{:flows, :asc, :id}
]
@impl Domain.Repo.Query
def filters,
do: [
%Domain.Repo.Filter{
name: :expiration,
title: "Expired",
type: :string,
values: [
{"Expired", "expired"},
{"Not Expired", "not_expired"}
],
fun: &filter_by_expired/2
}
]
def filter_by_expired(queryable, "expired") do
{queryable, dynamic([flows: flows], flows.expires_at < fragment("NOW()"))}
end
def filter_by_expired(queryable, "not_expired") do
{queryable, dynamic([flows: flows], flows.expires_at >= fragment("NOW()"))}
end
end

View File

@@ -39,6 +39,9 @@ defmodule Domain.Repo.Changeset do
Keyword.has_key?(changeset.errors, field)
end
def empty?(%Ecto.Changeset{} = changeset), do: Enum.empty?(changeset.changes)
def empty?(%{}), do: true
def empty?(%Ecto.Changeset{} = changeset, field) do
case fetch_field(changeset, field) do
:error -> true
@@ -505,7 +508,7 @@ defmodule Domain.Repo.Changeset do
data = Map.get(changeset.data, field)
changes = get_change(changeset, field)
if required? and is_nil(changes) and empty?(data) do
if required? and is_nil(changes) and empty_value?(data) do
add_error(changeset, field, "can't be blank", validation: :required)
else
%Changeset{} = nested_changeset = on_cast.(data || %{}, changes || %{})
@@ -546,7 +549,7 @@ defmodule Domain.Repo.Changeset do
Keyword.has_key?(changeset.errors, field)
end
defp empty?(term), do: is_nil(term) or term == %{}
defp empty_value?(term), do: is_nil(term) or term == %{}
defp dump(changeset, field, original_type) do
map =

View File

@@ -472,6 +472,12 @@ defmodule Web.CoreComponents do
"""
end
def icon(%{name: "firezone"} = assigns) do
~H"""
<img src={~p"/images/logo.svg"} class={["inline-block", @class]} {@rest} />
"""
end
def icon(%{name: "spinner"} = assigns) do
~H"""
<svg
@@ -783,14 +789,19 @@ defmodule Web.CoreComponents do
"""
attr :datetime, DateTime, default: nil
attr :relative_to, DateTime, required: false
attr :negative_class, :string, default: ""
def relative_datetime(assigns) do
assigns = assign_new(assigns, :relative_to, fn -> DateTime.utc_now() end)
assigns =
assign_new(assigns, :relative_to, fn -> DateTime.utc_now() end)
~H"""
<.popover :if={not is_nil(@datetime)}>
<:target>
<span class="underline underline-offset-2 decoration-dashed">
<span class={[
"underline underline-offset-2 decoration-1 decoration-dotted",
DateTime.compare(@datetime, @relative_to) == :lt && @negative_class
]}>
<%= Cldr.DateTime.Relative.to_string!(@datetime, Web.CLDR, relative_to: @relative_to)
|> String.capitalize() %>
</span>
@@ -1021,8 +1032,8 @@ defmodule Web.CoreComponents do
text-xs
rounded-l
py-0.5 px-1.5
text-neutral-900
bg-neutral-50
text-neutral-800
bg-neutral-100
border-neutral-100
border
]}
@@ -1035,7 +1046,7 @@ defmodule Web.CoreComponents do
rounded-r
mr-2 py-0.5 pl-1.5 pr-2.5
text-neutral-900
bg-neutral-100
bg-neutral-50
]}>
<span class="block truncate" title={get_identity_email(@identity)}>
<%= get_identity_email(@identity) %>
@@ -1068,14 +1079,25 @@ defmodule Web.CoreComponents do
class={~w[
rounded-l
py-0.5 px-1.5
text-neutral-900
bg-neutral-50
text-neutral-800
bg-neutral-100
border-neutral-100
border
]}
>
<.provider_icon adapter={@group.provider.adapter} class="h-3.5 w-3.5" />
</.link>
<div :if={not Actors.group_synced?(@group)} title="Manually managed in Firezone" class={~w[
inline-flex
rounded-l
py-0.5 px-1.5
text-neutral-800
bg-neutral-100
border-neutral-100
border
]}>
<.icon name="firezone" class="h-3.5 w-3.5" />
</div>
<.link
title={"View Group \"#{@group.name}\""}
navigate={~p"/#{@account}/groups/#{@group}"}
@@ -1083,10 +1105,10 @@ defmodule Web.CoreComponents do
text-xs
truncate
min-w-0
#{if(Actors.group_synced?(@group), do: "rounded-r pl-1.5 pr-2.5", else: "rounded px-1.5")}
rounded-r pl-1.5 pr-2.5
py-0.5
text-neutral-800
bg-neutral-100
text-neutral-900
bg-neutral-50
]}
>
<%= @group.name %>

View File

@@ -387,9 +387,11 @@ defmodule Web.FormComponents do
attr :id, :string, required: true, doc: "The id of the dialog"
attr :class, :string, default: "", doc: "Custom classes to be added to the button"
attr :style, :string, default: "danger", doc: "The style of the button"
attr :confirm_style, :string, default: "danger", doc: "The style of the confirm button"
attr :icon, :string, default: nil, doc: "The icon of the button"
attr :size, :string, default: "md", doc: "The size of the button"
attr :on_confirm, :string, required: true, doc: "The phx event to broadcast on confirm"
attr :disabled, :boolean, default: false, doc: "Whether the button is disabled"
attr :on_confirm_id, :string,
default: nil,
@@ -418,7 +420,7 @@ defmodule Web.FormComponents do
<%= render_slot(@dialog_title) %>
</h3>
<button
class="text-neutral-400 bg-transparent hover:text-accent-900"
class="text-neutral-400 bg-transparent hover:text-accent-900 ml-2"
type="submit"
value="cancel"
>
@@ -444,7 +446,7 @@ defmodule Web.FormComponents do
phx-click={@on_confirm}
phx-value-id={@on_confirm_id}
type="submit"
style="danger"
style={@confirm_style}
value="confirm"
class="py-2.5 px-5 ms-3"
>
@@ -454,7 +456,15 @@ defmodule Web.FormComponents do
</div>
</form>
</dialog>
<.button id={@id} style={@style} size={@size} icon={@icon} class={@class} phx-hook="ConfirmDialog">
<.button
id={@id}
style={@style}
size={@size}
icon={@icon}
class={@class}
disabled={@disabled}
phx-hook="ConfirmDialog"
>
<%= render_slot(@inner_block) %>
</.button>
"""
@@ -635,6 +645,15 @@ defmodule Web.FormComponents do
]
end
def button_style("disabled") do
button_style() ++
[
"text-neutral-200",
"border border-neutral-200",
"cursor-not-allowed"
]
end
def button_style(_style) do
button_style() ++
[

View File

@@ -347,4 +347,30 @@ defmodule Web.NavigationComponents do
</.link>
"""
end
@doc """
Renders links to the docs based off documentation portal path.
## Examples
<.website_link href="/pricing>Pricing</.website_link>
<.website_link href="/kb/deploy/gateways" class="text-neutral-900">Deploy Gateway(s)</.website_link>
<.website_link href={~p"/contact/sales"}>Contact Sales</.website_link>
"""
attr :path, :string, required: true
attr :fragment, :string, required: false, default: ""
attr :rest, :global
def docs_action(assigns) do
~H"""
<.link
title="View documentation for this page"
href={"https://www.firezone.dev/kb#{@path}?utm_source=product##{@fragment}"}
target="_blank"
{@rest}
>
<.icon name="hero-question-mark-circle" class="mr-2 w-5 h-5" />
</.link>
"""
end
end

View File

@@ -18,7 +18,10 @@ defmodule Web.TableComponents do
~H"""
<thead id={"#{@table_id}-header"} class="text-xs text-neutral-700 uppercase bg-neutral-50">
<tr>
<th :for={col <- @columns} class={["px-4 py-3 font-medium", Map.get(col, :class, "")]}>
<th
:for={col <- @columns}
class={["px-4 py-3 font-medium whitespace-nowrap", Map.get(col, :class, "")]}
>
<%= col[:label] %>
<.table_header_order_buttons
:if={col[:field]}

View File

@@ -9,15 +9,31 @@ defmodule Web.Actors.Components do
def actor_role(:account_user), do: "user"
def actor_role(:account_admin_user), do: "admin"
attr :token, :any, required: true
attr :class, :string, default: ""
def token_type_icon(assigns) do
~H"""
<.icon name={token_type_icon_name(@token.type)} class={@class} />
"""
end
defp token_type_icon_name(:browser), do: "hero-computer-window"
defp token_type_icon_name(:client), do: "hero-device-phone-mobile"
defp token_type_icon_name(:api_client), do: "hero-command-line"
attr :actor, :any, required: true
def actor_status(assigns) do
~H"""
<span :if={Actors.actor_disabled?(@actor)} class="text-red-800">
(Disabled)
Disabled
</span>
<span :if={Actors.actor_deleted?(@actor)} class="text-red-800">
(Deleted)
Deleted
</span>
<span :if={not Actors.actor_disabled?(@actor) and not Actors.actor_deleted?(@actor)}>
Active
</span>
"""
end
@@ -53,9 +69,11 @@ defmodule Web.Actors.Components do
<div>
<.input
:if={not Actors.actor_synced?(@actor)}
label="Name"
label="Full Name"
field={@form[:name]}
placeholder="Full Name"
placeholder={
if @type == :service_account, do: "E.g. My Backend Service", else: "E.g. John Smith"
}
required
/>
</div>
@@ -75,9 +93,9 @@ defmodule Web.Actors.Components do
required
/>
<p class="mt-2 text-xs text-gray-500">
Select <strong>Admin</strong>
to grant this user access to the admin portal. Otherwise, select <strong>User</strong>
to limit access to the Client apps only.
<strong>Admin</strong>
grants full access to the admin portal and client applications. <strong>User</strong>
grants access to client applications only.
</p>
</div>
"""
@@ -91,7 +109,7 @@ defmodule Web.Actors.Components do
<div>
<.input
label="Email"
placeholder="Email"
placeholder="Enter an email address"
field={@form[:provider_identifier]}
autocomplete="off"
/>
@@ -99,7 +117,7 @@ defmodule Web.Actors.Components do
<div>
<.input
label="Email Confirmation"
placeholder="Email Confirmation"
placeholder="Enter the same email as above"
field={@form[:provider_identifier_confirmation]}
autocomplete="off"
/>
@@ -112,7 +130,7 @@ defmodule Web.Actors.Components do
<div>
<.input
label="Email"
placeholder="Email"
placeholder="Enter an email address"
field={@form[:provider_identifier]}
autocomplete="off"
/>
@@ -124,7 +142,7 @@ defmodule Web.Actors.Components do
<div>
<.input
label="Email Confirmation"
placeholder="Email Confirmation"
placeholder="Enter the same email as above"
field={@form[:provider_identifier_confirmation]}
autocomplete="off"
/>

View File

@@ -44,7 +44,6 @@ defmodule Web.Actors.Edit do
</:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Edit User Details</h2>
<.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">

View File

@@ -48,14 +48,20 @@ defmodule Web.Actors.Index do
<.section>
<:title><%= @page_title %></:title>
<:action>
<.docs_action path="/deploy/users" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/actors/new"}>
Add Actor
</.add_button>
</:action>
<:help>
Actors are the people and services that can access your Resources.
</:help>
<:content>
<.flash_group flash={@flash} />
<.live_table
@@ -103,7 +109,11 @@ defmodule Web.Actors.Index do
</.peek>
</:col>
<:col :let={actor} label="last signed in">
<:col :let={actor} label="status" class="w-1/12">
<.actor_status actor={actor} />
</:col>
<:col :let={actor} label="last signed in" class="w-1/12">
<.relative_datetime datetime={actor.last_seen_at} />
</:col>
</.live_table>

View File

@@ -27,9 +27,6 @@ defmodule Web.Actors.ServiceAccounts.New do
<:title><%= @page_title %></:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">
Service Account details
</h2>
<.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">

View File

@@ -35,7 +35,7 @@ defmodule Web.Actors.ServiceAccounts.NewIdentity do
<%= @actor.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/service_accounts/#{@actor}/new_identity"}>
Add Token
Create Token
</.breadcrumb>
</.breadcrumbs>
@@ -43,7 +43,6 @@ defmodule Web.Actors.ServiceAccounts.NewIdentity do
<:title><%= @page_title %></:title>
<:content>
<div :if={is_nil(@encoded_token)} class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Token details</h2>
<.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">

View File

@@ -145,7 +145,8 @@ defmodule Web.Actors.Show do
<:title>
<%= actor_type(@actor.type) %>: <span class="font-medium"><%= @actor.name %></span>
<span :if={@actor.id == @subject.actor.id} class="text-sm text-neutral-400">(you)</span>
<span :if={not is_nil(@actor.deleted_at)} class="text-red-600">(deleted)</span>
<span :if={Actors.actor_deleted?(@actor)} class="text-red-600">(deleted)</span>
<span :if={Actors.actor_disabled?(@actor)} class="text-red-600">(disabled)</span>
</:title>
<:action :if={is_nil(@actor.deleted_at)}>
<.edit_button navigate={~p"/#{@account}/actors/#{@actor}/edit"}>
@@ -196,8 +197,9 @@ defmodule Web.Actors.Show do
<.vertical_table id="actor">
<.vertical_table_row>
<:label>Name</:label>
<:value><%= @actor.name %>
<.actor_status actor={@actor} /></:value>
<:value>
<%= @actor.name %>
</:value>
</.vertical_table_row>
<.vertical_table_row>
@@ -376,24 +378,7 @@ defmodule Web.Actors.Show do
<:col :let={token} :if={@actor.type != :service_account} label="identity" class="w-3/12">
<.identity_identifier account={@account} identity={token.identity} />
</:col>
<:col :let={token} :if={@actor.type == :service_account} label="name" class="w-2/12">
<%= token.name %>
</:col>
<:col :let={token} label="created">
<.created_by account={@account} schema={token} />
</:col>
<:col :let={token} label="last used">
<p>
<.relative_datetime datetime={token.last_seen_at} />
</p>
<p :if={not is_nil(token.last_seen_at)}>
<code class="text-xs"><.last_seen schema={token} /></code>
</p>
</:col>
<:col :let={token} label="expires">
<.relative_datetime datetime={token.expires_at} />
</:col>
<:col :let={token} label="client">
<:col :let={token} label="client" class="w-1/12">
<.intersperse_blocks :if={token.type == :client}>
<:separator>,&nbsp;</:separator>
@@ -407,6 +392,34 @@ defmodule Web.Actors.Show do
</.intersperse_blocks>
<span :if={token.type != :client}>N/A</span>
</:col>
<:col :let={token} :if={@actor.type == :service_account} label="name" class="w-2/12">
<%= token.name %>
</:col>
<:col :let={token} label="created" class="w-2/12">
<.created_by account={@account} schema={token} />
</:col>
<:col :let={token} label="expires" class="w-1/12">
<%= if DateTime.compare(token.expires_at, DateTime.utc_now()) == :lt do %>
<.popover>
<:target>
expired
</:target>
<:content>
<.datetime datetime={token.expires_at} />
</:content>
</.popover>
<% else %>
<.relative_datetime datetime={token.expires_at} negative_class="text-red-600" />
<% end %>
</:col>
<:col :let={token} label="last used" class="w-3/12">
<p>
<.relative_datetime datetime={token.last_seen_at} />
</p>
<p :if={not is_nil(token.last_seen_at)}>
<code class="text-xs"><.last_seen schema={token} /></code>
</p>
</:col>
<:action :let={token}>
<.button_with_confirmation
id={"revoke_token_#{token.id}"}
@@ -538,9 +551,7 @@ defmodule Web.Actors.Show do
metadata={@groups_metadata}
>
<:col :let={group} label="name">
<.link navigate={~p"/#{@account}/groups/#{group.id}"} class={[link_style()]}>
<%= group.name %>
</.link>
<.group account={@account} group={group} />
</:col>
<:empty>
<div class="text-center text-neutral-500 p-4">No Groups to display.</div>

View File

@@ -27,7 +27,6 @@ defmodule Web.Actors.Users.New do
<:title><%= @page_title %></:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">User details</h2>
<.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">

View File

@@ -54,14 +54,13 @@ defmodule Web.Actors.Users.NewIdentity do
<:title><%= @page_title %></:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Identity details</h2>
<.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.input
type="select"
label="Provider"
label="Identity Provider"
field={@form[:provider_id]}
options={
Enum.map(@providers, fn provider ->
@@ -72,9 +71,6 @@ defmodule Web.Actors.Users.NewIdentity do
placeholder="Provider"
required
/>
<p class="mt-2 text-xs text-gray-500">
Select the provider to use for signing in.
</p>
</div>
<.provider_form :if={@provider} form={@form} provider={@provider} />
</div>

View File

@@ -38,7 +38,6 @@ defmodule Web.Clients.Edit do
</:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Edit client details</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>

View File

@@ -1,5 +1,6 @@
defmodule Web.Clients.Index do
use Web, :live_view
import Web.Actors.Components
alias Domain.Clients
def mount(_params, _session, socket) do
@@ -54,6 +55,9 @@ defmodule Web.Clients.Index do
<:help>
Clients are end-user devices and servers that access your protected Resources.
</:help>
<:action>
<.docs_action path="/deploy/clients" />
</:action>
<:content>
<.flash_group flash={@flash} />
<.live_table
@@ -70,12 +74,17 @@ defmodule Web.Clients.Index do
<.link navigate={~p"/#{@account}/clients/#{client.id}"} class={[link_style()]}>
<%= client.name %>
</.link>
<.icon :if={not is_nil(client.verified_at)} name="hero-shield-check" class="w-4 h-4" />
<.icon
:if={not is_nil(client.verified_at)}
name="hero-shield-check"
class="w-4 h-4"
title="Device attributes of this client are manually verified"
/>
</div>
</:col>
<:col :let={client} label="user">
<.link navigate={~p"/#{@account}/actors/#{client.actor.id}"} class={[link_style()]}>
<%= client.actor.name %>
<.actor_name_and_role account={@account} actor={client.actor} />
</.link>
</:col>
<:col :let={client} label="status">

View File

@@ -75,51 +75,12 @@ defmodule Web.Clients.Show do
<span :if={not is_nil(@client.deleted_at)} class="text-red-600">(deleted)</span>
</:title>
<:action :if={is_nil(@client.deleted_at) and not is_nil(@client.verified_at)}>
<.button_with_confirmation
id="remove_client_verification"
style="danger"
icon="hero-shield-exclamation"
on_confirm="remove_client_verification"
>
<:dialog_title>Remove verification</:dialog_title>
<:dialog_content>
Are you sure you want to remove verification of this Client?
</:dialog_content>
<:dialog_confirm_button>
Remove
</:dialog_confirm_button>
<:dialog_cancel_button>
Cancel
</:dialog_cancel_button>
Remove verification
</.button_with_confirmation>
</:action>
<:action :if={is_nil(@client.deleted_at) and is_nil(@client.verified_at)}>
<.button_with_confirmation
id="verify_client"
style="warning"
icon="hero-shield-check"
on_confirm="verify_client"
>
<:dialog_title>Verify Client</:dialog_title>
<:dialog_content>
Are you sure you want to verify this Client?
</:dialog_content>
<:dialog_confirm_button>
Verify
</:dialog_confirm_button>
<:dialog_cancel_button>
Cancel
</:dialog_cancel_button>
Verify
</.button_with_confirmation>
</:action>
<:action :if={is_nil(@client.deleted_at)}>
<.edit_button navigate={~p"/#{@account}/clients/#{@client}/edit"}>
Edit Client
</.edit_button>
</:action>
<:content>
<.vertical_table id="client">
<.vertical_table_row>
@@ -143,12 +104,6 @@ defmodule Web.Clients.Show do
<:label>Status</:label>
<:value><.connection_status class="ml-1/2" schema={@client} /></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Verification</:label>
<:value>
<.verified_by account={@account} schema={@client} />
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Owner</:label>
<:value>
@@ -188,7 +143,7 @@ defmodule Web.Clients.Show do
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Client version</:label>
<:label>Version</:label>
<:value><%= @client.last_seen_version %></:value>
</.vertical_table_row>
<.vertical_table_row>
@@ -208,11 +163,60 @@ defmodule Web.Clients.Show do
</:value>
</.vertical_table_row>
</.vertical_table>
</:content>
</.section>
<h2 class="mt-6 mb-4 text-xl leading-none tracking-tight text-neutral-900">
Device Attributes
</h2>
<.section>
<:title>
Device Attributes
</:title>
<:help>
Information about the device that the Client is running on.
</:help>
<:action :if={is_nil(@client.deleted_at) and not is_nil(@client.verified_at)}>
<.button_with_confirmation
id="remove_client_verification"
style="danger"
icon="hero-shield-exclamation"
on_confirm="remove_client_verification"
>
<:dialog_title>Remove verification</:dialog_title>
<:dialog_content>
Are you sure you want to remove verification of this Client?
</:dialog_content>
<:dialog_confirm_button>
Remove
</:dialog_confirm_button>
<:dialog_cancel_button>
Cancel
</:dialog_cancel_button>
Remove verification
</.button_with_confirmation>
</:action>
<:action :if={is_nil(@client.deleted_at) and is_nil(@client.verified_at)}>
<.button_with_confirmation
id="verify_client"
style="warning"
icon="hero-shield-check"
on_confirm="verify_client"
>
<:dialog_title>Verify Client</:dialog_title>
<:dialog_content>
Are you sure you want to verify this Client?
</:dialog_content>
<:dialog_confirm_button>
Verify
</:dialog_confirm_button>
<:dialog_cancel_button>
Cancel
</:dialog_cancel_button>
Verify
</.button_with_confirmation>
</:action>
<:content>
<.vertical_table id="posture">
<.vertical_table_row>
<:label>
@@ -250,6 +254,23 @@ defmodule Web.Clients.Show do
<.last_seen schema={@client} />
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>
<.popover>
<:target>
Verification
<.icon name="hero-question-mark-circle" class="w-3 h-3 mb-1 text-neutral-400" />
</:target>
<:content>
Policies can be configured to require verification in order to access a Resource.
</:content>
</.popover>
</:label>
<:value>
<.verified_by account={@account} schema={@client} />
</:value>
</.vertical_table_row>
</.vertical_table>
</:content>
</.section>

View File

@@ -43,7 +43,6 @@ defmodule Web.Groups.Edit do
</:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Edit group details</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>

View File

@@ -141,13 +141,26 @@ defmodule Web.Groups.EditActors do
</.live_table>
<div class="flex justify-end">
<.button_with_confirmation id="save_changes" style="primary" class="m-4" on_confirm="submit">
<:dialog_title>Apply changes to Group Actors</:dialog_title>
<.button_with_confirmation
id="save_changes"
style={(@added == %{} and @removed == %{} && "disabled") || "primary"}
confirm_style="primary"
class="m-4"
on_confirm="submit"
disabled={@added == %{} and @removed == %{}}
>
<:dialog_title>Confirm changes to Group Actors</:dialog_title>
<:dialog_content>
<%= confirm_message(@added, @removed) %>
<div class="mb-2">
You're about to apply the following membership changes for the
<strong><%= @group.name %></strong>
group:
</div>
<.confirm_message added={@added} removed={@removed} />
</:dialog_content>
<:dialog_confirm_button>
Save
Confirm
</:dialog_confirm_button>
<:dialog_cancel_button>
Cancel
@@ -234,20 +247,19 @@ defmodule Web.Groups.EditActors do
Map.has_key?(added, actor.id)
end
# TODO: this should be replaced by a new state of a form which will render impact of a change
defp confirm_message(added, removed) do
added_names = Enum.map(added, fn {_id, name} -> name end)
removed_names = Enum.map(removed, fn {_id, name} -> name end)
defp confirm_message(assigns) do
~H"""
<ul>
<li :for={{_id, name} <- @added} class="mb-2">
<.icon name="hero-plus" class="h-3.5 w-3.5 mr-2 text-green-500" />
<%= name %>
</li>
add = if added_names != [], do: "add #{Enum.join(added_names, ", ")}"
remove = if removed_names != [], do: "remove #{Enum.join(removed_names, ", ")}"
change = [add, remove] |> Enum.reject(&is_nil/1) |> Enum.join(" and ")
if change == "" do
# Don't show confirmation message if no changes were made
nil
else
"Are you sure you want to #{change}?"
end
<li :for={{_id, name} <- @removed} class="mb-2">
<.icon name="hero-minus" class="h-3.5 w-3.5 mr-2 text-red-500" />
<%= name %>
</li>
</ul>
"""
end
end

View File

@@ -47,14 +47,21 @@ defmodule Web.Groups.Index do
<:title>
Groups
</:title>
<:action>
<.docs_action path="/deploy/groups" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/groups/new"}>
Add Group
</.add_button>
</:action>
<:help>
Groups organize Actors and form the basis of the Firezone access control model.
</:help>
<:content>
<.flash_group flash={@flash} />
<.live_table
@@ -67,9 +74,7 @@ defmodule Web.Groups.Index do
metadata={@groups_metadata}
>
<:col :let={group} field={{:groups, :name}} label="name" class="w-2/4">
<.link navigate={~p"/#{@account}/groups/#{group.id}"} class={[link_style()]}>
<%= group.name %>
</.link>
<.group account={@account} group={group} />
<span :if={Actors.group_deleted?(group)} class="text-xs text-neutral-100">
(deleted)

View File

@@ -24,17 +24,16 @@ defmodule Web.Groups.New do
<:title><%= @page_title %></:title>
<:content>
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">
Group details
</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<.input type="hidden" field={@form[:type]} value="static" />
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.input label="Name" field={@form[:name]} placeholder="E.g. Engineering" required />
<p class="text-sm mt-2 text-neutral-600">
Enter a name for this Group.
</p>
<.input
label="Group Name"
field={@form[:name]}
placeholder="E.g. Engineering"
required
/>
</div>
</div>
<.submit_button>

View File

@@ -48,7 +48,7 @@ defmodule Web.Policies.Edit do
<:title><%= @page_title %></:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Policy details</h2>
<legend class="mb-4 text-xl text-neutral-900">Details</legend>
<.form for={@form} phx-submit="submit" phx-change="validate">
<.base_error form={@form} field={:base} />

View File

@@ -50,6 +50,9 @@ defmodule Web.Policies.Index do
<.section>
<:title><%= @page_title %></:title>
<:action>
<.docs_action path="/deploy/policies" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/policies/new"}>
Add Policy
@@ -91,7 +94,7 @@ defmodule Web.Policies.Index do
<%= if is_nil(policy.disabled_at) do %>
Active
<% else %>
Disabled
<span class="text-red-800">Disabled</span>
<% end %>
<% else %>
Deleted

View File

@@ -35,7 +35,7 @@ defmodule Web.Policies.New do
<:title><%= @page_title %></:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Policy details</h2>
<legend class="mb-4 text-xl text-neutral-900">Details</legend>
<.form for={@form} phx-submit="submit" phx-change="validate">
<.base_error form={@form} field={:base} />

View File

@@ -116,7 +116,7 @@ defmodule Web.Resources.Components do
~H"""
<fieldset class="flex flex-col gap-2">
<div class="mb-1 flex items-center justify-between">
<legend>Traffic Restriction</legend>
<legend class="text-xl">Traffic Restriction</legend>
<%= if @traffic_filters_enabled? == false do %>
<.link navigate={~p"/#{@account}/settings/billing"} class="text-sm text-primary-500">
@@ -136,7 +136,7 @@ defmodule Web.Resources.Components do
@traffic_filters_enabled? == false && "opacity-50",
"mt-4"
]}>
<div class="flex items-top h-20">
<div class="flex items-top mb-4">
<.input type="hidden" name={"#{@form.name}[tcp][protocol]"} value="tcp" />
<div class="mt-2.5 w-24">
<.input
@@ -169,7 +169,7 @@ defmodule Web.Resources.Components do
</div>
</div>
<div class="flex items-top h-20">
<div class="flex items-top mb-4">
<.input type="hidden" name={"#{@form.name}[udp][protocol]"} value="udp" />
<div class="mt-2.5 w-24">
<.input
@@ -201,7 +201,7 @@ defmodule Web.Resources.Components do
</div>
</div>
<div class="flex items-top h-20">
<div class="flex items-top mb-4">
<.input type="hidden" name={"#{@form.name}[icmp][protocol]"} value="icmp" />
<div class="mt-2.5 w-24">
@@ -287,7 +287,11 @@ defmodule Web.Resources.Components do
~H"""
<fieldset class="flex flex-col gap-2" {@rest}>
<legend class="mb-2">Sites</legend>
<legend class="text-xl mb-4">Sites</legend>
<p class="text-sm text-neutral-500">
When multiple sites are selected, the client will automatically connect to the closest one based on its geographical location.
</p>
<.error :for={error <- @errors} data-validation-error-for="connections">
<%= error %>

View File

@@ -47,8 +47,6 @@ defmodule Web.Resources.Edit do
</:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Edit Resource details</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit} class="space-y-4 lg:space-y-6">
<div :if={@resource.type != :internet}>
<p class="mb-2 text-sm text-neutral-900">

View File

@@ -57,6 +57,9 @@ defmodule Web.Resources.Index do
Resources define the subnets, hosts, and applications for which you want to manage access. You can manage Resources per Site
in the <.link navigate={~p"/#{@account}/sites"} class={link_style()}>Sites</.link> section.
</:help>
<:action>
<.docs_action path="/deploy/resources" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/resources/new"}>
Add Resource

View File

@@ -32,7 +32,7 @@ defmodule Web.Resources.New do
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Resource details</h2>
<legend class="text-xl mb-4">Details</legend>
<.form for={@form} class="space-y-4 lg:space-y-6" phx-submit="submit" phx-change="change">
<div>
<p class="mb-2 text-sm text-neutral-900">

View File

@@ -30,7 +30,6 @@ defmodule Web.Settings.Account.Edit do
</:title>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl text-neutral-900">Edit account details</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>

View File

@@ -63,6 +63,9 @@ defmodule Web.Settings.ApiClients.Index do
for more information.
</:help>
<:action>
<.docs_action path="/reference/rest-api" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/settings/api_clients/new"}>
Add API Client

View File

@@ -32,24 +32,35 @@ defmodule Web.Settings.DNS do
<:title>
DNS
</:title>
<:action>
<.docs_action path="/deploy/dns" />
</:action>
<:help>
Configure the default resolver used by connected Clients.
Queries for Resources will <strong>always</strong>
use Firezone's internal DNS.
All other queries will use the DNS servers configured here or the Client's
system resolvers if no servers are configured.
<p class="mt-2">
<.website_link path="/kb/deploy/dns">
Read more about configuring DNS in Firezone.
</.website_link>
</p>
Configure the upstream resolver used by connected Clients.
Queries for Resources will <strong>always</strong> use Firezone's internal DNS.
All other queries will use the resolvers configured here or the Client's
system resolvers if none are configured.
</:help>
<:content>
<div class="max-w-2xl px-4 py-8 mx-auto">
<.flash kind={:success} flash={@flash} phx-click="lv:clear-flash" />
<h2 class="mb-4 text-xl text-neutral-900">Client DNS</h2>
<p class="mb-4 text-neutral-500">
DNS servers will be used in the order they are listed below.
<.flash kind={:success} flash={@flash} phx-click="lv:clear-flash" />
<% empty? =
Domain.Repo.Changeset.empty?(@form.source) and
Enum.empty?(@account.config.clients_upstream_dns) %>
<p :if={not empty?} class="mb-4 text-neutral-500">
Upstream resolvers will be used by Clients in the order they are listed below.
</p>
<p :if={empty?} class="text-neutral-500">
No upstream resolvers have been configured. Click <strong>New Resolver</strong>
to add one.
</p>
<.form for={@form} phx-submit={:submit} phx-change={:change}>
@@ -74,12 +85,8 @@ defmodule Web.Settings.DNS do
value={dns[:protocol].value}
/>
</div>
<div class="w-8/12">
<.input
label="Address"
field={dns[:address]}
placeholder="DNS Server Address"
/>
<div class="w-3/4">
<.input label="Address" field={dns[:address]} placeholder="E.g. 1.1.1.1" />
</div>
<div class="w-1/12 flex">
<div class="pt-7">
@@ -89,7 +96,10 @@ defmodule Web.Settings.DNS do
value={dns.index}
phx-click={JS.dispatch("change")}
>
<.icon name="hero-trash" class="text-red-500 w-6 h-6 relative top-2" />
<.icon
name="hero-trash"
class="-ml-1 text-red-500 w-5 h-5 relative top-2"
/>
</button>
</div>
</div>
@@ -104,7 +114,7 @@ defmodule Web.Settings.DNS do
value="new"
phx-click={JS.dispatch("change")}
>
New DNS Server
New Resolver
</.button>
<.error
:for={error <- dns_config_errors(@form.source.changes)}
@@ -117,7 +127,7 @@ defmodule Web.Settings.DNS do
<p class="text-sm text-neutral-500">
<strong>Note:</strong>
It is highly recommended to to specify <strong>both</strong>
IPv4 and IPv6 addresses when adding custom resolvers. Otherwise, Clients without IPv4
IPv4 and IPv6 addresses when adding upstream resolvers. Otherwise, Clients without IPv4
or IPv6 connectivity may not be able to resolve DNS queries.
</p>
<.submit_button>

View File

@@ -55,16 +55,16 @@ defmodule Web.Settings.IdentityProviders.Index do
<:title>
Identity Providers
</:title>
<:action>
<.docs_action path="/authenticate" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/settings/identity_providers/new"}>
Add Identity Provider
</.add_button>
</:action>
<:help>
<.website_link path="/kb/authenticate">
Read more
</.website_link>
about how authentication works in Firezone.
Identity providers authenticate and sync your users and groups with an external source.
</:help>
<:content>
<.flash_group flash={@flash} />

View File

@@ -65,9 +65,8 @@ defmodule Web.SignIn.Email do
<div>
<p>
If <strong><%= @provider_identifier %></strong> is registered, a sign in token has
been sent to that email. Please copy and paste this into the form below to proceed
with your login.
If <strong><%= @provider_identifier %></strong>
is registered, a sign-in token has been sent.
</p>
<form
id="verify-sign-in-token"

View File

@@ -133,7 +133,7 @@ defmodule Web.SignUp do
~H"""
<div class="space-y-6">
<div class="text-center text-neutral-900">
Your account has been created!
<p class="text-xl font-medium">Your account has been created!</p>
<p>Please check your email for sign in instructions.</p>
</div>
<div class="text-center">
@@ -141,7 +141,7 @@ defmodule Web.SignUp do
<table class="border-collapse w-full text-sm">
<tbody>
<tr>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900 font-bold]}>
Account Name:
</td>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
@@ -149,7 +149,7 @@ defmodule Web.SignUp do
</td>
</tr>
<tr>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900 font-bold]}>
Account Slug:
</td>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
@@ -157,7 +157,7 @@ defmodule Web.SignUp do
</td>
</tr>
<tr>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900 font-bold]}>
Sign In URL:
</td>
<td class={~w[border-b border-neutral-100 py-4 text-neutral-900]}>
@@ -204,8 +204,8 @@ defmodule Web.SignUp do
<.input
field={@form[:email]}
type="text"
label="Email"
placeholder="Enter your work email here"
label="Work Email"
placeholder="E.g. foo@example.com"
required
autofocus
phx-debounce="300"
@@ -215,8 +215,8 @@ defmodule Web.SignUp do
<.input
field={account[:name]}
type="text"
label="Account Name"
placeholder="Enter an account name"
label="Company Name"
placeholder="E.g. Example Corp"
required
phx-debounce="300"
/>
@@ -228,7 +228,7 @@ defmodule Web.SignUp do
field={actor[:name]}
type="text"
label="Your Name"
placeholder="Enter your name here"
placeholder="E.g. John Smith"
required
phx-debounce="300"
/>
@@ -293,8 +293,6 @@ defmodule Web.SignUp do
changeset =
attrs
|> maybe_put_default_account_name(account_name_changed?)
|> maybe_put_default_actor_name(actor_name_changed?)
|> Registration.changeset()
|> Map.put(:action, :validate)
@@ -313,8 +311,6 @@ defmodule Web.SignUp do
changeset =
attrs
|> maybe_put_default_account_name()
|> maybe_put_default_actor_name()
|> put_in(["actor", "type"], :account_admin_user)
|> Registration.changeset()
|> Map.put(:action, :insert)
@@ -382,33 +378,6 @@ defmodule Web.SignUp do
end
end
defp maybe_put_default_account_name(attrs, account_name_changed? \\ true)
defp maybe_put_default_account_name(attrs, true) do
attrs
end
defp maybe_put_default_account_name(attrs, false) do
case String.split(attrs["email"], "@", parts: 2) do
[default_name | _] when byte_size(default_name) > 0 ->
put_in(attrs, ["account", "name"], "#{default_name}'s account")
_ ->
attrs
end
end
defp maybe_put_default_actor_name(attrs, actor_name_changed? \\ true)
defp maybe_put_default_actor_name(attrs, true) do
attrs
end
defp maybe_put_default_actor_name(attrs, false) do
[default_name | _] = String.split(attrs["email"], "@", parts: 2)
put_in(attrs, ["actor", "name"], default_name)
end
defp register_account(socket, registration) do
Ecto.Multi.new()
|> Ecto.Multi.run(

View File

@@ -49,6 +49,11 @@ defmodule Web.Sites.Index do
<:title>
Sites
</:title>
<:action>
<.docs_action path="/deploy/sites" />
</:action>
<:action>
<.add_button navigate={~p"/#{@account}/sites/new"}>
Add Site

View File

@@ -20,16 +20,15 @@ defmodule Web.Sites.New do
<:content>
<.flash kind={:error} flash={@flash} />
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<h2 class="mb-6 text-xl text-neutral-900">
Site details
</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.input label="Name" field={@form[:name]} placeholder="Name of this Site" required />
<p class="mt-2 text-xs text-neutral-500">
Enter a name for this Site.
</p>
<.input
label="Name"
field={@form[:name]}
placeholder="Enter a name for this Site"
required
/>
</div>
</div>
<.submit_button>

View File

@@ -129,6 +129,9 @@ defmodule Web.Sites.Show do
see all <.icon name="hero-arrow-right" class="w-2 h-2" />
</.link>
</:title>
<:action>
<.docs_action path="/deploy/gateways" />
</:action>
<:action :if={is_nil(@group.deleted_at)}>
<.add_button navigate={~p"/#{@account}/sites/#{@group}/new_token"}>
Deploy Gateway
@@ -245,9 +248,14 @@ defmodule Web.Sites.Show do
<:col :let={resource} label="Authorized groups">
<.peek peek={Map.fetch!(@resource_actor_groups_peek, resource.id)}>
<:empty>
None -
<div class="mr-1">
<.icon
name="hero-exclamation-triangle-solid"
class="inline-block w-3.5 h-3.5 text-red-500"
/> None.
</div>
<.link
class={["px-1", link_style()]}
class={[link_style(), "mr-1"]}
navigate={~p"/#{@account}/policies/new?resource_id=#{resource}&site_id=#{@group}"}
>
Create a Policy

View File

@@ -186,7 +186,7 @@ defmodule Web.LiveTable do
defp filter(%{filter: %{type: {:string, :websearch}}} = assigns) do
~H"""
<div class="flex items-center order-last md:w-56">
<div class="flex items-center order-last">
<div class="relative w-full" phx-feedback-for={@form[@filter.name].name}>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<.icon name="hero-magnifying-glass" class="w-5 h-5 text-neutral-500" />
@@ -200,7 +200,7 @@ defmodule Web.LiveTable do
placeholder={"Search by " <> @filter.title}
class={[
"bg-neutral-50 border border-neutral-300 text-neutral-900 text-sm rounded",
"block w-full pl-10 p-2",
"block w-full md:w-72 pl-10 p-2",
"disabled:bg-neutral-50 disabled:text-neutral-500 disabled:border-neutral-300 disabled:shadow-none",
"focus:outline-none focus:border-1 focus:ring-0",
@form[@filter.name].errors != [] && "border-rose-400"
@@ -219,7 +219,7 @@ defmodule Web.LiveTable do
defp filter(%{filter: %{type: {:string, :email}}} = assigns) do
~H"""
<div class="flex items-center order-last md:w-56">
<div class="flex items-center order-last">
<div class="relative w-full" phx-feedback-for={@form[@filter.name].name}>
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<.icon name="hero-magnifying-glass" class="w-5 h-5 text-neutral-500" />
@@ -233,7 +233,7 @@ defmodule Web.LiveTable do
placeholder={"Search by " <> @filter.title}
class={[
"bg-neutral-50 border border-neutral-300 text-neutral-900 text-sm rounded",
"block w-full pl-10 p-2",
"block w-full md:w-72 pl-10 p-2",
"disabled:bg-neutral-50 disabled:text-neutral-500 disabled:border-neutral-300 disabled:shadow-none",
"focus:outline-none focus:border-1 focus:ring-0",
@form[@filter.name].errors != [] && "border-rose-400"

View File

@@ -53,7 +53,7 @@ defmodule Web.Live.Actors.ServiceAccounts.NewIdentityTest do
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Actors"
assert breadcrumbs =~ actor.name
assert breadcrumbs =~ "Add Token"
assert breadcrumbs =~ "Create Token"
end
test "renders form", %{

View File

@@ -92,8 +92,7 @@ defmodule Web.Live.Clients.ShowTest do
assert table["status"] =~ "Offline"
assert table["created"]
assert table["last started"]
assert table["verification"] =~ "Not Verified"
assert table["client version"] =~ client.last_seen_version
assert table["version"] =~ client.last_seen_version
assert table["user agent"] =~ client.last_seen_user_agent
table =
@@ -104,6 +103,7 @@ defmodule Web.Live.Clients.ShowTest do
assert table["file id"] == client.external_id
assert table["verification"] =~ "Not Verified"
assert table["device serial"] =~ to_string(client.device_serial)
assert table["device uuid"] =~ to_string(client.device_uuid)
assert table["app installation id"] =~ to_string(client.firebase_installation_id)
@@ -322,7 +322,7 @@ defmodule Web.Live.Clients.ShowTest do
table =
lv
|> element("#client")
|> element("#posture")
|> render()
|> vertical_table_to_map()

View File

@@ -212,7 +212,7 @@ defmodule Web.Live.Groups.EditActorsTest do
|> render_click()
lv
|> element("button[type=submit]", "Save")
|> element("button[type=submit]", "Confirm")
|> render_click()
assert_redirected(lv, ~p"/#{account}/groups/#{group}")

View File

@@ -237,7 +237,7 @@ defmodule Web.Live.Sites.ShowTest do
Enum.each(resource_rows, fn row ->
assert row["name"] =~ resource.name
assert row["address"] =~ resource.address
assert row["authorized groups"] == "None - Create a Policy to grant access."
assert row["authorized groups"] == "None. Create a Policy to grant access."
end)
end