Give the repository page the composed repository view

9fc34a2cd254 · AtlantisPleb · · parent 63deb31d1c06

Give the repository page the composed repository view

The page was a stack of cards. It now uses the view the component library
already had: the owner trail, the section bar, the file tree, and the rail
beside it.

Only the sections that exist are drawn. Pull requests, wiki, insights and
settings have no routes, so no tabs are rendered for them, and the Issues tab
carries a count only when there is something to count.

The tree's ref bar states how many branches and tags there are but not how many
commits, because that total is not a bounded query and an invented number
beside two real ones is worse than one missing number. The ref list stays
below: the bar counts refs, and without the list a branch that is not the
default one has no trace anywhere on the page.

Clone moved into the ref bar as a copy control, and stays a card while the
repository is empty -- an empty repository has no ref bar, and telling someone
how to push the first commit is the only thing the page can do for them.

Two dead links removed on the way. The rail linked a guessed `README.md`, so a
repository whose readme is named anything else got a link to a 404; it links
the file that was actually found, under its own name. And `repo_view/1`
defaulted the owner's name to a link to `/OWNER`, which nothing serves -- an
absent path now means the owner is plain text rather than a link to a 404.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents_web/components/ui.ex
  • modified lib/openagents_web/live/code_repo_live.ex
  • modified test/openagents_web/live/code_live_test.exs

Diff

3 files changed, +158 -48

lib/openagents_web/components/ui.ex modified +8 -4

@@ -1114,7 +1114,12 @@ defmodule OpenAgentsWeb.UI do

1114 1114
1115 1115
  attr :owner_path, :string,
1116 1116
    default: nil,
1117
    doc: "where the owner's name leads; `/OWNER` by default"
1117
    doc: """
1118
    Where the owner's name leads. Absent means it leads nowhere and is drawn as
1119
    plain text, because there is not necessarily anything at `/OWNER` -- this
1120
    defaulted to that path, and on a deployment with no namespace page every
1121
    repository header carried a link to a 404.
1122
    """
1118 1123
1119 1124
  attr :visibility, :atom, values: [:public, :private], default: :public
1120 1125
  attr :class, :any, default: nil

@@ -1125,8 +1130,6 @@ defmodule OpenAgentsWeb.UI do

1125 1130
  slot :about, doc: "the trailing rail, normally one `repo_about/1`"
1126 1131
1127 1132
  def repo_view(assigns) do
1128
    assigns = assign(assigns, :owner_path, assigns.owner_path || "/#{assigns.owner}")
1129
1130 1133
    ~H"""
1131 1134
    <div class={["repo-page", @class]} {@rest}>
1132 1135
      <header class="repo-page__identity">

@@ -1138,7 +1141,8 @@ defmodule OpenAgentsWeb.UI do

1138 1141
          aria-hidden="true"
1139 1142
        />
1140 1143
        <.breadcrumb class="repo-page__trail" label={"#{@owner} / #{@repo}"}>
1141
          <:item navigate={@owner_path}>{@owner}</:item>
1144
          <:item :if={@owner_path} navigate={@owner_path}>{@owner}</:item>
1145
          <:item :if={is_nil(@owner_path)}>{@owner}</:item>
1142 1146
          <:item>{@repo}</:item>
1143 1147
        </.breadcrumb>
1144 1148
        <.badge variant={:dim}>{visibility_label(@visibility)}</.badge>
lib/openagents_web/live/code_repo_live.ex modified +145 -43

@@ -42,6 +42,14 @@ defmodule OpenAgentsWeb.CodeRepoLive do

42 42
        _ -> []
43 43
      end
44 44
45
    entries =
46
      case head && Browse.tree(repository, head) do
47
        {:ok, entries} -> entries
48
        _ -> []
49
      end
50
51
    refs = if head, do: Browse.refs(repository), else: []
52
45 53
    # A repository that is still provisioning is the one state this page cannot
46 54
    # render usefully, and it is also the one state that ends on its own. The
47 55
    # provisioner and the importer announce each transition, so the page hears

@@ -61,7 +69,12 @@ defmodule OpenAgentsWeb.CodeRepoLive do

61 69
     |> assign(:head, head)
62 70
     |> assign(:readme, readme)
63 71
     |> assign(:commits, commits)
64
     |> assign(:refs, if(head, do: Browse.refs(repository), else: []))
72
     |> assign(:latest, List.first(commits))
73
     |> assign(:refs, refs)
74
     |> assign(:entries, entries)
75
     |> assign(:branch_count, Enum.count(refs, &(&1.kind == :branch)))
76
     |> assign(:tag_count, Enum.count(refs, &(&1.kind == :tag)))
77
     |> assign(:open_issue_count, open_issue_count(repository))
65 78
     |> assign(:clone_url, RepositoryAccess.clone_url(repository))}
66 79
  rescue
67 80
    Ecto.NoResultsError -> raise OpenAgentsWeb.PublicNotFoundError

@@ -88,6 +101,35 @@ defmodule OpenAgentsWeb.CodeRepoLive do

88 101
89 102
  defp short(sha), do: String.slice(sha, 0, 12)
90 103
104
  # The tab carries a count only when there is something to count, the way the
105
  # component's own `count` attribute is defined: nil renders no badge.
106
  defp open_issue_count(repository) do
107
    case OpenAgents.Issues.list_issues(repository, state: "open") do
108
      [] -> nil
109
      issues -> length(issues)
110
    end
111
  rescue
112
    # The tab is decoration over another context's data. A repository whose
113
    # issues cannot be read is still a repository whose code should render.
114
    _error -> nil
115
  end
116
117
  # `%cI` from `Browse.log/3`, which is strict ISO 8601.
118
  defp committed_at(%{committed_at: stamp}) when is_binary(stamp) do
119
    case DateTime.from_iso8601(stamp) do
120
      {:ok, at, _offset} -> at
121
      _unparseable -> nil
122
    end
123
  end
124
125
  defp committed_at(_commit), do: nil
126
127
  defp initial(name) when is_binary(name) do
128
    name |> String.trim() |> String.first() |> Kernel.||("?") |> String.upcase()
129
  end
130
131
  defp initial(_name), do: "?"
132
91 133
  @impl true
92 134
  def render(assigns) do
93 135
    ~H"""

@@ -98,25 +140,20 @@ defmodule OpenAgentsWeb.CodeRepoLive do

98 140
      title="Code"
99 141
    >
100 142
      <main id="code-repo-page" class="app-shell code-shell">
101
        <section class="code" aria-label="Repository">
102
          <header class="code-heading">
103
            <div>
104
              <h1>{@owner}/{@repo}</h1>
105
              <p class="code-meta">
106
                <.badge variant={if(@repository.visibility == "public", do: :success, else: :dim)}>
107
                  {@repository.visibility}
108
                </.badge>
109
                <span :if={@head}>head <code>{short(@head)}</code></span>
110
                <span :if={@repository.description}>{@repository.description}</span>
111
              </p>
112
            </div>
113
            <div class="code-actions">
114
              <.button navigate={"#{@base}/issues"} variant={:secondary} size={:sm}>Issues</.button>
115
              <.button navigate={"#{@base}/projects"} variant={:secondary} size={:sm}>
116
                Projects
117
              </.button>
118
            </div>
119
          </header>
143
        <.repo_view
144
          owner={@owner}
145
          repo={@repo}
146
          visibility={if @repository.visibility == "public", do: :public, else: :private}
147
        >
148
          <:tabs>
149
            <.repo_tabs>
150
              <:tab icon="code" navigate={@base} current>Code</:tab>
151
              <:tab icon="empty-circle" navigate={"#{@base}/issues"} count={@open_issue_count}>
152
                Issues
153
              </:tab>
154
              <:tab icon="cube" navigate={"#{@base}/projects"}>Projects</:tab>
155
            </.repo_tabs>
156
          </:tabs>
120 157
121 158
          <.alert
122 159
            :if={@repository.lifecycle_state == "provisioning"}

@@ -174,33 +211,69 @@ defmodule OpenAgentsWeb.CodeRepoLive do

174 211
            </p>
175 212
          </.card>
176 213
177
          <.card :if={@repository.lifecycle_state == "ready"} id="repo-clone">
178
            <h2>Clone</h2>
179
            <code class="block break-all">git clone {@clone_url}</code>
180
          </.card>
181
182 214
          <.empty
183 215
            :if={@repository.lifecycle_state == "ready" and is_nil(@head)}
184 216
            id="repo-empty"
185 217
            title="This repository is empty"
186 218
          >
187 219
            Push the first commit to <code>{@repository.default_branch}</code>
188
            with the clone URL above.
220
            with the clone URL below.
189 221
          </.empty>
190 222
191
          <.card :if={@head} id="repo-commits">
192
            <h2>Recent commits</h2>
193
            <ol class="code-commits">
194
              <li :for={commit <- @commits}>
195
                <.text_button navigate={"#{@base}/commit/#{short(commit.sha)}"}>
196
                  <code>{short(commit.sha)}</code>
197
                </.text_button>
198
                <span class="code-commit-line">{commit.subject}</span>
199
                <span class="code-commit-author">{commit.author}</span>
200
              </li>
201
            </ol>
223
          <%!-- Only while the repository has nothing in it. Once there is a
224
          tree, the ref bar carries a Clone control and a second copy of the
225
          same command underneath it is noise -- but an empty repository has no
226
          ref bar, and telling someone how to push the first commit is the only
227
          thing this page can usefully do for them. --%>
228
          <.card :if={@repository.lifecycle_state == "ready" and is_nil(@head)} id="repo-clone">
229
            <h2>Clone</h2>
230
            <code class="block break-all">git clone {@clone_url}</code>
202 231
          </.card>
203 232
233
          <%!-- Counts rather than a list: the ref bar states how many branches
234
          and tags there are, and each name is reachable from there. `commits`
235
          is left unset because the total is not a bounded query -- an invented
236
          number beside two real ones is worse than one missing number. --%>
237
          <.file_table
238
            :if={@head}
239
            owner={@owner}
240
            repo={@repo}
241
            ref={@repository.default_branch}
242
            entries={@entries}
243
            branches={@branch_count}
244
            tags={@tag_count}
245
          >
246
            <:actions>
247
              <.copy_button
248
                id="repo-clone-copy"
249
                text={"git clone #{@clone_url}"}
250
                label="Clone"
251
                copied_label="Copied"
252
              />
253
            </:actions>
254
            <:commit :if={@latest}>
255
              <.avatar size={:sm} fallback={initial(@latest.author)} label={@latest.author} />
256
              <strong>{@latest.author}</strong>
257
              <span>{@latest.subject}</span>
258
              <.text_button navigate={"#{@base}/commit/#{short(@latest.sha)}"}>
259
                <code>{short(@latest.sha)}</code>
260
              </.text_button>
261
              <time :if={committed_at(@latest)} datetime={@latest.committed_at}>
262
                {Calendar.strftime(committed_at(@latest), "%Y-%m-%d")}
263
              </time>
264
            </:commit>
265
          </.file_table>
266
267
          <.card :if={@readme} id="repo-readme">
268
            <h2>{@readme.name}</h2>
269
            <div class="code-markdown">
270
              {OpenAgents.Markdown.to_html(@readme.blob.content)}
271
            </div>
272
          </.card>
273
274
          <%!-- The ref bar counts branches and tags; this is where their names
275
          and heads actually are. Without it the counts are the only trace of a
276
          branch that is not the default one. --%>
204 277
          <.card :if={@head} id="repo-refs">
205 278
            <h2>Refs</h2>
206 279
            <ul class="code-refs">

@@ -214,13 +287,42 @@ defmodule OpenAgentsWeb.CodeRepoLive do

214 287
            </ul>
215 288
          </.card>
216 289
217
          <.card :if={@readme} id="repo-readme">
218
            <h2>{@readme.name}</h2>
219
            <div class="code-markdown">
220
              {OpenAgents.Markdown.to_html(@readme.blob.content)}
221
            </div>
290
          <.card :if={@head} id="repo-commits">
291
            <h2>Recent commits</h2>
292
            <ol class="code-commits">
293
              <li :for={commit <- @commits}>
294
                <.text_button navigate={"#{@base}/commit/#{short(commit.sha)}"}>
295
                  <code>{short(commit.sha)}</code>
296
                </.text_button>
297
                <span class="code-commit-line">{commit.subject}</span>
298
                <span class="code-commit-author">{commit.author}</span>
299
              </li>
300
            </ol>
222 301
          </.card>
223
        </section>
302
303
          <:about>
304
            <.repo_about description={@repository.description}>
305
              <%!-- The file that is actually there, under the name it actually
306
              has. A fixed `README.md` is a guess, and a repository whose readme
307
              is named anything else gets a rail link to a 404. --%>
308
              <:link
309
                :if={@readme}
310
                icon="book"
311
                navigate={"#{@base}/blob/#{@repository.default_branch}/#{@readme.name}"}
312
              >
313
                {@readme.name}
314
              </:link>
315
              <:link icon="empty-circle" navigate={"#{@base}/issues"}>Issues</:link>
316
              <:link icon="cube" navigate={"#{@base}/projects"}>Projects</:link>
317
              <:stat icon="branch">
318
                {@branch_count} {if @branch_count == 1, do: "branch", else: "branches"}
319
              </:stat>
320
              <:stat :if={@tag_count > 0} icon="tag">
321
                {@tag_count} {if @tag_count == 1, do: "tag", else: "tags"}
322
              </:stat>
323
            </.repo_about>
324
          </:about>
325
        </.repo_view>
224 326
      </main>
225 327
    </Layouts.app>
226 328
    """
test/openagents_web/live/code_live_test.exs modified +5 -1

@@ -165,7 +165,11 @@ defmodule OpenAgentsWeb.CodeLiveTest do

165 165
      browsable()
166 166
      {:ok, _view, html} = live(conn, "/OpenAgentsInc/openagents.com")
167 167
168
      assert html =~ "<h1>OpenAgentsInc/openagents.com</h1>"
168
      # The identity is the owner trail of the composed repository view now,
169
      # not a bare heading.
170
      assert html =~ ~s(aria-label="OpenAgentsInc / openagents.com")
171
      assert html =~ ~s(class="repo-view)
172
      assert html =~ ~s(class="repo-tabs)
169 173
      assert html =~ "Add the transparency audit fixture"
170 174
      assert html =~ "First commit"
171 175
      assert html =~ ~s(id="repo-refs")

This page updates live while a promote is in flight · changelog