feat(homepage): implement SearchRoute method and enhance item configuration with sorting and visibility features, introduce All and Favorite categories

This commit is contained in:
yusing
2025-09-04 06:30:37 +08:00
parent 866b95f85b
commit 7753c90a7e
7 changed files with 304 additions and 64 deletions

View File

@@ -25,6 +25,15 @@ func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
return list return list
} }
func (cfg *Config) SearchRoute(alias string) types.Route {
for _, p := range cfg.providers.Range {
if r, ok := p.GetRoute(alias); ok {
return r
}
}
return nil
}
func (cfg *Config) Statistics() map[string]any { func (cfg *Config) Statistics() map[string]any {
var rps, streams types.RouteStats var rps, streams types.RouteStats
var total uint16 var total uint16

View File

@@ -15,6 +15,7 @@ import (
"github.com/yusing/go-proxy/internal/notif" "github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/proxmox" "github.com/yusing/go-proxy/internal/proxmox"
"github.com/yusing/go-proxy/internal/serialization" "github.com/yusing/go-proxy/internal/serialization"
"github.com/yusing/go-proxy/internal/types"
) )
type ( type (
@@ -51,6 +52,7 @@ type (
Reload() gperr.Error Reload() gperr.Error
Statistics() map[string]any Statistics() map[string]any
RouteProviderList() []RouteProviderListResponse RouteProviderList() []RouteProviderListResponse
SearchRoute(alias string) types.Route
Context() context.Context Context() context.Context
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
AutoCertProvider() *autocert.Provider AutoCertProvider() *autocert.Provider

View File

@@ -3,33 +3,59 @@ package homepage
import ( import (
"slices" "slices"
"github.com/yusing/ds/ordered"
"github.com/yusing/go-proxy/internal/homepage/widgets" "github.com/yusing/go-proxy/internal/homepage/widgets"
"github.com/yusing/go-proxy/internal/serialization" "github.com/yusing/go-proxy/internal/serialization"
) )
type ( type (
Homepage map[string]Category // @name HomepageItems HomepageMap struct {
Category []*Item // @name HomepageCategory *ordered.Map[string, *Category]
} // @name HomepageItemsMap
Homepage []*Category // @name HomepageItems
Category struct {
Items []*Item `json:"items"`
Name string `json:"name"`
} // @name HomepageCategory
ItemConfig struct { ItemConfig struct {
Show bool `json:"show"` Show bool `json:"show"`
Name string `json:"name"` // display name Name string `json:"name"` // display name
Icon *IconURL `json:"icon" swaggertype:"string"` Icon *IconURL `json:"icon" swaggertype:"string"`
Category string `json:"category"` Category string `json:"category" validate:"omitempty"`
Description string `json:"description" aliases:"desc"` Description string `json:"description" aliases:"desc"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
SortOrder int `json:"sort_order"` Favorite bool `json:"favorite"`
}
Item struct {
*ItemConfig
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"` WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
} // @name HomepageItemConfig
Widget struct {
Label string `json:"label"`
Value string `json:"value"`
} // @name HomepageItemWidget
Item struct {
ItemConfig
SortOrder int `json:"sort_order"` // sort order in category
FavSortOrder int `json:"fav_sort_order"` // sort order in favorite
AllSortOrder int `json:"all_sort_order"` // sort order in all
Widgets []Widget `json:"widgets,omitempty"`
Alias string `json:"alias"` Alias string `json:"alias"`
Provider string `json:"provider"` Provider string `json:"provider"`
OriginURL string `json:"origin_url"` OriginURL string `json:"origin_url"`
} } // @name HomepageItem
)
const (
CategoryAll = "All"
CategoryFavorites = "Favorites"
CategoryHidden = "Hidden"
CategoryOthers = "Others"
) )
func init() { func init() {
@@ -40,22 +66,85 @@ func init() {
}) })
} }
func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig { func NewHomepageMap(total int) *HomepageMap {
return overrideConfigInstance.GetOverride(alias, cfg) m := &HomepageMap{
Map: ordered.NewMap[string, *Category](ordered.WithCapacity(10)),
}
m.Set(CategoryFavorites, &Category{
Items: make([]*Item, 0), // no capacity reserved for this category
Name: CategoryFavorites,
})
m.Set(CategoryAll, &Category{
Items: make([]*Item, 0, total),
Name: CategoryAll,
})
m.Set(CategoryHidden, &Category{
Items: make([]*Item, 0),
Name: CategoryHidden,
})
return m
} }
func (c Homepage) Add(item *Item) { func (cfg Item) GetOverride() Item {
if c[item.Category] == nil { return overrideConfigInstance.GetOverride(cfg)
c[item.Category] = make(Category, 0) }
}
c[item.Category] = append(c[item.Category], item) func (c HomepageMap) Add(item *Item) {
slices.SortStableFunc(c[item.Category], func(a, b *Item) int { c.add(item, item.Category)
if a.SortOrder < b.SortOrder { // add to all category even if item is hidden
return -1 c.add(item, CategoryAll)
} if item.Show {
if a.SortOrder > b.SortOrder { if item.Favorite {
return 1 c.add(item, CategoryFavorites)
} }
return 0 } else {
}) c.add(item, CategoryHidden)
}
}
func (c HomepageMap) add(item *Item, categoryName string) {
category := c.Get(categoryName)
if category == nil {
category = &Category{
Items: make([]*Item, 0),
Name: categoryName,
}
c.Set(categoryName, category)
}
category.Items = append(category.Items, item)
}
func (c *Category) Sort() {
switch c.Name {
case CategoryFavorites:
slices.SortStableFunc(c.Items, func(a, b *Item) int {
if a.FavSortOrder < b.FavSortOrder {
return -1
}
if a.FavSortOrder > b.FavSortOrder {
return 1
}
return 0
})
case CategoryAll:
slices.SortStableFunc(c.Items, func(a, b *Item) int {
if a.AllSortOrder < b.AllSortOrder {
return -1
}
if a.AllSortOrder > b.AllSortOrder {
return 1
}
return 0
})
default:
slices.SortStableFunc(c.Items, func(a, b *Item) int {
if a.SortOrder < b.SortOrder {
return -1
}
if a.SortOrder > b.SortOrder {
return 1
}
return 0
})
}
} }

View File

@@ -10,7 +10,7 @@ import (
func TestOverrideItem(t *testing.T) { func TestOverrideItem(t *testing.T) {
a := &Item{ a := &Item{
Alias: "foo", Alias: "foo",
ItemConfig: &ItemConfig{ ItemConfig: ItemConfig{
Show: false, Show: false,
Name: "Foo", Name: "Foo",
Icon: &IconURL{ Icon: &IconURL{
@@ -20,7 +20,7 @@ func TestOverrideItem(t *testing.T) {
Category: "App", Category: "App",
}, },
} }
want := &ItemConfig{ want := ItemConfig{
Show: true, Show: true,
Name: "Bar", Name: "Bar",
Category: "Test", Category: "Test",
@@ -30,7 +30,107 @@ func TestOverrideItem(t *testing.T) {
}, },
} }
overrides := GetOverrideConfig() overrides := GetOverrideConfig()
overrides.Initialize()
overrides.OverrideItem(a.Alias, want) overrides.OverrideItem(a.Alias, want)
got := a.GetOverride(a.Alias) got := a.GetOverride()
ExpectEqual(t, got, want) ExpectEqual(t, got, Item{
ItemConfig: want,
Alias: a.Alias,
})
}
func TestOverrideItem_PreservesURL(t *testing.T) {
a := &Item{
Alias: "svc",
ItemConfig: ItemConfig{
Show: true,
Name: "Service",
URL: "http://origin.local",
},
}
wantCfg := ItemConfig{
Show: true,
Name: "Overridden",
URL: "http://should-not-apply",
}
overrides := GetOverrideConfig()
overrides.Initialize()
overrides.OverrideItem(a.Alias, wantCfg)
got := a.GetOverride()
ExpectEqual(t, got.URL, "http://origin.local")
ExpectEqual(t, got.Name, "Overridden")
}
func TestVisibilityFavoriteAndSortOrders(t *testing.T) {
a := &Item{
Alias: "alpha",
ItemConfig: ItemConfig{
Show: true,
Name: "Alpha",
Category: "Apps",
Favorite: false,
},
}
overrides := GetOverrideConfig()
overrides.Initialize()
overrides.SetItemsVisibility([]string{a.Alias}, false)
overrides.SetItemsFavorite([]string{a.Alias}, true)
overrides.SetSortOrder(a.Alias, 5)
overrides.SetAllSortOrder(a.Alias, 9)
overrides.SetFavSortOrder(a.Alias, 2)
got := a.GetOverride()
ExpectEqual(t, got.Show, false)
ExpectEqual(t, got.Favorite, true)
ExpectEqual(t, got.SortOrder, 5)
ExpectEqual(t, got.AllSortOrder, 9)
ExpectEqual(t, got.FavSortOrder, 2)
}
func TestCategoryDefaultedWhenEmpty(t *testing.T) {
a := &Item{
Alias: "no-cat",
ItemConfig: ItemConfig{
Show: true,
Name: "NoCat",
},
}
got := a.GetOverride()
ExpectEqual(t, got.Category, CategoryOthers)
}
func TestOverrideItems_Bulk(t *testing.T) {
a := &Item{
Alias: "bulk-1",
ItemConfig: ItemConfig{
Show: true,
Name: "Bulk1",
Category: "X",
},
}
b := &Item{
Alias: "bulk-2",
ItemConfig: ItemConfig{
Show: true,
Name: "Bulk2",
Category: "Y",
},
}
overrides := GetOverrideConfig()
overrides.Initialize()
overrides.OverrideItems(map[string]ItemConfig{
a.Alias: {Show: true, Name: "A*", Category: "AX"},
b.Alias: {Show: false, Name: "B*", Category: "BY"},
})
ga := a.GetOverride()
gb := b.GetOverride()
ExpectEqual(t, ga.Name, "A*")
ExpectEqual(t, ga.Category, "AX")
ExpectEqual(t, gb.Name, "B*")
ExpectEqual(t, gb.Category, "BY")
ExpectEqual(t, gb.Show, false)
} }

View File

@@ -94,8 +94,8 @@ func NewIconKey(source IconSource, reference string) IconKey {
} }
func (k IconKey) SourceRef() (IconSource, string) { func (k IconKey) SourceRef() (IconSource, string) {
parts := strings.Split(string(k), "/") source, ref, _ := strings.Cut(string(k), "/")
return IconSource(parts[0]), parts[1] return IconSource(source), ref
} }
func InitIconListCache() { func InitIconListCache() {

View File

@@ -9,10 +9,13 @@ import (
) )
type OverrideConfig struct { type OverrideConfig struct {
ItemOverrides map[string]*ItemConfig `json:"item_overrides"` ItemOverrides map[string]ItemConfig `json:"item_overrides"`
DisplayOrder map[string]int `json:"display_order"` DisplayOrder map[string]int `json:"display_order"`
CategoryOrder map[string]int `json:"category_order"` CategoryOrder map[string]int `json:"category_order"`
ItemVisibility map[string]bool `json:"item_visibility"` AllSortOrder map[string]int `json:"all_sort_order"`
FavSortOrder map[string]int `json:"fav_sort_order"`
ItemVisibility map[string]bool `json:"item_visibility"`
ItemFavorite map[string]bool `json:"item_favorite"`
mu sync.RWMutex mu sync.RWMutex
} }
@@ -23,57 +26,94 @@ func GetOverrideConfig() *OverrideConfig {
} }
func (c *OverrideConfig) Initialize() { func (c *OverrideConfig) Initialize() {
c.ItemOverrides = make(map[string]*ItemConfig) c.ItemOverrides = make(map[string]ItemConfig)
c.DisplayOrder = make(map[string]int) c.DisplayOrder = make(map[string]int)
c.CategoryOrder = make(map[string]int) c.CategoryOrder = make(map[string]int)
c.AllSortOrder = make(map[string]int)
c.FavSortOrder = make(map[string]int)
c.ItemVisibility = make(map[string]bool) c.ItemVisibility = make(map[string]bool)
c.ItemFavorite = make(map[string]bool)
} }
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) { func (c *OverrideConfig) OverrideItem(alias string, override ItemConfig) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.ItemOverrides[alias] = override c.ItemOverrides[alias] = override
} }
func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) { func (c *OverrideConfig) OverrideItems(items map[string]ItemConfig) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
maps.Copy(c.ItemOverrides, items) maps.Copy(c.ItemOverrides, items)
} }
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig { func (c *OverrideConfig) GetOverride(item Item) Item {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
if itemOverride, hasOverride := c.ItemOverrides[alias]; hasOverride {
itemOverride.URL = item.URL // NOTE: we don't want to override the URL if overrides, hasOverride := c.ItemOverrides[item.Alias]; hasOverride {
item = itemOverride overrides.URL = item.URL // NOTE: we don't want to override the URL
item.ItemConfig = overrides
} }
if show, ok := c.ItemVisibility[alias]; ok {
clone := *item if show, ok := c.ItemVisibility[item.Alias]; ok {
clone.Show = show item.Show = show
return &clone }
if fav, ok := c.ItemFavorite[item.Alias]; ok {
item.Favorite = fav
}
if displayOrder, ok := c.DisplayOrder[item.Alias]; ok {
item.SortOrder = displayOrder
}
if allSortOrder, ok := c.AllSortOrder[item.Alias]; ok {
item.AllSortOrder = allSortOrder
}
if favSortOrder, ok := c.FavSortOrder[item.Alias]; ok {
item.FavSortOrder = favSortOrder
}
if item.Category == "" {
item.Category = CategoryOthers
} }
return item return item
} }
func (c *OverrideConfig) SetSortOrder(key string, value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayOrder[key] = value
}
func (c *OverrideConfig) SetAllSortOrder(key string, value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.AllSortOrder[key] = value
}
func (c *OverrideConfig) SetFavSortOrder(key string, value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.FavSortOrder[key] = value
}
func (c *OverrideConfig) SetItemsVisibility(keys []string, value bool) {
c.mu.Lock()
defer c.mu.Unlock()
for _, key := range keys {
c.ItemVisibility[key] = value
}
}
func (c *OverrideConfig) SetItemsFavorite(keys []string, value bool) {
c.mu.Lock()
defer c.mu.Unlock()
for _, key := range keys {
c.ItemFavorite[key] = value
}
}
func (c *OverrideConfig) SetCategoryOrder(key string, value int) { func (c *OverrideConfig) SetCategoryOrder(key string, value int) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.CategoryOrder[key] = value c.CategoryOrder[key] = value
} }
func (c *OverrideConfig) UnhideItems(keys []string) {
c.mu.Lock()
defer c.mu.Unlock()
for _, key := range keys {
c.ItemVisibility[key] = true
}
}
func (c *OverrideConfig) HideItems(keys []string) {
c.mu.Lock()
defer c.mu.Unlock()
for _, key := range keys {
c.ItemVisibility[key] = false
}
}

View File

@@ -28,8 +28,8 @@ type (
IdlewatcherConfig() *IdlewatcherConfig IdlewatcherConfig() *IdlewatcherConfig
HealthCheckConfig() *HealthCheckConfig HealthCheckConfig() *HealthCheckConfig
LoadBalanceConfig() *LoadBalancerConfig LoadBalanceConfig() *LoadBalancerConfig
HomepageConfig() *homepage.ItemConfig HomepageItem() homepage.Item
HomepageItem() *homepage.Item DisplayName() string
ContainerInfo() *Container ContainerInfo() *Container
GetAgent() *agent.AgentConfig GetAgent() *agent.AgentConfig