|
| 1 | +<!-- |
| 2 | +SPDX-FileCopyrightText: 2025 James Harton |
| 3 | +
|
| 4 | +SPDX-License-Identifier: Apache-2.0 |
| 5 | +--> |
| 6 | + |
| 7 | +# How to Customise Dashboard Layout |
| 8 | + |
| 9 | +Create a custom LiveView that uses BB.LiveView components with your own layout. |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +- Completed [Your First Dashboard](../tutorials/01-your-first-dashboard.md) |
| 14 | +- Understanding of Phoenix LiveView |
| 15 | + |
| 16 | +## Step 1: Create a Custom LiveView |
| 17 | + |
| 18 | +Instead of using the built-in dashboard, create your own LiveView that uses individual components: |
| 19 | + |
| 20 | +```elixir |
| 21 | +# lib/my_app_web/live/robot_live.ex |
| 22 | +defmodule MyAppWeb.RobotLive do |
| 23 | + use MyAppWeb, :live_view |
| 24 | + |
| 25 | + alias BB.LiveView.Components.{Safety, JointControl, Visualisation, EventStream} |
| 26 | + |
| 27 | + @impl true |
| 28 | + def mount(_params, _session, socket) do |
| 29 | + robot_module = MyRobot |
| 30 | + robot = robot_module.robot() |
| 31 | + |
| 32 | + # Subscribe to robot events |
| 33 | + if connected?(socket) do |
| 34 | + BB.subscribe(robot_module, [:state_machine]) |
| 35 | + BB.subscribe(robot_module, [:safety]) |
| 36 | + BB.subscribe(robot_module, [:sensor]) |
| 37 | + end |
| 38 | + |
| 39 | + {:ok, assign(socket, |
| 40 | + robot_module: robot_module, |
| 41 | + robot: robot, |
| 42 | + positions: initial_positions(robot) |
| 43 | + )} |
| 44 | + end |
| 45 | + |
| 46 | + @impl true |
| 47 | + def render(assigns) do |
| 48 | + ~H""" |
| 49 | + <div class="grid grid-cols-3 gap-4 p-4"> |
| 50 | + <div class="col-span-1"> |
| 51 | + <.live_component |
| 52 | + module={Safety} |
| 53 | + id="safety" |
| 54 | + robot_module={@robot_module} |
| 55 | + robot={@robot} |
| 56 | + /> |
| 57 | + </div> |
| 58 | + |
| 59 | + <div class="col-span-2"> |
| 60 | + <.live_component |
| 61 | + module={Visualisation} |
| 62 | + id="visualisation" |
| 63 | + robot_module={@robot_module} |
| 64 | + robot={@robot} |
| 65 | + /> |
| 66 | + </div> |
| 67 | + |
| 68 | + <div class="col-span-3"> |
| 69 | + <.live_component |
| 70 | + module={JointControl} |
| 71 | + id="joints" |
| 72 | + robot_module={@robot_module} |
| 73 | + robot={@robot} |
| 74 | + /> |
| 75 | + </div> |
| 76 | + </div> |
| 77 | + """ |
| 78 | + end |
| 79 | + |
| 80 | + @impl true |
| 81 | + def handle_info({:bb, [:sensor | path], %{payload: joint_state}}, socket) do |
| 82 | + joint_name = List.last(path) |
| 83 | + positions = Map.put(socket.assigns.positions, joint_name, hd(joint_state.positions)) |
| 84 | + |
| 85 | + send_update(Visualisation, id: "visualisation", event: {:positions_updated, positions}) |
| 86 | + send_update(JointControl, id: "joints", event: {:positions_updated, positions}) |
| 87 | + |
| 88 | + {:noreply, assign(socket, positions: positions)} |
| 89 | + end |
| 90 | + |
| 91 | + def handle_info({:bb, [:state_machine], %{payload: transition}}, socket) do |
| 92 | + send_update(Safety, id: "safety", event: {:state_changed, transition.to}) |
| 93 | + {:noreply, socket} |
| 94 | + end |
| 95 | + |
| 96 | + def handle_info(_msg, socket), do: {:noreply, socket} |
| 97 | + |
| 98 | + defp initial_positions(robot) do |
| 99 | + robot.joints |
| 100 | + |> Map.keys() |
| 101 | + |> Map.new(fn name -> {name, 0.0} end) |
| 102 | + end |
| 103 | +end |
| 104 | +``` |
| 105 | + |
| 106 | +## Step 2: Route to Your LiveView |
| 107 | + |
| 108 | +Add a route to your custom LiveView: |
| 109 | + |
| 110 | +```elixir |
| 111 | +# lib/my_app_web/router.ex |
| 112 | +scope "/", MyAppWeb do |
| 113 | + pipe_through :browser |
| 114 | + live "/robot", RobotLive |
| 115 | +end |
| 116 | + |
| 117 | +# Still need asset routes for JS/CSS |
| 118 | +import BB.LiveView.Router |
| 119 | +scope "/" do |
| 120 | + bb_dashboard "/__bb_dashboard__", MyRobot # Hidden route just for assets |
| 121 | +end |
| 122 | +``` |
| 123 | + |
| 124 | +Alternatively, serve assets manually by adding the static plug: |
| 125 | + |
| 126 | +```elixir |
| 127 | +# lib/my_app_web/endpoint.ex |
| 128 | +plug BB.LiveView.Plugs.Static |
| 129 | +``` |
| 130 | + |
| 131 | +## Step 3: Handle Component Events |
| 132 | + |
| 133 | +Components communicate through events. Handle them in your LiveView: |
| 134 | + |
| 135 | +```elixir |
| 136 | +@impl true |
| 137 | +def handle_info({:joint_position_changed, positions}, socket) do |
| 138 | + # Slider was moved - update visualisation immediately |
| 139 | + send_update(Visualisation, id: "visualisation", event: {:positions_updated, positions}) |
| 140 | + {:noreply, socket} |
| 141 | +end |
| 142 | + |
| 143 | +def handle_info({:command_result, result}, socket) do |
| 144 | + # Command completed - show notification |
| 145 | + {:noreply, put_flash(socket, :info, "Command result: #{inspect(result)}")} |
| 146 | +end |
| 147 | +``` |
| 148 | + |
| 149 | +## Step 4: Select Components |
| 150 | + |
| 151 | +Use only the components you need: |
| 152 | + |
| 153 | +```elixir |
| 154 | +# Minimal control interface |
| 155 | +def render(assigns) do |
| 156 | + ~H""" |
| 157 | + <div class="flex gap-4"> |
| 158 | + <.live_component module={Safety} id="safety" robot_module={@robot_module} robot={@robot} /> |
| 159 | + <.live_component module={JointControl} id="joints" robot_module={@robot_module} robot={@robot} /> |
| 160 | + </div> |
| 161 | + """ |
| 162 | +end |
| 163 | + |
| 164 | +# Monitoring only (no control) |
| 165 | +def render(assigns) do |
| 166 | + ~H""" |
| 167 | + <div class="grid grid-cols-2 gap-4"> |
| 168 | + <.live_component module={Visualisation} id="vis" robot_module={@robot_module} robot={@robot} /> |
| 169 | + <.live_component module={EventStream} id="events" robot_module={@robot_module} /> |
| 170 | + </div> |
| 171 | + """ |
| 172 | +end |
| 173 | +``` |
| 174 | + |
| 175 | +## Available Components |
| 176 | + |
| 177 | +| Component | Module | Purpose | |
| 178 | +|-----------|--------|---------| |
| 179 | +| Safety | `BB.LiveView.Components.Safety` | Arm/disarm controls | |
| 180 | +| JointControl | `BB.LiveView.Components.JointControl` | Position sliders | |
| 181 | +| Visualisation | `BB.LiveView.Components.Visualisation` | 3D view | |
| 182 | +| EventStream | `BB.LiveView.Components.EventStream` | Message monitor | |
| 183 | +| Command | `BB.LiveView.Components.Command` | Command forms | |
| 184 | +| Parameters | `BB.LiveView.Components.Parameters` | Parameter editor | |
| 185 | + |
| 186 | +## Component Props |
| 187 | + |
| 188 | +All components require: |
| 189 | +- `id` - Unique identifier for LiveComponent |
| 190 | +- `robot_module` - Your robot module (e.g., `MyRobot`) |
| 191 | +- `robot` - The compiled robot struct from `robot_module.robot()` |
| 192 | + |
| 193 | +## Styling |
| 194 | + |
| 195 | +Components use Tailwind CSS classes. Override styles by: |
| 196 | + |
| 197 | +1. Adding custom CSS after the BB assets |
| 198 | +2. Using CSS specificity to override defaults |
| 199 | +3. Wrapping components in custom containers |
| 200 | + |
| 201 | +```heex |
| 202 | +<div class="my-custom-wrapper"> |
| 203 | + <.live_component module={Safety} id="safety" ... /> |
| 204 | +</div> |
| 205 | +``` |
| 206 | + |
| 207 | +## Common Patterns |
| 208 | + |
| 209 | +### Responsive Layout |
| 210 | + |
| 211 | +```heex |
| 212 | +<div class="flex flex-col lg:flex-row gap-4"> |
| 213 | + <div class="lg:w-1/3"> |
| 214 | + <.live_component module={Safety} ... /> |
| 215 | + <.live_component module={JointControl} ... /> |
| 216 | + </div> |
| 217 | + <div class="lg:w-2/3"> |
| 218 | + <.live_component module={Visualisation} ... /> |
| 219 | + </div> |
| 220 | +</div> |
| 221 | +``` |
| 222 | + |
| 223 | +### Tabbed Interface |
| 224 | + |
| 225 | +```heex |
| 226 | +<div> |
| 227 | + <div class="tabs"> |
| 228 | + <button phx-click="tab" phx-value-tab="control">Control</button> |
| 229 | + <button phx-click="tab" phx-value-tab="monitor">Monitor</button> |
| 230 | + </div> |
| 231 | +
|
| 232 | + <%= case @tab do %> |
| 233 | + <% "control" -> %> |
| 234 | + <.live_component module={JointControl} ... /> |
| 235 | + <% "monitor" -> %> |
| 236 | + <.live_component module={EventStream} ... /> |
| 237 | + <% end %> |
| 238 | +</div> |
| 239 | +``` |
| 240 | + |
| 241 | +## Troubleshooting |
| 242 | + |
| 243 | +### Components not updating |
| 244 | + |
| 245 | +Ensure you're: |
| 246 | +1. Subscribed to the correct PubSub channels |
| 247 | +2. Forwarding events with `send_update/3` |
| 248 | +3. Using unique component IDs |
| 249 | + |
| 250 | +### 3D visualisation not rendering |
| 251 | + |
| 252 | +Check that: |
| 253 | +1. The Three.js assets are being served |
| 254 | +2. The component has a container with height |
| 255 | +3. WebGL is available in the browser |
0 commit comments