neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
CI: an organization's live wall
9ce1a6e · gmorell · 2026-11-03 16:11
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/workflows.ex
+21
−4
@@ -1388,13 +1388,31 @@ defmodule GitGud.Workflows do
| 1388 | 1388 | """ |
| 1389 | 1389 | def list_running_jobs_for_user(user_id, repo_ids) |
| 1390 | 1390 | when is_integer(user_id) and is_list(repo_ids) do |
| 1391 | + running_jobs_query() | |
| 1392 | + |> where([_j, r], r.repository_id in ^repo_ids or r.triggered_by_id == ^user_id) | |
| 1393 | + |> Repo.all() | |
| 1394 | + end | |
| 1395 | + | |
| 1396 | + @doc """ | |
| 1397 | + The jobs running right now in `repo_ids` — an organization's, as far | |
| 1398 | + as the viewer may see them (visibility is the caller's, as for | |
| 1399 | + `list_runs_for_repos/2`). Same order and preloads as | |
| 1400 | + `list_running_jobs_for_user/2`. | |
| 1401 | + """ | |
| 1402 | + def list_running_jobs_for_repos([]), do: [] | |
| 1403 | + | |
| 1404 | + def list_running_jobs_for_repos(repo_ids) when is_list(repo_ids) do | |
| 1405 | + running_jobs_query() | |
| 1406 | + |> where([_j, r], r.repository_id in ^repo_ids) | |
| 1407 | + |> Repo.all() | |
| 1408 | + end | |
| 1409 | + | |
| 1410 | + defp running_jobs_query do | |
| 1391 | 1411 | steps_query = from(s in WorkflowStep, order_by: s.number) |
| 1392 | 1412 | |
| 1393 | 1413 | from(j in WorkflowJob, |
| 1394 | 1414 | join: r in assoc(j, :workflow_run), |
| 1395 | − where: | |
| 1396 | − j.status == "running" and | |
| 1397 | − (r.repository_id in ^repo_ids or r.triggered_by_id == ^user_id), | |
| 1415 | + where: j.status == "running", | |
| 1398 | 1416 | order_by: [asc_nulls_last: j.started_at, asc: j.id], |
| 1399 | 1417 | preload: [ |
| 1400 | 1418 | :runner, |
@@ -1402,7 +1420,6 @@ defmodule GitGud.Workflows do
| 1402 | 1420 | workflow_run: {r, repository: [:owner, :organization]} |
| 1403 | 1421 | ] |
| 1404 | 1422 | ) |
| 1405 | − |> Repo.all() | |
| 1406 | 1423 | end |
| 1407 | 1424 | |
| 1408 | 1425 | @doc """ |
renamed
lib/git_gud_web/live/user_live/ci_wall.ex → lib/git_gud_web/live/ci_wall_live.ex
+93
−37
@@ -1,13 +1,19 @@
| 1 | −defmodule GitGudWeb.UserLive.CiWall do | |
| 1 | +defmodule GitGudWeb.CiWallLive do | |
| 2 | 2 | @moduledoc """ |
| 3 | − The live CI wall: every job running right now in the repositories a | |
| 4 | − user cares about (`Workflows.list_running_jobs_for_user/2`), one card | |
| 5 | − each, tiled to fill the screen below the nav (`CiWallLayout`). | |
| 3 | + The live CI wall: every job running right now, one card each, tiled to | |
| 4 | + fill the screen below the nav (`CiWallLive.Layout`). Two scopes, the | |
| 5 | + same as the two run lists they sit beside: | |
| 6 | 6 | |
| 7 | − `/users/ci/live` puts the longest-running job first, at the left, full | |
| 8 | − height, and the newest stacked at the right; `/users/ci/live/newest` | |
| 9 | − reverses the order, so the newest job gets the room. `r` flips between | |
| 10 | − the two. | |
| 7 | + * `/users/ci/live` — the repositories a user cares about, and runs | |
| 8 | + they triggered (`Workflows.list_running_jobs_for_user/2`), as on | |
| 9 | + My CI | |
| 10 | + * `/orgs/:handle/actions/live` — an organization's repositories the | |
| 11 | + viewer may see (`Repositories.visible_repo_ids_for_org/2`), as on | |
| 12 | + its Actions page, and to whoever may see that page | |
| 13 | + | |
| 14 | + Either puts the longest-running job first, at the left, full height, | |
| 15 | + and the newest stacked at the right; `…/live/newest` reverses the | |
| 16 | + order, so the newest job gets the room. `r` flips between the two. | |
| 11 | 17 | |
| 12 | 18 | Kept live three ways: |
| 13 | 19 |
@@ -27,10 +33,11 @@ defmodule GitGudWeb.UserLive.CiWall do
| 27 | 33 | use GitGudWeb, :live_view |
| 28 | 34 | |
| 29 | 35 | alias GitGud.Git |
| 36 | + alias GitGud.Organizations | |
| 30 | 37 | alias GitGud.Repositories |
| 31 | 38 | alias GitGud.Repositories.Storage |
| 32 | 39 | alias GitGud.Workflows |
| 33 | − alias GitGudWeb.UserLive.CiWallLayout | |
| 40 | + alias GitGudWeb.CiWallLive.Layout | |
| 34 | 41 | |
| 35 | 42 | @max_cols 6 |
| 36 | 43 | @tail_lines 40 |
@@ -38,38 +45,73 @@ defmodule GitGudWeb.UserLive.CiWall do
| 38 | 45 | @flush_ms 200 |
| 39 | 46 | |
| 40 | 47 | @impl true |
| 48 | + def mount(%{"handle" => handle}, _session, socket) do | |
| 49 | + org = Organizations.get_organization_by_handle!(handle) | |
| 50 | + user = socket.assigns.current_scope && socket.assigns.current_scope.user | |
| 51 | + | |
| 52 | + if GitGudWeb.OrgLive.Actions.can_view?(org, user) do | |
| 53 | + base = ~p"/orgs/#{org.handle}/actions/live" | |
| 54 | + | |
| 55 | + {:ok, | |
| 56 | + socket | |
| 57 | + |> assign(:scope, {:repos, Repositories.visible_repo_ids_for_org(org, user)}) | |
| 58 | + |> assign(:org, org) | |
| 59 | + |> assign(:page_title, "Live CI — @#{org.handle}") | |
| 60 | + |> assign(:oldest_path, base) | |
| 61 | + |> assign(:newest_path, base <> "/newest") | |
| 62 | + |> assign(:list_path, ~p"/orgs/#{org.handle}/actions") | |
| 63 | + |> setup()} | |
| 64 | + else | |
| 65 | + {:ok, | |
| 66 | + socket | |
| 67 | + |> put_flash(:error, "You're not authorized to view this organization.") | |
| 68 | + |> push_navigate(to: ~p"/")} | |
| 69 | + end | |
| 70 | + end | |
| 71 | + | |
| 41 | 72 | def mount(_params, _session, socket) do |
| 42 | 73 | user = socket.assigns.current_scope.user |
| 43 | 74 | |
| 44 | − if connected?(socket), | |
| 45 | − do: Phoenix.PubSub.subscribe(GitGud.PubSub, Workflows.ci_topic()) | |
| 46 | − | |
| 47 | 75 | {:ok, |
| 48 | 76 | socket |
| 49 | − |> assign(:user, user) | |
| 50 | − |> assign(:repo_ids, Repositories.relevant_repo_ids_for_user(user)) | |
| 77 | + |> assign(:scope, {:user, user.id, Repositories.relevant_repo_ids_for_user(user)}) | |
| 78 | + |> assign(:org, nil) | |
| 51 | 79 | |> assign(:page_title, "Live CI") |
| 52 | − |> assign(:jobs, []) | |
| 53 | − |> assign(:tails, %{}) | |
| 54 | − |> assign(:step_jobs, %{}) | |
| 55 | − |> assign(:run_topics, MapSet.new()) | |
| 56 | − |> assign(:dirty_tails, MapSet.new()) | |
| 57 | − |> assign(:reload_pending?, false) | |
| 58 | − |> assign(:flush_pending?, false) | |
| 59 | − |> assign(:tiling, CiWallLayout.tile(0, @max_cols)) | |
| 60 | − |> assign(:columns, []) | |
| 61 | − |> stream_configure(:cards, dom_id: &"ci-card-#{&1.id}") | |
| 62 | − |> stream(:cards, [])} | |
| 80 | + |> assign(:oldest_path, ~p"/users/ci/live") | |
| 81 | + |> assign(:newest_path, ~p"/users/ci/live/newest") | |
| 82 | + |> assign(:list_path, ~p"/users/ci") | |
| 83 | + |> setup()} | |
| 84 | + end | |
| 85 | + | |
| 86 | + defp setup(socket) do | |
| 87 | + if connected?(socket), | |
| 88 | + do: Phoenix.PubSub.subscribe(GitGud.PubSub, Workflows.ci_topic()) | |
| 89 | + | |
| 90 | + socket | |
| 91 | + |> assign(:jobs, []) | |
| 92 | + |> assign(:tails, %{}) | |
| 93 | + |> assign(:step_jobs, %{}) | |
| 94 | + |> assign(:run_topics, MapSet.new()) | |
| 95 | + |> assign(:dirty_tails, MapSet.new()) | |
| 96 | + |> assign(:reload_pending?, false) | |
| 97 | + |> assign(:flush_pending?, false) | |
| 98 | + |> assign(:tiling, Layout.tile(0, @max_cols)) | |
| 99 | + |> assign(:columns, []) | |
| 100 | + |> stream_configure(:cards, dom_id: &"ci-card-#{&1.id}") | |
| 101 | + |> stream(:cards, []) | |
| 63 | 102 | end |
| 64 | 103 | |
| 65 | 104 | @impl true |
| 66 | − def handle_params(_params, _uri, socket) do | |
| 105 | + def handle_params(_params, _uri, %{assigns: %{scope: _}} = socket) do | |
| 67 | 106 | {:noreply, socket |> assign(:order, socket.assigns.live_action) |> load()} |
| 68 | 107 | end |
| 69 | 108 | |
| 109 | + # Turned away in `mount/3`, on its way out. | |
| 110 | + def handle_params(_params, _uri, socket), do: {:noreply, socket} | |
| 111 | + | |
| 70 | 112 | @impl true |
| 71 | 113 | def handle_event("key", %{"key" => key}, socket) when key in ["r", "R"], |
| 72 | − do: {:noreply, push_patch(socket, to: flipped_path(socket.assigns.order))} | |
| 114 | + do: {:noreply, push_patch(socket, to: flipped_path(socket.assigns))} | |
| 73 | 115 | |
| 74 | 116 | def handle_event("key", _params, socket), do: {:noreply, socket} |
| 75 | 117 |
@@ -130,9 +172,9 @@ defmodule GitGudWeb.UserLive.CiWall do
| 130 | 172 | # follow the jobs shown, and a job new to the wall starts with its log |
| 131 | 173 | # tail as stored. |
| 132 | 174 | defp load(socket) do |
| 133 | − %{user: user, repo_ids: repo_ids, tails: tails} = socket.assigns | |
| 175 | + %{scope: scope, tails: tails} = socket.assigns | |
| 134 | 176 | |
| 135 | − jobs = Workflows.list_running_jobs_for_user(user.id, repo_ids) | |
| 177 | + jobs = running_jobs(scope) | |
| 136 | 178 | jobs = if socket.assigns.order == :newest, do: Enum.reverse(jobs), else: jobs |
| 137 | 179 | |
| 138 | 180 | tails = |
@@ -140,7 +182,7 @@ defmodule GitGudWeb.UserLive.CiWall do
| 140 | 182 | {job.id, Map.get_lazy(tails, job.id, fn -> stored_tail(job) end)} |
| 141 | 183 | end) |
| 142 | 184 | |
| 143 | − tiling = CiWallLayout.tile(length(jobs), @max_cols) | |
| 185 | + tiling = Layout.tile(length(jobs), @max_cols) | |
| 144 | 186 | |
| 145 | 187 | socket = |
| 146 | 188 | socket |
@@ -149,7 +191,7 @@ defmodule GitGudWeb.UserLive.CiWall do
| 149 | 191 | |> assign(:tails, tails) |
| 150 | 192 | |> assign(:step_jobs, for(j <- jobs, s <- j.steps, into: %{}, do: {s.id, j.id})) |
| 151 | 193 | |> assign(:tiling, tiling) |
| 152 | − |> assign(:columns, CiWallLayout.column_counts(length(jobs), @max_cols)) | |
| 194 | + |> assign(:columns, Layout.column_counts(length(jobs), @max_cols)) | |
| 153 | 195 | |
| 154 | 196 | cards = |
| 155 | 197 | jobs |> Enum.zip(tiling.spans) |> Enum.map(fn {job, span} -> card(socket, job, span) end) |
@@ -204,8 +246,13 @@ defmodule GitGudWeb.UserLive.CiWall do
| 204 | 246 | |
| 205 | 247 | defp line(_), do: "" |
| 206 | 248 | |
| 207 | − defp flipped_path(:newest), do: ~p"/users/ci/live" | |
| 208 | − defp flipped_path(_oldest), do: ~p"/users/ci/live/newest" | |
| 249 | + defp running_jobs({:user, user_id, repo_ids}), | |
| 250 | + do: Workflows.list_running_jobs_for_user(user_id, repo_ids) | |
| 251 | + | |
| 252 | + defp running_jobs({:repos, repo_ids}), do: Workflows.list_running_jobs_for_repos(repo_ids) | |
| 253 | + | |
| 254 | + defp flipped_path(%{order: :newest, oldest_path: path}), do: path | |
| 255 | + defp flipped_path(%{newest_path: path}), do: path | |
| 209 | 256 | |
| 210 | 257 | @impl true |
| 211 | 258 | def render(assigns) do |
@@ -228,7 +275,16 @@ defmodule GitGudWeb.UserLive.CiWall do
| 228 | 275 | if(@jobs == [], do: "bg-base-content/30", else: "bg-info") |
| 229 | 276 | ]} /> |
| 230 | 277 | </span> |
| 231 | − <h1 class="font-semibold tracking-tight">Live CI</h1> | |
| 278 | + <h1 class="font-semibold tracking-tight"> | |
| 279 | + Live CI | |
| 280 | + <.link | |
| 281 | + :if={@org} | |
| 282 | + navigate={~p"/orgs/#{@org.handle}"} | |
| 283 | + class="font-mono font-normal opacity-60 link link-hover ml-1" | |
| 284 | + > | |
| 285 | + @{@org.handle} | |
| 286 | + </.link> | |
| 287 | + </h1> | |
| 232 | 288 | <span id="ci-wall-count" class="text-sm tabular-nums opacity-60"> |
| 233 | 289 | {running_label(length(@jobs))} |
| 234 | 290 | </span> |
@@ -257,7 +313,7 @@ defmodule GitGudWeb.UserLive.CiWall do
| 257 | 313 | > |
| 258 | 314 | <.link |
| 259 | 315 | id="ci-wall-oldest" |
| 260 | − patch={~p"/users/ci/live"} | |
| 316 | + patch={@oldest_path} | |
| 261 | 317 | aria-current={@order == :oldest && "true"} |
| 262 | 318 | class={order_class(@order == :oldest)} |
| 263 | 319 | > |
@@ -265,7 +321,7 @@ defmodule GitGudWeb.UserLive.CiWall do
| 265 | 321 | </.link> |
| 266 | 322 | <.link |
| 267 | 323 | id="ci-wall-newest" |
| 268 | − patch={~p"/users/ci/live/newest"} | |
| 324 | + patch={@newest_path} | |
| 269 | 325 | aria-current={@order == :newest && "true"} |
| 270 | 326 | class={order_class(@order == :newest)} |
| 271 | 327 | > |
@@ -274,7 +330,7 @@ defmodule GitGudWeb.UserLive.CiWall do
| 274 | 330 | </div> |
| 275 | 331 | <kbd class="hidden lg:inline kbd kbd-xs opacity-60" title="Press r to reverse">r</kbd> |
| 276 | 332 | <.link |
| 277 | − navigate={~p"/users/ci"} | |
| 333 | + navigate={@list_path} | |
| 278 | 334 | id="ci-wall-list" |
| 279 | 335 | class="btn btn-ghost btn-xs gap-1 opacity-70 hover:opacity-100" |
| 280 | 336 | > |
renamed
lib/git_gud_web/live/user_live/ci_wall_layout.ex → lib/git_gud_web/live/ci_wall_live/layout.ex
+1
−1
@@ -1,4 +1,4 @@
| 1 | −defmodule GitGudWeb.UserLive.CiWallLayout do | |
| 1 | +defmodule GitGudWeb.CiWallLive.Layout do | |
| 2 | 2 | @moduledoc """ |
| 3 | 3 | How the live CI wall tiles `n` cards across at most `max_cols` |
| 4 | 4 | columns. |
modified
lib/git_gud_web/live/org_live/actions.ex
+16
−6
@@ -57,11 +57,12 @@ defmodule GitGudWeb.OrgLive.Actions do
| 57 | 57 | defp pad(n) when n < 10, do: "0#{n}" |
| 58 | 58 | defp pad(n), do: "#{n}" |
| 59 | 59 | |
| 60 | − defp can_view?(%{visibility: "public"}, _user), do: true | |
| 61 | − defp can_view?(%{visibility: "internal"}, %{} = _user), do: true | |
| 62 | − defp can_view?(%{visibility: "internal"}, nil), do: false | |
| 63 | − defp can_view?(%{visibility: "private"} = org, %{} = user), do: Organizations.member?(org, user) | |
| 64 | − defp can_view?(_, _), do: false | |
| 60 | + @doc "Whether `user` (or nobody, signed out) may see the org's CI — here and on its live wall." | |
| 61 | + def can_view?(%{visibility: "public"}, _user), do: true | |
| 62 | + def can_view?(%{visibility: "internal"}, %{} = _user), do: true | |
| 63 | + def can_view?(%{visibility: "internal"}, nil), do: false | |
| 64 | + def can_view?(%{visibility: "private"} = org, %{} = user), do: Organizations.member?(org, user) | |
| 65 | + def can_view?(_, _), do: false | |
| 65 | 66 | |
| 66 | 67 | defp load_runs(socket) do |
| 67 | 68 | repo_ids = socket.assigns.repo_ids |
@@ -133,7 +134,16 @@ defmodule GitGudWeb.OrgLive.Actions do
| 133 | 134 | @{@org.handle} |
| 134 | 135 | </.link> |
| 135 | 136 | </h1> |
| 136 | − <.filter_tabs current={@filter} active_count={@active_count} /> | |
| 137 | + <div class="flex items-center gap-3"> | |
| 138 | + <.link | |
| 139 | + navigate={~p"/orgs/#{@org.handle}/actions/live"} | |
| 140 | + id="org-ci-live-wall-link" | |
| 141 | + class="btn btn-sm btn-ghost gap-1" | |
| 142 | + > | |
| 143 | + <.icon name="hero-squares-2x2-micro" class="size-4" /> Live wall | |
| 144 | + </.link> | |
| 145 | + <.filter_tabs current={@filter} active_count={@active_count} /> | |
| 146 | + </div> | |
| 137 | 147 | </header> |
| 138 | 148 | |
| 139 | 149 | <section :if={@duration_series != []} class="rounded-box border border-base-300 p-3"> |
modified
lib/git_gud_web/router.ex
+6
−2
@@ -245,8 +245,8 @@ defmodule GitGudWeb.Router do
| 245 | 245 | live "/orgs/:handle/settings/audit/:category/*refine", AuditLive.Org, :index |
| 246 | 246 | live "/users/ci", UserLive.Ci, :index |
| 247 | 247 | # The live wall of running jobs; the second reverses its order. |
| 248 | − live "/users/ci/live", UserLive.CiWall, :oldest | |
| 249 | − live "/users/ci/live/newest", UserLive.CiWall, :newest | |
| 248 | + live "/users/ci/live", CiWallLive, :oldest | |
| 249 | + live "/users/ci/live/newest", CiWallLive, :newest | |
| 250 | 250 | live "/r/:owner/:name/fork", RepoLive.Fork, :new |
| 251 | 251 | live "/orgs", OrgLive.Index, :index |
| 252 | 252 | live "/orgs/new", OrgLive.New, :new |
@@ -310,6 +310,10 @@ defmodule GitGudWeb.Router do
| 310 | 310 | live "/u/:handle/syms/:sym", SymsLive.Show, :show |
| 311 | 311 | live "/orgs/:handle", OrgLive.Show, :show |
| 312 | 312 | live "/orgs/:handle/actions", OrgLive.Actions, :index |
| 313 | + # Its live wall of running jobs, for whoever may see the page above | |
| 314 | + # (a public org's to anyone); the second reverses its order. | |
| 315 | + live "/orgs/:handle/actions/live", CiWallLive, :oldest | |
| 316 | + live "/orgs/:handle/actions/live/newest", CiWallLive, :newest | |
| 313 | 317 | live "/r/:owner/:name", RepoLive.Show, :show |
| 314 | 318 | live "/r/:owner/:name/tree/:ref/*path", RepoLive.Tree, :show |
| 315 | 319 | live "/r/:owner/:name/blob/:ref/*path", RepoLive.Blob, :show |
added
test/git_gud_web/live/ci_wall_live/layout_test.exs
+41
−0
@@ -0,0 +1,41 @@
| 1 | +defmodule GitGudWeb.CiWallLive.LayoutTest do | |
| 2 | + use ExUnit.Case, async: true | |
| 3 | + | |
| 4 | + alias GitGudWeb.CiWallLive.Layout | |
| 5 | + | |
| 6 | + test "no cards, no grid" do | |
| 7 | + assert Layout.tile(0, 6) == %{cols: 0, rows: 0, spans: []} | |
| 8 | + end | |
| 9 | + | |
| 10 | + test "up to six cards, each a full-height column" do | |
| 11 | + for n <- 1..6 do | |
| 12 | + assert Layout.tile(n, 6) == %{cols: n, rows: 1, spans: List.duplicate(1, n)} | |
| 13 | + end | |
| 14 | + end | |
| 15 | + | |
| 16 | + test "the seventh card pairs the rightmost column; the oldest keep full height" do | |
| 17 | + assert Layout.tile(7, 6) == %{cols: 6, rows: 2, spans: [2, 2, 2, 2, 2, 1, 1]} | |
| 18 | + assert Layout.column_counts(7, 6) == [1, 1, 1, 1, 1, 2] | |
| 19 | + end | |
| 20 | + | |
| 21 | + test "pairs fill in from the right until every column holds two" do | |
| 22 | + assert Layout.tile(11, 6).spans == [2] ++ List.duplicate(1, 10) | |
| 23 | + assert Layout.tile(12, 6) == %{cols: 6, rows: 2, spans: List.duplicate(1, 12)} | |
| 24 | + end | |
| 25 | + | |
| 26 | + test "past that, threes fill in from the right the same way" do | |
| 27 | + assert Layout.tile(13, 6) == | |
| 28 | + %{cols: 6, rows: 6, spans: List.duplicate(3, 10) ++ [2, 2, 2]} | |
| 29 | + | |
| 30 | + assert Layout.column_counts(13, 6) == [2, 2, 2, 2, 2, 3] | |
| 31 | + end | |
| 32 | + | |
| 33 | + test "every column is filled exactly" do | |
| 34 | + for n <- 1..40 do | |
| 35 | + %{cols: cols, rows: rows, spans: spans} = Layout.tile(n, 6) | |
| 36 | + assert length(spans) == n | |
| 37 | + assert Enum.sum(spans) == cols * rows | |
| 38 | + assert Enum.sum(Layout.column_counts(n, 6)) == n | |
| 39 | + end | |
| 40 | + end | |
| 41 | +end |
added
test/git_gud_web/live/ci_wall_live/org_test.exs
+132
−0
@@ -0,0 +1,132 @@
| 1 | +defmodule GitGudWeb.CiWallLive.OrgTest do | |
| 2 | + @moduledoc """ | |
| 3 | + `/orgs/:handle/actions/live` — the live CI wall, narrowed to one | |
| 4 | + organization, for whoever may see its Actions page. What matters is | |
| 5 | + what leaks: jobs from outside the org, and jobs in the org's | |
| 6 | + repositories the viewer may not see. | |
| 7 | + """ | |
| 8 | + use GitGudWeb.ConnCase, async: false | |
| 9 | + | |
| 10 | + import GitGud.AccountsFixtures | |
| 11 | + import GitGud.ForgeFixtures | |
| 12 | + import Phoenix.LiveViewTest | |
| 13 | + | |
| 14 | + alias GitGud.Organizations | |
| 15 | + alias GitGud.Repo | |
| 16 | + alias GitGud.Repositories | |
| 17 | + alias GitGud.Workflows.WorkflowJob | |
| 18 | + alias GitGud.Workflows.WorkflowRun | |
| 19 | + | |
| 20 | + setup do | |
| 21 | + owner = user_fixture() | |
| 22 | + | |
| 23 | + {:ok, org} = | |
| 24 | + Organizations.create_organization(owner, %{ | |
| 25 | + "handle" => "org#{System.unique_integer([:positive])}", | |
| 26 | + "name" => "Test Org" | |
| 27 | + }) | |
| 28 | + | |
| 29 | + {:ok, public} = org_repo(org, owner, "public") | |
| 30 | + {:ok, private} = org_repo(org, owner, "private") | |
| 31 | + | |
| 32 | + {:ok, owner: owner, org: org, public: public, private: private} | |
| 33 | + end | |
| 34 | + | |
| 35 | + defp org_repo(org, owner, visibility), | |
| 36 | + do: | |
| 37 | + Repositories.create_repository_for_org(org, owner, %{ | |
| 38 | + "name" => unique_repo_name(), | |
| 39 | + "visibility" => visibility | |
| 40 | + }) | |
| 41 | + | |
| 42 | + defp running_job(repo) do | |
| 43 | + run = | |
| 44 | + %WorkflowRun{repository_id: repo.id} | |
| 45 | + |> WorkflowRun.create_changeset(%{ | |
| 46 | + number: System.unique_integer([:positive]), | |
| 47 | + workflow_path: ".forgejo/workflows/ci.yml", | |
| 48 | + trigger: "push", | |
| 49 | + head_sha: :crypto.strong_rand_bytes(20) | |
| 50 | + }) | |
| 51 | + |> Ecto.Changeset.change(status: "running") | |
| 52 | + |> Repo.insert!() | |
| 53 | + | |
| 54 | + %WorkflowJob{workflow_run_id: run.id} | |
| 55 | + |> WorkflowJob.create_changeset(%{job_id: "build", name: "build", definition: %{}}) | |
| 56 | + |> Ecto.Changeset.change(status: "running", started_at: DateTime.utc_now(:second)) | |
| 57 | + |> Repo.insert!() | |
| 58 | + end | |
| 59 | + | |
| 60 | + defp card_ids(lv) do | |
| 61 | + lv | |
| 62 | + |> render() | |
| 63 | + |> LazyHTML.from_fragment() | |
| 64 | + |> LazyHTML.query("#ci-wall-cards > article") | |
| 65 | + |> LazyHTML.attribute("id") | |
| 66 | + |> Enum.sort() | |
| 67 | + end | |
| 68 | + | |
| 69 | + defp path(org), do: "/orgs/#{org.handle}/actions/live" | |
| 70 | + | |
| 71 | + test "a member sees every running job in the org, and nothing outside it", %{ | |
| 72 | + conn: conn, | |
| 73 | + owner: owner, | |
| 74 | + org: org, | |
| 75 | + public: public, | |
| 76 | + private: private | |
| 77 | + } do | |
| 78 | + a = running_job(public) | |
| 79 | + b = running_job(private) | |
| 80 | + {_, personal} = repository_fixture(%{owner: owner}) | |
| 81 | + running_job(personal) | |
| 82 | + | |
| 83 | + {:ok, lv, _} = conn |> log_in_user(owner) |> live(path(org)) | |
| 84 | + | |
| 85 | + assert card_ids(lv) == Enum.sort(["ci-card-#{a.id}", "ci-card-#{b.id}"]) | |
| 86 | + assert has_element?(lv, "#ci-wall-list[href='/orgs/#{org.handle}/actions']") | |
| 87 | + end | |
| 88 | + | |
| 89 | + test "someone outside the org sees its public repositories' jobs only", %{ | |
| 90 | + conn: conn, | |
| 91 | + org: org, | |
| 92 | + public: public, | |
| 93 | + private: private | |
| 94 | + } do | |
| 95 | + a = running_job(public) | |
| 96 | + running_job(private) | |
| 97 | + | |
| 98 | + {:ok, lv, _} = conn |> log_in_user(user_fixture()) |> live(path(org)) | |
| 99 | + assert card_ids(lv) == ["ci-card-#{a.id}"] | |
| 100 | + | |
| 101 | + {:ok, lv, _} = live(conn, path(org)) | |
| 102 | + assert card_ids(lv) == ["ci-card-#{a.id}"] | |
| 103 | + end | |
| 104 | + | |
| 105 | + test "the order toggle stays within the org", %{ | |
| 106 | + conn: conn, | |
| 107 | + owner: owner, | |
| 108 | + org: org, | |
| 109 | + public: public | |
| 110 | + } do | |
| 111 | + running_job(public) | |
| 112 | + {:ok, lv, _} = conn |> log_in_user(owner) |> live(path(org)) | |
| 113 | + | |
| 114 | + render_keyup(lv, "key", %{"key" => "r"}) | |
| 115 | + assert_patch(lv, path(org) <> "/newest") | |
| 116 | + | |
| 117 | + render_keyup(lv, "key", %{"key" => "r"}) | |
| 118 | + assert_patch(lv, path(org)) | |
| 119 | + end | |
| 120 | + | |
| 121 | + test "a private org's wall turns away a non-member", %{conn: conn, owner: owner} do | |
| 122 | + {:ok, secret} = | |
| 123 | + Organizations.create_organization(owner, %{ | |
| 124 | + "handle" => "secret#{System.unique_integer([:positive])}", | |
| 125 | + "name" => "Secret", | |
| 126 | + "visibility" => "private" | |
| 127 | + }) | |
| 128 | + | |
| 129 | + assert {:error, {:live_redirect, %{to: "/"}}} = | |
| 130 | + conn |> log_in_user(user_fixture()) |> live(path(secret)) | |
| 131 | + end | |
| 132 | +end |
renamed
test/git_gud_web/live/user_live/ci_wall_test.exs → test/git_gud_web/live/ci_wall_live/user_test.exs
+1
−1
@@ -1,4 +1,4 @@
| 1 | −defmodule GitGudWeb.UserLive.CiWallTest do | |
| 1 | +defmodule GitGudWeb.CiWallLive.UserTest do | |
| 2 | 2 | @moduledoc """ |
| 3 | 3 | The live CI wall shows the jobs running in a user's repositories, one |
| 4 | 4 | card each, keeps up as jobs start, finish and log, and reverses its |
deleted
test/git_gud_web/live/user_live/ci_wall_layout_test.exs
+0
−41
@@ -1,41 +0,0 @@
| 1 | −defmodule GitGudWeb.UserLive.CiWallLayoutTest do | |
| 2 | − use ExUnit.Case, async: true | |
| 3 | − | |
| 4 | − alias GitGudWeb.UserLive.CiWallLayout | |
| 5 | − | |
| 6 | − test "no cards, no grid" do | |
| 7 | − assert CiWallLayout.tile(0, 6) == %{cols: 0, rows: 0, spans: []} | |
| 8 | − end | |
| 9 | − | |
| 10 | − test "up to six cards, each a full-height column" do | |
| 11 | − for n <- 1..6 do | |
| 12 | − assert CiWallLayout.tile(n, 6) == %{cols: n, rows: 1, spans: List.duplicate(1, n)} | |
| 13 | − end | |
| 14 | − end | |
| 15 | − | |
| 16 | − test "the seventh card pairs the rightmost column; the oldest keep full height" do | |
| 17 | − assert CiWallLayout.tile(7, 6) == %{cols: 6, rows: 2, spans: [2, 2, 2, 2, 2, 1, 1]} | |
| 18 | − assert CiWallLayout.column_counts(7, 6) == [1, 1, 1, 1, 1, 2] | |
| 19 | − end | |
| 20 | − | |
| 21 | − test "pairs fill in from the right until every column holds two" do | |
| 22 | − assert CiWallLayout.tile(11, 6).spans == [2] ++ List.duplicate(1, 10) | |
| 23 | − assert CiWallLayout.tile(12, 6) == %{cols: 6, rows: 2, spans: List.duplicate(1, 12)} | |
| 24 | − end | |
| 25 | − | |
| 26 | − test "past that, threes fill in from the right the same way" do | |
| 27 | − assert CiWallLayout.tile(13, 6) == | |
| 28 | − %{cols: 6, rows: 6, spans: List.duplicate(3, 10) ++ [2, 2, 2]} | |
| 29 | − | |
| 30 | − assert CiWallLayout.column_counts(13, 6) == [2, 2, 2, 2, 2, 3] | |
| 31 | − end | |
| 32 | − | |
| 33 | − test "every column is filled exactly" do | |
| 34 | − for n <- 1..40 do | |
| 35 | − %{cols: cols, rows: rows, spans: spans} = CiWallLayout.tile(n, 6) | |
| 36 | − assert length(spans) == n | |
| 37 | − assert Enum.sum(spans) == cols * rows | |
| 38 | − assert Enum.sum(CiWallLayout.column_counts(n, 6)) == n | |
| 39 | − end | |
| 40 | − end | |
| 41 | −end |
Parents: 5e0e514