mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
108
elixir/apps/web/lib/web/live/actors/edit.ex
Normal file
108
elixir/apps/web/lib/web/live/actors/edit.ex
Normal 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
|
||||
128
elixir/apps/web/lib/web/live/actors/index.ex
Normal file
128
elixir/apps/web/lib/web/live/actors/index.ex
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Web.Users.New do
|
||||
defmodule Web.Actors.New do
|
||||
use Web, :live_view
|
||||
|
||||
def render(assigns) do
|
||||
116
elixir/apps/web/lib/web/live/actors/show.ex
Normal file
116
elixir/apps/web/lib/web/live/actors/show.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user