Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/bob/artifacts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ defmodule Bob.Artifacts do
|> Repo.aggregate(:count, :id)
end

def artifact_page(filters, limit, offset) do
%{
results: search_artifacts(filters, limit, offset),
total: count_artifacts(filters)
}
end

def search_docker_tags(filters \\ %{}, limit \\ 100, offset \\ 0) do
filters
|> docker_tag_search_query()
Expand All @@ -237,6 +244,30 @@ defmodule Bob.Artifacts do
|> Repo.aggregate(:count, :id)
end

@doc """
Returns a page of matching Docker tags together with the total match count.

The page and the count are separate queries, so the total is kept coherent
with the page if tags change between them, and the count is skipped entirely
when the first page comes back empty.
"""
def docker_tag_page(filters, limit, offset) do
results = search_docker_tags(filters, limit, offset)

total =
if offset == 0 and results == [] do
0
else
filters
|> count_docker_tags()
# The page and count are separate queries, so keep the pager coherent if
# tags change between them.
|> max(offset + length(results))
end

%{results: results, total: total}
end

def distinct_kinds(), do: distinct_values(Artifact, :kind)
def distinct_arches(), do: distinct_values(Artifact, :arch)
def distinct_oses(), do: distinct_values(Artifact, :os)
Expand Down
23 changes: 23 additions & 0 deletions lib/bob/artifacts/artifact_search.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Bob.Artifacts.ArtifactSearch do
@page 100
@filter_keys ~w(query kind arch os)a

def page_size(), do: @page
def filter_keys(), do: @filter_keys

def parse_filters(params) do
%{
query: params["query"] || "",
kind: params["kind"] || "",
arch: params["arch"] || "",
os: params["os"] || ""
}
end

def parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end
end
30 changes: 30 additions & 0 deletions lib/bob/artifacts/docker_tag_search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,40 @@ defmodule Bob.Artifacts.DockerTagSearch do
@elixir_tag_regex ~r/^(.+)-erlang-(.+)-(alpine|ubuntu|debian)-(.+)$/
@version_regex ~r/^\d+(?:\.\d+)*(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?$/

@page 100
@filter_keys ~w(repo tag arch elixir_version erlang_version os os_version)a

def repos(), do: Enum.sort(@erlang_repos ++ @elixir_repos)
def arches(), do: @archs
def oses(), do: @oses

def page_size(), do: @page
def filter_keys(), do: @filter_keys

def parse_filters(params) do
tag = params["tag"] || ""

%{
repo: params["repo"] || "",
tag: tag,
arch: params["arch"] || "",
elixir_version: structured_param(params, "elixir_version", tag),
erlang_version: structured_param(params, "erlang_version", tag),
os: structured_param(params, "os", tag),
os_version: structured_param(params, "os_version", tag)
}
end

def parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end

defp structured_param(_params, _key, tag) when tag != "", do: ""
defp structured_param(params, key, _tag), do: params[key] || ""

def metadata(repo, tag) when repo in @erlang_repos do
case Regex.run(@erlang_tag_regex, tag, capture: :all_but_first) do
[erlang_version, os, os_version] ->
Expand Down
40 changes: 10 additions & 30 deletions lib/bob_web/live/artifacts_live.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
defmodule BobWeb.ArtifactsLive do
use BobWeb, :live_view

@page 100
alias Bob.Artifacts.ArtifactSearch

@page ArtifactSearch.page_size()
@filter_keys ArtifactSearch.filter_keys()

@impl true
def mount(_params, _session, socket) do
Expand Down Expand Up @@ -35,42 +38,24 @@ defmodule BobWeb.ArtifactsLive do

def handle_event("page", %{"dir" => dir}, socket) do
offset = max(socket.assigns.offset + step(dir), 0)

filters = [
query: socket.assigns.query,
kind: socket.assigns.kind,
arch: socket.assigns.arch,
os: socket.assigns.os
]

filters = Enum.map(@filter_keys, &{&1, socket.assigns[&1]})
{:noreply, push_patch(socket, to: ~p"/artifacts?#{query(filters, offset)}")}
end

defp step("next"), do: @page
defp step("prev"), do: -@page

defp filter_assigns(params) do
[
query: params["query"] || "",
kind: params["kind"] || "",
arch: params["arch"] || "",
os: params["os"] || "",
offset: parse_offset(params)
]
filters = ArtifactSearch.parse_filters(params)

Enum.map(@filter_keys, &{&1, filters[&1]}) ++ [offset: ArtifactSearch.parse_offset(params)]
end

defp query(filters, offset) do
filters = Enum.reject(filters, fn {_key, value} -> value in [nil, ""] end)
if offset > 0, do: filters ++ [offset: offset], else: filters
end

defp parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end

defp load_options(socket) do
assign(socket,
kinds: Bob.Artifacts.distinct_kinds(),
Expand All @@ -84,14 +69,9 @@ defmodule BobWeb.ArtifactsLive do

filters = %{query: q, kind: kind, arch: arch, os: os}

results =
Bob.Artifacts.search_artifacts(filters, @page, offset)
%{results: results, total: total} = Bob.Artifacts.artifact_page(filters, @page, offset)

assign(socket,
results: results,
total: Bob.Artifacts.count_artifacts(filters),
loading: false
)
assign(socket, results: results, total: total, loading: false)
end

defp fmt(nil), do: "—"
Expand Down
43 changes: 7 additions & 36 deletions lib/bob_web/live/docker_tags_live.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
defmodule BobWeb.DockerTagsLive do
use BobWeb, :live_view

@page 100
@filter_keys ~w(repo tag arch elixir_version erlang_version os os_version)a
alias Bob.Artifacts.DockerTagSearch

@page DockerTagSearch.page_size()
@filter_keys DockerTagSearch.filter_keys()

@impl true
def mount(_params, _session, socket) do
Expand Down Expand Up @@ -45,28 +47,9 @@ defmodule BobWeb.DockerTagsLive do
defp step("prev"), do: -@page

defp filter_assigns(params) do
tag = params["tag"] || ""

[
repo: params["repo"] || "",
tag: tag,
arch: params["arch"] || "",
elixir_version: structured_param(params, "elixir_version", tag),
erlang_version: structured_param(params, "erlang_version", tag),
os: structured_param(params, "os", tag),
os_version: structured_param(params, "os_version", tag),
offset: parse_offset(params)
]
end

defp structured_param(_params, _key, tag) when tag != "", do: ""
defp structured_param(params, key, _tag), do: params[key] || ""
filters = DockerTagSearch.parse_filters(params)

defp parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
Enum.map(@filter_keys, &{&1, filters[&1]}) ++ [offset: DockerTagSearch.parse_offset(params)]
end

defp query(filters, offset) do
Expand Down Expand Up @@ -96,19 +79,7 @@ defmodule BobWeb.DockerTagsLive do
os_version: os_version
}

results =
Bob.Artifacts.search_docker_tags(filters, @page, offset)

total =
if offset == 0 and results == [] do
0
else
filters
|> Bob.Artifacts.count_docker_tags()
# The page and count are separate queries, so keep the pager coherent if
# tags change between them.
|> max(offset + length(results))
end
%{results: results, total: total} = Bob.Artifacts.docker_tag_page(filters, @page, offset)

assign(socket, results: results, total: total, loading: false)
end
Expand Down
83 changes: 83 additions & 0 deletions lib/bob_web/plugs/search_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule BobWeb.Plugs.SearchJson do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would fit better as a phoenix controller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with using a controller is that we couldn't mount the LiveView at the router any more. LiveViews rendered from a controller can't use handle_params, so this would make the code more complex, that's why I decided to go for a plug. Or do you mean keeping the plug to dispatch, but calling a controller from the plug?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking a /api router scope with dedicated API-only controllers to keep the UI and API separate. Similar to how hexpm works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, going the explicit route. Sure, that works!

@moduledoc """
Serves the artifact and Docker tag search LiveViews as JSON for API clients
that send `Accept: application/json`, mirroring the LiveView for the same
query parameters. Other requests, and any other path, fall through untouched
to the regular browser pipeline and the LiveView.

This runs ahead of the `:accepts` plug so a JSON request is answered and
halted before that plug would reject it as not HTML.
"""

import Plug.Conn
import Phoenix.Controller, only: [json: 2]

alias Bob.Artifacts
alias Bob.Artifacts.{ArtifactSearch, DockerTagSearch}

def init(opts), do: opts

def call(conn, _opts) do
case conn.request_path do
"/artifacts" -> maybe_json(conn, &artifacts/1)
"/docker" -> maybe_json(conn, &docker_tags/1)
_other -> conn
end
end

defp maybe_json(conn, build) do
if json_requested?(conn) do
body = build.(fetch_query_params(conn).query_params)
conn |> json(body) |> halt()
else
conn
end
end

defp artifacts(params) do
offset = ArtifactSearch.parse_offset(params)
filters = ArtifactSearch.parse_filters(params)
page = Artifacts.artifact_page(filters, ArtifactSearch.page_size(), offset)

envelope(page, :artifacts, offset, ArtifactSearch.page_size(), &artifact/1)
end

defp docker_tags(params) do
offset = DockerTagSearch.parse_offset(params)
filters = DockerTagSearch.parse_filters(params)
page = Artifacts.docker_tag_page(filters, DockerTagSearch.page_size(), offset)

envelope(page, :tags, offset, DockerTagSearch.page_size(), &docker_tag/1)
end

defp envelope(%{results: results, total: total}, key, offset, page_size, row) do
%{key => Enum.map(results, row), total: total, offset: offset, page_size: page_size}
end

defp artifact(artifact) do
%{
kind: artifact.kind,
arch: artifact.arch,
os: artifact.os,
name: artifact.name,
ref: artifact.ref,
sha256: artifact.sha256,
built_at: artifact.built_at && DateTime.to_iso8601(artifact.built_at)
}
end

defp docker_tag(tag) do
%{
repo: tag.repo,
tag: tag.tag,
archs: tag.archs,
built_at: tag.built_at && DateTime.to_iso8601(tag.built_at)
}
end

defp json_requested?(conn) do
conn
|> get_req_header("accept")
|> Enum.any?(&String.contains?(&1, "application/json"))
end
end
4 changes: 4 additions & 0 deletions lib/bob_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ defmodule BobWeb.Router do
import BobWeb.UserAuth

pipeline :browser do
# Answers the search LiveViews as JSON for Accept: application/json before
# :accepts would reject the request as not HTML; a no-op for every other
# request, which falls through to the LiveView.
plug(BobWeb.Plugs.SearchJson)
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
Expand Down
Loading
Loading