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.Helpers.StatusHelpers 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)" attr :unresolved_alert_count, :integer, default: 0, doc: "number of unresolved alerts for the current organization" 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) # Get unresolved alert count from process dictionary (set/updated in on_mount hook) unresolved_alert_count = Process.get(:unresolved_alert_count, 0) assigns = Map.put(assigns, :unresolved_alert_count, unresolved_alert_count) # 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""" <.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} /> <.live_component module={ToweropsWeb.Live.Components.StatusTitleComponent} id="status-title" organization_id={@current_organization.id} /> <% end %> """ end attr :navigate, :string, required: true attr :active, :boolean, default: false attr :icon, :string, required: true attr :label, :string, required: true attr :badge, :integer, default: 0 defp sidebar_link(assigns) do ~H""" <.link navigate={@navigate} class={[ "relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", if(@active, do: "bg-blue-50 text-blue-700 border-l-2 border-blue-600 dark:bg-blue-900/20 dark:text-blue-400", else: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white" ) ]} data-sidebar-tooltip={@label} > <.icon name={@icon} class="h-5 w-5 shrink-0" /> {@label} <%= if @badge > 0 do %> {if @badge > 99, do: "99+", else: @badge} <% end %> """ 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 """ 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 @doc """ Returns the page title with status emoji prepended based on organization health. """ def status_title(assigns) do page_title = assigns[:page_title] || "TowerOps" organization = get_in(assigns, [:current_scope, Access.key(:organization)]) if organization do StatusHelpers.title_with_status(page_title, organization) else page_title end end end