feat(homepage): enhance homepage functionality with new item click tracking, sort methods and category management

- Added ItemClick endpoint to increment item click counts.
- Refactored Categories function to dynamically generate categories based on available items.
- Introduced sorting methods for homepage items and categories.
- Updated item configuration to include visibility, favorite status, and sort orders.
- Improved handling of item URLs and added support for websocket connections in item retrieval.
This commit is contained in:
yusing
2025-09-13 23:52:54 +08:00
parent 58a2dc73dd
commit 2c290a3916
12 changed files with 612 additions and 105 deletions

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/route/routes"
)
@@ -18,5 +19,24 @@ import (
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /homepage/categories [get]
func Categories(c *gin.Context) {
c.JSON(http.StatusOK, routes.HomepageCategories())
c.JSON(http.StatusOK, HomepageCategories())
}
func HomepageCategories() []string {
check := make(map[string]struct{})
categories := make([]string, 0)
categories = append(categories, homepage.CategoryAll)
categories = append(categories, homepage.CategoryFavorites)
for _, r := range routes.HTTP.Iter {
item := r.HomepageItem()
if item.Category == "" {
continue
}
if _, ok := check[item.Category]; ok {
continue
}
check[item.Category] = struct{}{}
categories = append(categories, item.Category)
}
return categories
}

View File

@@ -0,0 +1,36 @@
package homepageapi
import (
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
)
type HomepageOverrideItemClickParams struct {
Which string `form:"which" binding:"required"`
} // @name HomepageOverrideItemClickParams
// @x-id "item-click"
// @BasePath /api/v1
// @Summary Increment item click
// @Description Increment item click.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request query HomepageOverrideItemClickParams true "Increment item click"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/item_click [post]
func ItemClick(c *gin.Context) {
var params HomepageOverrideItemClickParams
if err := c.ShouldBindQuery(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.IncrementItemClicks(params.Which)
c.JSON(http.StatusOK, apitypes.Success("success"))
}

View File

@@ -1,27 +1,38 @@
package homepageapi
import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/lithammer/fuzzysearch/fuzzy"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/go-proxy/internal/route/routes"
)
type HomepageItemsRequest struct {
Category string `form:"category" validate:"omitempty"`
Provider string `form:"provider" validate:"omitempty"`
SearchQuery string `form:"search"` // Search query
Category string `form:"category"` // Category filter
Provider string `form:"provider"` // Provider filter
// Sort method
SortMethod homepage.SortMethod `form:"sort_method" default:"alphabetical" binding:"omitempty,oneof=clicks alphabetical custom"`
} // @name HomepageItemsRequest
// @x-id "items"
// @BasePath /api/v1
// @Summary Homepage items
// @Description Homepage items
// @Tags homepage
// @Tags homepage,websocket
// @Accept json
// @Produce json
// @Param category query string false "Category filter"
// @Param provider query string false "Provider filter"
// @Param query query HomepageItemsRequest false "Query parameters"
// @Success 200 {object} homepage.Homepage
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
@@ -42,5 +53,81 @@ func Items(c *gin.Context) {
hostname = host
}
c.JSON(http.StatusOK, routes.HomepageItems(proto, hostname, request.Category, request.Provider))
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
return HomepageItems(proto, hostname, &request), nil
})
} else {
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
}
}
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
switch proto {
case "http", "https":
default:
proto = "http"
}
hp := homepage.NewHomepageMap(routes.HTTP.Size())
if strings.Count(hostname, ".") > 1 {
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
}
for _, r := range routes.HTTP.Iter {
if request.Provider != "" && r.ProviderName() != request.Provider {
continue
}
item := r.HomepageItem()
if request.Category != "" && item.Category != request.Category {
continue
}
if request.SearchQuery != "" && !fuzzy.MatchFold(request.SearchQuery, item.Name) {
continue
}
// clear url if invalid
_, err := url.Parse(item.URL)
if err != nil {
item.URL = ""
}
// append hostname if provided and only if alias is not FQDN
if hostname != "" && item.URL == "" {
isFQDNAlias := strings.Contains(item.Alias, ".")
if !isFQDNAlias {
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
} else {
item.URL = fmt.Sprintf("%s://%s", proto, item.Alias)
}
}
// prepend protocol if not exists
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
}
hp.Add(&item)
}
ret := hp.Values()
// sort items in each category
for _, category := range ret {
category.Sort(request.SortMethod)
}
// sort categories
overrides := homepage.GetOverrideConfig()
slices.SortStableFunc(ret, func(a, b *homepage.Category) int {
// if category is "Hidden", move it to the end of the list
if a.Name == homepage.CategoryHidden {
return 1
}
if b.Name == homepage.CategoryHidden {
return -1
}
// sort categories by order in config
return overrides.CategoryOrder[a.Name] - overrides.CategoryOrder[b.Name]
})
return ret
}

View File

@@ -1,7 +1,6 @@
package homepageapi
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
@@ -15,16 +14,22 @@ type (
Value homepage.ItemConfig `json:"value"`
} // @name HomepageOverrideItemParams
HomepageOverrideItemsBatchParams struct {
Value map[string]*homepage.ItemConfig `json:"value"`
Value map[string]homepage.ItemConfig `json:"value"`
} // @name HomepageOverrideItemsBatchParams
HomepageOverrideCategoryOrderParams struct {
Which string `json:"which"`
Value int `json:"value"`
} // @name HomepageOverrideCategoryOrderParams
HomepageOverrideItemSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemSortOrderParams
HomepageOverrideItemAllSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemAllSortOrderParams
HomepageOverrideItemFavSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemFavSortOrderParams
HomepageOverrideItemVisibleParams struct {
Which []string `json:"which"`
Value bool `json:"value"`
} // @name HomepageOverrideItemVisibleParams
HomepageOverrideItemFavoriteParams HomepageOverrideItemVisibleParams // @name HomepageOverrideItemFavoriteParams
)
// @x-id "set-item"
@@ -46,7 +51,7 @@ func SetItem(c *gin.Context) {
return
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItem(params.Which, &params.Value)
overrides.OverrideItem(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
@@ -65,15 +70,8 @@ func SetItem(c *gin.Context) {
func SetItemsBatch(c *gin.Context) {
var params HomepageOverrideItemsBatchParams
if err := c.ShouldBindJSON(&params); err != nil {
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItems(params.Value)
@@ -95,22 +93,103 @@ func SetItemsBatch(c *gin.Context) {
func SetItemVisible(c *gin.Context) {
var params HomepageOverrideItemVisibleParams
if err := c.ShouldBindJSON(&params); err != nil {
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
if params.Value {
overrides.UnhideItems(params.Which)
} else {
overrides.HideItems(params.Which)
overrides.SetItemsVisibility(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-favorite"
// @BasePath /api/v1
// @Summary Set homepage item favorite
// @Description Set homepage item favorite.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemFavoriteParams true "Set item favorite"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_favorite [post]
func SetItemFavorite(c *gin.Context) {
var params HomepageOverrideItemFavoriteParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetItemsFavorite(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item sort order
// @Description Set homepage item sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemSortOrderParams true "Set item sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_sort_order [post]
func SetItemSortOrder(c *gin.Context) {
var params HomepageOverrideItemSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-all-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item all sort order
// @Description Set homepage item all sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemAllSortOrderParams true "Set item all sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_all_sort_order [post]
func SetItemAllSortOrder(c *gin.Context) {
var params HomepageOverrideItemAllSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetAllSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
// @x-id "set-item-fav-sort-order"
// @BasePath /api/v1
// @Summary Set homepage item fav sort order
// @Description Set homepage item fav sort order.
// @Tags homepage
// @Accept json
// @Produce json
// @Param request body HomepageOverrideItemFavSortOrderParams true "Set item fav sort order"
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/set/item_fav_sort_order [post]
func SetItemFavSortOrder(c *gin.Context) {
var params HomepageOverrideItemFavSortOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetFavSortOrder(params.Which, params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
@@ -129,15 +208,8 @@ func SetItemVisible(c *gin.Context) {
func SetCategoryOrder(c *gin.Context) {
var params HomepageOverrideCategoryOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
overrides := homepage.GetOverrideConfig()
overrides.SetCategoryOrder(params.Which, params.Value)

View File

@@ -66,6 +66,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
IsExplicit: isExplicit,
IsHostNetworkMode: c.HostConfig.NetworkMode == "host",
Running: c.Status == "running" || c.State == "running",
State: c.State,
}
if agent.IsDockerHostAgent(dockerHost) {
@@ -143,9 +144,11 @@ var databaseMPs = map[string]struct{}{
}
func isDatabase(c *types.Container) bool {
for _, m := range c.Mounts.Iter {
if _, ok := databaseMPs[m]; ok {
return true
if c.Mounts != nil { // only happens in test
for _, m := range c.Mounts.Iter {
if _, ok := databaseMPs[m]; ok {
return true
}
}
}

View File

@@ -2,34 +2,72 @@ package homepage
import (
"slices"
"strings"
"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
Alias string `json:"alias"`
Provider string `json:"provider"`
OriginURL string `json:"origin_url"`
}
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
Clicks int `json:"clicks"`
Widgets []Widget `json:"widgets,omitempty"`
Alias string `json:"alias"`
Provider string `json:"provider"`
OriginURL string `json:"origin_url"`
ContainerID string `json:"container_id,omitempty" extensions:"x-nullable"`
} // @name HomepageItem
SortMethod string // @name HomepageSortMethod
)
const (
CategoryAll = "All"
CategoryFavorites = "Favorites"
CategoryHidden = "Hidden"
CategoryOthers = "Others"
)
const (
SortMethodClicks = "clicks" // @name HomepageSortMethodClicks
SortMethodAlphabetical = "alphabetical" // @name HomepageSortMethodAlphabetical
SortMethodCustom = "custom" // @name HomepageSortMethodCustom
)
func init() {
@@ -40,22 +78,115 @@ 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)
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)
}
c[item.Category] = append(c[item.Category], item)
slices.SortStableFunc(c[item.Category], func(a, b *Item) int {
if a.SortOrder < b.SortOrder {
}
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(method SortMethod) {
switch method {
case SortMethodClicks:
c.sortByClicks()
case SortMethodAlphabetical:
c.sortByAlphabetical()
case SortMethodCustom:
c.sortByCustom()
}
}
func (c *Category) sortByClicks() {
slices.SortStableFunc(c.Items, func(a, b *Item) int {
if a.Clicks > b.Clicks {
return -1
}
if a.SortOrder > b.SortOrder {
if a.Clicks < b.Clicks {
return 1
}
return 0
// fallback to alphabetical
return strings.Compare(a.Name, b.Name)
})
}
func (c *Category) sortByAlphabetical() {
slices.SortStableFunc(c.Items, func(a, b *Item) int {
return strings.Compare(a.Name, b.Name)
})
}
func (c *Category) sortByCustom() {
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) {
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)
}

View File

@@ -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() {
@@ -120,6 +120,10 @@ func InitIconListCache() {
})
}
func TestClearIconsCache() {
clear(iconsCache.Icons)
}
func ListAvailableIcons() (*Cache, error) {
if common.IsTest {
return iconsCache, nil
@@ -384,7 +388,8 @@ func UpdateSelfhstIcons() error {
for _, item := range data {
var tag string
if item.Tags != "" {
tag = strutils.CommaSeperatedList(item.Tags)[0]
tag, _, _ = strings.Cut(item.Tags, ",")
tag = strings.TrimSpace(tag)
}
icon := &IconMeta{
DisplayName: item.Name,

View File

@@ -91,6 +91,8 @@ func runTests(t *testing.T, iconsCache *Cache, test []testCases) {
}
func TestListWalkxCodeIcons(t *testing.T) {
t.Cleanup(TestClearIconsCache)
MockHTTPGet([]byte(walkxcodeIcons))
if err := UpdateWalkxCodeIcons(); err != nil {
t.Fatal(err)
@@ -124,6 +126,7 @@ func TestListWalkxCodeIcons(t *testing.T) {
}
func TestListSelfhstIcons(t *testing.T) {
t.Cleanup(TestClearIconsCache)
MockHTTPGet([]byte(selfhstIcons))
if err := UpdateSelfhstIcons(); err != nil {
t.Fatal(err)
@@ -135,9 +138,6 @@ func TestListSelfhstIcons(t *testing.T) {
if len(iconsCache.Icons) != 3 {
t.Fatalf("expect 3 icons, got %d", len(iconsCache.Icons))
}
// if len(iconsCache.IconList) != 8 {
// t.Fatalf("expect 8 icons, got %d", len(iconsCache.IconList))
// }
test := []testCases{
{
Key: NewIconKey(IconSourceSelfhSt, "2fauth"),

View File

@@ -9,10 +9,14 @@ 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"`
ItemClicks map[string]int `json:"item_clicks"`
ItemVisibility map[string]bool `json:"item_visibility"`
ItemFavorite map[string]bool `json:"item_favorite"`
mu sync.RWMutex
}
@@ -23,57 +27,104 @@ 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.ItemClicks = 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 clicks, ok := c.ItemClicks[item.Alias]; ok {
item.Clicks = clicks
}
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) {
func (c *OverrideConfig) IncrementItemClicks(key 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
}
c.ItemClicks[key]++
}

View File

@@ -22,6 +22,8 @@ type (
ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"`
State container.ContainerState `json:"state"`
Agent *agent.AgentConfig `json:"agent"`
Labels map[string]string `json:"-"` // for creating routes

View File

@@ -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