mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Refactor Gateway Liveviews to use real data (#1760)
Why: * The previous Gateway 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. Note: These changes are only meant to involve the Gateway views (index/show/edit). More changes to other resources will follow(i.e. Resource, Users, Devices, etc...) --------- Signed-off-by: bmanifold <bmanifold@users.noreply.github.com> Co-authored-by: Andrew Dryga <andrew@dryga.com>
This commit is contained in:
@@ -101,12 +101,18 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_gateway_by_id(id, %Auth.Subject{} = subject) do
|
||||
def fetch_gateway_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()),
|
||||
true <- Validator.valid_uuid?(id) do
|
||||
Gateway.Query.by_id(id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch()
|
||||
|> case do
|
||||
{:ok, gateway} -> {:ok, Repo.preload(gateway, preload)}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
other -> other
|
||||
@@ -121,11 +127,16 @@ defmodule Domain.Gateways do
|
||||
|> Repo.preload(preload)
|
||||
end
|
||||
|
||||
def list_gateways(%Auth.Subject{} = subject) do
|
||||
def list_gateways(%Auth.Subject{} = subject, opts \\ []) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do
|
||||
Gateway.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
{:ok, gateways} =
|
||||
Gateway.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
|
||||
{:ok, Repo.preload(gateways, preload)}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
defmodule Domain.Resources do
|
||||
alias Domain.{Repo, Validator, Auth}
|
||||
alias Domain.Gateways
|
||||
alias Domain.Resources.{Authorizer, Resource}
|
||||
|
||||
def fetch_resource_by_id(id, %Auth.Subject{} = subject) do
|
||||
@@ -46,6 +47,44 @@ defmodule Domain.Resources do
|
||||
end
|
||||
end
|
||||
|
||||
def count_resources_for_gateway(%Gateways.Gateway{} = gateway, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_resources_permission(),
|
||||
Authorizer.view_available_resources_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
count =
|
||||
Resource.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Resource.Query.by_gateway_group_id(gateway.group_id)
|
||||
|> Repo.aggregate(:count)
|
||||
|
||||
{:ok, count}
|
||||
end
|
||||
end
|
||||
|
||||
def list_resources_for_gateway(%Gateways.Gateway{} = gateway, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_resources_permission(),
|
||||
Authorizer.view_available_resources_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
resources =
|
||||
Resource.Query.all()
|
||||
|> Resource.Query.by_account_id(subject.account.id)
|
||||
|> Resource.Query.by_gateway_group_id(gateway.group_id)
|
||||
|> Repo.all()
|
||||
|
||||
{:ok, resources}
|
||||
end
|
||||
end
|
||||
|
||||
def create_resource(attrs, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do
|
||||
changeset = Resource.Changeset.create_changeset(subject.account, attrs)
|
||||
|
||||
@@ -13,4 +13,23 @@ defmodule Domain.Resources.Resource.Query do
|
||||
def by_account_id(queryable \\ all(), account_id) do
|
||||
where(queryable, [resources: resources], resources.account_id == ^account_id)
|
||||
end
|
||||
|
||||
def by_gateway_group_id(queryable \\ all(), gateway_group_id) do
|
||||
queryable
|
||||
|> with_joined_connections()
|
||||
|> where([connections: connections], connections.gateway_group_id == ^gateway_group_id)
|
||||
end
|
||||
|
||||
def with_joined_connections(queryable \\ all()) do
|
||||
with_named_binding(queryable, :connections, fn queryable, binding ->
|
||||
queryable
|
||||
|> join(
|
||||
:inner,
|
||||
[resources: resources],
|
||||
connections in ^Domain.Resources.Connection.Query.all(),
|
||||
on: connections.resource_id == resources.id,
|
||||
as: ^binding
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,9 +2,9 @@ defimpl String.Chars, for: Postgrex.INET do
|
||||
def to_string(%Postgrex.INET{} = inet), do: Domain.Types.INET.to_string(inet)
|
||||
end
|
||||
|
||||
# defimpl Phoenix.HTML.Safe, for: Postgrex.INET do
|
||||
# def to_iodata(%Postgrex.INET{} = inet), do: Domain.Types.INET.to_string(inet)
|
||||
# end
|
||||
defimpl Phoenix.HTML.Safe, for: Postgrex.INET do
|
||||
def to_iodata(%Postgrex.INET{} = inet), do: Domain.Types.INET.to_string(inet)
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Postgrex.INET do
|
||||
def encode(%Postgrex.INET{} = struct, opts) do
|
||||
@@ -16,6 +16,6 @@ defimpl String.Chars, for: Domain.Types.IPPort do
|
||||
def to_string(%Domain.Types.IPPort{} = ip_port), do: Domain.Types.IPPort.to_string(ip_port)
|
||||
end
|
||||
|
||||
# defimpl Phoenix.HTML.Safe, for: Domain.Types.IPPort do
|
||||
# def to_iodata(%Domain.Types.IPPort{} = ip_port), do: Domain.Types.IPPort.to_string(ip_port)
|
||||
# end
|
||||
defimpl Phoenix.HTML.Safe, for: Domain.Types.IPPort do
|
||||
def to_iodata(%Domain.Types.IPPort{} = ip_port), do: Domain.Types.IPPort.to_string(ip_port)
|
||||
end
|
||||
|
||||
@@ -171,7 +171,7 @@ IO.puts("Created gateway groups:")
|
||||
IO.puts(" #{gateway_group.name_prefix} token: #{Gateways.encode_token!(gateway_group_token)}")
|
||||
IO.puts("")
|
||||
|
||||
{:ok, gateway} =
|
||||
{:ok, gateway1} =
|
||||
Gateways.upsert_gateway(gateway_group_token, %{
|
||||
external_id: Ecto.UUID.generate(),
|
||||
name_suffix: "gw-#{Domain.Crypto.rand_string(5)}",
|
||||
@@ -180,12 +180,28 @@ IO.puts("")
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}}
|
||||
})
|
||||
|
||||
{:ok, gateway2} =
|
||||
Gateways.upsert_gateway(gateway_group_token, %{
|
||||
external_id: Ecto.UUID.generate(),
|
||||
name_suffix: "gw-#{Domain.Crypto.rand_string(5)}",
|
||||
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
|
||||
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}}
|
||||
})
|
||||
|
||||
IO.puts("Created gateways:")
|
||||
gateway_name = "#{gateway_group.name_prefix}-#{gateway.name_suffix}"
|
||||
gateway_name = "#{gateway_group.name_prefix}-#{gateway1.name_suffix}"
|
||||
IO.puts(" #{gateway_name}:")
|
||||
IO.puts(" External UUID: #{gateway.external_id}")
|
||||
IO.puts(" Public Key: #{gateway.public_key}")
|
||||
IO.puts(" IPv4: #{gateway.ipv4} IPv6: #{gateway.ipv6}")
|
||||
IO.puts(" External UUID: #{gateway1.external_id}")
|
||||
IO.puts(" Public Key: #{gateway1.public_key}")
|
||||
IO.puts(" IPv4: #{gateway1.ipv4} IPv6: #{gateway1.ipv6}")
|
||||
IO.puts("")
|
||||
|
||||
gateway_name = "#{gateway_group.name_prefix}-#{gateway2.name_suffix}"
|
||||
IO.puts(" #{gateway_name}:")
|
||||
IO.puts(" External UUID: #{gateway1.external_id}")
|
||||
IO.puts(" Public Key: #{gateway2.public_key}")
|
||||
IO.puts(" IPv4: #{gateway2.ipv4} IPv6: #{gateway2.ipv6}")
|
||||
IO.puts("")
|
||||
|
||||
{:ok, dns_resource} =
|
||||
|
||||
@@ -400,6 +400,15 @@ defmodule Domain.GatewaysTest do
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Gateways.Authorizer.manage_gateways_permission()]]}}
|
||||
end
|
||||
|
||||
# TODO: add a test that soft-deleted assocs are not preloaded
|
||||
test "associations are preloaded when opts given", %{account: account, subject: subject} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
{:ok, gateway} = fetch_gateway_by_id(gateway.id, subject, preload: [:group, :account])
|
||||
|
||||
assert Ecto.assoc_loaded?(gateway.group) == true
|
||||
assert Ecto.assoc_loaded?(gateway.account) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_gateways/1" do
|
||||
@@ -438,6 +447,18 @@ defmodule Domain.GatewaysTest do
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Gateways.Authorizer.manage_gateways_permission()]]}}
|
||||
end
|
||||
|
||||
# TODO: add a test that soft-deleted assocs are not preloaded
|
||||
test "associations are preloaded when opts given", %{account: account, subject: subject} do
|
||||
GatewaysFixtures.create_gateway(account: account)
|
||||
GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
{:ok, gateways} = list_gateways(subject, preload: [:group, :account])
|
||||
assert length(gateways) == 2
|
||||
|
||||
assert Enum.all?(gateways, fn g -> Ecto.assoc_loaded?(g.group) end) == true
|
||||
assert Enum.all?(gateways, fn g -> Ecto.assoc_loaded?(g.account) end) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_connected_gateways_for_resource/1" do
|
||||
|
||||
@@ -29,18 +29,15 @@ defmodule Domain.ResourcesTest do
|
||||
end
|
||||
|
||||
test "returns resource when resource exists", %{account: account, subject: subject} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
resource = ResourcesFixtures.create_resource(account: account, gateway: gateway)
|
||||
resource = ResourcesFixtures.create_resource(account: account)
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
end
|
||||
|
||||
test "does not return deleted resources", %{account: account, subject: subject} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
{:ok, resource} =
|
||||
ResourcesFixtures.create_resource(account: account, gateway: gateway)
|
||||
ResourcesFixtures.create_resource(account: account)
|
||||
|> delete_resource(subject)
|
||||
|
||||
assert fetch_resource_by_id(resource.id, subject) == {:error, :not_found}
|
||||
@@ -138,6 +135,172 @@ defmodule Domain.ResourcesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_resources_for_gateway/2" do
|
||||
test "returns empty list when there are no resources associated to gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
assert list_resources_for_gateway(gateway, subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not list resources that are not associated to the gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
ResourcesFixtures.create_resource()
|
||||
|
||||
assert list_resources_for_gateway(gateway, subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not list deleted resources associated to gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|> delete_resource(subject)
|
||||
|
||||
assert list_resources_for_gateway(gateway, subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "returns all resources for a given gateway and account user subject", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
ResourcesFixtures.create_resource(account: account)
|
||||
|
||||
assert {:ok, resources} = list_resources_for_gateway(gateway, subject)
|
||||
assert length(resources) == 2
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage resources", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert list_resources_for_gateway(gateway, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Resources.Authorizer.manage_resources_permission(),
|
||||
Resources.Authorizer.view_available_resources_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_resources_for_gateway/2" do
|
||||
test "returns zero when there are no resources associated to gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
assert count_resources_for_gateway(gateway, subject) == {:ok, 0}
|
||||
end
|
||||
|
||||
test "does not count resources that are not associated to the gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
ResourcesFixtures.create_resource(account: account)
|
||||
|
||||
assert count_resources_for_gateway(gateway, subject) == {:ok, 1}
|
||||
end
|
||||
|
||||
test "does not count deleted resources associated to gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|> delete_resource(subject)
|
||||
|
||||
assert count_resources_for_gateway(gateway, subject) == {:ok, 1}
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage resources",
|
||||
%{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = GatewaysFixtures.create_group(account: account, subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account, group: group)
|
||||
|
||||
ResourcesFixtures.create_resource(
|
||||
account: account,
|
||||
gateway_groups: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert count_resources_for_gateway(gateway, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Resources.Authorizer.manage_resources_permission(),
|
||||
Resources.Authorizer.view_available_resources_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_resource/2" do
|
||||
test "returns changeset error on empty attrs", %{subject: subject} do
|
||||
assert {:error, changeset} = create_resource(%{}, subject)
|
||||
|
||||
@@ -6,6 +6,54 @@ const fs = require("fs")
|
||||
const path = require("path")
|
||||
const defaultTheme = require("tailwindcss/defaultTheme")
|
||||
|
||||
|
||||
const firezoneColors = {
|
||||
// See our brand palette in Figma.
|
||||
// These have been reversed to match Tailwind's default order.
|
||||
|
||||
// primary: orange
|
||||
"heat-wave": {
|
||||
50: "#fff9f5",
|
||||
100: "#fff1e5",
|
||||
200: "#ffddc2",
|
||||
300: "#ffbc85",
|
||||
400: "#ff9a47",
|
||||
450: "#ff7300",
|
||||
500: "#ff7605",
|
||||
600: "#c25700",
|
||||
700: "#7f3900",
|
||||
800: "#5c2900",
|
||||
900: "#331700",
|
||||
},
|
||||
// accent: violet
|
||||
"electric-violet": {
|
||||
50: "#f8f5ff",
|
||||
100: "#ece5ff",
|
||||
200: "#d2c2ff",
|
||||
300: "#a585ff",
|
||||
400: "#7847ff",
|
||||
450: "#5e00d6",
|
||||
500: "#4805ff",
|
||||
600: "#3400c2",
|
||||
700: "#37007f",
|
||||
800: "#28005c",
|
||||
900: "#160033",
|
||||
},
|
||||
// neutral: night-rider
|
||||
"night-rider": {
|
||||
50: "#fcfcfc",
|
||||
100: "#f8f7f7",
|
||||
200: "#ebebea",
|
||||
300: "#dfdedd",
|
||||
400: "#c7c4c2",
|
||||
500: "#a7a3a0",
|
||||
600: "#90867f",
|
||||
700: "#766a60",
|
||||
800: "#4c3e33",
|
||||
900: "#1b140e",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// Use "media" to synchronize dark mode with the OS, "class" to require manual toggle
|
||||
darkMode: "class",
|
||||
@@ -22,17 +70,21 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#FD4F00",
|
||||
primary: {
|
||||
"50": "#eff6ff",
|
||||
"100": "#dbeafe",
|
||||
"200": "#bfdbfe",
|
||||
"300": "#93c5fd",
|
||||
"400": "#60a5fa",
|
||||
"500": "#3b82f6",
|
||||
"600": "#2563eb",
|
||||
"700": "#1d4ed8",
|
||||
"800": "#1e40af", "900": "#1e3a8a"
|
||||
}
|
||||
primary: firezoneColors["heat-wave"],
|
||||
accent: firezoneColors["electric-violet"],
|
||||
neutral: firezoneColors["night-rider"]
|
||||
//primary: {
|
||||
// "50": "#eff6ff",
|
||||
// "100": "#dbeafe",
|
||||
// "200": "#bfdbfe",
|
||||
// "300": "#93c5fd",
|
||||
// "400": "#60a5fa",
|
||||
// "500": "#3b82f6",
|
||||
// "600": "#2563eb",
|
||||
// "700": "#1d4ed8",
|
||||
// "800": "#1e40af",
|
||||
// "900": "#1e3a8a"
|
||||
//}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -99,6 +99,8 @@ defmodule Web do
|
||||
import Phoenix.HTML
|
||||
# Core UI components and translation
|
||||
import Web.CoreComponents
|
||||
import Web.FormComponents
|
||||
import Web.TableComponents
|
||||
import Web.Gettext
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
|
||||
@@ -94,7 +94,10 @@ defmodule Web.CoreComponents do
|
||||
<%= for tab <- @tab do %>
|
||||
<li class="mr-2" role="presentation">
|
||||
<button
|
||||
class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"
|
||||
class={~w[
|
||||
inline-block p-4 border-b-2 border-transparent rounded-t-lg
|
||||
hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300
|
||||
]}
|
||||
id={"#{tab.id}-tab"}
|
||||
data-tabs-target={"##{tab.id}"}
|
||||
type="button"
|
||||
@@ -177,126 +180,39 @@ defmodule Web.CoreComponents do
|
||||
def button_group(assigns) do
|
||||
~H"""
|
||||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-l-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
|
||||
>
|
||||
<button type="button" class={~w[
|
||||
px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200
|
||||
rounded-l-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2
|
||||
focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600
|
||||
dark:text-white dark:hover:text-white dark:hover:bg-gray-600
|
||||
dark:focus:ring-blue-500 dark:focus:text-white
|
||||
]}>
|
||||
<%= render_slot(@first) %>
|
||||
</button>
|
||||
<%= for middle <- @middle do %>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border-t border-b border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
|
||||
>
|
||||
<button type="button" class={~w[
|
||||
px-4 py-2 text-sm font-medium text-gray-900 bg-white border-t border-b
|
||||
border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2
|
||||
focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600
|
||||
dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500
|
||||
dark:focus:text-white
|
||||
]}>
|
||||
<%= render_slot(middle) %>
|
||||
</button>
|
||||
<% end %>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-r-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
|
||||
>
|
||||
<button type="button" class={~w[
|
||||
px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200
|
||||
rounded-r-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2
|
||||
focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600
|
||||
dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500
|
||||
dark:focus:text-white
|
||||
]}>
|
||||
<%= render_slot(@last) %>
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a submit button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.submit_button>
|
||||
Save
|
||||
</.submit_button>
|
||||
"""
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def submit_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-5 py-2.5 mt-4 sm:mt-6 text-sm font-medium text-center text-white bg-primary-700 rounded-lg focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900 hover:bg-primary-800"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a delete button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.delete_button path={Routes.user_path(@conn, :edit, @user.id)}/>
|
||||
Edit user
|
||||
</.delete_button>
|
||||
"""
|
||||
attr :phx_click, :string, doc: "Action to perform when the button is clicked"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def delete_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
class="text-red-600 inline-flex items-center hover:text-white border border-red-600 hover:bg-red-600 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600 dark:focus:ring-red-900"
|
||||
>
|
||||
<!-- XXX: Fix icon for dark mode -->
|
||||
<!-- <.icon name="hero-trash-solid" class="text-red-600 w-5 h-5 mr-1 -ml-1" /> -->
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an add button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.add_button navigate={~p"/users/new"}>
|
||||
Add user
|
||||
</.add_button>
|
||||
"""
|
||||
attr :navigate, :any, required: true, doc: "Path to navigate to"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def add_button(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="flex items-center justify-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-3.5 w-3.5 mr-2" />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an edit button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.edit_button path={Routes.user_path(@conn, :edit, @user.id)}/>
|
||||
Edit user
|
||||
</.edit_button>
|
||||
"""
|
||||
attr :navigate, :any, required: true, doc: "Path to navigate to"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def edit_button(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="flex items-center justify-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800"
|
||||
>
|
||||
<.icon name="hero-pencil-solid" class="h-3.5 w-3.5 mr-2" />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a paginator bar.
|
||||
|
||||
@@ -325,60 +241,66 @@ defmodule Web.CoreComponents do
|
||||
</span>
|
||||
<ul class="inline-flex items-stretch -space-x-px">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center h-full py-1.5 px-3 ml-0 text-gray-500 bg-white rounded-l-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center h-full py-1.5 px-3 ml-0 text-gray-500 bg-white rounded-l-lg
|
||||
border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
<span class="sr-only">Previous</span>
|
||||
<.icon name="hero-chevron-left" class="w-5 h-5" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white border
|
||||
border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
1
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white
|
||||
border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
2
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
aria-current="page"
|
||||
class="flex items-center justify-center text-sm z-10 py-2 px-3 leading-tight text-primary-600 bg-primary-50 border border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<a href="#" aria-current="page" class={~w[
|
||||
flex items-center justify-center text-sm z-10 py-2 px-3 leading-tight text-primary-600 bg-primary-50
|
||||
border border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700
|
||||
dark:bg-gray-700 dark:text-white
|
||||
]}>
|
||||
<%= @page %>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white
|
||||
border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
...
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center text-sm py-2 px-3 leading-tight text-gray-500 bg-white
|
||||
border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
<%= @total_pages %>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center justify-center h-full py-1.5 px-3 leading-tight text-gray-500 bg-white rounded-r-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<a href="#" class={~w[
|
||||
flex items-center justify-center h-full py-1.5 px-3 leading-tight text-gray-500 bg-white rounded-r-lg
|
||||
border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700
|
||||
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
]}>
|
||||
<span class="sr-only">Next</span>
|
||||
<.icon name="hero-chevron-right" class="w-5 h-5" />
|
||||
</a>
|
||||
@@ -564,227 +486,6 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a simple form.
|
||||
|
||||
## Examples
|
||||
|
||||
<.simple_form for={@form} phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:email]} label="Email"/>
|
||||
<.input field={@form[:username]} label="Username" />
|
||||
<:actions>
|
||||
<.button>Save</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
attr :for, :any, required: true, doc: "the datastructure for the form"
|
||||
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(autocomplete name rel action enctype method novalidate target),
|
||||
doc: "the arbitrary HTML attributes to apply to the form tag"
|
||||
|
||||
slot :inner_block, required: true
|
||||
slot :actions, doc: "the slot for form actions, such as a submit button"
|
||||
|
||||
def simple_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||
<div class="space-y-8 bg-white">
|
||||
<%= render_slot(@inner_block, f) %>
|
||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||
<%= render_slot(action, f) %>
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.button>Send!</.button>
|
||||
<.button phx-click="go" class="ml-2">Send!</.button>
|
||||
"""
|
||||
attr :type, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
attr :rest, :global, include: ~w(disabled form name value)
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type={@type}
|
||||
class={[
|
||||
"phx-submit-loading:opacity-75",
|
||||
"text-white bg-primary-600 font-medium rounded-lg text-sm px-5 py-2.5 text-center",
|
||||
"hover:bg-primary-700",
|
||||
"focus:ring-4 focus:outline-none focus:ring-primary-300",
|
||||
"dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
|
||||
"active:text-white/80",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an input with label and error messages.
|
||||
|
||||
A `%Phoenix.HTML.Form{}` and field name may be passed to the input
|
||||
to build input names and error messages, or all the attributes and
|
||||
errors may be passed explicitly.
|
||||
|
||||
## Examples
|
||||
|
||||
<.input field={@form[:email]} type="email" />
|
||||
<.input name="my-input" errors={["oh no!"]} />
|
||||
"""
|
||||
attr :id, :any, default: nil
|
||||
attr :name, :any
|
||||
attr :label, :string, default: nil
|
||||
attr :value, :any
|
||||
|
||||
attr :type, :string,
|
||||
default: "text",
|
||||
values: ~w(checkbox color date datetime-local email file hidden month number password
|
||||
range radio search select tel text textarea time url week)
|
||||
|
||||
attr :field, Phoenix.HTML.FormField,
|
||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||
|
||||
attr :errors, :list, default: []
|
||||
attr :checked, :boolean, doc: "the checked flag for checkbox and radio inputs"
|
||||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(autocomplete cols disabled form list max maxlength min minlength
|
||||
pattern placeholder readonly required rows size step)
|
||||
|
||||
slot :inner_block
|
||||
|
||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns.id || field.id)
|
||||
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||
|> assign_new(:value, fn -> field.value end)
|
||||
|> input()
|
||||
end
|
||||
|
||||
def input(%{type: "radio"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<label class="flex items-center gap-2 text-gray-900 dark:text-gray-300">
|
||||
<input
|
||||
type="radio"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value={@value}
|
||||
checked={@checked}
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "checkbox", value: value} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
|
||||
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
||||
<input type="hidden" name={@name} value="false" />
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
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"
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
||||
</select>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "textarea"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
||||
"min-h-[6rem] border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
||||
"border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
/>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a standard form label.
|
||||
"""
|
||||
@@ -838,82 +539,6 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Renders a table with generic styling.
|
||||
|
||||
## Examples
|
||||
|
||||
<.table id="users" rows={@users}>
|
||||
<:col :let={user} label="id"><%= user.id %></:col>
|
||||
<:col :let={user} label="username"><%= user.username %></:col>
|
||||
</.table>
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
attr :rows, :list, required: true
|
||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
||||
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
|
||||
|
||||
attr :row_item, :any,
|
||||
default: &Function.identity/1,
|
||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
end
|
||||
|
||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||
|
||||
def table(assigns) do
|
||||
assigns =
|
||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
||||
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
|
||||
end
|
||||
|
||||
~H"""
|
||||
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
|
||||
<table class="w-[40rem] mt-11 sm:w-full">
|
||||
<thead class="text-sm text-left leading-6 text-zinc-500">
|
||||
<tr>
|
||||
<th :for={col <- @col} class="p-0 pr-6 pb-4 font-normal"><%= col[:label] %></th>
|
||||
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
id={@id}
|
||||
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
|
||||
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
|
||||
>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
|
||||
<td
|
||||
:for={{col, i} <- Enum.with_index(@col)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
||||
>
|
||||
<div class="block py-4 pr-6">
|
||||
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
||||
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
||||
<%= render_slot(col, @row_item.(row)) %>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td :if={@action != []} class="relative w-14 p-0">
|
||||
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
||||
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
||||
<span
|
||||
:for={action <- @action}
|
||||
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
<%= render_slot(action, @row_item.(row)) %>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a data list.
|
||||
|
||||
@@ -1066,6 +691,27 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :type, :string, default: "default"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def badge(assigns) do
|
||||
colors = %{
|
||||
"success" => "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
|
||||
"danger" => "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
|
||||
"warning" => "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
|
||||
"info" => "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
|
||||
"default" => "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"
|
||||
}
|
||||
|
||||
assigns = assign(assigns, colors: colors)
|
||||
|
||||
~H"""
|
||||
<span class={"text-xs font-medium mr-2 px-2.5 py-0.5 rounded #{@colors[@type]}"}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
@@ -1140,4 +786,38 @@ defmodule Web.CoreComponents do
|
||||
def translate_errors(errors, field) when is_list(errors) do
|
||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a string the represents a relative time for a given Datetime
|
||||
from the current time or a given base time
|
||||
"""
|
||||
attr :relative, DateTime, required: true
|
||||
attr :relative_to, DateTime, required: false, default: DateTime.utc_now()
|
||||
|
||||
def relative_datetime(assigns) do
|
||||
# Note: This code was written with the intent to be replace by the following in the future:
|
||||
# https://github.com/elixir-cldr/cldr_dates_times/blob/main/lib/cldr/datetime/relative.ex
|
||||
diff = DateTime.diff(assigns[:relative_to], assigns[:relative])
|
||||
|
||||
diff_str =
|
||||
cond do
|
||||
diff <= -24 * 3600 -> "in #{div(-diff, 24 * 3600)}day(s)"
|
||||
diff <= -3600 -> "in #{div(-diff, 3600)}hour(s)"
|
||||
diff <= -60 -> "in #{div(-diff, 60)}minute(s)"
|
||||
diff <= -5 -> "in #{-diff}seconds"
|
||||
diff <= 5 -> "now"
|
||||
diff <= 60 -> "#{diff}seconds ago"
|
||||
diff <= 3600 -> "#{div(diff, 60)} minute(s) ago"
|
||||
diff <= 24 * 3600 -> "#{div(diff, 3600)} hour(s) ago"
|
||||
true -> "#{div(diff, 24 * 3600)} day(s) ago"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, diff_str: diff_str)
|
||||
|
||||
~H"""
|
||||
<span>
|
||||
<%= @diff_str %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
defmodule Web.FormComponents do
|
||||
@moduledoc """
|
||||
Provides Form UI components.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Web, :verified_routes
|
||||
import Web.CoreComponents, only: [icon: 1, error: 1, label: 1, translate_error: 1]
|
||||
|
||||
### Inputs ###
|
||||
|
||||
@doc """
|
||||
Renders an input with label and error messages.
|
||||
|
||||
A `%Phoenix.HTML.Form{}` and field name may be passed to the input
|
||||
to build input names and error messages, or all the attributes and
|
||||
errors may be passed explicitly.
|
||||
|
||||
## Examples
|
||||
|
||||
<.input field={@form[:email]} type="email" />
|
||||
<.input name="my-input" errors={["oh no!"]} />
|
||||
"""
|
||||
attr :id, :any, default: nil
|
||||
attr :name, :any
|
||||
attr :label, :string, default: nil
|
||||
attr :value, :any
|
||||
|
||||
attr :type, :string,
|
||||
default: "text",
|
||||
values: ~w(checkbox color date datetime-local email file hidden month number password
|
||||
range radio search select tel text textarea time url week)
|
||||
|
||||
attr :field, Phoenix.HTML.FormField,
|
||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||
|
||||
attr :errors, :list, default: []
|
||||
attr :checked, :boolean, doc: "the checked flag for checkbox and radio inputs"
|
||||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(autocomplete cols disabled form list max maxlength min minlength
|
||||
pattern placeholder readonly required rows size step)
|
||||
|
||||
slot :inner_block
|
||||
|
||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns.id || field.id)
|
||||
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||
|> assign_new(:value, fn -> field.value end)
|
||||
|> input()
|
||||
end
|
||||
|
||||
def input(%{type: "radio"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<label class="flex items-center gap-2 text-gray-900 dark:text-gray-300">
|
||||
<input
|
||||
type="radio"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value={@value}
|
||||
checked={@checked}
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "checkbox", value: value} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
|
||||
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
||||
<input type="hidden" name={@name} value="false" />
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
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"
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
||||
</select>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "textarea"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
||||
"min-h-[6rem] border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
||||
"border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
/>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
### Buttons ###
|
||||
|
||||
@doc """
|
||||
Renders a button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.button>Send!</.button>
|
||||
<.button phx-click="go" class="ml-2">Send!</.button>
|
||||
"""
|
||||
attr :type, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
attr :rest, :global, include: ~w(disabled form name value)
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type={@type}
|
||||
class={[
|
||||
"phx-submit-loading:opacity-75",
|
||||
"text-white bg-primary-600 font-medium rounded-lg text-sm px-5 py-2.5 text-center",
|
||||
"hover:bg-primary-700",
|
||||
"focus:ring-4 focus:outline-none focus:ring-primary-300",
|
||||
"dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
|
||||
"active:text-white/80",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a submit button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.submit_button>
|
||||
Save
|
||||
</.submit_button>
|
||||
"""
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def submit_button(assigns) do
|
||||
~H"""
|
||||
<button type="submit" class={~w[
|
||||
inline-flex items-center px-5 py-2.5 mt-4 sm:mt-6 text-sm font-medium text-center text-white
|
||||
bg-primary-700 rounded-lg focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900
|
||||
hover:bg-primary-800
|
||||
]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a delete button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.delete_button path={Routes.user_path(@conn, :edit, @user.id)}/>
|
||||
Edit user
|
||||
</.delete_button>
|
||||
"""
|
||||
attr :phx_click, :string, doc: "Action to perform when the button is clicked"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def delete_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
class="text-red-600 inline-flex items-center hover:text-white border border-red-600 hover:bg-red-600 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600 dark:focus:ring-red-900"
|
||||
>
|
||||
<!-- XXX: Fix icon for dark mode -->
|
||||
<!-- <.icon name="hero-trash-solid" class="text-red-600 w-5 h-5 mr-1 -ml-1" /> -->
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an add button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.add_button navigate={~p"/users/new"}>
|
||||
Add user
|
||||
</.add_button>
|
||||
"""
|
||||
attr :navigate, :any, required: true, doc: "Path to navigate to"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def add_button(assigns) do
|
||||
~H"""
|
||||
<.link navigate={@navigate} class={~w[
|
||||
flex items-center justify-center text-white bg-primary-500 hover:bg-primary-600
|
||||
focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2
|
||||
dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800
|
||||
]}>
|
||||
<.icon name="hero-plus" class="h-3.5 w-3.5 mr-2" />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an edit button.
|
||||
|
||||
## Examples
|
||||
|
||||
<.edit_button path={Routes.user_path(@conn, :edit, @user.id)}/>
|
||||
Edit user
|
||||
</.edit_button>
|
||||
"""
|
||||
attr :navigate, :any, required: true, doc: "Path to navigate to"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def edit_button(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="flex items-center justify-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800"
|
||||
>
|
||||
<.icon name="hero-pencil-solid" class="h-3.5 w-3.5 mr-2" />
|
||||
<%= render_slot(@inner_block) %>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
### Forms ###
|
||||
|
||||
@doc """
|
||||
Renders a simple form.
|
||||
|
||||
## Examples
|
||||
|
||||
<.simple_form for={@form} phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:email]} label="Email"/>
|
||||
<.input field={@form[:username]} label="Username" />
|
||||
<:actions>
|
||||
<.button>Save</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
attr :for, :any, required: true, doc: "the datastructure for the form"
|
||||
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(autocomplete name rel action enctype method novalidate target),
|
||||
doc: "the arbitrary HTML attributes to apply to the form tag"
|
||||
|
||||
slot :inner_block, required: true
|
||||
slot :actions, doc: "the slot for form actions, such as a submit button"
|
||||
|
||||
def simple_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||
<div class="space-y-8 bg-white">
|
||||
<%= render_slot(@inner_block, f) %>
|
||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||
<%= render_slot(action, f) %>
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
200
elixir/apps/web/lib/web/components/table_components.ex
Normal file
200
elixir/apps/web/lib/web/components/table_components.ex
Normal file
@@ -0,0 +1,200 @@
|
||||
defmodule Web.TableComponents do
|
||||
@moduledoc """
|
||||
Provides Table UI components.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Web, :verified_routes
|
||||
import Web.Gettext
|
||||
import Web.CoreComponents
|
||||
|
||||
@doc ~S"""
|
||||
Renders a table with generic styling.
|
||||
|
||||
## Examples
|
||||
|
||||
<.table id="users" rows={@users}>
|
||||
<:col :let={user} label="id"><%= user.id %></:col>
|
||||
<:col :let={user} label="username"><%= user.username %></:col>
|
||||
</.table>
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
attr :rows, :list, required: true
|
||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
||||
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
|
||||
|
||||
attr :row_item, :any,
|
||||
default: &Function.identity/1,
|
||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
attr :sortable, :string
|
||||
end
|
||||
|
||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||
|
||||
def table(assigns) do
|
||||
assigns =
|
||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
||||
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
|
||||
end
|
||||
|
||||
~H"""
|
||||
<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 :for={col <- @col} class="px-4 py-3">
|
||||
<%= col[:label] %>
|
||||
<.icon
|
||||
:if={col[:sortable] == "true"}
|
||||
name="hero-chevron-up-down-solid"
|
||||
class="w-4 h-4 ml-1"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">
|
||||
<span class="sr-only"><%= gettext("Actions") %></span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="border-b dark:border-gray-700">
|
||||
<td
|
||||
:for={{col, _i} <- Enum.with_index(@col)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={[
|
||||
"px-4 py-3",
|
||||
@row_click && "hover:cursor-pointer"
|
||||
]}
|
||||
>
|
||||
<%= render_slot(col, @row_item.(row)) %>
|
||||
</td>
|
||||
<td :if={@action != []} class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id={"#{@row_id.(row)}-dropdown-button"}
|
||||
data-dropdown-toggle={"#{@row_id.(row)}-dropdown"}
|
||||
class={~w[
|
||||
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={"#{@row_id.(row)}-dropdown" } class={~w[
|
||||
hidden z-10 w-44 bg-white rounded divide-y divide-gray-100
|
||||
shadow border border-gray-300 dark:bg-gray-700 dark:divide-gray-600"
|
||||
]}>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby={"#{@row_id.(row)}-dropdown-button"}
|
||||
>
|
||||
<li :for={action <- @action}>
|
||||
<%= render_slot(action, @row_item.(row)) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Renders a table with groups and generic styling.
|
||||
|
||||
The component is expecting the rows data to be in the form of a list
|
||||
of tuples, where the first element of a given tuple is the group and
|
||||
the second element of the tuple is a list of elements under that group
|
||||
|
||||
## Examples
|
||||
|
||||
<.table_with_groups id="users" rows={@grouped_users}>
|
||||
<:col label="user group"></:col>
|
||||
<:col :let={user} label="id"><%= user.id %></:col>
|
||||
<:col :let={user} label="username"><%= user.username %></:col>
|
||||
</.table>
|
||||
"""
|
||||
|
||||
attr :id, :string, required: true
|
||||
attr :rows, :list, required: true
|
||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
||||
|
||||
attr :row_item, :any,
|
||||
default: &Function.identity/1,
|
||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
attr :sortable, :string
|
||||
end
|
||||
|
||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||
|
||||
def table_with_groups(assigns) do
|
||||
~H"""
|
||||
<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 :for={col <- @col} class="px-4 py-3">
|
||||
<%= col[:label] %>
|
||||
<.icon
|
||||
:if={col[:sortable] == "true"}
|
||||
name="hero-chevron-up-down-solid"
|
||||
class="w-4 h-4 ml-1"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">
|
||||
<span class="sr-only"><%= gettext("Actions") %></span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {group, items} <- @rows do %>
|
||||
<tr class="bg-neutral-300">
|
||||
<td class="px-4 py-2">
|
||||
<%= group.name_prefix %>
|
||||
</td>
|
||||
<td colspan={length(@col)}></td>
|
||||
</tr>
|
||||
<tr :for={item <- items} class="border-b dark:border-gray-700">
|
||||
<td :for={col <- @col} class="px-4 py-3">
|
||||
<%= render_slot(col, item) %>
|
||||
</td>
|
||||
<td :if={@action != []} class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id={"#{@row_id.(item)}-dropdown-button"}
|
||||
data-dropdown-toggle={"#{@row_id.(item)}-dropdown"}
|
||||
class={~w[
|
||||
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={"#{@row_id.(item)}-dropdown" } class={~w[
|
||||
hidden z-10 w-44 bg-white rounded divide-y divide-gray-100
|
||||
shadow border border-gray-300 dark:bg-gray-700 dark:divide-gray-600"
|
||||
]}>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby={"#{@row_id.(item)}-dropdown-button"}
|
||||
>
|
||||
<li :for={action <- @action}>
|
||||
<%= render_slot(action, @row_item.(item)) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,13 @@
|
||||
defmodule Web.GatewaysLive.Edit do
|
||||
use Web, :live_view
|
||||
|
||||
alias Domain.Gateways
|
||||
|
||||
def mount(%{"id" => id} = _params, _session, socket) do
|
||||
{:ok, gateway} = Gateways.fetch_gateway_by_id(id, socket.assigns.subject, preload: :group)
|
||||
{:ok, assign(socket, gateway: gateway)}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.section_header>
|
||||
@@ -9,17 +16,17 @@ defmodule Web.GatewaysLive.Edit do
|
||||
%{label: "Home", path: ~p"/#{@subject.account}/dashboard"},
|
||||
%{label: "Gateways", path: ~p"/#{@subject.account}/gateways"},
|
||||
%{
|
||||
label: "gcp-primary",
|
||||
path: ~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"
|
||||
label: "#{@gateway.name_suffix}",
|
||||
path: ~p"/#{@subject.account}/gateways/#{@gateway.id}"
|
||||
},
|
||||
%{
|
||||
label: "Edit",
|
||||
path: ~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"
|
||||
path: ~p"/#{@subject.account}/gateways/#{@gateway.id}/edit"
|
||||
}
|
||||
]} />
|
||||
</:breadcrumbs>
|
||||
<:title>
|
||||
Editing Gateway <code>gcp-primary</code>
|
||||
Editing Gateway <code><%= @gateway.name_suffix %></code>
|
||||
</:title>
|
||||
</.section_header>
|
||||
|
||||
@@ -38,6 +45,7 @@ defmodule Web.GatewaysLive.Edit do
|
||||
id="gateway-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"
|
||||
required=""
|
||||
value={@gateway.name_suffix}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
defmodule Web.GatewaysLive.Index do
|
||||
use Web, :live_view
|
||||
|
||||
alias Domain.Gateways
|
||||
alias Domain.Resources
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
subject = socket.assigns.subject
|
||||
{:ok, gateways} = Gateways.list_gateways(subject, preload: :group)
|
||||
|
||||
{_, resources} =
|
||||
Enum.map_reduce(gateways, %{}, fn g, acc ->
|
||||
{:ok, count} = Resources.count_resources_for_gateway(g, subject)
|
||||
{count, Map.put(acc, g.id, count)}
|
||||
end)
|
||||
|
||||
grouped_gateways = Enum.group_by(gateways, fn g -> g.group end)
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
grouped_gateways: grouped_gateways,
|
||||
resources: resources
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.section_header>
|
||||
@@ -15,357 +39,95 @@ defmodule Web.GatewaysLive.Index do
|
||||
</:title>
|
||||
<:actions>
|
||||
<.add_button navigate={~p"/#{@subject.account}/gateways/new"}>
|
||||
Add Gateway
|
||||
Add Instance Group
|
||||
</.add_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
<!-- Gateways 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>
|
||||
Deleted
|
||||
</: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">
|
||||
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">
|
||||
Remote IP
|
||||
<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">
|
||||
Linked resources
|
||||
<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"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
hungry-hippo
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">11.231.231.5</code>
|
||||
<code class="block text-xs">2001:0db8:85a3:0000:0000:8a2e:0370:7334</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
GitLab, Jira, Confluence, and 2 more
|
||||
</.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="gateway-1-dropdown-button"
|
||||
data-dropdown-toggle="gateway-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="gateway-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="gateway-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"
|
||||
>
|
||||
Delete
|
||||
</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"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
gcp-primary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">1.1.1.2</code>
|
||||
<code class="block text-xs">2156:0db8:85a3:0000:0000:8a2e:0370:0001</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
10.56.7.0/24
|
||||
</.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="gateway-2-dropdown-button"
|
||||
data-dropdown-toggle="gateway-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="gateway-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="gateway-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"
|
||||
>
|
||||
Delete
|
||||
</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"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
gcp-secondary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">1.1.1.2</code>
|
||||
<code class="block text-xs">2156:0db8:85a3:0000:0000:8a2e:0370:0002</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
10.56.7.0/24
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-yellow-900 dark:text-yellow-300">
|
||||
Last seen 2 hours ago
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="gateway-3-dropdown-button"
|
||||
data-dropdown-toggle="gateway-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="gateway-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="gateway-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"
|
||||
>
|
||||
Delete
|
||||
</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"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
wavering-walrus
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">
|
||||
12.47.11.102
|
||||
</code>
|
||||
<code class="block text-xs">
|
||||
2006:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
</code>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
GitLab, Jira, Confluence, and 2 more
|
||||
</.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">
|
||||
Deleted 6 months ago
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="gateway-4-dropdown-button"
|
||||
data-dropdown-toggle="gateway-4-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="gateway-4-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="gateway-4-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"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<.resource_filter />
|
||||
<.table_with_groups id="grouped-gateways" rows={@grouped_gateways} row_id={&"gateway-#{&1.id}"}>
|
||||
<:col label="INSTANCE GROUP"></:col>
|
||||
<:col :let={gateway} label="INSTANCE">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/#{gateway.id}"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
<%= gateway.name_suffix %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={gateway} label="REMOTE IP">
|
||||
<code class="block text-xs">
|
||||
<%= gateway.ipv4 %>
|
||||
</code>
|
||||
<code class="block text-xs">
|
||||
<%= gateway.ipv6 %>
|
||||
</code>
|
||||
</:col>
|
||||
<:col :let={gateway} label="RESOURCES">
|
||||
<.badge>
|
||||
<%= @resources[gateway.id] || "0" %>
|
||||
</.badge>
|
||||
</:col>
|
||||
<:col :let={_gateway} label="STATUS">
|
||||
<.badge type="success">
|
||||
TODO: Online
|
||||
</.badge>
|
||||
</:col>
|
||||
<:action :let={gateway}>
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/gateways/#{gateway.id}"}
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Show
|
||||
</.link>
|
||||
</:action>
|
||||
<:action :let={_gateway}>
|
||||
<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_with_groups>
|
||||
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@subject.account}/gateways"} />
|
||||
</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>
|
||||
Deleted
|
||||
</:last>
|
||||
</.button_group>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
defmodule Web.GatewaysLive.Show do
|
||||
use Web, :live_view
|
||||
|
||||
alias Domain.Gateways
|
||||
alias Domain.Resources
|
||||
|
||||
def mount(%{"id" => id} = _params, _session, socket) do
|
||||
{:ok, gateway} = Gateways.fetch_gateway_by_id(id, socket.assigns.subject, preload: :group)
|
||||
{:ok, resources} = Resources.list_resources_for_gateway(gateway, socket.assigns.subject)
|
||||
{:ok, assign(socket, gateway: gateway, resources: resources)}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.section_header>
|
||||
@@ -9,21 +18,14 @@ defmodule Web.GatewaysLive.Show do
|
||||
%{label: "Home", path: ~p"/#{@subject.account}/dashboard"},
|
||||
%{label: "Gateways", path: ~p"/#{@subject.account}/gateways"},
|
||||
%{
|
||||
label: "gcp-primary",
|
||||
path: ~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"
|
||||
label: @gateway.name_suffix,
|
||||
path: ~p"/#{@subject.account}/gateways/#{@gateway.id}"
|
||||
}
|
||||
]} />
|
||||
</:breadcrumbs>
|
||||
<:title>
|
||||
Viewing Gateway <code>gcp-primary</code>
|
||||
Gateway: <code><%= @gateway.name_suffix %></code>
|
||||
</:title>
|
||||
<:actions>
|
||||
<.edit_button navigate={
|
||||
~p"/#{@subject.account}/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"
|
||||
}>
|
||||
Edit Gateway
|
||||
</.edit_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
<!-- Gateway details -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden">
|
||||
@@ -34,10 +36,23 @@ defmodule Web.GatewaysLive.Show do
|
||||
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"
|
||||
>
|
||||
Name
|
||||
Instance Group Name
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
gcp-primary
|
||||
<.badge type="info">
|
||||
<%= @gateway.group.name_prefix %>
|
||||
</.badge>
|
||||
</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"
|
||||
>
|
||||
Instance Name
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<%= @gateway.name_suffix %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -48,7 +63,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
Connectivity
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Peer to Peer
|
||||
TODO: Peer to Peer
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -59,7 +74,9 @@ defmodule Web.GatewaysLive.Show do
|
||||
Status
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Online
|
||||
<.badge type="success">
|
||||
TODO: Online
|
||||
</.badge>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -70,7 +87,9 @@ defmodule Web.GatewaysLive.Show do
|
||||
Location
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
San Jose, CA
|
||||
<code>
|
||||
<%= @gateway.last_seen_remote_ip %>
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -81,7 +100,9 @@ defmodule Web.GatewaysLive.Show do
|
||||
Last seen
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
1 hour ago in San Francisco, CA
|
||||
<.relative_datetime relative={@gateway.last_seen_at} />
|
||||
<br />
|
||||
<%= @gateway.last_seen_at %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -92,7 +113,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
Remote IPv4
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code>69.100.123.11</code>
|
||||
<code><%= @gateway.ipv4 %></code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -103,7 +124,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
Remote IPv6
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code>2001:0db8:85a3:0000:0000:8a2e:0370:7334</code>
|
||||
<code><%= @gateway.ipv6 %></code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -114,7 +135,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
Transfer
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
4.43 GB up, 1.23 GB down
|
||||
TODO: 4.43 GB up, 1.23 GB down
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -125,18 +146,10 @@ defmodule Web.GatewaysLive.Show do
|
||||
Gateway version
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
v1.01 for Linux/x86_64
|
||||
</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">
|
||||
Linux 5.10.25-1-MANJARO x86_64
|
||||
<%= "Gateway Version: #{@gateway.last_seen_version}" %>
|
||||
<br />
|
||||
|
||||
<%= "User Agent: #{@gateway.last_seen_user_agent}" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -147,7 +160,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
Deployment method
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Docker
|
||||
TODO: Docker
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -162,52 +175,19 @@ defmodule Web.GatewaysLive.Show do
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-900 uppercase dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Address
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Engineering GitLab
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
gitlab.company.com
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
SJC VPC-1
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
172.16.45.0/24
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<.table id="resources" rows={@resources}>
|
||||
<:col :let={resource} label="NAME">
|
||||
<.link
|
||||
navigate={~p"/#{@subject.account}/resources/#{resource.id}"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
<%= resource.name %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={resource} label="ADDRESS">
|
||||
<%= resource.address %>
|
||||
</:col>
|
||||
</.table>
|
||||
</div>
|
||||
|
||||
<.section_header>
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Web.SettingsLive.IdentityProviders.New.Components do
|
||||
use Phoenix.Component
|
||||
use Web, :verified_routes
|
||||
import Web.CoreComponents
|
||||
import Web.FormComponents
|
||||
|
||||
@doc """
|
||||
Conditionally renders form fields corresponding to a given provisioning strategy type.
|
||||
|
||||
Reference in New Issue
Block a user