mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
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:
@@ -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
|
||||
}
|
||||
|
||||
36
internal/api/v1/homepage/item_click.go
Normal file
36
internal/api/v1/homepage/item_click.go
Normal 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(¶ms); 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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, ¶ms.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(¶ms); 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, ¶ms); 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(¶ms); 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, ¶ms); 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(¶ms); 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(¶ms); 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(¶ms); 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(¶ms); 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(¶ms); 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, ¶ms); 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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]++
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user