Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/server/projects_ui.go
Simon Chrzanowski 7095ab67ef Keep the Projects link on an empty projects page
ProjectsListData.Projects hid Layout.Projects, so the header left the
link out when no project had been uploaded.
2026-11-04 16:16:35 +01:00

187 lines
5.6 KiB
Go

package server
import (
"net/http"
"net/url"
"slices"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/inventory"
"github.com/go-chi/chi/v5"
)
// ProjectsListData contains data for rendering the projects list page.
type ProjectsListData struct {
Layout
// Rows isn't called Projects, which would hide Layout.Projects from
// the header.
Rows []ProjectRow
}
// ProjectRow is a project in the UI.
type ProjectRow struct {
Name string
Ref string
Branch string
UploadedAt string
Components int64
}
// ProjectShowData contains data for rendering a project's page.
type ProjectShowData struct {
Layout
Project ProjectRow
Components []ProjectComponentRow
Outdated int
}
// ProjectComponentRow is a package version a project uses, with what the
// proxy knows about it.
type ProjectComponentRow struct {
Ecosystem string
Name string
Version string
// Cached is set when the proxy has a package record, so the package
// page exists; CachedEcosystem and CachedName address it.
Cached bool
CachedEcosystem string
CachedName string
LatestVersion string
Outdated bool
}
// VersionUsage lists the projects using one version of a package.
type VersionUsage struct {
Version string
EscapedVersion string
// Cached is set when the proxy has a record of the version.
Cached bool
Projects []string
}
// mountProjectUI registers the project inventory pages when
// projects.enabled is set, and DELETE /ui/api/projects/{project} for the
// delete button on a project's page. Like DELETE /api/projects/{project} it
// requires the bearer token; it sits under /ui so that an ingress can treat
// the UI's requests together.
func (s *Server) mountProjectUI(ui chi.Router) {
if !s.cfg.Projects.Enabled {
return
}
ui.Get("/projects", s.handleProjectsList)
ui.Get("/projects/*", s.handleProjectShow)
api := NewProjectsAPIHandler(s.db, s.logger)
ui.With(requireBearerToken(s.cfg.Projects.TokenExpanded())).Delete("/api/projects/*", api.HandleDelete)
}
func (s *Server) handleProjectsList(w http.ResponseWriter, r *http.Request) {
projects, err := s.db.ListProjects()
if err != nil {
s.logger.Error("failed to list projects", "error", err)
http.Error(w, "failed to list projects", http.StatusInternalServerError)
return
}
data := ProjectsListData{Layout: s.layoutFor(r), Rows: make([]ProjectRow, len(projects))}
for i, p := range projects {
data.Rows[i] = projectRow(p)
}
if err := s.templates.Render(w, "projects_list", data); err != nil {
s.logger.Error("failed to render projects list", "error", err)
}
}
func (s *Server) handleProjectShow(w http.ResponseWriter, r *http.Request) {
name, err := projectPath(r)
if err == nil {
err = inventory.ValidateProjectName(name)
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
project, err := s.db.GetProject(name)
if err != nil {
s.logger.Error("failed to get project", "project", name, "error", err)
http.Error(w, "failed to get project", http.StatusInternalServerError)
return
}
if project == nil {
http.Error(w, "project not found", http.StatusNotFound)
return
}
components, err := s.db.GetProjectComponents(project.ID)
if err != nil {
s.logger.Error("failed to get project components", "project", name, "error", err)
http.Error(w, "failed to get project components", http.StatusInternalServerError)
return
}
data := ProjectShowData{Layout: s.layoutFor(r), Project: projectRow(*project)}
for _, c := range components {
row := ProjectComponentRow{
Ecosystem: c.Ecosystem,
Name: c.Name,
Version: c.Version,
Cached: c.CachedName.Valid,
CachedEcosystem: c.CachedEcosystem.String,
CachedName: c.CachedName.String,
LatestVersion: c.LatestVersion.String,
Outdated: inventory.Outdated(c.Ecosystem, c.Version, c.LatestVersion.String),
}
if row.Outdated {
data.Outdated++
}
data.Components = append(data.Components, row)
}
if err := s.templates.Render(w, "project_show", data); err != nil {
s.logger.Error("failed to render project page", "error", err)
}
}
func projectRow(p database.Project) ProjectRow {
return ProjectRow{
Name: p.Name,
Ref: p.Ref.String,
Branch: p.Branch.String,
UploadedAt: formatTimeAgo(p.UploadedAt),
Components: p.Components,
}
}
// packageUsedBy lists the projects using each version of pkg, highest
// version first, or nothing when the project inventory is disabled. versions
// are the versions the proxy has records of; a version only projects know of
// is listed too.
func (s *Server) packageUsedBy(pkg *database.Package, versions []database.Version) []VersionUsage {
if !s.cfg.Projects.Enabled {
return nil
}
usages, err := s.db.GetProjectsUsingPackage(pkg.PURL, "")
if err != nil {
s.logger.Error("failed to get projects using package", "purl", pkg.PURL, "error", err)
return nil
}
known := make(map[string]bool, len(versions))
for i := range versions {
known[versions[i].Version()] = true
}
byVersion := map[string]*VersionUsage{}
var result []*VersionUsage
for _, u := range usages {
vu, ok := byVersion[u.Version]
if !ok {
vu = &VersionUsage{Version: u.Version, EscapedVersion: url.PathEscape(u.Version), Cached: known[u.Version]}
byVersion[u.Version] = vu
result = append(result, vu)
}
vu.Projects = append(vu.Projects, u.Name)
}
slices.SortFunc(result, func(a, b *VersionUsage) int {
return inventory.CompareVersions(pkg.Ecosystem, b.Version, a.Version)
})
out := make([]VersionUsage, len(result))
for i, vu := range result {
out[i] = *vu
}
return out
}