Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

feat: add sorting and pagination to garden list

Replace LiveStream-based garden list with Flop-powered pagination and
sorting. Adds Flop.Schema derive to Garden with name sorting and 20-item
pages. Switches the LiveView from stream assigns to plain assigns with
Flop metadata for pagination controls.

sow-126

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

+86 -29
+1
apps/sower/lib/sower/orchestration.ex
··· 20 20 defdelegate get_garden_sid(sid), to: Garden 21 21 defdelegate list_gardens(), to: Garden 22 22 defdelegate list_gardens_with_latest_deployment(), to: Garden 23 + defdelegate list_gardens_flop(params \\ %{}), to: Garden 23 24 defdelegate update_garden(garden, attrs), to: Garden 24 25 25 26 # Subscription delegates
+30
apps/sower/lib/sower/orchestration/garden.ex
··· 12 12 @derive {Jason.Encoder, only: [:sid, :local_sid]} 13 13 @derive {Phoenix.Param, key: :sid} 14 14 15 + @derive { 16 + Flop.Schema, 17 + filterable: [], 18 + sortable: [:name, :inserted_at], 19 + default_limit: 20, 20 + default_order: %{ 21 + order_by: [:name], 22 + order_directions: [:asc] 23 + } 24 + } 25 + 15 26 schema "gardens" do 16 27 field :sid, SowerClient.Sid, autogenerate: true 17 28 field :name, :string ··· 53 64 select: %{a | latest_deployment: d} 54 65 ) 55 66 |> Repo.all() 67 + end 68 + 69 + def list_gardens_flop(params \\ %{}) do 70 + latest_deployment_query = 71 + from(d in Deployment, 72 + where: d.garden_id == parent_as(:garden).id, 73 + order_by: [desc: d.inserted_at], 74 + limit: 1 75 + ) 76 + 77 + query = 78 + from(a in __MODULE__, 79 + as: :garden, 80 + left_lateral_join: d in subquery(latest_deployment_query), 81 + on: true, 82 + select: %{a | latest_deployment: d} 83 + ) 84 + 85 + Flop.validate_and_run(query, params, for: __MODULE__) 56 86 end 57 87 58 88 def get_garden(
+45 -15
apps/sower/lib/sower_web/live/garden_live/index.ex
··· 6 6 alias Sower.Orchestration.Garden 7 7 alias SowerWeb.Presence 8 8 9 - @impl true 9 + @impl Phoenix.LiveView 10 10 def mount(_params, _session, socket) do 11 11 if connected?(socket) do 12 12 Phoenix.PubSub.subscribe(Sower.PubSub, "garden:presence") 13 13 end 14 14 15 - {:ok, 16 - stream(socket, :gardens, Orchestration.list_gardens_with_latest_deployment()) 17 - |> assign(:garden_presence, Presence.list("garden:presence"))} 15 + {:ok, assign(socket, :garden_presence, Presence.list("garden:presence"))} 18 16 end 19 17 20 - @impl true 18 + @impl Phoenix.LiveView 21 19 def handle_params(params, _url, socket) do 20 + socket = 21 + case Orchestration.list_gardens_flop(params) do 22 + {:ok, {gardens, meta}} -> 23 + assign(socket, gardens: gardens, meta: meta) 24 + 25 + {:error, meta} -> 26 + assign(socket, gardens: [], meta: meta) 27 + end 28 + 22 29 {:noreply, apply_action(socket, socket.assigns.live_action, params)} 23 30 end 24 31 ··· 34 41 |> assign(:garden, nil) 35 42 end 36 43 37 - @impl true 38 - def handle_info({SowerWeb.GardenLive.FormComponent, {:saved, garden}}, socket) do 39 - {:noreply, stream_insert(socket, :gardens, garden)} 44 + @impl Phoenix.LiveView 45 + def handle_info({SowerWeb.GardenLive.FormComponent, {:saved, _garden}}, socket) do 46 + case Orchestration.list_gardens_flop(socket.assigns.meta.flop) do 47 + {:ok, {gardens, meta}} -> 48 + {:noreply, assign(socket, gardens: gardens, meta: meta)} 49 + 50 + {:error, meta} -> 51 + {:noreply, assign(socket, gardens: [], meta: meta)} 52 + end 40 53 end 41 54 42 - @impl true 55 + @impl Phoenix.LiveView 43 56 def handle_info(%Broadcast{topic: "garden:presence", event: "presence_diff"}, socket) do 44 - # update the presence list, then touch the stream to force a table refresh 45 57 socket = 46 - socket 47 - |> assign(:garden_presence, Presence.list("garden:presence")) 48 - |> stream(:gardens, Orchestration.list_gardens_with_latest_deployment()) 58 + case Orchestration.list_gardens_flop(socket.assigns.meta.flop) do 59 + {:ok, {gardens, meta}} -> 60 + assign(socket, 61 + gardens: gardens, 62 + meta: meta, 63 + garden_presence: Presence.list("garden:presence") 64 + ) 65 + 66 + {:error, meta} -> 67 + assign(socket, 68 + gardens: [], 69 + meta: meta, 70 + garden_presence: Presence.list("garden:presence") 71 + ) 72 + end 49 73 50 74 {:noreply, socket} 51 75 end 52 76 53 - @impl true 77 + @impl Phoenix.LiveView 54 78 def handle_event("delete", %{"id" => id}, socket) do 55 79 garden = Orchestration.get_garden!(id) 56 80 {:ok, _} = Orchestration.delete_garden(garden) 57 81 58 - {:noreply, stream_delete(socket, :gardens, garden)} 82 + case Orchestration.list_gardens_flop(socket.assigns.meta.flop) do 83 + {:ok, {gardens, meta}} -> 84 + {:noreply, assign(socket, gardens: gardens, meta: meta)} 85 + 86 + {:error, meta} -> 87 + {:noreply, assign(socket, gardens: [], meta: meta)} 88 + end 59 89 end 60 90 end
+10 -14
apps/sower/lib/sower_web/live/garden_live/index.html.heex
··· 5 5 6 6 <.table 7 7 id="gardens" 8 - rows={@streams.gardens} 9 - row_click={fn {_id, garden} -> JS.navigate(~p"/gardens/#{garden}") end} 8 + rows={@gardens} 9 + row_click={fn garden -> JS.navigate(~p"/gardens/#{garden}") end} 10 + meta={@meta} 11 + path={~p"/gardens"} 10 12 action_hide_on={:sm} 11 13 > 12 - <:col :let={{_id, garden}} label="Name">{garden.name}</:col> 13 - <:col :let={{_id, garden}} label="Online"> 14 + <:col :let={garden} label="Name" field={:name}>{garden.name}</:col> 15 + <:col :let={garden} label="Online"> 14 16 <.online state={garden.sid in Map.keys(@garden_presence)} /> 15 17 </:col> 16 - <:col :let={{_id, garden}} label="Deploy"> 18 + <:col :let={garden} label="Deploy"> 17 19 <.result result={garden.latest_deployment && garden.latest_deployment.result} /> 18 20 </:col> 19 - <:action :let={{_id, garden}}> 21 + <:action :let={garden}> 20 22 <div class="sr-only"> 21 23 <.link navigate={~p"/gardens/#{garden}"}>Show</.link> 22 24 </div> 23 25 </:action> 24 - <:action :let={{id, garden}}> 25 - <.link 26 - phx-click={JS.push("delete", value: %{id: garden.id}) |> hide("##{id}")} 27 - data-confirm="Are you sure?" 28 - > 29 - Delete 30 - </.link> 31 - </:action> 32 26 </.table> 27 + 28 + <.pagination meta={@meta} path={~p"/gardens"} /> 33 29 34 30 <.modal 35 31 :if={@live_action in [:new, :edit]}