defmodule GitGudWeb.OrgLive.Dashboard do
@moduledoc """
`/orgs/:handle/dashboard` — the signed-in dashboard, narrowed to one
organization.
Same panels as `GitGudWeb.PageLive.Home`, same components, one
difference in framing: the landing dashboard is about *you*, so it
only ever shows work you opened. An org dashboard is about the
organization, so it shows everyone's open work alongside yours —
otherwise it would be the landing page with fewer rows on it, which
is not worth a route.
Scoped by `Repositories.visible_repo_ids_for_org/2`, so every panel
inherits the org's visibility rules and a member of a private org
sees what they may see and nothing else. Non-members can reach a
public org's dashboard; everything on it is already public.
"""
use GitGudWeb, :live_view
import GitGudWeb.DashboardComponents
alias GitGud.Issues
alias GitGud.Organizations
alias GitGud.Organizations.Organization
alias GitGud.Profiles
alias GitGud.PullRequests
alias GitGud.Repositories
alias GitGud.Workflows
@panel_limit 6
@impl true
def mount(%{"handle" => handle}, _session, socket) do
org = Organizations.get_organization_by_handle!(handle)
user = socket.assigns.current_scope.user
if can_view?(org, user) do
if connected?(socket),
do: Phoenix.PubSub.subscribe(GitGud.PubSub, Workflows.ci_topic())
{:ok,
socket
|> assign(:org, org)
|> assign(:user, user)
|> assign(:is_admin?, Organizations.org_admin?(org, user))
|> assign(:repo_ids, Repositories.visible_repo_ids_for_org(org, user))
|> assign(:pins, Profiles.list_pins(org))
|> assign(:page_title, "Dashboard — @#{org.handle}")
|> load_work()
|> load_repos()
|> load_ci()}
else
{:ok,
socket
|> put_flash(:error, "You're not authorized to view this organization.")
|> push_navigate(to: ~p"/")}
end
end
# Mirrors `GitGudWeb.OrgLive.Actions` — the two org-wide views should
# never disagree about who may look.
defp can_view?(%{visibility: "public"}, _user), do: true
defp can_view?(%{visibility: "internal"}, %{}), do: true
defp can_view?(%{visibility: "private"} = org, %{} = user), do: Organizations.member?(org, user)
defp can_view?(_, _), do: false
defp load_work(socket) do
%{user: user, repo_ids: ids} = socket.assigns
# `maintained ∩ org` rather than all maintained: the point of this
# page is that everything on it is about this organization.
maintained =
user
|> Repositories.maintained_repo_ids_for_user()
|> intersect(ids)
socket
|> assign(
:requested_reviews,
PullRequests.list_open_review_requests(user.id, repo_ids: ids, limit: @panel_limit)
)
|> assign(
:review_queue,
PullRequests.list_open_awaiting_review(user.id, maintained, limit: @panel_limit)
)
|> assign(
:my_prs,
PullRequests.list_open_in_repos(ids, author_id: user.id, limit: @panel_limit)
)
|> assign(:my_issues, Issues.list_open_in_repos(ids, author_id: user.id, limit: @panel_limit))
|> assign(:open_prs, PullRequests.list_open_in_repos(ids, limit: @panel_limit))
|> assign(:open_issues, Issues.list_open_in_repos(ids, limit: @panel_limit))
end
defp intersect(a, b) do
set = MapSet.new(b)
Enum.filter(a, &MapSet.member?(set, &1))
end
defp load_repos(socket) do
assign(
socket,
:recent_repos,
Repositories.list_recently_pushed(socket.assigns.repo_ids, limit: @panel_limit)
)
end
defp load_ci(socket) do
ids = socket.assigns.repo_ids
socket
|> assign(
:active_runs,
Workflows.list_runs_for_repos(ids, statuses: Workflows.active_statuses())
)
|> assign(:recent_runs, Workflows.list_runs_for_repos(ids, limit: @panel_limit))
end
@impl true
def handle_info({:ci_run_changed, _run_id}, socket), do: {:noreply, load_ci(socket)}
def handle_info(_msg, socket), do: {:noreply, socket}
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-6 pt-4">
<header class="flex items-baseline justify-between gap-4 flex-wrap">
<div class="flex items-center gap-3 min-w-0">
<.avatar name={@org.handle} size="md" alt={@org.handle} />
<div class="min-w-0">
<h1 class="text-2xl font-semibold truncate">
{Organization.display(@org)}
</h1>
<nav class="text-xs flex gap-3 opacity-80 mt-1">
<.link navigate={~p"/orgs/#{@org.handle}"} class="link link-hover">Overview</.link>
<.link navigate={~p"/orgs/#{@org.handle}/actions"} class="link link-hover">CI</.link>
<.link
:if={@is_admin?}
navigate={~p"/orgs/#{@org.handle}/settings/members"}
class="link link-hover"
>
Members
</.link>
</nav>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<.link
:if={@is_admin?}
id="org-new-repo"
navigate={~p"/repositories/new/#{@org.handle}"}
class="btn btn-sm btn-primary"
>
<.icon name="hero-plus-micro" class="size-4" /> New repository
</.link>
<.link
navigate={~p"/orgs/#{@org.handle}"}
class="btn btn-sm btn-ghost border border-base-300"
>
Overview
</.link>
</div>
</header>
<div class="grid lg:grid-cols-2 gap-4">
<.panel
title="Needs your review"
hint={review_hint(@requested_reviews)}
empty="Nothing waiting on you here."
count={length(@requested_reviews) + length(@review_queue)}
>
<.work_row
:for={pr <- @requested_reviews}
path={~p"/r/#{repo_handle(pr.repository)}/#{pr.repository.name}/pulls/#{pr.number}"}
repo={repo_path(pr.repository)}
number={pr.number}
title={pr.title}
meta={"you were asked to review · #{ago(pr.inserted_at)}"}
/>
<.work_row
:for={pr <- @review_queue}
path={~p"/r/#{repo_handle(pr.repository)}/#{pr.repository.name}/pulls/#{pr.number}"}
repo={repo_path(pr.repository)}
number={pr.number}
title={pr.title}
meta={"by #{author_name(pr)} · #{ago(pr.inserted_at)}"}
/>
</.panel>
<.panel
title="Your work here"
hint="Open PRs and issues you opened in this organization"
empty="Nothing open from you here."
count={length(@my_prs) + length(@my_issues)}
>
<.work_row
:for={pr <- @my_prs}
path={~p"/r/#{repo_handle(pr.repository)}/#{pr.repository.name}/pulls/#{pr.number}"}
repo={repo_path(pr.repository)}
number={pr.number}
title={pr.title}
meta={"pull request · #{ago(pr.inserted_at)}"}
/>
<.work_row
:for={issue <- @my_issues}
path={
~p"/r/#{repo_handle(issue.repository)}/#{issue.repository.name}/issues/#{issue.number}"
}
repo={repo_path(issue.repository)}
number={issue.number}
title={issue.title}
meta={"issue · #{ago(issue.inserted_at)}"}
/>
</.panel>
<.panel
title="Open pull requests"
hint="Everyone's, across this organization"
empty="No open pull requests."
count={length(@open_prs)}
>
<.work_row
:for={pr <- @open_prs}
path={~p"/r/#{repo_handle(pr.repository)}/#{pr.repository.name}/pulls/#{pr.number}"}
repo={repo_path(pr.repository)}
number={pr.number}
title={pr.title}
meta={"by #{author_name(pr)} · #{ago(pr.inserted_at)}"}
/>
</.panel>
<.panel
title="Open issues"
hint="Everyone's, across this organization"
empty="No open issues."
count={length(@open_issues)}
>
<.work_row
:for={issue <- @open_issues}
path={
~p"/r/#{repo_handle(issue.repository)}/#{issue.repository.name}/issues/#{issue.number}"
}
repo={repo_path(issue.repository)}
number={issue.number}
title={issue.title}
meta={"by #{author_name(issue)} · #{ago(issue.inserted_at)}"}
/>
</.panel>
<.panel
title="CI"
hint={
if @active_runs == [],
do: "Latest runs across this organization",
else: "#{length(@active_runs)} running now"
}
empty="No runs yet."
count={length(@recent_runs)}
more_label="CI dashboard"
more_path={~p"/orgs/#{@org.handle}/actions"}
>
<.ci_row :for={run <- @recent_runs} run={run} />
</.panel>
</div>
<section :if={@pins != []}>
<h2 class="text-lg font-semibold mb-2">Pinned</h2>
<GitGudWeb.PinnedReposGrid.grid pins={@pins} />
</section>
<section :if={@recent_repos != []}>
<header class="flex items-baseline justify-between mb-2">
<h2 class="text-lg font-semibold">Recently pushed</h2>
<.link navigate={~p"/orgs/#{@org.handle}"} class="text-xs link link-hover opacity-60">
All repositories →
</.link>
</header>
<ul class="divide-y divide-base-300">
<.repo_row :for={repo <- @recent_repos} repo={repo} />
</ul>
</section>
</div>
</Layouts.app>
"""
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.9 KiB · text
History
b868907