defmodule ToweropsWeb.Layouts do @moduledoc """ This module holds layouts and related functionality used by your application. """ use ToweropsWeb, :html alias ToweropsWeb.Components.ConsentPrompt alias ToweropsWeb.Components.CookieConsent alias ToweropsWeb.Live.Components.GlobalSearchComponent require Logger # Embed all files in layouts/* within this module. # The default root.html.heex file contains the HTML # skeleton of your application, namely HTML headers # and other static content. embed_templates "layouts/*" @doc """ Renders your app layout. This function is typically invoked from every template, and it often contains your application menu, sidebar, or similar. ## Examples

Content

""" attr :flash, :map, required: true, doc: "the map of flash messages" attr :current_scope, :map, default: nil, doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)" attr :timezone, :string, default: "UTC", doc: "the user's timezone" attr :requires_cookie_consent, :boolean, default: false, doc: "whether to show the cookie consent banner" slot :inner_block, required: true def app(assigns) do # Get cookie consent from process dictionary (set in plug for non-authenticated pages) requires_cookie_consent = Process.get(:requires_cookie_consent, false) assigns = Map.put(assigns, :requires_cookie_consent, requires_cookie_consent) ~H"""
{render_slot(@inner_block)}
<.footer timezone={Map.get(assigns, :timezone, "UTC")} />
<.flash_group flash={@flash} /> """ end @doc """ Renders the authenticated app layout with navigation. Use this for pages that require organization context. ## Examples

Content

""" attr :flash, :map, required: true, doc: "the map of flash messages" attr :current_scope, :map, default: nil, doc: "the current scope (contains user, organization, timezone, and impersonation state)" attr :active_page, :string, default: nil, doc: "the currently active page (dashboard, sites, device, or alerts)" slot :inner_block, required: true def authenticated(assigns) do # Get cookie consent from process dictionary (set in on_mount hook) requires_cookie_consent = Process.get(:requires_cookie_consent, false) assigns = Map.put(assigns, :requires_cookie_consent, requires_cookie_consent) # Extract organization and timezone from scope for template convenience current_organization = assigns[:current_scope] && assigns.current_scope.organization timezone = (assigns[:current_scope] && assigns.current_scope.timezone) || "UTC" # Get policies needing consent from assigns (set in CheckPolicyConsent plug) policies_needing_consent = Map.get(assigns, :policies_needing_consent, []) # Check if this is the help page for banner display always_show_banner = if Map.get(assigns, :active_page) == "help", do: "true", else: "false" assigns = assigns |> Map.put(:current_organization, current_organization) |> Map.put(:timezone, timezone) |> Map.put(:policies_needing_consent, policies_needing_consent) |> Map.put(:always_show_banner, always_show_banner) ~H"""
<%= if @current_scope && @current_scope.impersonating? do %>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" /> Impersonating: {@current_scope.user.email}
<.link href={~p"/admin/impersonate"} method="delete" class="px-3 py-1 bg-white text-yellow-900 hover:bg-yellow-50 rounded font-medium text-sm" > Exit
<% end %>
<.icon name="hero-heart" class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />

Thanks for testing Towerops!

We'd love to hear your feedback, suggestions, or help you with any questions. Please email us at hi@towerops.net for support, feature requests, or just to say hello!

{render_slot(@inner_block)}
<.footer timezone={Map.get(assigns, :timezone, "UTC")} />
<.flash_group flash={@flash} /> <%= if @current_scope && @current_scope.user do %> <% end %> <%= if @current_organization do %> <.live_component module={GlobalSearchComponent} id="global-search" organization_id={@current_organization.id} /> <% end %> """ end attr :navigate, :string, required: true attr :active, :boolean, default: false slot :inner_block, required: true defp nav_link(assigns) do ~H""" <.link navigate={@navigate} class={[ "inline-flex items-center border-b-2 px-2 h-14 sm:h-16 text-sm transition-colors", if(@active, do: "border-gray-900 text-gray-900 font-semibold dark:border-white dark:text-white", else: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 font-medium dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-200" ) ]} > {render_slot(@inner_block)} """ end attr :navigate, :string, required: true attr :active, :boolean, default: false slot :inner_block, required: true defp mobile_nav_link(assigns) do ~H""" <.link navigate={@navigate} class={[ "flex items-center gap-3 rounded-md px-3 py-2 text-base font-medium", if(@active, do: "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white", else: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white" ) ]} > {render_slot(@inner_block)} """ end @doc """ Shows the flash group with standard titles and content. ## Examples <.flash_group flash={@flash} /> """ attr :flash, :map, required: true, doc: "the map of flash messages" attr :id, :string, default: "flash-group", doc: "the optional id of flash container" def flash_group(assigns) do ~H"""
<.flash kind={:info} flash={@flash} /> <.flash kind={:error} flash={@flash} /> <.flash id="client-error" kind={:error} title={gettext("We can't find the internet")} phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")} phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})} hidden > {gettext("Attempting to reconnect")} <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> <.flash id="server-error" kind={:error} title={gettext("Something went wrong!")} phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")} phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})} hidden > {gettext("Attempting to reconnect")} <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
""" end @doc """ Provides dark vs light theme toggle based on themes defined in app.css. See in root.html.heex which applies the theme before page load. """ def theme_toggle(assigns) do ~H"""
""" end @doc """ Renders the admin layout. Use this for admin pages that require superuser access. ## Examples

Admin Content

""" attr :flash, :map, required: true, doc: "the map of flash messages" attr :timezone, :string, default: "UTC", doc: "the user's timezone" attr :requires_cookie_consent, :boolean, default: false, doc: "whether to show the cookie consent banner" slot :inner_block, required: true def admin(assigns) do # Get cookie consent from process dictionary (set in on_mount hook) requires_cookie_consent = Process.get(:requires_cookie_consent, false) assigns = Map.put(assigns, :requires_cookie_consent, requires_cookie_consent) ~H"""
{render_slot(@inner_block)}
<.footer timezone={Map.get(assigns, :timezone, "UTC")} />
<.flash_group flash={@flash} /> """ end @doc """ Renders the application footer with copyright notice and deploy timestamp. """ attr :timezone, :string, default: "UTC" def footer(assigns) do alias ToweropsWeb.TimeHelpers build_time = Towerops.Application.build_timestamp() timezone = Map.get(assigns, :timezone, "UTC") assigns = assigns |> assign(:build_time, build_time) |> assign(:time_ago, TimeHelpers.format_time_ago(build_time)) |> assign(:formatted_time, TimeHelpers.format_iso8601(build_time, timezone)) ~H""" """ end end