Refactor Actor and Device Liveviews (#1824)

Why:

* The previous Actor and Device Liveviews had used static views and data
as a starting point for fleshing out the web UI. This commit builds on
that and replaces (most) of the static data with data from the database,
as well as updating the static Liveview templates to use components
where possible.
This commit is contained in:
bmanifold
2023-08-04 15:17:11 -04:00
committed by GitHub
parent 44e4295a4e
commit 11a31ae088
17 changed files with 611 additions and 940 deletions

View File

@@ -129,12 +129,18 @@ defmodule Domain.Actors do
end
end
def fetch_actor_by_id(id, %Auth.Subject{} = subject) do
def fetch_actor_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()),
true <- Validator.valid_uuid?(id) do
Actor.Query.by_id(id)
|> Authorizer.for_subject(subject)
|> Repo.fetch()
|> case do
{:ok, actor} -> {:ok, Repo.preload(actor, preload)}
{:error, reason} -> {:error, reason}
end
else
false -> {:error, :not_found}
other -> other
@@ -156,14 +162,17 @@ defmodule Domain.Actors do
end
def list_actors(%Auth.Subject{} = subject, opts \\ []) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do
{hydrate, _opts} = Keyword.pop(opts, :hydrate, [])
{preload, _opts} = Keyword.pop(opts, :preload, [])
{hydrate, _opts} = Keyword.pop(opts, :hydrate, [])
Actor.Query.all()
|> Authorizer.for_subject(subject)
# TODO: add filters
|> hydrate_fields(hydrate)
|> Repo.list()
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do
{:ok, actors} =
Actor.Query.all()
|> Authorizer.for_subject(subject)
|> hydrate_fields(hydrate)
|> Repo.list()
{:ok, Repo.preload(actors, preload)}
end
end

View File

@@ -26,7 +26,9 @@ defmodule Domain.Devices do
|> Repo.aggregate(:count)
end
def fetch_device_by_id(id, %Auth.Subject{} = subject) do
def fetch_device_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
required_permissions =
{:one_of,
[
@@ -39,6 +41,10 @@ defmodule Domain.Devices do
Device.Query.by_id(id)
|> Authorizer.for_subject(subject)
|> Repo.fetch()
|> case do
{:ok, device} -> {:ok, Repo.preload(device, preload)}
{:error, reason} -> {:error, reason}
end
else
false -> {:error, :not_found}
other -> other
@@ -53,7 +59,9 @@ defmodule Domain.Devices do
|> Repo.preload(preload)
end
def list_devices(%Auth.Subject{} = subject) do
def list_devices(%Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
required_permissions =
{:one_of,
[
@@ -62,9 +70,12 @@ defmodule Domain.Devices do
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
Device.Query.all()
|> Authorizer.for_subject(subject)
|> Repo.list()
{:ok, devices} =
Device.Query.all()
|> Authorizer.for_subject(subject)
|> Repo.list()
{:ok, Repo.preload(devices, preload)}
end
end

View File

@@ -146,6 +146,31 @@ end
IO.puts("")
user_iphone =
Domain.Devices.upsert_device(
%{
name: "FZ User iPhone",
external_id: Ecto.UUID.generate(),
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412"
},
unprivileged_subject
)
admin_iphone =
Domain.Devices.upsert_device(
%{
name: "FZ Admin iPhone",
external_id: Ecto.UUID.generate(),
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412"
},
admin_subject
)
IO.puts("Devices created")
IO.puts("")
relay_group =
account
|> Relays.Group.Changeset.create_changeset(

View File

@@ -624,6 +624,17 @@ defmodule Domain.ActorsTest do
{:unauthorized,
[missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}}
end
test "associations are preloaded when opts given" do
account = AccountsFixtures.create_account()
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
identity = AuthFixtures.create_identity(account: account, actor: actor)
subject = AuthFixtures.create_subject(identity)
{:ok, actor} = fetch_actor_by_id(actor.id, subject, preload: :identities)
assert Ecto.assoc_loaded?(actor.identities) == true
end
end
describe "fetch_actor_by_id/1" do
@@ -704,6 +715,22 @@ defmodule Domain.ActorsTest do
{:unauthorized,
[missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}}
end
test "associations are preloaded when opts given" do
account = AccountsFixtures.create_account()
actor1 = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
identity1 = AuthFixtures.create_identity(account: account, actor: actor1)
subject = AuthFixtures.create_subject(identity1)
actor2 = ActorsFixtures.create_actor(type: :account_user, account: account)
AuthFixtures.create_identity(account: account, actor: actor2)
{:ok, actors} = list_actors(subject, preload: :identities)
assert length(actors) == 2
assert Enum.all?(actors, fn a -> Ecto.assoc_loaded?(a.identities) end) == true
end
end
describe "create_actor/4" do

View File

@@ -101,13 +101,15 @@
Dashboard
</.sidebar_item>
<.sidebar_item_group id="organization">
<:name>Organization</:name>
<:item navigate={~p"/#{@account}/actors"}>Users</:item>
<:item navigate={~p"/#{@account}/groups"}>Groups</:item>
<:item navigate={~p"/#{@account}/devices"}>Devices</:item>
</.sidebar_item_group>
<.sidebar_item navigate={~p"/#{@account}/actors"} icon="hero-user-circle-solid">
Actors
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/groups"} icon="hero-user-group-solid">
Groups
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/devices"} icon="hero-device-phone-mobile-solid">
Devices
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/gateways"} icon="hero-arrow-left-on-rectangle-solid">
Gateways
@@ -121,7 +123,7 @@
Policies
</.sidebar_item>
<.sidebar_item_group id="settings">
<.sidebar_item_group id="settings" icon="hero-cog-solid">
<:name>Settings</:name>
<:item navigate={~p"/#{@account}/settings/account"}>Account</:item>

View File

@@ -55,7 +55,7 @@ defmodule Web.NavigationComponents do
end
attr :id, :string, required: true, doc: "ID of the nav group container"
# attr :icon, :string, required: true
attr :icon, :string, required: true
# attr :navigate, :string, required: true
slot :name, required: true
@@ -77,7 +77,7 @@ defmodule Web.NavigationComponents do
aria-controls={"dropdown-#{@id}"}
data-collapse-toggle={"dropdown-#{@id}"}
>
<.icon name="hero-user-group-solid" class={~w[
<.icon name={@icon} class={~w[
w-6 h-6 text-gray-500
transition duration-75
group-hover:text-gray-900

View File

@@ -223,9 +223,14 @@ defmodule Web.TableComponents do
</.vertical_table>
"""
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block
def vertical_table(assigns) do
~H"""
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<table class={["w-full text-sm text-left text-gray-500 dark:text-gray-400", @class]}>
<tbody>
<%= render_slot(@inner_block) %>
</tbody>
@@ -250,19 +255,26 @@ defmodule Web.TableComponents do
</.vertical_table_row>
"""
attr :label_class, :string, default: nil
attr :value_class, :string, default: nil
slot :label, doc: "the slot for rendering the label of a row"
slot :value, doc: "the slot for rendering the value of a row"
def vertical_table_row(assigns) do
~H"""
<tr class="border-b border-gray-200 dark:border-gray-700">
<th scope="row" class={~w[
text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap
bg-gray-50 dark:text-white dark:bg-gray-800
]}>
<th
scope="row"
class={[
"text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap",
"bg-gray-50 dark:text-white dark:bg-gray-800",
@label_class
]}
>
<%= render_slot(@label) %>
</th>
<td class="px-6 py-4">
<td class={["px-6 py-4", @value_class]}>
<%= render_slot(@value) %>
</td>
</tr>

View File

@@ -0,0 +1,108 @@
defmodule Web.Actors.Edit do
use Web, :live_view
alias Domain.Actors
def mount(%{"id" => id} = _params, _session, socket) do
{:ok, actor} = Actors.fetch_actor_by_id(id, socket.assigns.subject)
{:ok, assign(socket, actor: actor)}
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Actors</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/#{@actor.id}"}>
<%= @actor.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/#{@actor.id}/edit"}>
Edit
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Editing User: <code><%= @actor.name %></code>
</:title>
</.header>
<!-- Update User -->
<section class="bg-white dark:bg-gray-900">
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit User Details</h2>
<form action="#">
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.label for="first-name">
Name
</.label>
<.input type="text" name="name" id="name" value={@actor.name} required="" />
</div>
<div>
<.label for="email">
Email
</.label>
<.input
aria-describedby="email-explanation"
type="email"
name="email"
id="email"
value="TODO: Email here"
/>
<p id="email-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
We'll send a confirmation email to both the current email address and the updated one to confirm the change.
</p>
</div>
<div>
<.label for="confirm-email">
Confirm email
</.label>
<.input type="email" name="confirm-email" id="confirm-email" value="TODO" />
</div>
<div>
<.label for="user-role">
Role
</.label>
<.input
type="select"
id="user-role"
name="user-role"
options={[
"End User": :account_user,
Admin: :account_admin_user,
"Service Account": :service_account
]}
value={@actor.type}
/>
<p id="role-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Select Admin to make this user an administrator of your organization.
</p>
</div>
<div>
<.label for="user-groups">
Groups
</.label>
<.input
type="select"
multiple={true}
name="user-groups"
id="user-groups"
options={["TODO: Devops", "TODO: Engineering", "TODO: Accounting"]}
value=""
/>
<p id="groups-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Select one or more groups to allow this user access to resources.
</p>
</div>
</div>
<div class="flex items-center space-x-4">
<.button type="submit">
Save
</.button>
</div>
</form>
</div>
</section>
"""
end
end

View File

@@ -0,0 +1,128 @@
defmodule Web.Actors.Index do
use Web, :live_view
alias Domain.Actors
def mount(_params, _session, socket) do
{_, actors} = Actors.list_actors(socket.assigns.subject, preload: [identities: :provider])
{:ok, assign(socket, actors: actors)}
end
defp actor_icon(type) do
case type do
:account_user -> "hero-user-circle-solid"
:account_admin_user -> "hero-user-circle-solid"
:service_account -> "hero-server-solid"
end
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Actors</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
All Actors
</:title>
<:actions>
<.add_button navigate={~p"/#{@account}/actors/new"}>
Add a new user
</.add_button>
</:actions>
</.header>
<!-- Users Table -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<.resource_filter />
<.table id="actors" rows={@actors} row_id={&"user-#{&1.id}"}>
<:col :let={actor} label="TYPE" sortable="false">
<.icon name={actor_icon(actor.type)} class="w-5 h-5" />
</:col>
<:col :let={actor} label="NAME" sortable="false">
<.link
navigate={~p"/#{@account}/actors/#{actor.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= actor.name %>
</.link>
</:col>
<:col :let={actor} label="IDENTIFIERS" sortable="false">
<%= for identity <- actor.identities do %>
<%= "#{identity.provider.name}: #{identity.provider_identifier}" %>
<br />
<% end %>
</:col>
<:col :let={_actor} label="GROUPS" sortable="false">
<!-- TODO: Determine how user groups will work -->
<%= "TODO Admin, Engineering, 3 more..." %>
</:col>
<:col :let={_actor} label="LAST ACTIVE" sortable="false">
<!-- TODO: Determine what last active means for a user -->
<%= "TODO Today at 2:30pm" %>
</:col>
<:action>
<.link
navigate={~p"/#{@account}/actors/#{@subject.actor.id}"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</.link>
</:action>
<:action>
<.link
navigate={~p"/#{@account}/actors/#{@subject.actor.id}/edit"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Edit
</.link>
</:action>
<:action>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Delete
</a>
</:action>
</.table>
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/actors"} />
</div>
"""
end
defp resource_filter(assigns) do
~H"""
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
<div class="w-full md:w-1/2">
<form class="flex items-center">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<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-gray-500 dark:text-gray-400" />
</div>
<input
type="text"
id="simple-search"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
required=""
/>
</div>
</form>
</div>
<.button_group>
<:first>
All
</:first>
<:middle>
Users
</:middle>
<:last>
Service Accounts
</:last>
</.button_group>
</div>
"""
end
end

View File

@@ -1,4 +1,4 @@
defmodule Web.Users.New do
defmodule Web.Actors.New do
use Web, :live_view
def render(assigns) do

View File

@@ -0,0 +1,116 @@
defmodule Web.Actors.Show do
use Web, :live_view
alias Domain.Actors
def mount(%{"id" => id} = _params, _session, socket) do
{:ok, actor} =
Actors.fetch_actor_by_id(id, socket.assigns.subject, preload: [identities: [:provider]])
{:ok, assign(socket, actor: actor)}
end
defp account_type_to_string(type) do
case type do
:account_admin_user -> "Admin"
:account_user -> "User"
:service_account -> "Service Account"
end
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Actors</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/#{@actor.id}"}>
<%= @actor.name %>
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Viewing User: <code><%= @actor.name %></code>
</:title>
<:actions>
<.edit_button navigate={~p"/#{@account}/actors/#{@actor.id}/edit"}>
Edit user
</.edit_button>
</:actions>
</.header>
<!-- User Details -->
<div class="bg-white dark:bg-gray-800 overflow-hidden lg:w-3/4 mb-4">
<h5 class="bg-slate-200 p-4 text-2xl font-bold text-gray-900 dark:text-white">User Info</h5>
<.vertical_table>
<.vertical_table_row label_class="w-1/5">
<:label>Name</:label>
<:value><%= @actor.name %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Source</:label>
<:value>TODO: Manually created by Jamil Bou Kheir on May 3rd, 2023.</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Role</:label>
<:value>
<%= account_type_to_string(@actor.type) %>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Groups</:label>
<:value>TODO: Groups Here</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Last Active</:label>
<:value>TODO: Last Active Here</:value>
</.vertical_table_row>
</.vertical_table>
</div>
<div class="bg-white dark:bg-gray-800 overflow-hidden lg:w-3/4">
<h5 class="p-4 text-2xl font-bold bg-slate-200 text-gray-900 dark:text-white">
Authentication Identities
</h5>
<.identity :for={identity <- @actor.identities} class="mb-4">
<:provider><%= identity.provider.name %></:provider>
<:identity><%= identity.provider_identifier %></:identity>
<:last_auth><%= identity.last_seen_at %></:last_auth>
</.identity>
</div>
<.header>
<:title>
Danger zone
</:title>
<:actions>
<.delete_button>
Delete user
</.delete_button>
</:actions>
</.header>
"""
end
attr(:rest, :global)
slot(:provider)
slot(:identity)
slot(:last_auth)
def identity(assigns) do
~H"""
<div {@rest}>
<.vertical_table class="table-fixed">
<.vertical_table_row label_class="w-1/5">
<:label>Provider</:label>
<:value><%= render_slot(@provider) %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Identity</:label>
<:value><%= render_slot(@identity) %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Last Authentication</:label>
<:value><%= render_slot(@last_auth) %></:value>
</.vertical_table_row>
</.vertical_table>
</div>
"""
end
end

View File

@@ -1,6 +1,14 @@
defmodule Web.Devices.Index do
use Web, :live_view
alias Domain.Devices
def mount(_params, _session, socket) do
{_, devices} = Devices.list_devices(socket.assigns.subject, preload: :actor)
{:ok, assign(socket, devices: devices)}
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
@@ -13,264 +21,83 @@ defmodule Web.Devices.Index do
</.header>
<!-- Devices Table -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
<div class="w-full md:w-1/2">
<form class="flex items-center">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<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-gray-500 dark:text-gray-400" />
</div>
<input
type="text"
id="simple-search"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
required=""
/>
</div>
</form>
</div>
<.button_group>
<:first>
All
</:first>
<:middle>
Online
</:middle>
<:last>
Archived
</:last>
</.button_group>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Client
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
User
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Status
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/devices/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
v1.01 Linux
</.link>
</th>
<td class="px-4 py-3">
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
John Doe
</.link>
</td>
<td class="px-4 py-3">
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
Online
</span>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="device-1-dropdown-button"
data-dropdown-toggle="device-1-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="device-1-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="device-1-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Archive
</a>
</li>
</ul>
</div>
</td>
</tr>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/devices/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
v1.01 iOS
</.link>
</th>
<td class="px-4 py-3">
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Steve Johnson
</.link>
</td>
<td class="px-4 py-3">
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-300">
Last seen 2 hours ago
</span>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="device-2-dropdown-button"
data-dropdown-toggle="device-2-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="device-2-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="device-2-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Archive
</a>
</li>
</ul>
</div>
</td>
</tr>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/devices/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
v1.01 macOS
</.link>
</th>
<td class="px-4 py-3">
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Steinberg, Gabriel
</.link>
</td>
<td class="px-4 py-3">
<span class="bg-red-100 text-red-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">
Archived 6 months ago
</span>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="device-3-dropdown-button"
data-dropdown-toggle="device-3-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="device-3-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="device-3-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Archive
</a>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<.resource_filter />
<.table id="devices" rows={@devices} row_id={&"device-#{&1.id}"}>
<:col :let={device} label="CLIENT" sortable="true">
<.link
navigate={~p"/#{@account}/devices/#{device.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= device.name %>
</.link>
</:col>
<:col :let={device} label="USER" sortable="true">
<.link
navigate={~p"/#{@account}/actors/#{device.actor.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= device.actor.name %>
</.link>
</:col>
<:col :let={_device} label="STATUS" sortable="true">
<.badge type="success">
TODO: Online
</.badge>
</:col>
<:action :let={device}>
<.link
navigate={~p"/#{@account}/devices/#{device.id}"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</.link>
</:action>
<:action>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
TODO: Archive
</a>
</:action>
</.table>
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/devices"} />
</div>
"""
end
defp resource_filter(assigns) do
~H"""
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
<div class="w-full md:w-1/2">
<form class="flex items-center">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<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-gray-500 dark:text-gray-400" />
</div>
<input
type="text"
id="simple-search"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
required=""
/>
</div>
</form>
</div>
<.button_group>
<:first>
All
</:first>
<:middle>
Online
</:middle>
<:last>
Archived
</:last>
</.button_group>
</div>
"""
end
end

View File

@@ -1,141 +1,79 @@
defmodule Web.Devices.Show do
use Web, :live_view
alias Domain.Devices
def mount(%{"id" => id} = _params, _session, socket) do
{:ok, device} = Devices.fetch_device_by_id(id, socket.assigns.subject, preload: :actor)
{:ok, assign(socket, device: device)}
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/devices"}>Devices</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/devices/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
Jamil's Macbook Pro
<.breadcrumb path={~p"/#{@account}/devices/#{@device.id}"}>
<%= @device.name %>
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Device details
Device Details
</:title>
</.header>
<!-- Device Details -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<tbody>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
<.vertical_table>
<.vertical_table_row>
<:label>Identifier</:label>
<:value><%= @device.id %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Owner</:label>
<:value>
<.link
navigate={~p"/#{@account}/actors/#{@device.actor.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Identifier
</th>
<td class="px-6 py-4">
BF0F8D31FA89
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
User
</th>
<td class="px-6 py-4">
<.link
navigate={~p"/#{@account}/actors/55DDA8CB-69A7-48FC-9048-639021C205A2"}
class="text-blue-600 hover:underline"
>
Andrew Dryga
</.link>
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
First seen
</th>
<td class="px-6 py-4">
3 days ago in Bangalore, India
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Last seen
</th>
<td class="px-6 py-4">
1 hour ago in San Francisco, CA
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Remote IPv4
</th>
<td class="px-6 py-4">
<code>69.100.123.11</code>
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Remote IPv6
</th>
<td class="px-6 py-4">
<code>2001:0db8:85a3:0000:0000:8a2e:0370:7334</code>
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Transfer
</th>
<td class="px-6 py-4">
4.43 GB up, 1.23 GB down
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Client version
</th>
<td class="px-6 py-4">
v1.01 for macOS/arm64
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
OS version
</th>
<td class="px-6 py-4">
macOS 13.4.1
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Machine type
</th>
<td class="px-6 py-4">
Macbook Pro
</td>
</tr>
</tbody>
</table>
<%= @device.actor.name %>
</.link>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>First Seen</:label>
<:value>TODO</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Last Seen</:label>
<:value>TODO</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Remote IPv4</:label>
<:value><code><%= @device.ipv4 %></code></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Remote IPv6</:label>
<:value><code><%= @device.ipv6 %></code></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Transfer</:label>
<:value>TODO</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Client Version</:label>
<:value>TODO</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>OS Version</:label>
<:value>TODO</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Machine Type</:label>
<:value>TODO</:value>
</.vertical_table_row>
</.vertical_table>
</div>
<.header>

View File

@@ -1,128 +0,0 @@
defmodule Web.Users.Edit do
use Web, :live_view
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Users</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
Jamil Bou Kheir
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}>
Edit
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Editing user <code>Bou Kheir, Jamil</code>
</:title>
</.header>
<!-- Update User -->
<section class="bg-white dark:bg-gray-900">
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit user details</h2>
<form action="#">
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.label for="first-name">
First Name
</.label>
<input
type="text"
name="first-name"
id="first-name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
value="Steve"
required=""
/>
</div>
<div class="w-full">
<.label for="last-name">
Last Name
</.label>
<input
type="text"
name="last-name"
id="last-name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
value="Johnson"
required=""
/>
</div>
<div>
<.label for="email">
Email
</.label>
<input
aria-describedby="email-explanation"
type="email"
name="email"
id="email"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
value="steve@tesla.com"
/>
<p id="email-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
We'll send a confirmation email to both the current email address and the updated one to confirm the change.
</p>
</div>
<div>
<.label for="confirm-email">
Confirm email
</.label>
<input
type="email"
name="confirm-email"
id="confirm-email"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
value="steve@tesla.com"
/>
</div>
<div>
<.label for="user-role">
Role
</.label>
<select
aria-described-by="role-explanation"
id="user-role"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option value="end-user">End user</option>
<option selected value="admin">Admin</option>
</select>
<p id="role-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Select Admin to make this user an administrator of your organization.
</p>
</div>
<div>
<.label for="user-groups">
Groups
</.label>
<select
multiple
aria-described-by="groups-explanation"
id="user-groups"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option selected value="engineering">Engineering</option>
<option value="devops">DevOps</option>
<option selected value="devsecops">DevSecOps</option>
</select>
<p id="groups-explanation" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Select one or more groups to allow this user access to resources.
</p>
</div>
</div>
<div class="flex items-center space-x-4">
<button
type="submit"
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
Save
</button>
</div>
</form>
</div>
</section>
"""
end
end

View File

@@ -1,278 +0,0 @@
defmodule Web.Users.Index do
use Web, :live_view
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Users</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
All users
</:title>
<:actions>
<.add_button navigate={~p"/#{@account}/actors/new"}>
Add a new user
</.add_button>
</:actions>
</.header>
<!-- Users Table -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
<div class="w-full md:w-1/2">
<form class="flex items-center">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<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-gray-500 dark:text-gray-400" />
</div>
<input
type="text"
id="simple-search"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
required=""
/>
</div>
</form>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Name
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Identifiers
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Groups
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<div class="flex items-center">
Last active
<a href="#">
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
</a>
</div>
</th>
<th scope="col" class="px-4 py-3">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Bou Kheir, Jamil
</.link>
</th>
<td class="px-4 py-3">
<strong>email:</strong>jamil@firezone.dev,<strong>okta:</strong>jamil@firezone.dev
</td>
<td class="px-4 py-3">Admin, Engineering, 3 more...</td>
<td class="px-4 py-3">Today at 2:30 pm</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="user-1-dropdown-button"
data-dropdown-toggle="user-1-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="user-1-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="user-1-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Edit
</.link>
</li>
</ul>
<div class="py-1">
<a
href="#"
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>
Delete
</a>
</div>
</div>
</td>
</tr>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Dryga, Andrew
</.link>
</th>
<td class="px-4 py-3">
<strong>email:</strong>a@firezone.dev,<strong>okta:</strong>andrew.dryga@firezone.dev
</td>
<td class="px-4 py-3">Admin, DevOps, Engineering, 3 more...</td>
<td class="px-4 py-3">Just now</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="user-2-dropdown-button"
data-dropdown-toggle="user-2-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="user-2-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="user-2-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Edit
</.link>
</li>
</ul>
<div class="py-1">
<a
href="#"
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>
Delete
</a>
</div>
</div>
</td>
</tr>
<tr class="border-b dark:border-gray-700">
<th
scope="row"
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Steinberg, Gabriel
</.link>
</th>
<td class="px-4 py-3">
<strong>email:</strong>a@firezone.dev,<strong>okta:</strong>andrew.dryga@firezone.dev
</td>
<td class="px-4 py-3">DevOps, IT, Security</td>
<td class="px-4 py-3">A month ago</td>
<td class="px-4 py-3 flex items-center justify-end">
<button
id="user-3-dropdown-button"
data-dropdown-toggle="user-3-dropdown"
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
type="button"
>
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
</button>
<div
id="user-3-dropdown"
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
>
<ul
class="py-1 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="user-3-dropdown-button"
>
<li>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Show
</a>
</li>
<li>
<.link
navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Edit
</.link>
</li>
</ul>
<div class="py-1">
<a
href="#"
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>
Delete
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/actors"} />
</div>
"""
end
end

View File

@@ -1,126 +0,0 @@
defmodule Web.Users.Show do
use Web, :live_view
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/actors"}>Users</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
Jamil Bou Kheir
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Viewing User <code>Bou Kheir, Jamil</code>
</:title>
<:actions>
<.edit_button navigate={~p"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}>
Edit user
</.edit_button>
</:actions>
</.header>
<!-- User Details -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<tbody>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
First name
</th>
<td class="px-6 py-4">
Steve
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Last name
</th>
<td class="px-6 py-4">
Johnson
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Source
</th>
<td class="px-6 py-4">
Manually created by
<a href="#" class="text-blue-600 hover:underline">Jamil Bou Kheir</a>
on May 3rd, 2023.
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Email
</th>
<td class="px-6 py-4">
steve@tesla.com <span class="text-gray-400">- Verified</span>
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Role
</th>
<td class="px-6 py-4">
Admin
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Groups
</th>
<td class="px-6 py-4">
<.link
navigate={~p"/#{@account}/groups/55DDA8CB-69A7-48FC-9048-639021C205A2"}
class="text-blue-600 hover:underline"
>
Engineering
</.link>
</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th
scope="row"
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
>
Last active
</th>
<td class="px-6 py-4">
1 hour ago
</td>
</tr>
</tbody>
</table>
</div>
<.header>
<:title>
Danger zone
</:title>
<:actions>
<.delete_button>
Delete user
</.delete_button>
</:actions>
</.header>
"""
end
end

View File

@@ -100,7 +100,7 @@ defmodule Web.Router do
] do
live "/dashboard", Dashboard
scope "/actors", Users do
scope "/actors", Actors do
live "/", Index
live "/new", New
live "/:id/edit", Edit