Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: show live seed deployment status on deployment show page

Display real-time seed deployment state (pending/downloading/activating/
completed) on the deployment show page. Broadcasts seed status changes
via PubSub so the LiveView updates without page refresh.

sow-72

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+144 -12
+14 -2
apps/sower/lib/sower/orchestration/seed_deployment.ex
··· 36 36 |> changeset(%{state: status.status}) 37 37 |> Repo.update(skip_org_id: true) 38 38 |> case do 39 - {:ok, _} -> {:ok, %{}} 40 - error -> error 39 + {:ok, _} -> 40 + broadcast_seed_status(deployment.sid) 41 + {:ok, %{}} 42 + 43 + error -> 44 + error 41 45 end 42 46 end 43 47 end ··· 101 105 nil -> {:error, :seed_not_in_deployment} 102 106 seed_deployment -> {:ok, seed_deployment} 103 107 end 108 + end 109 + 110 + defp broadcast_seed_status(deployment_sid) do 111 + Phoenix.PubSub.broadcast( 112 + Sower.PubSub, 113 + "deployment:#{deployment_sid}", 114 + {:seed_deployment, :updated} 115 + ) 104 116 end 105 117 end
+39
apps/sower/lib/sower_web/components/sower_components.ex
··· 299 299 end 300 300 301 301 attr :state, :atom, required: true 302 + 303 + def seed_deployment_status(assigns) do 304 + ~H""" 305 + <%= case @state do %> 306 + <% :pending -> %> 307 + <span class="inline-flex items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400"> 308 + <span class="relative flex h-2.5 w-2.5" role="img" aria-label="Pending"> 309 + <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-zinc-400" /> 310 + </span> 311 + Pending 312 + </span> 313 + <% :downloading -> %> 314 + <span class="inline-flex items-center gap-1.5 text-sm text-blue-600 dark:text-blue-400"> 315 + <span class="relative flex h-2.5 w-2.5" role="img" aria-label="Downloading"> 316 + <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-500 opacity-75" /> 317 + <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-blue-500" /> 318 + </span> 319 + Downloading 320 + </span> 321 + <% :activating -> %> 322 + <span class="inline-flex items-center gap-1.5 text-sm text-blue-600 dark:text-blue-400"> 323 + <span class="relative flex h-2.5 w-2.5" role="img" aria-label="Activating"> 324 + <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-500 opacity-75" /> 325 + <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-blue-500" /> 326 + </span> 327 + Activating 328 + </span> 329 + <% :completed -> %> 330 + <span class="inline-flex items-center gap-1.5 text-sm text-green-600 dark:text-green-400"> 331 + <span class="relative flex h-2.5 w-2.5" role="img" aria-label="Completed"> 332 + <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" /> 333 + </span> 334 + Completed 335 + </span> 336 + <% end %> 337 + """ 338 + end 339 + 340 + attr :state, :atom, required: true 302 341 attr :result, :atom, default: nil 303 342 attr :compact, :boolean, default: false 304 343
+19 -10
apps/sower/lib/sower_web/live/deployment_live/show.ex
··· 98 98 result={sd.result} 99 99 compact 100 100 /> 101 + <.seed_deployment_status :if={!sd.result} state={sd.state} /> 101 102 <.button 102 103 :if={sd.log} 103 104 variant={:secondary} ··· 163 164 end 164 165 end 165 166 166 - @impl true 167 + @impl Phoenix.LiveView 167 168 def handle_info({:deployment, _event, %Sower.Orchestration.Deployment{} = deployment}, socket) do 168 - case Orchestration.get_deployment_sid(deployment.sid) do 169 - nil -> 170 - {:noreply, socket} 171 - 172 - deployment -> 173 - deployment = 174 - Sower.Repo.preload(deployment, seed_deployments: :seed, subscriptions: [], garden: []) 169 + {:noreply, refresh_deployment(socket, deployment.sid)} 170 + end 175 171 176 - {:noreply, assign(socket, :deployment, deployment)} 177 - end 172 + def handle_info({:seed_deployment, :updated}, socket) do 173 + {:noreply, refresh_deployment(socket, socket.assigns.deployment.sid)} 178 174 end 179 175 180 176 @impl true ··· 234 230 |> assign(:page_title, "Show Deployment") 235 231 |> assign(:expanded_seed_logs, MapSet.new()) 236 232 |> assign(:retrying, false) 233 + end 234 + 235 + defp refresh_deployment(socket, sid) do 236 + case Orchestration.get_deployment_sid(sid) do 237 + nil -> 238 + socket 239 + 240 + deployment -> 241 + deployment = 242 + Sower.Repo.preload(deployment, seed_deployments: :seed, subscriptions: [], garden: []) 243 + 244 + assign(socket, :deployment, deployment) 245 + end 237 246 end 238 247 end
+72
apps/sower/test/sower_web/live/deployment_live_show_test.exs
··· 226 226 assert_redirect(show_live, ~p"/deployments/#{retried.sid}") 227 227 end 228 228 229 + test "displays seed deployment state", %{conn: conn, user: user} do 230 + Sower.Repo.put_org_id(user.org_id) 231 + garden = garden_fixture() 232 + seed = seed_fixture() 233 + 234 + deployment = 235 + deployment_fixture(%{ 236 + garden_id: garden.id, 237 + seeds: [seed], 238 + subscriptions: [], 239 + state: :acknowledged, 240 + result: nil, 241 + deployed_at: nil 242 + }) 243 + 244 + seed_deployment = 245 + Sower.Repo.get_by!( 246 + Sower.Orchestration.SeedDeployment, 247 + [deployment_id: deployment.id, seed_id: seed.id], 248 + skip_org_id: true 249 + ) 250 + 251 + seed_deployment 252 + |> change(%{state: :downloading}) 253 + |> Sower.Repo.update!(skip_org_id: true) 254 + 255 + {:ok, _show_live, html} = live(conn, ~p"/deployments/#{deployment.sid}") 256 + 257 + assert html =~ "Downloading" 258 + end 259 + 260 + test "updates seed deployment state via PubSub broadcast", %{conn: conn, user: user} do 261 + Sower.Repo.put_org_id(user.org_id) 262 + garden = garden_fixture() 263 + seed = seed_fixture() 264 + 265 + deployment = 266 + deployment_fixture(%{ 267 + garden_id: garden.id, 268 + seeds: [seed], 269 + subscriptions: [], 270 + state: :acknowledged, 271 + result: nil, 272 + deployed_at: nil 273 + }) 274 + 275 + {:ok, show_live, html} = live(conn, ~p"/deployments/#{deployment.sid}") 276 + 277 + assert html =~ "Pending" 278 + 279 + # Update seed deployment state in DB 280 + seed_deployment = 281 + Sower.Repo.get_by!( 282 + Sower.Orchestration.SeedDeployment, 283 + [deployment_id: deployment.id, seed_id: seed.id], 284 + skip_org_id: true 285 + ) 286 + 287 + seed_deployment 288 + |> change(%{state: :activating}) 289 + |> Sower.Repo.update!(skip_org_id: true) 290 + 291 + # Broadcast seed status change 292 + Phoenix.PubSub.broadcast!( 293 + Sower.PubSub, 294 + "deployment:#{deployment.sid}", 295 + {:seed_deployment, :updated} 296 + ) 297 + 298 + assert render(show_live) =~ "Activating" 299 + end 300 + 229 301 test "shows error when retry is already in progress", %{conn: conn, user: user} do 230 302 Sower.Repo.put_org_id(user.org_id) 231 303 garden = garden_fixture()