Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ config :observer_web,
* [[`PR-32`](https://github.com/thiagoesteves/observer_web/pull/32)] Adding port/process actions.
* [[`PR-33`](https://github.com/thiagoesteves/observer_web/pull/33)] Adding port/process memory monitor.
* [[`PR-34`](https://github.com/thiagoesteves/observer_web/pull/34)] Changing config variable definitions from ObserverWeb.Telemetry to root of observer_web
* [[`PR-35`](https://github.com/thiagoesteves/observer_web/pull/35)] Adding new version feature that will notify users when observer_web versions don't match across nodes.

# 🚀 Previous Releases
* [0.1.12 (2025-10-12)](https://github.com/thiagoesteves/observer_web/blob/v0.1.12/CHANGELOG.md)
Expand Down
2 changes: 1 addition & 1 deletion lib/observer_web/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule ObserverWeb.Application do
# NOTE: DO NOT start these servers when running tests.
if_not_test do
defp telemetry_servers do
[{ObserverWeb.Telemetry.Storage, telemetry_server_config()}]
[ObserverWeb.Version.Server, {ObserverWeb.Telemetry.Storage, telemetry_server_config()}]
end

defp telemetry_server_config do
Expand Down
13 changes: 13 additions & 0 deletions lib/observer_web/version.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule ObserverWeb.Version do
@moduledoc """
This module contains the version context
"""

alias ObserverWeb.Version.Server

### ==========================================================================
### Public API
### ==========================================================================
@spec status :: Server.t()
def status, do: Server.status()
end
102 changes: 102 additions & 0 deletions lib/observer_web/version/server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
defmodule ObserverWeb.Version.Server do
@moduledoc """
Monitors and reports Observer Web versions across connected nodes.
"""

use GenServer

alias ObserverWeb.Rpc

@wait_interval 60_000
@rpc_timeout 1_000

@type t :: %__MODULE__{
status: :ok | :warning | :empty,
local: String.t() | nil,
nodes: tuple() | nil
}

defstruct status: :empty,
local: nil,
nodes: nil

@table_name :observer_web_versions
@key :state

### ==========================================================================
### Callback functions
### ==========================================================================

@spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()}
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end

@impl true
def init(_args) do
:ets.new(@table_name, [:set, :protected, :named_table])

state = %__MODULE__{}
:ets.insert(@table_name, {@key, state})
{:ok, state, {:continue, :check_versions}}
end

@impl true
def handle_continue(:check_versions, _state) do
state = update_versions()
schedule_update()
{:noreply, state}
end

@impl true
def handle_info(:check_versions, _state) do
state = update_versions()
schedule_update()
{:noreply, state}
end

### ==========================================================================
### Public APIs
### ==========================================================================
@spec status :: __MODULE__.t()
def status do
[{_, value}] = :ets.lookup(@table_name, @key)
value
rescue
_ ->
%__MODULE__{}
end

### ==========================================================================
### Private Functions
### ==========================================================================
defp schedule_update,
do: Process.send_after(self(), :check_versions, @wait_interval)

defp update_versions do
local = to_string(Application.spec(:observer_web, :vsn) || "")

nodes =
[Node.self() | Node.list()]
|> Enum.reduce(%{}, fn node, acc ->
case Rpc.call(node, Application, :spec, [:observer_web, :vsn], @rpc_timeout) do
version when is_list(version) ->
Map.put(acc, node, version |> to_string())

_error ->
acc
end
end)

all_same? = Enum.all?(nodes, fn {_node, v} -> v == local end)

state = %__MODULE__{
status: if(all_same?, do: :ok, else: :warning),
local: local,
nodes: nodes
}

:ets.insert(@table_name, {@key, state})
state
end
end
39 changes: 39 additions & 0 deletions lib/web/components/core.ex
Original file line number Diff line number Diff line change
Expand Up @@ -465,4 +465,43 @@ defmodule Observer.Web.Components.Core do
</label>
"""
end

@doc """
Renders a tooltip around a given inner element.

## Assigns
* `:label` - The text to show inside the tooltip.
* `:position` - Optional, one of `:top`, `:bottom`, `:left`, `:right`. Defaults to `:top`.
"""
attr :label, :string, required: true
attr :position, :atom, default: :bottom
slot :inner_block, required: true

def tooltip(assigns) do
~H"""
<div class="relative inline-flex group max-w-full">
{render_slot(@inner_block)}

<div class={
tooltip_position_class(@position) <>
" hidden group-hover:flex opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-white dark:bg-gray-600 text-gray-800 dark:text-gray-200 text-xs rounded py-1 px-2 z-50 max-w-[90vw] text-left whitespace-pre-line break-words"
}>
{@label}
</div>
</div>
"""
end

# Helper to handle tooltip positioning
defp tooltip_position_class(:top),
do: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2"

defp tooltip_position_class(:bottom),
do: "absolute top-full left-1/2 -translate-x-1/2 mt-2"

defp tooltip_position_class(:left),
do: "absolute right-full top-1/2 -translate-y-1/2 mr-2"

defp tooltip_position_class(:right),
do: "absolute left-full top-1/2 -translate-y-1/2 ml-2"
end
14 changes: 14 additions & 0 deletions lib/web/components/icons.ex
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,18 @@ defmodule Observer.Web.Components.Icons do
</.svg_outline>
"""
end

attr :rest, :global

def exclamation_circle(assigns) do
~H"""
<.svg_outline {@rest}>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</.svg_outline>
"""
end
end
4 changes: 4 additions & 0 deletions lib/web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ defmodule Observer.Web.Layouts do
"""
end

attr :params, :map, required: true
attr :page, :atom, required: true
attr :socket, :map, required: true

def nav(assigns) do
~H"""
<nav class="flex space-x-1">
Expand Down
8 changes: 7 additions & 1 deletion lib/web/components/layouts/live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
<.nav socket={@socket} page={@page.name} params={@params} />

<div class="ml-auto flex items-center space-x-3">
<.live_component module={Observer.Web.ThemeComponent} id="theme" theme={@theme} />
<.live_component
module={Observer.Web.SettingsComponent}
id="settings"
user={@user}
theme={@theme}
version={@version}
/>
</div>
</header>
{@inner_content}
Expand Down
4 changes: 3 additions & 1 deletion lib/web/live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Observer.Web.IndexLive do
alias Observer.Web.Apps.Page, as: AppsPage
alias Observer.Web.Metrics.Page, as: MetricsPage
alias Observer.Web.Tracing.Page, as: TracingPage
alias ObserverWeb.Version

@impl Phoenix.LiveView
def mount(params, session, socket) do
Expand All @@ -20,6 +21,7 @@ defmodule Observer.Web.IndexLive do

page = resolve_page(params)
theme = restore_state(socket, "theme", "system")
version = Version.status()

Process.put(:routing, {socket, prefix})

Expand All @@ -28,7 +30,7 @@ defmodule Observer.Web.IndexLive do
|> assign(params: params, page: page)
|> assign(live_path: live_path, live_transport: live_transport)
|> assign(access: access, csp_nonces: csp_nonces, resolver: resolver, user: user)
|> assign(theme: theme)
|> assign(theme: theme, version: version)
|> page.comp.handle_mount()

{:ok, socket}
Expand Down
Loading