diff --git a/internal/config/query.go b/internal/config/query.go index e940df94..1851304e 100644 --- a/internal/config/query.go +++ b/internal/config/query.go @@ -25,6 +25,15 @@ func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse { 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 { var rps, streams types.RouteStats var total uint16 diff --git a/internal/config/types/config.go b/internal/config/types/config.go index 0cb14e43..d4383edf 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -15,6 +15,7 @@ import ( "github.com/yusing/go-proxy/internal/notif" "github.com/yusing/go-proxy/internal/proxmox" "github.com/yusing/go-proxy/internal/serialization" + "github.com/yusing/go-proxy/internal/types" ) type ( @@ -51,6 +52,7 @@ type ( Reload() gperr.Error Statistics() map[string]any RouteProviderList() []RouteProviderListResponse + SearchRoute(alias string) types.Route Context() context.Context VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) AutoCertProvider() *autocert.Provider diff --git a/internal/homepage/homepage.go b/internal/homepage/homepage.go index c2da470b..2169d4b7 100644 --- a/internal/homepage/homepage.go +++ b/internal/homepage/homepage.go @@ -3,33 +3,59 @@ package homepage import ( "slices" + "github.com/yusing/ds/ordered" "github.com/yusing/go-proxy/internal/homepage/widgets" "github.com/yusing/go-proxy/internal/serialization" ) type ( - Homepage map[string]Category // @name HomepageItems - Category []*Item // @name HomepageCategory + HomepageMap struct { + *ordered.Map[string, *Category] + } // @name HomepageItemsMap + + Homepage []*Category // @name HomepageItems + Category struct { + Items []*Item `json:"items"` + Name string `json:"name"` + } // @name HomepageCategory ItemConfig struct { Show bool `json:"show"` Name string `json:"name"` // display name Icon *IconURL `json:"icon" swaggertype:"string"` - Category string `json:"category"` + Category string `json:"category" validate:"omitempty"` Description string `json:"description" aliases:"desc"` URL string `json:"url,omitempty"` - SortOrder int `json:"sort_order"` - } - - Item struct { - *ItemConfig + Favorite bool `json:"favorite"` 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"` Provider string `json:"provider"` OriginURL string `json:"origin_url"` - } + } // @name HomepageItem +) + +const ( + CategoryAll = "All" + CategoryFavorites = "Favorites" + CategoryHidden = "Hidden" + CategoryOthers = "Others" ) func init() { @@ -40,22 +66,85 @@ func init() { }) } -func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig { - return overrideConfigInstance.GetOverride(alias, cfg) +func NewHomepageMap(total int) *HomepageMap { + 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) { - if c[item.Category] == nil { - c[item.Category] = make(Category, 0) - } - c[item.Category] = append(c[item.Category], item) - slices.SortStableFunc(c[item.Category], func(a, b *Item) int { - if a.SortOrder < b.SortOrder { - return -1 - } - if a.SortOrder > b.SortOrder { - return 1 - } - return 0 - }) +func (cfg Item) GetOverride() Item { + return overrideConfigInstance.GetOverride(cfg) +} + +func (c HomepageMap) Add(item *Item) { + c.add(item, item.Category) + // add to all category even if item is hidden + c.add(item, CategoryAll) + if item.Show { + if item.Favorite { + c.add(item, CategoryFavorites) + } + } 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 + }) + } } diff --git a/internal/homepage/homepage_test.go b/internal/homepage/homepage_test.go index cda36899..3602e75d 100644 --- a/internal/homepage/homepage_test.go +++ b/internal/homepage/homepage_test.go @@ -10,7 +10,7 @@ import ( func TestOverrideItem(t *testing.T) { a := &Item{ Alias: "foo", - ItemConfig: &ItemConfig{ + ItemConfig: ItemConfig{ Show: false, Name: "Foo", Icon: &IconURL{ @@ -20,7 +20,7 @@ func TestOverrideItem(t *testing.T) { Category: "App", }, } - want := &ItemConfig{ + want := ItemConfig{ Show: true, Name: "Bar", Category: "Test", @@ -30,7 +30,107 @@ func TestOverrideItem(t *testing.T) { }, } overrides := GetOverrideConfig() + overrides.Initialize() overrides.OverrideItem(a.Alias, want) - got := a.GetOverride(a.Alias) - ExpectEqual(t, got, want) + got := a.GetOverride() + 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) } diff --git a/internal/homepage/list_icons.go b/internal/homepage/list_icons.go index ceb10722..b3f2ce1b 100644 --- a/internal/homepage/list_icons.go +++ b/internal/homepage/list_icons.go @@ -94,8 +94,8 @@ func NewIconKey(source IconSource, reference string) IconKey { } func (k IconKey) SourceRef() (IconSource, string) { - parts := strings.Split(string(k), "/") - return IconSource(parts[0]), parts[1] + source, ref, _ := strings.Cut(string(k), "/") + return IconSource(source), ref } func InitIconListCache() { diff --git a/internal/homepage/override_config.go b/internal/homepage/override_config.go index d520ec21..fb5e5edf 100644 --- a/internal/homepage/override_config.go +++ b/internal/homepage/override_config.go @@ -9,10 +9,13 @@ import ( ) type OverrideConfig struct { - ItemOverrides map[string]*ItemConfig `json:"item_overrides"` - DisplayOrder map[string]int `json:"display_order"` - CategoryOrder map[string]int `json:"category_order"` - ItemVisibility map[string]bool `json:"item_visibility"` + ItemOverrides map[string]ItemConfig `json:"item_overrides"` + DisplayOrder map[string]int `json:"display_order"` + CategoryOrder map[string]int `json:"category_order"` + 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 } @@ -23,57 +26,94 @@ func GetOverrideConfig() *OverrideConfig { } func (c *OverrideConfig) Initialize() { - c.ItemOverrides = make(map[string]*ItemConfig) + c.ItemOverrides = make(map[string]ItemConfig) c.DisplayOrder = 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.ItemFavorite = make(map[string]bool) } -func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) { +func (c *OverrideConfig) OverrideItem(alias string, override ItemConfig) { c.mu.Lock() defer c.mu.Unlock() c.ItemOverrides[alias] = override } -func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) { +func (c *OverrideConfig) OverrideItems(items map[string]ItemConfig) { c.mu.Lock() defer c.mu.Unlock() maps.Copy(c.ItemOverrides, items) } -func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig { +func (c *OverrideConfig) GetOverride(item Item) Item { c.mu.RLock() defer c.mu.RUnlock() - if itemOverride, hasOverride := c.ItemOverrides[alias]; hasOverride { - itemOverride.URL = item.URL // NOTE: we don't want to override the URL - item = itemOverride + + if overrides, hasOverride := c.ItemOverrides[item.Alias]; hasOverride { + 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 - clone.Show = show - return &clone + + if show, ok := c.ItemVisibility[item.Alias]; ok { + item.Show = show + } + 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 } +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) { c.mu.Lock() defer c.mu.Unlock() 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 - } -} diff --git a/internal/types/routes.go b/internal/types/routes.go index 761eeb61..cf62f37a 100644 --- a/internal/types/routes.go +++ b/internal/types/routes.go @@ -28,8 +28,8 @@ type ( IdlewatcherConfig() *IdlewatcherConfig HealthCheckConfig() *HealthCheckConfig LoadBalanceConfig() *LoadBalancerConfig - HomepageConfig() *homepage.ItemConfig - HomepageItem() *homepage.Item + HomepageItem() homepage.Item + DisplayName() string ContainerInfo() *Container GetAgent() *agent.AgentConfig