diff --git a/handlers/validator.go b/handlers/validator.go index b2a8f5ff..3a5ba78d 100644 --- a/handlers/validator.go +++ b/handlers/validator.go @@ -45,13 +45,13 @@ func Validator(w http.ResponseWriter, r *http.Request) { // search by index^ validatorIndex, err := strconv.ParseUint(vars["idxOrPubKey"], 10, 64) if err == nil && validatorIndex < uint64(len(validatorSetRsp.Data)) { - validator = &validatorSetRsp.Data[validatorIndex] + validator = validatorSetRsp.Data[validatorIndex] } } else { // search by pubkey for _, val := range validatorSetRsp.Data { if bytes.Equal(val.Validator.PubKey, validatorPubKey) { - validator = &val + validator = val break } } diff --git a/handlers/validators.go b/handlers/validators.go index a51954ea..00ab33a3 100644 --- a/handlers/validators.go +++ b/handlers/validators.go @@ -1,8 +1,10 @@ package handlers import ( + "bytes" "fmt" "net/http" + "sort" "strconv" "strings" "time" @@ -40,35 +42,39 @@ func Validators(w http.ResponseWriter, r *http.Request) { if urlArgs.Has("q") { stateFilter = urlArgs.Get("q") } - data.Data = getValidatorsPageData(firstIdx, pageSize, stateFilter) + var sortOrder string + if urlArgs.Has("o") { + sortOrder = urlArgs.Get("o") + } + data.Data = getValidatorsPageData(firstIdx, pageSize, stateFilter, sortOrder) if handleTemplateError(w, r, "validators.go", "Validators", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { return // an error has occurred and was processed } } -func getValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter string) *models.ValidatorsPageData { +func getValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter string, sortOrder string) *models.ValidatorsPageData { pageData := &models.ValidatorsPageData{} - pageCacheKey := fmt.Sprintf("validators:%v:%v:%v", firstValIdx, pageSize, stateFilter) + pageCacheKey := fmt.Sprintf("validators:%v:%v:%v:%v", firstValIdx, pageSize, stateFilter, sortOrder) pageData = services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { - pageData, cacheTimeout := buildValidatorsPageData(firstValIdx, pageSize, stateFilter) + pageData, cacheTimeout := buildValidatorsPageData(firstValIdx, pageSize, stateFilter, sortOrder) pageCall.CacheTimeout = cacheTimeout return pageData }).(*models.ValidatorsPageData) return pageData } -func buildValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter string) (*models.ValidatorsPageData, time.Duration) { - logrus.Printf("validators page called: %v:%v:%v", firstValIdx, pageSize, stateFilter) +func buildValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter string, sortOrder string) (*models.ValidatorsPageData, time.Duration) { + logrus.Printf("validators page called: %v:%v:%v:%v", firstValIdx, pageSize, stateFilter, sortOrder) pageData := &models.ValidatorsPageData{} cacheTime := 10 * time.Minute // get latest validator set - var validatorSet []rpctypes.ValidatorEntry + var validatorSet []*rpctypes.ValidatorEntry validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet() if validatorSetRsp == nil { cacheTime = 5 * time.Minute - validatorSet = []rpctypes.ValidatorEntry{} + validatorSet = []*rpctypes.ValidatorEntry{} } else { validatorSet = validatorSetRsp.Data } @@ -77,7 +83,60 @@ func buildValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter st // TODO: apply filter //} - totalValidatorCount := uint64(len(validatorSet)) + // apply sort order + validatorSetLen := len(validatorSet) + if sortOrder == "" { + sortOrder = "index" + } + if sortOrder != "index" { + sortedValidatorSet := make([]*rpctypes.ValidatorEntry, validatorSetLen) + copy(sortedValidatorSet, validatorSet) + + switch sortOrder { + case "index-d": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Index > sortedValidatorSet[b].Index + }) + case "pubkey": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return bytes.Compare(sortedValidatorSet[a].Validator.PubKey, sortedValidatorSet[b].Validator.PubKey) < 0 + }) + case "pubkey-d": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return bytes.Compare(sortedValidatorSet[a].Validator.PubKey, sortedValidatorSet[b].Validator.PubKey) > 0 + }) + case "balance": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Balance < sortedValidatorSet[b].Balance + }) + case "balance-d": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Balance > sortedValidatorSet[b].Balance + }) + case "activation": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Validator.ActivationEpoch < sortedValidatorSet[b].Validator.ActivationEpoch + }) + case "activation-d": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Validator.ActivationEpoch > sortedValidatorSet[b].Validator.ActivationEpoch + }) + case "exit": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Validator.ExitEpoch < sortedValidatorSet[b].Validator.ExitEpoch + }) + case "exit-d": + sort.Slice(sortedValidatorSet, func(a, b int) bool { + return sortedValidatorSet[a].Validator.ExitEpoch > sortedValidatorSet[b].Validator.ExitEpoch + }) + } + validatorSet = sortedValidatorSet + } else { + pageData.IsDefaultSorting = true + } + pageData.Sorting = sortOrder + + totalValidatorCount := uint64(validatorSetLen) if firstValIdx == 0 { pageData.IsDefaultPage = true } else if firstValIdx > totalValidatorCount { diff --git a/rpctypes/beaconapi.go b/rpctypes/beaconapi.go index 74a4ce5c..240df138 100644 --- a/rpctypes/beaconapi.go +++ b/rpctypes/beaconapi.go @@ -87,7 +87,7 @@ type EpochAssignments struct { } type StandardV1StateValidatorsResponse struct { - Data []ValidatorEntry `json:"data"` + Data []*ValidatorEntry `json:"data"` } type StandardV1GenesisResponse struct { diff --git a/services/beaconservice.go b/services/beaconservice.go index 47befcf8..046b5d52 100644 --- a/services/beaconservice.go +++ b/services/beaconservice.go @@ -737,8 +737,12 @@ func (bs *BeaconService) GetValidatorActivity() (map[uint64]uint8, uint64) { for epochIdx := int64(idxHeadEpoch); epochIdx >= int64(idxMinEpoch); epochIdx-- { epoch := uint64(epochIdx) _, epochVotes := bs.indexer.GetEpochVotes(epoch) - for valIdx := range epochVotes.ActivityMap { - activityMap[valIdx]++ + if epochVotes == nil { + epochLimit-- + } else { + for valIdx := range epochVotes.ActivityMap { + activityMap[valIdx]++ + } } } diff --git a/static/css/layout.css b/static/css/layout.css index 84ff33ed..43a7cc72 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -277,3 +277,18 @@ span.validator-label { text-overflow: ellipsis; overflow: hidden; } + +.table-sorting thead .col-sorting { + float: right; +} + +.table-sorting thead .sort-link { + margin-left: -3px; + color: var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color))); + font-size: 14px; + opacity: 0.3; +} + +.table-sorting thead .sort-link:hover, .table-sorting thead .sort-link.active { + opacity: 1; +} diff --git a/templates/validators/validators.html b/templates/validators/validators.html index f42bfcf9..0776a83f 100644 --- a/templates/validators/validators.html +++ b/templates/validators/validators.html @@ -29,6 +29,9 @@

Validators Overvie {{ if not .IsDefaultPage }} {{ end }} + {{ if not .IsDefaultSorting }} + + {{ end }} {{ if .StateFilter }} {{ end }} @@ -49,16 +52,46 @@

Validators Overvie --> -
+
- - - + + + - - + + @@ -133,19 +166,19 @@

Validators Overvie diff --git a/types/models/validators.go b/types/models/validators.go index 2c5d807d..26d6b69d 100644 --- a/types/models/validators.go +++ b/types/models/validators.go @@ -6,22 +6,23 @@ import ( // ValidatorsPageData is a struct to hold info for the validators page type ValidatorsPageData struct { - Validators []*ValidatorsPageDataValidator `json:"validators"` - ValidatorCount uint64 `json:"validator_count"` - FirstValidator uint64 `json:"first_validx"` - LastValidator uint64 `json:"last_validx"` - StateFilter string `json:"state_filter"` - - IsDefaultPage bool `json:"default_page"` - TotalPages uint64 `json:"total_pages"` - PageSize uint64 `json:"page_size"` - CurrentPageIndex uint64 `json:"page_index"` - CurrentPageValIdx uint64 `json:"page_validx"` - PrevPageIndex uint64 `json:"prev_page_index"` - PrevPageValIdx uint64 `json:"prev_page_validx"` - NextPageIndex uint64 `json:"next_page_index"` - NextPageValIdx uint64 `json:"next_page_validx"` - LastPageValIdx uint64 `json:"last_page_validx"` + Validators []*ValidatorsPageDataValidator `json:"validators"` + ValidatorCount uint64 `json:"validator_count"` + FirstValidator uint64 `json:"first_validx"` + LastValidator uint64 `json:"last_validx"` + StateFilter string `json:"state_filter"` + Sorting string `json:"sorting"` + IsDefaultSorting bool `json:"default_sorting"` + IsDefaultPage bool `json:"default_page"` + TotalPages uint64 `json:"total_pages"` + PageSize uint64 `json:"page_size"` + CurrentPageIndex uint64 `json:"page_index"` + CurrentPageValIdx uint64 `json:"page_validx"` + PrevPageIndex uint64 `json:"prev_page_index"` + PrevPageValIdx uint64 `json:"prev_page_validx"` + NextPageIndex uint64 `json:"next_page_index"` + NextPageValIdx uint64 `json:"next_page_validx"` + LastPageValIdx uint64 `json:"last_page_validx"` } type ValidatorsPageDataValidator struct {

IndexPublic KeyBalance + Index +
+ + +
+
+ Public Key +
+ + +
+
+ Balance +
+ + +
+
StateActivationExit + Activation +
+ + +
+
+ Exit +
+ + +
+
W/address