From 643f6999586bddabb1b817d3732c14a0434ac9e3 Mon Sep 17 00:00:00 2001 From: Ondrej Kokes Date: Fri, 4 Sep 2026 18:02:09 +0200 Subject: [PATCH] experimental filtering pills --- internal/server/dashboard.go | 102 ++++++++++++------ internal/server/server.go | 27 +++-- internal/server/templates.go | 1 + .../server/templates/pages/packages_list.html | 60 +++++------ internal/server/templates_test.go | 49 +++++++-- 5 files changed, 164 insertions(+), 75 deletions(-) diff --git a/internal/server/dashboard.go b/internal/server/dashboard.go index a3ac9ebd..e9ff88de 100644 --- a/internal/server/dashboard.go +++ b/internal/server/dashboard.go @@ -2,6 +2,7 @@ package server import ( "html/template" + "sort" "strings" "github.com/git-pkgs/proxy/internal/database" @@ -110,13 +111,21 @@ type SearchResultItem struct { // PackagesListPageData contains data for rendering the packages list page. type PackagesListPageData struct { Layout - Ecosystem string - SortBy string - Results []SearchResultItem - Count int - Page int - PerPage int - TotalPages int + Ecosystem string + SortBy string + Results []SearchResultItem + Count int + TotalPackages int64 + EcosystemFilters []EcosystemFilter + Page int + PerPage int + TotalPages int +} + +// EcosystemFilter represents an ecosystem filter pill with a package count. +type EcosystemFilter struct { + Ecosystem string + Count int64 } func supportedEcosystems() []string { @@ -157,49 +166,82 @@ func ecosystemBadgeLabel(ecosystem string) string { } } -func ecosystemBadgeClasses(ecosystem string) string { - base := "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium" - +func ecosystemColorClasses(ecosystem string) string { switch ecosystem { case "npm", "maven": - return base + " bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300" + return "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300" case "cargo": - return base + " bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300" + return "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300" case "gem": - return base + " bg-pink-100 text-pink-700 dark:bg-pink-900/50 dark:text-pink-300" - case "go": - return base + " bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300" + return "bg-pink-100 text-pink-700 dark:bg-pink-900/50 dark:text-pink-300" + case "go", "golang": + return "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300" case "hex": - return base + " bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300" + return "bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300" case "pub": - return base + " bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" + return "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" case "pypi": - return base + " bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300" + return "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300" case "nuget": - return base + " bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300" + return "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300" case "composer": - return base + " bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300" + return "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300" case "conan": - return base + " bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-300" + return "bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-300" case "conda": - return base + " bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300" + return "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300" case "cran": - return base + " bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300" + return "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300" case "julia": - return base + " bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300" + return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300" case "swift": - return base + " bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300" + return "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300" case "oci": - return base + " bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300" + return "bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300" case "deb": - return base + " bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300" + return "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300" case "rpm": - return base + " bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300" + return "bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300" case "alpine": - return base + " bg-lime-100 text-lime-800 dark:bg-lime-900/50 dark:text-lime-300" + return "bg-lime-100 text-lime-800 dark:bg-lime-900/50 dark:text-lime-300" default: - return base + " bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" + return "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" + } +} + +func ecosystemBadgeClasses(ecosystem string) string { + return "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium " + ecosystemColorClasses(ecosystem) +} + +func ecosystemPillClasses(ecosystem string) string { + return "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium hover:opacity-90 " + ecosystemColorClasses(ecosystem) +} + +func buildEcosystemFilters(counts map[string]int64) []EcosystemFilter { + var filters []EcosystemFilter + seen := make(map[string]bool, len(counts)) + + for _, eco := range supportedEcosystems() { + count := counts[eco] + if count > 0 { + filters = append(filters, EcosystemFilter{Ecosystem: eco, Count: count}) + seen[eco] = true + } + } + + var extra []string + for eco, count := range counts { + if count > 0 && !seen[eco] { + extra = append(extra, eco) + } } + sort.Strings(extra) + + for _, eco := range extra { + filters = append(filters, EcosystemFilter{Ecosystem: eco, Count: counts[eco]}) + } + + return filters } func getRegistryConfigs(baseURL string) []RegistryConfig { diff --git a/internal/server/server.go b/internal/server/server.go index 7613f52b..569dd4ca 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -798,15 +798,26 @@ func (s *Server) handlePackagesList(w http.ResponseWriter, r *http.Request) { totalPages := int((total + int64(limit) - 1) / int64(limit)) + var ecosystemFilters []EcosystemFilter + var totalPackages int64 + if stats, err := s.db.GetCacheStats(); err != nil { + s.logger.Error("failed to get cache stats for ecosystem filters", "error", err) + } else { + totalPackages = stats.TotalPackages + ecosystemFilters = buildEcosystemFilters(stats.EcosystemCounts) + } + data := PackagesListPageData{ - Layout: s.layoutFor(r), - Ecosystem: ecosystem, - SortBy: sortBy, - Results: items, - Count: int(total), - Page: page, - PerPage: limit, - TotalPages: totalPages, + Layout: s.layoutFor(r), + Ecosystem: ecosystem, + SortBy: sortBy, + Results: items, + Count: int(total), + TotalPackages: totalPackages, + EcosystemFilters: ecosystemFilters, + Page: page, + PerPage: limit, + TotalPages: totalPages, } if err := s.templates.Render(w, "packages_list", data); err != nil { diff --git a/internal/server/templates.go b/internal/server/templates.go index 217da413..9e6f8a96 100644 --- a/internal/server/templates.go +++ b/internal/server/templates.go @@ -28,6 +28,7 @@ func (t *Templates) load() error { "sub": func(a, b int) int { return a - b }, "supportedEcosystems": supportedEcosystems, "ecosystemBadgeClass": ecosystemBadgeClasses, + "ecosystemPillClass": ecosystemPillClasses, "ecosystemBadgeLabel": ecosystemBadgeLabel, } diff --git a/internal/server/templates/pages/packages_list.html b/internal/server/templates/pages/packages_list.html index 95897db5..76a6bc1b 100644 --- a/internal/server/templates/pages/packages_list.html +++ b/internal/server/templates/pages/packages_list.html @@ -4,23 +4,31 @@

Cached Packages

- {{.Count}} packages{{if .Ecosystem}} in {{.Ecosystem}}{{end}} + {{.Count}} packages{{if .Ecosystem}} in {{ecosystemBadgeLabel .Ecosystem}}{{end}}

-
-
- - +
-
+
- @@ -53,11 +61,14 @@

Cached Packages

{{end}}
-
- {{.Hits}} hits - {{.SizeFormatted}} +
+ + {{.Hits}} + hits + + {{.SizeFormatted}} {{if .CachedAt}} - {{.CachedAt}} + {{.CachedAt}} {{end}}
@@ -66,7 +77,7 @@

Cached Packages

{{else}}
-

No cached packages found{{if .Ecosystem}} in {{.Ecosystem}}{{end}}

+

No cached packages found{{if .Ecosystem}} in {{ecosystemBadgeLabel .Ecosystem}}{{end}}

Return to dashboard
{{end}} @@ -94,21 +105,10 @@

Cached Packages

{{end}} diff --git a/internal/server/templates_test.go b/internal/server/templates_test.go index a2d3b7da..869ebacd 100644 --- a/internal/server/templates_test.go +++ b/internal/server/templates_test.go @@ -63,13 +63,15 @@ func TestTemplatesRenderAllPages(t *testing.T) { TotalPages: 0, }}, {"packages_list", PackagesListPageData{ - Ecosystem: "", - SortBy: defaultSortBy, - Results: []SearchResultItem{{Ecosystem: "npm", Name: "express", Hits: 200, SizeFormatted: "2 MB"}}, - Count: 1, - Page: 1, - PerPage: 50, - TotalPages: 1, + Ecosystem: "", + SortBy: defaultSortBy, + Results: []SearchResultItem{{Ecosystem: "npm", Name: "express", Hits: 200, SizeFormatted: "2 MB"}}, + Count: 1, + TotalPackages: 1, + EcosystemFilters: []EcosystemFilter{{Ecosystem: "npm", Count: 1}}, + Page: 1, + PerPage: 50, + TotalPages: 1, }}, {"package_show", PackageShowData{ Package: &database.Package{ @@ -568,6 +570,39 @@ func TestEcosystemBadgeClasses(t *testing.T) { } } +func TestBuildEcosystemFilters(t *testing.T) { + filters := buildEcosystemFilters(map[string]int64{ + "npm": 3, + "cargo": 2, + "empty": 0, + }) + + if len(filters) != 2 { + t.Fatalf("expected 2 filters, got %d", len(filters)) + } + if filters[0].Ecosystem != "cargo" || filters[0].Count != 2 { + t.Errorf("first filter = %#v, want cargo with count 2", filters[0]) + } + if filters[1].Ecosystem != "npm" || filters[1].Count != 3 { + t.Errorf("second filter = %#v, want npm with count 3", filters[1]) + } + + extra := buildEcosystemFilters(map[string]int64{"custom": 1}) + if len(extra) != 1 || extra[0].Ecosystem != "custom" { + t.Errorf("unexpected extra filters: %#v", extra) + } +} + +func TestEcosystemPillClasses(t *testing.T) { + classes := ecosystemPillClasses("npm") + if !strings.Contains(classes, "rounded-full") { + t.Error("pill classes should use rounded-full") + } + if !strings.Contains(classes, "bg-red-100") { + t.Error("npm pill should use npm colors") + } +} + func TestFormatSize(t *testing.T) { tests := []struct { bytes int64