Merge branch 'feature/community-filter' into develop
This commit is contained in:
commit
8d3188351a
@ -107,56 +107,6 @@ type RouteserversResponse struct {
|
||||
Routeservers []Routeserver `json:"routeservers"`
|
||||
}
|
||||
|
||||
// Neighbours
|
||||
type Neighbours []*Neighbour
|
||||
|
||||
type Neighbour struct {
|
||||
Id string `json:"id"`
|
||||
|
||||
// Mandatory fields
|
||||
Address string `json:"address"`
|
||||
Asn int `json:"asn"`
|
||||
State string `json:"state"`
|
||||
Description string `json:"description"`
|
||||
RoutesReceived int `json:"routes_received"`
|
||||
RoutesFiltered int `json:"routes_filtered"`
|
||||
RoutesExported int `json:"routes_exported"`
|
||||
RoutesPreferred int `json:"routes_preferred"`
|
||||
RoutesAccepted int `json:"routes_accepted"`
|
||||
RoutesPipeFiltered int `json:"routes_pipe_filtered"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
LastError string `json:"last_error"`
|
||||
|
||||
// Original response
|
||||
Details map[string]interface{} `json:"details"`
|
||||
}
|
||||
|
||||
// Implement sorting interface for routes
|
||||
func (neighbours Neighbours) Len() int {
|
||||
return len(neighbours)
|
||||
}
|
||||
|
||||
func (neighbours Neighbours) Less(i, j int) bool {
|
||||
return neighbours[i].Asn < neighbours[j].Asn
|
||||
}
|
||||
|
||||
func (neighbours Neighbours) Swap(i, j int) {
|
||||
neighbours[i], neighbours[j] = neighbours[j], neighbours[i]
|
||||
}
|
||||
|
||||
type NeighboursResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Neighbours Neighbours `json:"neighbours"`
|
||||
}
|
||||
|
||||
// Neighbours response is cacheable
|
||||
func (self *NeighboursResponse) CacheTtl() time.Duration {
|
||||
now := time.Now().UTC()
|
||||
return self.Api.Ttl.Sub(now)
|
||||
}
|
||||
|
||||
type NeighboursLookupResults map[int]Neighbours
|
||||
|
||||
// BGP
|
||||
type Community []int
|
||||
|
||||
@ -248,143 +198,3 @@ func (bgp BgpInfo) HasLargeCommunity(community Community) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Prefixes
|
||||
type Route struct {
|
||||
Id string `json:"id"`
|
||||
NeighbourId string `json:"neighbour_id"`
|
||||
|
||||
Network string `json:"network"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metric int `json:"metric"`
|
||||
Bgp BgpInfo `json:"bgp"`
|
||||
Age time.Duration `json:"age"`
|
||||
Type []string `json:"type"` // [BGP, unicast, univ]
|
||||
Primary bool `json:"primary"`
|
||||
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
type Routes []*Route
|
||||
|
||||
// Implement sorting interface for routes
|
||||
func (routes Routes) Len() int {
|
||||
return len(routes)
|
||||
}
|
||||
|
||||
func (routes Routes) Less(i, j int) bool {
|
||||
return routes[i].Network < routes[j].Network
|
||||
}
|
||||
|
||||
func (routes Routes) Swap(i, j int) {
|
||||
routes[i], routes[j] = routes[j], routes[i]
|
||||
}
|
||||
|
||||
type RoutesResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Imported Routes `json:"imported"`
|
||||
Filtered Routes `json:"filtered"`
|
||||
NotExported Routes `json:"not_exported"`
|
||||
}
|
||||
|
||||
func (self *RoutesResponse) CacheTtl() time.Duration {
|
||||
now := time.Now().UTC()
|
||||
return self.Api.Ttl.Sub(now)
|
||||
}
|
||||
|
||||
type TimedResponse struct {
|
||||
RequestDuration float64 `json:"request_duration_ms"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
}
|
||||
|
||||
type PaginatedResponse struct {
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type FilterableResponse struct {
|
||||
FiltersAvailable *SearchFilters `json:"filters_available"`
|
||||
FiltersApplied *SearchFilters `json:"filters_applied"`
|
||||
}
|
||||
|
||||
type PaginatedRoutesResponse struct {
|
||||
*RoutesResponse
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
// Lookup Prefixes
|
||||
type LookupRoute struct {
|
||||
Id string `json:"id"`
|
||||
NeighbourId string `json:"neighbour_id"`
|
||||
Neighbour *Neighbour `json:"neighbour"`
|
||||
|
||||
State string `json:"state"` // Filtered, Imported, ...
|
||||
|
||||
Routeserver Routeserver `json:"routeserver"`
|
||||
|
||||
Network string `json:"network"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metric int `json:"metric"`
|
||||
Bgp BgpInfo `json:"bgp"`
|
||||
Age time.Duration `json:"age"`
|
||||
Type []string `json:"type"` // [BGP, unicast, univ]
|
||||
Primary bool `json:"primary"`
|
||||
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
// Implement sorting interface for lookup routes
|
||||
func (routes LookupRoutes) Len() int {
|
||||
return len(routes)
|
||||
}
|
||||
|
||||
func (routes LookupRoutes) Less(i, j int) bool {
|
||||
return routes[i].Network < routes[j].Network
|
||||
}
|
||||
|
||||
func (routes LookupRoutes) Swap(i, j int) {
|
||||
routes[i], routes[j] = routes[j], routes[i]
|
||||
}
|
||||
|
||||
type LookupRoutes []*LookupRoute
|
||||
|
||||
// TODO: Naming is a bit yuck
|
||||
type LookupRoutesResponse struct {
|
||||
*PaginatedResponse
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
}
|
||||
|
||||
// TODO: Refactor this (might be legacy)
|
||||
type RoutesLookupResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
}
|
||||
|
||||
type RoutesLookupResponseGlobal struct {
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
|
||||
// Pagination
|
||||
TotalRoutes int `json:"total_routes"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
|
||||
// Meta
|
||||
Time float64 `json:"query_duration_ms"`
|
||||
}
|
||||
|
||||
type PaginatedRoutesLookupResponse struct {
|
||||
TimedResponse
|
||||
FilterableResponse
|
||||
|
||||
Api ApiStatus `json:"api"` // Add to provide cache status information
|
||||
|
||||
Imported *LookupRoutesResponse `json:"imported"`
|
||||
Filtered *LookupRoutesResponse `json:"filtered"`
|
||||
}
|
||||
|
55
backend/api/response_neighbors.go
Normal file
55
backend/api/response_neighbors.go
Normal file
@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Neighbours
|
||||
type Neighbours []*Neighbour
|
||||
|
||||
type Neighbour struct {
|
||||
Id string `json:"id"`
|
||||
|
||||
// Mandatory fields
|
||||
Address string `json:"address"`
|
||||
Asn int `json:"asn"`
|
||||
State string `json:"state"`
|
||||
Description string `json:"description"`
|
||||
RoutesReceived int `json:"routes_received"`
|
||||
RoutesFiltered int `json:"routes_filtered"`
|
||||
RoutesExported int `json:"routes_exported"`
|
||||
RoutesPreferred int `json:"routes_preferred"`
|
||||
RoutesAccepted int `json:"routes_accepted"`
|
||||
RoutesPipeFiltered int `json:"routes_pipe_filtered"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
LastError string `json:"last_error"`
|
||||
|
||||
// Original response
|
||||
Details map[string]interface{} `json:"details"`
|
||||
}
|
||||
|
||||
// Implement sorting interface for routes
|
||||
func (neighbours Neighbours) Len() int {
|
||||
return len(neighbours)
|
||||
}
|
||||
|
||||
func (neighbours Neighbours) Less(i, j int) bool {
|
||||
return neighbours[i].Asn < neighbours[j].Asn
|
||||
}
|
||||
|
||||
func (neighbours Neighbours) Swap(i, j int) {
|
||||
neighbours[i], neighbours[j] = neighbours[j], neighbours[i]
|
||||
}
|
||||
|
||||
type NeighboursResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Neighbours Neighbours `json:"neighbours"`
|
||||
}
|
||||
|
||||
// Neighbours response is cacheable
|
||||
func (self *NeighboursResponse) CacheTtl() time.Duration {
|
||||
now := time.Now().UTC()
|
||||
return self.Api.Ttl.Sub(now)
|
||||
}
|
||||
|
||||
type NeighboursLookupResults map[int]Neighbours
|
191
backend/api/response_routes.go
Normal file
191
backend/api/response_routes.go
Normal file
@ -0,0 +1,191 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Prefixes
|
||||
type Route struct {
|
||||
Id string `json:"id"`
|
||||
NeighbourId string `json:"neighbour_id"`
|
||||
|
||||
Network string `json:"network"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metric int `json:"metric"`
|
||||
Bgp BgpInfo `json:"bgp"`
|
||||
Age time.Duration `json:"age"`
|
||||
Type []string `json:"type"` // [BGP, unicast, univ]
|
||||
Primary bool `json:"primary"`
|
||||
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
// Implement Filterable interface for routes
|
||||
func (self *Route) MatchSourceId(id int) bool {
|
||||
return true // A route has no source info so we exclude this filter
|
||||
}
|
||||
|
||||
func (self *Route) MatchAsn(asn int) bool {
|
||||
return true // Same here
|
||||
}
|
||||
|
||||
// Only community filters are interesting at this point:
|
||||
func (self *Route) MatchCommunity(community Community) bool {
|
||||
return self.Bgp.HasCommunity(community)
|
||||
}
|
||||
|
||||
func (self *Route) MatchExtCommunity(community ExtCommunity) bool {
|
||||
return self.Bgp.HasExtCommunity(community)
|
||||
}
|
||||
|
||||
func (self *Route) MatchLargeCommunity(community Community) bool {
|
||||
return self.Bgp.HasLargeCommunity(community)
|
||||
}
|
||||
|
||||
type Routes []*Route
|
||||
|
||||
// Implement sorting interface for routes
|
||||
func (routes Routes) Len() int {
|
||||
return len(routes)
|
||||
}
|
||||
|
||||
func (routes Routes) Less(i, j int) bool {
|
||||
return routes[i].Network < routes[j].Network
|
||||
}
|
||||
|
||||
func (routes Routes) Swap(i, j int) {
|
||||
routes[i], routes[j] = routes[j], routes[i]
|
||||
}
|
||||
|
||||
type RoutesResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Imported Routes `json:"imported"`
|
||||
Filtered Routes `json:"filtered"`
|
||||
NotExported Routes `json:"not_exported"`
|
||||
}
|
||||
|
||||
func (self *RoutesResponse) CacheTtl() time.Duration {
|
||||
now := time.Now().UTC()
|
||||
return self.Api.Ttl.Sub(now)
|
||||
}
|
||||
|
||||
type TimedResponse struct {
|
||||
RequestDuration float64 `json:"request_duration_ms"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
}
|
||||
|
||||
type PaginatedResponse struct {
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type FilterableResponse struct {
|
||||
FiltersAvailable *SearchFilters `json:"filters_available"`
|
||||
FiltersApplied *SearchFilters `json:"filters_applied"`
|
||||
}
|
||||
|
||||
type PaginatedRoutesResponse struct {
|
||||
*RoutesResponse
|
||||
TimedResponse
|
||||
FilterableResponse
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
// Lookup Prefixes
|
||||
type LookupRoute struct {
|
||||
Id string `json:"id"`
|
||||
NeighbourId string `json:"neighbour_id"`
|
||||
Neighbour *Neighbour `json:"neighbour"`
|
||||
|
||||
State string `json:"state"` // Filtered, Imported, ...
|
||||
|
||||
Routeserver Routeserver `json:"routeserver"`
|
||||
|
||||
Network string `json:"network"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metric int `json:"metric"`
|
||||
Bgp BgpInfo `json:"bgp"`
|
||||
Age time.Duration `json:"age"`
|
||||
Type []string `json:"type"` // [BGP, unicast, univ]
|
||||
Primary bool `json:"primary"`
|
||||
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
// Implement Filterable interface for lookup routes
|
||||
func (self *LookupRoute) MatchSourceId(id int) bool {
|
||||
return self.Routeserver.Id == id
|
||||
}
|
||||
|
||||
func (self *LookupRoute) MatchAsn(asn int) bool {
|
||||
return self.Neighbour.Asn == asn
|
||||
}
|
||||
|
||||
// Only community filters are interesting at this point:
|
||||
func (self *LookupRoute) MatchCommunity(community Community) bool {
|
||||
return self.Bgp.HasCommunity(community)
|
||||
}
|
||||
|
||||
func (self *LookupRoute) MatchExtCommunity(community ExtCommunity) bool {
|
||||
return self.Bgp.HasExtCommunity(community)
|
||||
}
|
||||
|
||||
func (self *LookupRoute) MatchLargeCommunity(community Community) bool {
|
||||
return self.Bgp.HasLargeCommunity(community)
|
||||
}
|
||||
|
||||
// Implement sorting interface for lookup routes
|
||||
func (routes LookupRoutes) Len() int {
|
||||
return len(routes)
|
||||
}
|
||||
|
||||
func (routes LookupRoutes) Less(i, j int) bool {
|
||||
return routes[i].Network < routes[j].Network
|
||||
}
|
||||
|
||||
func (routes LookupRoutes) Swap(i, j int) {
|
||||
routes[i], routes[j] = routes[j], routes[i]
|
||||
}
|
||||
|
||||
type LookupRoutes []*LookupRoute
|
||||
|
||||
// TODO: Naming is a bit yuck
|
||||
type LookupRoutesResponse struct {
|
||||
*PaginatedResponse
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
}
|
||||
|
||||
// TODO: Refactor this (might be legacy)
|
||||
type RoutesLookupResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
}
|
||||
|
||||
type RoutesLookupResponseGlobal struct {
|
||||
Routes LookupRoutes `json:"routes"`
|
||||
|
||||
// Pagination
|
||||
TotalRoutes int `json:"total_routes"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
|
||||
// Meta
|
||||
Time float64 `json:"query_duration_ms"`
|
||||
}
|
||||
|
||||
type PaginatedRoutesLookupResponse struct {
|
||||
TimedResponse
|
||||
FilterableResponse
|
||||
|
||||
Api ApiStatus `json:"api"` // Add to provide cache status information
|
||||
|
||||
Imported *LookupRoutesResponse `json:"imported"`
|
||||
Filtered *LookupRoutesResponse `json:"filtered"`
|
||||
}
|
@ -20,10 +20,12 @@ API Search
|
||||
* Handle filter criteria
|
||||
|
||||
*/
|
||||
type FilterValue interface{}
|
||||
|
||||
type SearchFilter struct {
|
||||
Cardinality int `json:"cardinality"`
|
||||
Name string `json:"name"`
|
||||
Value interface{} `json:"value"`
|
||||
Value FilterValue `json:"value"`
|
||||
}
|
||||
|
||||
type SearchFilterGroup struct {
|
||||
@ -33,50 +35,58 @@ type SearchFilterGroup struct {
|
||||
filtersIdx map[string]int
|
||||
}
|
||||
|
||||
type Filterable interface {
|
||||
MatchSourceId(sourceId int) bool
|
||||
MatchAsn(asn int) bool
|
||||
MatchCommunity(community Community) bool
|
||||
MatchExtCommunity(community ExtCommunity) bool
|
||||
MatchLargeCommunity(community Community) bool
|
||||
}
|
||||
|
||||
/*
|
||||
Search comparators
|
||||
*/
|
||||
type SearchFilterComparator func(route *LookupRoute, value interface{}) bool
|
||||
type SearchFilterComparator func(route Filterable, value interface{}) bool
|
||||
|
||||
func searchFilterMatchSource(route *LookupRoute, value interface{}) bool {
|
||||
func searchFilterMatchSource(route Filterable, value interface{}) bool {
|
||||
sourceId, ok := value.(int)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return route.Routeserver.Id == sourceId
|
||||
return route.MatchSourceId(sourceId)
|
||||
}
|
||||
|
||||
func searchFilterMatchAsn(route *LookupRoute, value interface{}) bool {
|
||||
func searchFilterMatchAsn(route Filterable, value interface{}) bool {
|
||||
asn, ok := value.(int)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return route.Neighbour.Asn == asn
|
||||
return route.MatchAsn(asn)
|
||||
}
|
||||
|
||||
func searchFilterMatchCommunity(route *LookupRoute, value interface{}) bool {
|
||||
func searchFilterMatchCommunity(route Filterable, value interface{}) bool {
|
||||
community, ok := value.(Community)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return route.Bgp.HasCommunity(community)
|
||||
return route.MatchCommunity(community)
|
||||
}
|
||||
|
||||
func searchFilterMatchExtCommunity(route *LookupRoute, value interface{}) bool {
|
||||
func searchFilterMatchExtCommunity(route Filterable, value interface{}) bool {
|
||||
community, ok := value.(ExtCommunity)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return route.Bgp.HasExtCommunity(community)
|
||||
return route.MatchExtCommunity(community)
|
||||
}
|
||||
|
||||
func searchFilterMatchLargeCommunity(route *LookupRoute, value interface{}) bool {
|
||||
func searchFilterMatchLargeCommunity(route Filterable, value interface{}) bool {
|
||||
community, ok := value.(Community)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return route.Bgp.HasLargeCommunity(community)
|
||||
return route.MatchLargeCommunity(community)
|
||||
}
|
||||
|
||||
func selectCmpFuncByKey(key string) SearchFilterComparator {
|
||||
@ -104,7 +114,7 @@ func selectCmpFuncByKey(key string) SearchFilterComparator {
|
||||
return cmp
|
||||
}
|
||||
|
||||
func (self *SearchFilterGroup) MatchAny(route *LookupRoute) bool {
|
||||
func (self *SearchFilterGroup) MatchAny(route Filterable) bool {
|
||||
// Check if we have any filter to match
|
||||
if len(self.Filters) == 0 {
|
||||
return true // no filter, everything matches
|
||||
@ -126,7 +136,7 @@ func (self *SearchFilterGroup) MatchAny(route *LookupRoute) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *SearchFilterGroup) MatchAll(route *LookupRoute) bool {
|
||||
func (self *SearchFilterGroup) MatchAll(route Filterable) bool {
|
||||
// Check if we have any filter to match
|
||||
if len(self.Filters) == 0 {
|
||||
return true // no filter, everything matches. Like above.
|
||||
@ -241,7 +251,7 @@ func (self *SearchFilterGroup) AddFilters(filters []*SearchFilter) {
|
||||
- Extract ASN, source, bgp communites,
|
||||
- Find Filter in group, increment result count if required.
|
||||
*/
|
||||
func (self *SearchFilters) UpdateFromRoute(route *LookupRoute) {
|
||||
func (self *SearchFilters) UpdateFromLookupRoute(route *LookupRoute) {
|
||||
// Add source
|
||||
self.GetGroupByKey(SEARCH_KEY_SOURCES).AddFilter(&SearchFilter{
|
||||
Name: route.Routeserver.Name,
|
||||
@ -278,6 +288,34 @@ func (self *SearchFilters) UpdateFromRoute(route *LookupRoute) {
|
||||
}
|
||||
}
|
||||
|
||||
// This is the same as above, but only the communities
|
||||
// are considered.
|
||||
func (self *SearchFilters) UpdateFromRoute(route *Route) {
|
||||
|
||||
// Add communities
|
||||
communities := self.GetGroupByKey(SEARCH_KEY_COMMUNITIES)
|
||||
for _, c := range route.Bgp.Communities {
|
||||
communities.AddFilter(&SearchFilter{
|
||||
Name: c.String(),
|
||||
Value: c,
|
||||
})
|
||||
}
|
||||
extCommunities := self.GetGroupByKey(SEARCH_KEY_EXT_COMMUNITIES)
|
||||
for _, c := range route.Bgp.ExtCommunities {
|
||||
extCommunities.AddFilter(&SearchFilter{
|
||||
Name: c.String(),
|
||||
Value: c,
|
||||
})
|
||||
}
|
||||
largeCommunities := self.GetGroupByKey(SEARCH_KEY_LARGE_COMMUNITIES)
|
||||
for _, c := range route.Bgp.LargeCommunities {
|
||||
largeCommunities.AddFilter(&SearchFilter{
|
||||
Name: c.String(),
|
||||
Value: c,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Build filter struct from query params:
|
||||
For example a query string of:
|
||||
@ -345,7 +383,7 @@ func FiltersFromQuery(query url.Values) (*SearchFilters, error) {
|
||||
Match a route. Check if route matches all filters.
|
||||
Unless all filters are blank.
|
||||
*/
|
||||
func (self *SearchFilters) MatchRoute(route *LookupRoute) bool {
|
||||
func (self *SearchFilters) MatchRoute(route Filterable) bool {
|
||||
sources := self.GetGroupByKey(SEARCH_KEY_SOURCES)
|
||||
if !sources.MatchAny(route) {
|
||||
return false
|
||||
|
@ -5,7 +5,26 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeTestRoute() *LookupRoute {
|
||||
func makeTestRoute() *Route {
|
||||
route := &Route{
|
||||
Bgp: BgpInfo{
|
||||
Communities: []Community{
|
||||
Community{23, 42},
|
||||
Community{111, 11},
|
||||
},
|
||||
ExtCommunities: []ExtCommunity{
|
||||
ExtCommunity{"ro", 23, 123},
|
||||
},
|
||||
LargeCommunities: []Community{
|
||||
Community{1000, 23, 42},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
func makeTestLookupRoute() *LookupRoute {
|
||||
route := &LookupRoute{
|
||||
Bgp: BgpInfo{
|
||||
Communities: []Community{
|
||||
@ -129,7 +148,7 @@ func TestSearchFiltersFromQuery(t *testing.T) {
|
||||
|
||||
func TestSearchFilterCompareRoute(t *testing.T) {
|
||||
// Check filter matches
|
||||
route := makeTestRoute()
|
||||
route := makeTestLookupRoute()
|
||||
|
||||
// Source
|
||||
if searchFilterMatchSource(route, 3) != true {
|
||||
@ -173,7 +192,7 @@ func TestSearchFilterCompareRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchFilterMatchRoute(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
route := makeTestLookupRoute()
|
||||
|
||||
query := "asns=2342,23042&large_communities=1000:23:42&sources=1,2,3&q=foo"
|
||||
values, err := url.ParseQuery(query)
|
||||
@ -195,7 +214,7 @@ func TestSearchFilterMatchRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchFilterExcludeRoute(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
route := makeTestLookupRoute()
|
||||
|
||||
query := "asns=2342,23042&large_communities=42:23:42&sources=1,2,3&q=foo"
|
||||
values, err := url.ParseQuery(query)
|
||||
@ -216,9 +235,7 @@ func TestSearchFilterExcludeRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
// Communities should match all aswell
|
||||
func TestSearchFilterExcludeRouteCommunity(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
|
||||
func testSearchFilterCommunities(route Filterable, t *testing.T) {
|
||||
query := "communities=23:42,111:11"
|
||||
values, err := url.ParseQuery(query)
|
||||
if err != nil {
|
||||
@ -255,10 +272,13 @@ func TestSearchFilterExcludeRouteCommunity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check that ext. communities work
|
||||
func TestSearchFilterExtCommunities(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
func TestSearchFilterLookupRouteCommunity(t *testing.T) {
|
||||
route := makeTestLookupRoute()
|
||||
testSearchFilterCommunities(route, t)
|
||||
}
|
||||
|
||||
// Check that ext. communities work
|
||||
func testSearchFilterExtCommunities(route Filterable, t *testing.T) {
|
||||
query := "ext_communities=ro:23:123"
|
||||
values, err := url.ParseQuery(query)
|
||||
if err != nil {
|
||||
@ -294,3 +314,61 @@ func TestSearchFilterExtCommunities(t *testing.T) {
|
||||
t.Error("Route should not have matched filters!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchFilterRouteExtCommunities(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
testSearchFilterExtCommunities(route, t)
|
||||
}
|
||||
|
||||
func TestSearchFilterLookupRouteExtCommunities(t *testing.T) {
|
||||
route := makeTestLookupRoute()
|
||||
testSearchFilterExtCommunities(route, t)
|
||||
}
|
||||
|
||||
// Check that large communities work aswell
|
||||
func testSearchFilterLargeCommunities(route Filterable, t *testing.T) {
|
||||
query := "large_communities=1000:23:42"
|
||||
values, err := url.ParseQuery(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
filters, err := FiltersFromQuery(values)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if filters.MatchRoute(route) == false {
|
||||
t.Error("Route should have matched filters!")
|
||||
}
|
||||
|
||||
// Now check that all communities need to match
|
||||
query = "large_communities=1002:111:11"
|
||||
values, err = url.ParseQuery(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
filters, err = FiltersFromQuery(values)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if filters.MatchRoute(route) != false {
|
||||
t.Error("Route should not have matched filters!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchFilterRouteLargeCommunities(t *testing.T) {
|
||||
route := makeTestRoute()
|
||||
testSearchFilterLargeCommunities(route, t)
|
||||
}
|
||||
|
||||
func TestSearchFilterLookupRouteLargeCommunities(t *testing.T) {
|
||||
route := makeTestLookupRoute()
|
||||
testSearchFilterLargeCommunities(route, t)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Handle routes
|
||||
@ -28,6 +29,9 @@ func apiRoutesListReceived(
|
||||
req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
// Measure response time
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -42,19 +46,45 @@ func apiRoutesListReceived(
|
||||
}
|
||||
|
||||
// Filter routes based on criteria if present
|
||||
routes := apiQueryFilterNextHopGateway(req, "q", result.Imported)
|
||||
allRoutes := apiQueryFilterNextHopGateway(req, "q", result.Imported)
|
||||
routes := api.Routes{}
|
||||
|
||||
// Apply other (commmunity) filters
|
||||
filters, err := api.FiltersFromQuery(req.URL.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtersAvailable := api.NewSearchFilters()
|
||||
for _, r := range allRoutes {
|
||||
if !filters.MatchRoute(r) {
|
||||
continue // Exclude route from results set
|
||||
}
|
||||
routes = append(routes, r)
|
||||
filtersAvailable.UpdateFromRoute(r)
|
||||
}
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesAcceptedPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
queryDuration := time.Since(t0)
|
||||
|
||||
// Make paginated response
|
||||
response := api.PaginatedRoutesResponse{
|
||||
RoutesResponse: &api.RoutesResponse{
|
||||
Api: result.Api,
|
||||
Imported: routes,
|
||||
},
|
||||
TimedResponse: api.TimedResponse{
|
||||
RequestDuration: DurationMs(queryDuration),
|
||||
},
|
||||
FilterableResponse: api.FilterableResponse{
|
||||
FiltersAvailable: filtersAvailable,
|
||||
FiltersApplied: filters,
|
||||
},
|
||||
Pagination: pagination,
|
||||
}
|
||||
|
||||
@ -65,6 +95,8 @@ func apiRoutesListFiltered(
|
||||
req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -79,19 +111,45 @@ func apiRoutesListFiltered(
|
||||
}
|
||||
|
||||
// Filter routes based on criteria if present
|
||||
routes := apiQueryFilterNextHopGateway(req, "q", result.Filtered)
|
||||
allRoutes := apiQueryFilterNextHopGateway(req, "q", result.Filtered)
|
||||
routes := api.Routes{}
|
||||
|
||||
// Apply other (commmunity) filters
|
||||
filters, err := api.FiltersFromQuery(req.URL.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtersAvailable := api.NewSearchFilters()
|
||||
for _, r := range allRoutes {
|
||||
if !filters.MatchRoute(r) {
|
||||
continue // Exclude route from results set
|
||||
}
|
||||
routes = append(routes, r)
|
||||
filtersAvailable.UpdateFromRoute(r)
|
||||
}
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesFilteredPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
queryDuration := time.Since(t0)
|
||||
|
||||
// Make response
|
||||
response := api.PaginatedRoutesResponse{
|
||||
RoutesResponse: &api.RoutesResponse{
|
||||
Api: result.Api,
|
||||
Filtered: routes,
|
||||
},
|
||||
TimedResponse: api.TimedResponse{
|
||||
RequestDuration: DurationMs(queryDuration),
|
||||
},
|
||||
FilterableResponse: api.FilterableResponse{
|
||||
FiltersAvailable: filtersAvailable,
|
||||
FiltersApplied: filters,
|
||||
},
|
||||
Pagination: pagination,
|
||||
}
|
||||
|
||||
@ -102,6 +160,8 @@ func apiRoutesListNotExported(
|
||||
req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -115,19 +175,46 @@ func apiRoutesListNotExported(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes := apiQueryFilterNextHopGateway(req, "q", result.NotExported)
|
||||
// Filter routes based on criteria if present
|
||||
allRoutes := apiQueryFilterNextHopGateway(req, "q", result.NotExported)
|
||||
routes := api.Routes{}
|
||||
|
||||
// Apply other (commmunity) filters
|
||||
filters, err := api.FiltersFromQuery(req.URL.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtersAvailable := api.NewSearchFilters()
|
||||
for _, r := range allRoutes {
|
||||
if !filters.MatchRoute(r) {
|
||||
continue // Exclude route from results set
|
||||
}
|
||||
routes = append(routes, r)
|
||||
filtersAvailable.UpdateFromRoute(r)
|
||||
}
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesNotExportedPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
queryDuration := time.Since(t0)
|
||||
|
||||
// Make response
|
||||
response := api.PaginatedRoutesResponse{
|
||||
RoutesResponse: &api.RoutesResponse{
|
||||
Api: result.Api,
|
||||
NotExported: routes,
|
||||
},
|
||||
TimedResponse: api.TimedResponse{
|
||||
RequestDuration: DurationMs(queryDuration),
|
||||
},
|
||||
FilterableResponse: api.FilterableResponse{
|
||||
FiltersAvailable: filtersAvailable,
|
||||
FiltersApplied: filters,
|
||||
},
|
||||
Pagination: pagination,
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ func apiLookupPrefixGlobal(
|
||||
break
|
||||
}
|
||||
|
||||
filtersAvailable.UpdateFromRoute(r)
|
||||
filtersAvailable.UpdateFromLookupRoute(r)
|
||||
}
|
||||
|
||||
// Homogenize results
|
||||
|
128
client/components/filters/editor.jsx
Normal file
128
client/components/filters/editor.jsx
Normal file
@ -0,0 +1,128 @@
|
||||
|
||||
import _ from 'underscore'
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {push} from 'react-router-redux'
|
||||
|
||||
import {cloneFilters,
|
||||
hasFilters}
|
||||
from 'components/filters/state'
|
||||
|
||||
import {FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES}
|
||||
from './groups'
|
||||
|
||||
import {RouteserversSelect,
|
||||
PeersFilterSelect,
|
||||
CommunitiesSelect}
|
||||
from './widgets'
|
||||
|
||||
/*
|
||||
* Helper: Add and remove filter
|
||||
*/
|
||||
function _applyFilterValue(filters, group, value) {
|
||||
let nextFilters = cloneFilters(filters);
|
||||
nextFilters[group].filters.push({
|
||||
value: value,
|
||||
});
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
function _removeFilterValue(filters, group, value) {
|
||||
const svalue = value.toString();
|
||||
let nextFilters = cloneFilters(filters);
|
||||
let groupFilters = nextFilters[group].filters;
|
||||
nextFilters[group].filters = _.filter(groupFilters, (f) => {
|
||||
return f.value.toString() !== svalue;
|
||||
});
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
class FiltersEditor extends React.Component {
|
||||
addFilter(group, value) {
|
||||
let nextFilters = _applyFilterValue(
|
||||
this.props.applied, group, value
|
||||
);
|
||||
this.props.dispatch(push(
|
||||
this.props.makeLinkProps(Object.assign({}, this.props.link, {
|
||||
filtersApplied: nextFilters,
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
removeFilter(group, sourceId) {
|
||||
let nextFilters = _removeFilterValue(
|
||||
this.props.applied, group, sourceId
|
||||
);
|
||||
|
||||
this.props.dispatch(push(
|
||||
this.props.makeLinkProps(Object.assign({}, this.props.link, {
|
||||
filtersApplied: nextFilters,
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!hasFilters(this.props.available)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="card lookup-filters-editor">
|
||||
{this.props.availableSources.length > 0 && <h2>Route server</h2>}
|
||||
<RouteserversSelect onChange={(value) => this.addFilter(FILTER_GROUP_SOURCES, value)}
|
||||
onRemove={(value) => this.removeFilter(FILTER_GROUP_SOURCES, value)}
|
||||
available={this.props.availableSources}
|
||||
applied={this.props.appliedSources} />
|
||||
|
||||
{this.props.availableAsns.length > 0 && <h2>Neighbor</h2>}
|
||||
<PeersFilterSelect onChange={(value) => this.addFilter(FILTER_GROUP_ASNS, value)}
|
||||
onRemove={(value) => this.removeFilter(FILTER_GROUP_ASNS, value)}
|
||||
available={this.props.availableAsns}
|
||||
applied={this.props.appliedAsns} />
|
||||
|
||||
{(this.props.availableCommunities.communities.legnth > 0 ||
|
||||
this.props.availableCommunities.ext.length > 0 ||
|
||||
this.props.availableCommunities.large.length > 0 ) && <h2>Communities</h2>}
|
||||
<CommunitiesSelect onChange={(group, value) => this.addFilter(group, value)}
|
||||
onRemove={(group, value) => this.removeFilter(group, value)}
|
||||
available={this.props.availableCommunities}
|
||||
applied={this.props.appliedCommunities} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state, props) => ({
|
||||
isLoading: state.lookup.isLoading,
|
||||
|
||||
link: props.linkProps,
|
||||
|
||||
available: props.filtersAvailable,
|
||||
applied: props.filtersApplied,
|
||||
|
||||
availableSources: props.filtersAvailable[FILTER_GROUP_SOURCES].filters,
|
||||
appliedSources: props.filtersApplied[FILTER_GROUP_SOURCES].filters,
|
||||
|
||||
availableAsns: props.filtersAvailable[FILTER_GROUP_ASNS].filters,
|
||||
appliedAsns: props.filtersApplied[FILTER_GROUP_ASNS].filters,
|
||||
|
||||
availableCommunities: {
|
||||
communities: props.filtersAvailable[FILTER_GROUP_COMMUNITIES].filters,
|
||||
ext: props.filtersAvailable[FILTER_GROUP_EXT_COMMUNITIES].filters,
|
||||
large: props.filtersAvailable[FILTER_GROUP_LARGE_COMMUNITIES].filters,
|
||||
},
|
||||
appliedCommunities: {
|
||||
communities: props.filtersApplied[FILTER_GROUP_COMMUNITIES].filters,
|
||||
ext: props.filtersApplied[FILTER_GROUP_EXT_COMMUNITIES].filters,
|
||||
large: props.filtersApplied[FILTER_GROUP_LARGE_COMMUNITIES].filters,
|
||||
},
|
||||
})
|
||||
)(FiltersEditor);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES,
|
||||
} from './filter-groups'
|
||||
} from './groups'
|
||||
|
||||
|
||||
function _makeFilter(value) {
|
152
client/components/filters/state.jsx
Normal file
152
client/components/filters/state.jsx
Normal file
@ -0,0 +1,152 @@
|
||||
|
||||
import _ from 'underscore'
|
||||
|
||||
import {FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES}
|
||||
from './groups'
|
||||
|
||||
import {decodeFiltersSources,
|
||||
decodeFiltersAsns,
|
||||
decodeFiltersCommunities,
|
||||
decodeFiltersExtCommunities,
|
||||
decodeFiltersLargeCommunities}
|
||||
from 'components/filters/encoding'
|
||||
|
||||
export const initialFilterState = [
|
||||
{"key": "sources", "filters": []},
|
||||
{"key": "asns", "filters": []},
|
||||
{"key": "communities", "filters": []},
|
||||
{"key": "ext_communities", "filters": []},
|
||||
{"key": "large_communities", "filters": []},
|
||||
];
|
||||
|
||||
export function cloneFilters(filters) {
|
||||
const nextFilters = [
|
||||
Object.assign({}, filters[FILTER_GROUP_SOURCES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_ASNS]),
|
||||
Object.assign({}, filters[FILTER_GROUP_COMMUNITIES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_EXT_COMMUNITIES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_LARGE_COMMUNITIES]),
|
||||
];
|
||||
|
||||
nextFilters[FILTER_GROUP_SOURCES].filters =
|
||||
[...nextFilters[FILTER_GROUP_SOURCES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_ASNS].filters =
|
||||
[...nextFilters[FILTER_GROUP_ASNS].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_COMMUNITIES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_EXT_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_EXT_COMMUNITIES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_LARGE_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_LARGE_COMMUNITIES].filters];
|
||||
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode filters applied from params
|
||||
*/
|
||||
export function decodeFiltersApplied(params) {
|
||||
let groups = cloneFilters(initialFilterState);
|
||||
|
||||
groups[FILTER_GROUP_SOURCES].filters = decodeFiltersSources(params);
|
||||
groups[FILTER_GROUP_ASNS].filters = decodeFiltersAsns(params);
|
||||
groups[FILTER_GROUP_COMMUNITIES].filters = decodeFiltersCommunities(params);
|
||||
groups[FILTER_GROUP_EXT_COMMUNITIES].filters = decodeFiltersExtCommunities(params);
|
||||
groups[FILTER_GROUP_LARGE_COMMUNITIES].filters = decodeFiltersLargeCommunities(params);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge filters
|
||||
*/
|
||||
export function mergeFilters(a, b) {
|
||||
let groups = cloneFilters(initialFilterState);
|
||||
let setCmp = [];
|
||||
setCmp[FILTER_GROUP_SOURCES] = cmpFilterValue;
|
||||
setCmp[FILTER_GROUP_ASNS] = cmpFilterValue;
|
||||
setCmp[FILTER_GROUP_COMMUNITIES] = cmpFilterCommunity;
|
||||
setCmp[FILTER_GROUP_EXT_COMMUNITIES] = cmpFilterCommunity;
|
||||
setCmp[FILTER_GROUP_LARGE_COMMUNITIES] = cmpFilterCommunity;
|
||||
|
||||
for (const i in groups) {
|
||||
groups[i].filters = mergeFilterSet(setCmp[i], a[i].filters, b[i].filters);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge list of filters
|
||||
*/
|
||||
function mergeFilterSet(inSet, a, b) {
|
||||
let result = a;
|
||||
for (const f of b) {
|
||||
const present = inSet(result, f);
|
||||
if (present) {
|
||||
// Update filter cardinality
|
||||
present.cardinality = Math.max(f.cardinality, present.cardinality);
|
||||
continue;
|
||||
}
|
||||
result.push(f);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Does a single group have any filters?
|
||||
*/
|
||||
export function groupHasFilters(group) {
|
||||
return group.filters.length > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Filters set compare
|
||||
*/
|
||||
function cmpFilterValue(set, filter) {
|
||||
for (const f of set) {
|
||||
if(f.value == filter.value) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cmpFilterCommunity(set, filter) {
|
||||
for (const f of set) {
|
||||
let match = true;
|
||||
for (const i in f.value) {
|
||||
if (f.value[i] != filter.value[i]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Do we have filters in general?
|
||||
*/
|
||||
export function hasFilters(groups) {
|
||||
for (const g of groups) {
|
||||
if (groupHasFilters(g)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -4,47 +4,24 @@ import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {push} from 'react-router-redux'
|
||||
|
||||
import CommunityLabel
|
||||
from 'components/routeservers/communities/label'
|
||||
import {makeReadableCommunity}
|
||||
from 'components/routeservers/communities/utils'
|
||||
|
||||
import {makeLinkProps, cloneFilters} from './state'
|
||||
|
||||
import {FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
import {FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES}
|
||||
from './filter-groups'
|
||||
from './groups'
|
||||
|
||||
|
||||
/*
|
||||
* Helper: Add and remove filter
|
||||
*/
|
||||
function _applyFilterValue(filters, group, value) {
|
||||
let nextFilters = cloneFilters(filters);
|
||||
nextFilters[group].filters.push({
|
||||
value: value,
|
||||
});
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
function _removeFilterValue(filters, group, value) {
|
||||
const svalue = value.toString();
|
||||
let nextFilters = cloneFilters(filters);
|
||||
let groupFilters = nextFilters[group].filters;
|
||||
nextFilters[group].filters = _.filter(groupFilters, (f) => {
|
||||
return f.value.toString() !== svalue;
|
||||
});
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
|
||||
class RouteserversSelect extends React.Component {
|
||||
export class RouteserversSelect extends React.Component {
|
||||
render() {
|
||||
// Nothing to do if we don't have filters
|
||||
if (this.props.available.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort filters available
|
||||
const sortedFiltersAvailable = this.props.available.sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
@ -105,8 +82,13 @@ class RouteserversSelect extends React.Component {
|
||||
}
|
||||
|
||||
|
||||
class PeersFilterSelect extends React.Component {
|
||||
export class PeersFilterSelect extends React.Component {
|
||||
render() {
|
||||
// Nothing to do if we don't have filters
|
||||
if (this.props.available.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort filters available
|
||||
const sortedFiltersAvailable = this.props.available.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
@ -172,19 +154,29 @@ class _CommunitiesSelect extends React.Component {
|
||||
propagateChange(value) {
|
||||
// Decode value
|
||||
const [group, community] = value.split(",", 2);
|
||||
const filterValue = community.split(":"); // spew.
|
||||
const filterValue = community.split(":"); // spew.
|
||||
|
||||
this.props.onChange(group, filterValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Nothing to do if we don't have filters
|
||||
if (this.props.available.communities.length == 0 &&
|
||||
this.props.available.ext.length == 0 &&
|
||||
this.props.available.large.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const communitiesAvailable = this.props.available.communities.sort((a, b) => {
|
||||
return (a.value[0] - b.value[0]) * 100000 + (a.value[1] - b.value[1]);
|
||||
});
|
||||
|
||||
/*
|
||||
const extCommunitiesAvailable = this.props.available.ext.sort((a, b) => {
|
||||
return (a.value[1] - b.value[1]) * 100000 + (a.value[2] - b.value[2]);
|
||||
});
|
||||
*/
|
||||
const extCommunitiesAvailable = []; // They don't work. for now.
|
||||
|
||||
const largeCommunitiesAvailable = this.props.available.large.sort((a, b) => {
|
||||
return (a.value[0] - b.value[0]) * 10000000000 +
|
||||
@ -242,17 +234,17 @@ class _CommunitiesSelect extends React.Component {
|
||||
const appliedCommunities = this.props.applied.communities.map((filter) => {
|
||||
const name = makeReadableCommunity(this.props.communities, filter.value);
|
||||
return makeCommunity(FILTER_GROUP_COMMUNITIES, name, filter);
|
||||
});
|
||||
});
|
||||
|
||||
const appliedExtCommunities = this.props.applied.ext.map((filter) => {
|
||||
const name = makeReadableCommunity(this.props.communities, filter.value);
|
||||
return makeCommunity(FILTER_GROUP_EXT_COMMUNITIES, name, filter);
|
||||
});
|
||||
});
|
||||
|
||||
const appliedLargeCommunities = this.props.applied.large.map((filter) => {
|
||||
const name = makeReadableCommunity(this.props.communities, filter.value);
|
||||
return makeCommunity(FILTER_GROUP_LARGE_COMMUNITIES, name, filter);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<table className="select-ctrl">
|
||||
@ -270,12 +262,12 @@ class _CommunitiesSelect extends React.Component {
|
||||
</option>
|
||||
{communitiesOptions.length > 0 &&
|
||||
<optgroup label="Communities">
|
||||
{communitiesOptions}
|
||||
{communitiesOptions}
|
||||
</optgroup>}
|
||||
|
||||
{extCommunitiesOptions.length > 0 &&
|
||||
<optgroup label="Ext. Communities">
|
||||
{extCommunitiesOptions}
|
||||
{extCommunitiesOptions}
|
||||
</optgroup>}
|
||||
|
||||
{largeCommunitiesOptions.length > 0 &&
|
||||
@ -291,100 +283,11 @@ class _CommunitiesSelect extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const CommunitiesSelect = connect(
|
||||
export const CommunitiesSelect = connect(
|
||||
(state) => ({
|
||||
communities: state.config.bgp_communities,
|
||||
})
|
||||
)(_CommunitiesSelect);
|
||||
|
||||
|
||||
class FiltersEditor extends React.Component {
|
||||
addFilter(group, value) {
|
||||
let nextFilters = _applyFilterValue(
|
||||
this.props.applied, group, value
|
||||
);
|
||||
this.props.dispatch(push(
|
||||
makeLinkProps(Object.assign({}, this.props.link, {
|
||||
filtersApplied: nextFilters,
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
removeFilter(group, sourceId) {
|
||||
let nextFilters = _removeFilterValue(
|
||||
this.props.applied, group, sourceId
|
||||
);
|
||||
|
||||
this.props.dispatch(push(
|
||||
makeLinkProps(Object.assign({}, this.props.link, {
|
||||
filtersApplied: nextFilters,
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.hasRoutes) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="card lookup-filters-editor">
|
||||
<h2>Route server</h2>
|
||||
<RouteserversSelect onChange={(value) => this.addFilter(FILTER_GROUP_SOURCES, value)}
|
||||
onRemove={(value) => this.removeFilter(FILTER_GROUP_SOURCES, value)}
|
||||
available={this.props.availableSources}
|
||||
applied={this.props.appliedSources} />
|
||||
|
||||
<h2>Neighbor</h2>
|
||||
<PeersFilterSelect onChange={(value) => this.addFilter(FILTER_GROUP_ASNS, value)}
|
||||
onRemove={(value) => this.removeFilter(FILTER_GROUP_ASNS, value)}
|
||||
available={this.props.availableAsns}
|
||||
applied={this.props.appliedAsns} />
|
||||
|
||||
<h2>Communities</h2>
|
||||
<CommunitiesSelect onChange={(group, value) => this.addFilter(group, value)}
|
||||
onRemove={(group, value) => this.removeFilter(group, value)}
|
||||
available={this.props.availableCommunities}
|
||||
applied={this.props.appliedCommunities} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
isLoading: state.lookup.isLoading,
|
||||
hasRoutes: state.lookup.routesFiltered.length > 0 ||
|
||||
state.lookup.routesImported.length > 0,
|
||||
|
||||
link: {
|
||||
pageReceived: 0, // Reset pagination on filter change
|
||||
pageFiltered: 0,
|
||||
query: state.lookup.query,
|
||||
filtersApplied: state.lookup.filtersApplied,
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
},
|
||||
|
||||
available: state.lookup.filtersAvailable,
|
||||
applied: state.lookup.filtersApplied,
|
||||
|
||||
availableSources: state.lookup.filtersAvailable[FILTER_GROUP_SOURCES].filters,
|
||||
appliedSources: state.lookup.filtersApplied[FILTER_GROUP_SOURCES].filters,
|
||||
|
||||
availableAsns: state.lookup.filtersAvailable[FILTER_GROUP_ASNS].filters,
|
||||
appliedAsns: state.lookup.filtersApplied[FILTER_GROUP_ASNS].filters,
|
||||
|
||||
availableCommunities: {
|
||||
communities: state.lookup.filtersAvailable[FILTER_GROUP_COMMUNITIES].filters,
|
||||
ext: state.lookup.filtersAvailable[FILTER_GROUP_EXT_COMMUNITIES].filters,
|
||||
large: state.lookup.filtersAvailable[FILTER_GROUP_LARGE_COMMUNITIES].filters,
|
||||
},
|
||||
appliedCommunities: {
|
||||
communities: state.lookup.filtersApplied[FILTER_GROUP_COMMUNITIES].filters,
|
||||
ext: state.lookup.filtersApplied[FILTER_GROUP_EXT_COMMUNITIES].filters,
|
||||
large: state.lookup.filtersApplied[FILTER_GROUP_LARGE_COMMUNITIES].filters,
|
||||
},
|
||||
|
||||
})
|
||||
)(FiltersEditor);
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import {filtersUrlEncode} from './filter-encoding'
|
||||
import {filtersUrlEncode} from 'components/filters/encoding'
|
||||
|
||||
export const SET_LOOKUP_QUERY_VALUE = '@lookup/SET_LOOKUP_QUERY_VALUE';
|
||||
|
||||
@ -61,7 +61,7 @@ export function loadResults(query, filters, pageImported=0, pageFiltered=0) {
|
||||
|
||||
// Build querystring
|
||||
const q = `q=${query}&page_filtered=${pageFiltered}&page_imported=${pageImported}`;
|
||||
const f = filtersUrlEncode(filters);
|
||||
const f = filtersUrlEncode(filters);
|
||||
axios.get(`/api/v1/lookup/prefix?${q}${f}`)
|
||||
.then(
|
||||
(res) => {
|
||||
|
@ -1,25 +0,0 @@
|
||||
|
||||
export const APPLY_FILTER = "@lookup/APPLY_FILTER";
|
||||
|
||||
export function applyFilterValue(group, value) {
|
||||
return {
|
||||
type: APPLY_FILTER,
|
||||
payload: {
|
||||
group: group,
|
||||
filter: {
|
||||
value: value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyFilter(group, filter) {
|
||||
return {
|
||||
type: APPLY_FILTER,
|
||||
payload: {
|
||||
group: group,
|
||||
filter: filter,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -52,7 +52,7 @@ class Lookup extends React.Component {
|
||||
pathname: "/search",
|
||||
search: `?q=${q}`
|
||||
};
|
||||
|
||||
|
||||
// Set lookup params
|
||||
this.props.dispatch(setLookupQueryValue(q));
|
||||
this.debouncedDispatch(replace(destination));
|
||||
@ -101,11 +101,11 @@ export default connect(
|
||||
error: state.lookup.error,
|
||||
routes: {
|
||||
filtered: {
|
||||
loading: lookup.isLoading,
|
||||
loading: lookup.isLoading,
|
||||
totalResults: lookup.totalRoutesFiltered,
|
||||
},
|
||||
received: {
|
||||
loading: lookup.isLoading,
|
||||
loading: lookup.isLoading,
|
||||
totalResults: lookup.totalRoutesImported,
|
||||
},
|
||||
notExported: {
|
||||
|
@ -6,10 +6,12 @@ import PageHeader from 'components/page-header'
|
||||
|
||||
import Lookup from 'components/lookup'
|
||||
import LookupSummary from 'components/lookup/results-summary'
|
||||
import LookupFilters from 'components/lookup/filters'
|
||||
import FiltersEditor from 'components/filters/editor'
|
||||
|
||||
import Content from 'components/content'
|
||||
|
||||
import {makeLinkProps} from './state'
|
||||
|
||||
class _LookupView extends React.Component {
|
||||
render() {
|
||||
if (this.props.enabled == false) {
|
||||
@ -23,7 +25,10 @@ class _LookupView extends React.Component {
|
||||
</div>
|
||||
<div className="col-aside-details col-lg-3 col-md-12">
|
||||
<LookupSummary />
|
||||
<LookupFilters />
|
||||
<FiltersEditor makeLinkProps={makeLinkProps}
|
||||
linkProps={this.props.linkProps}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
filtersAvailable={this.props.filtersAvailable} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -33,7 +38,20 @@ class _LookupView extends React.Component {
|
||||
const LookupView = connect(
|
||||
(state) => {
|
||||
return {
|
||||
enabled: state.config.prefix_lookup_enabled
|
||||
enabled: state.config.prefix_lookup_enabled,
|
||||
|
||||
filtersAvailable: state.lookup.filtersAvailable,
|
||||
filtersApplied: state.lookup.filtersApplied,
|
||||
|
||||
linkProps: {
|
||||
anchor: "filtered",
|
||||
page: 0,
|
||||
pageReceived: 0, // Reset pagination on filter change
|
||||
pageFiltered: 0,
|
||||
query: state.lookup.query,
|
||||
filtersApplied: state.lookup.filtersApplied,
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
},
|
||||
}
|
||||
}
|
||||
)(_LookupView);
|
||||
|
@ -12,40 +12,11 @@ import {
|
||||
RESET,
|
||||
} from './actions'
|
||||
|
||||
import {
|
||||
APPLY_FILTER,
|
||||
} from './filter-actions'
|
||||
|
||||
import {
|
||||
FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES,
|
||||
} from './filter-groups'
|
||||
|
||||
import {
|
||||
decodeFiltersSources,
|
||||
decodeFiltersAsns,
|
||||
decodeFiltersCommunities,
|
||||
decodeFiltersExtCommunities,
|
||||
decodeFiltersLargeCommunities,
|
||||
} from './filter-encoding'
|
||||
|
||||
import {
|
||||
cloneFilters
|
||||
} from './state'
|
||||
import {cloneFilters, decodeFiltersApplied, initialFilterState}
|
||||
from 'components/filters/state'
|
||||
|
||||
const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
|
||||
|
||||
const initialFilterState = [
|
||||
{"key": "sources", "filters": []},
|
||||
{"key": "asns", "filters": []},
|
||||
{"key": "communities", "filters": []},
|
||||
{"key": "ext_communities", "filters": []},
|
||||
{"key": "large_communities", "filters": []},
|
||||
];
|
||||
|
||||
const initialState = {
|
||||
query: "",
|
||||
queryValue: "",
|
||||
@ -87,21 +58,6 @@ const getScrollAnchor = function(hash) {
|
||||
return hash.substr(hash.indexOf('-')+1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode filters applied from params
|
||||
*/
|
||||
const _decodeFiltersApplied = function(params) {
|
||||
let groups = cloneFilters(initialFilterState);
|
||||
|
||||
groups[FILTER_GROUP_SOURCES].filters = decodeFiltersSources(params);
|
||||
groups[FILTER_GROUP_ASNS].filters = decodeFiltersAsns(params);
|
||||
groups[FILTER_GROUP_COMMUNITIES].filters = decodeFiltersCommunities(params);
|
||||
groups[FILTER_GROUP_EXT_COMMUNITIES].filters = decodeFiltersExtCommunities(params);
|
||||
groups[FILTER_GROUP_LARGE_COMMUNITIES].filters = decodeFiltersLargeCommunities(params);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Restore lookup query state from location paramenters
|
||||
@ -114,7 +70,7 @@ const _handleLocationChange = function(state, payload) {
|
||||
const anchor = getScrollAnchor(payload.hash);
|
||||
|
||||
// Restore filters applied from location
|
||||
const filtersApplied = _decodeFiltersApplied(params);
|
||||
const filtersApplied = decodeFiltersApplied(params);
|
||||
|
||||
return Object.assign({}, state, {
|
||||
anchor: anchor,
|
||||
@ -182,6 +138,7 @@ export default function reducer(state=initialState, action) {
|
||||
queryValue: action.payload.query,
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
case LOAD_RESULTS_SUCCESS:
|
||||
if (state.query != action.payload.query) {
|
||||
return state;
|
||||
|
@ -6,7 +6,7 @@ import {connect} from 'react-redux'
|
||||
import {Link} from 'react-router'
|
||||
import {replace} from 'react-router-redux'
|
||||
|
||||
import {filtersEqual} from './filter-groups'
|
||||
import {filtersEqual} from 'components/filters/groups'
|
||||
|
||||
import FilterReason
|
||||
from 'components/routeservers/communities/filter-reason'
|
||||
|
@ -4,17 +4,13 @@
|
||||
* Manage state
|
||||
*/
|
||||
|
||||
import {
|
||||
filtersUrlEncode
|
||||
} from './filter-encoding'
|
||||
import {filtersUrlEncode} from 'components/filters/encoding'
|
||||
|
||||
import {
|
||||
FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES,
|
||||
} from './filter-groups'
|
||||
import {FILTER_GROUP_SOURCES,
|
||||
FILTER_GROUP_ASNS,
|
||||
FILTER_GROUP_COMMUNITIES,
|
||||
FILTER_GROUP_EXT_COMMUNITIES,
|
||||
FILTER_GROUP_LARGE_COMMUNITIES} from 'components/filters/groups'
|
||||
|
||||
/*
|
||||
* Maybe this can be customized and injected into
|
||||
@ -52,9 +48,9 @@ export function makeLinkProps(props) {
|
||||
const query = props.routing.query.q || "";
|
||||
|
||||
const search = `?${pagination}&q=${query}${filtering}`;
|
||||
let hash = "";
|
||||
let hash = null;
|
||||
if (props.anchor) {
|
||||
hash += `#routes-${props.anchor}`;
|
||||
hash = `#routes-${props.anchor}`;
|
||||
}
|
||||
|
||||
const linkTo = {
|
||||
@ -66,30 +62,3 @@ export function makeLinkProps(props) {
|
||||
return linkTo;
|
||||
}
|
||||
|
||||
export function cloneFilters(filters) {
|
||||
const nextFilters = [
|
||||
Object.assign({}, filters[FILTER_GROUP_SOURCES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_ASNS]),
|
||||
Object.assign({}, filters[FILTER_GROUP_COMMUNITIES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_EXT_COMMUNITIES]),
|
||||
Object.assign({}, filters[FILTER_GROUP_LARGE_COMMUNITIES]),
|
||||
];
|
||||
|
||||
nextFilters[FILTER_GROUP_SOURCES].filters =
|
||||
[...nextFilters[FILTER_GROUP_SOURCES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_ASNS].filters =
|
||||
[...nextFilters[FILTER_GROUP_ASNS].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_COMMUNITIES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_EXT_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_EXT_COMMUNITIES].filters];
|
||||
|
||||
nextFilters[FILTER_GROUP_LARGE_COMMUNITIES].filters =
|
||||
[...nextFilters[FILTER_GROUP_LARGE_COMMUNITIES].filters];
|
||||
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import {apiError} from 'components/errors/actions'
|
||||
import {filtersUrlEncode} from 'components/filters/encoding'
|
||||
|
||||
export const ROUTES_RECEIVED = "received";
|
||||
export const ROUTES_FILTERED = "filtered";
|
||||
@ -22,14 +23,16 @@ export const FETCH_ROUTES_NOT_EXPORTED_ERROR = "@routes/FETCH_ROUTES_NOT_EXPOR
|
||||
export const SET_FILTER_QUERY_VALUE = "@routes/SET_FILTER_QUERY_VALUE";
|
||||
|
||||
// Url helper
|
||||
function routesUrl(type, rsId, pId, page, query) {
|
||||
function routesUrl(type, rsId, pId, page, query, filters) {
|
||||
let rtype = type;
|
||||
if (type == ROUTES_NOT_EXPORTED) {
|
||||
rtype = "not-exported"; // This is a bit ugly
|
||||
}
|
||||
|
||||
let base = `/api/v1/routeservers/${rsId}/neighbors/${pId}/routes/${rtype}`
|
||||
let params = `?page=${page}&q=${query}`
|
||||
const filtersEncoded = filtersUrlEncode(filters);
|
||||
const base = `/api/v1/routeservers/${rsId}/neighbors/${pId}/routes/${rtype}`
|
||||
const params = `?page=${page}&q=${query}${filtersEncoded}`
|
||||
|
||||
return base + params;
|
||||
};
|
||||
|
||||
@ -43,11 +46,13 @@ function routesRequest(type) {
|
||||
}
|
||||
|
||||
function routesSuccess(type) {
|
||||
return (routes, pagination, apiStatus) => ({
|
||||
return (routes, pagination, filtersAvailable, filtersApplied, apiStatus) => ({
|
||||
type: type,
|
||||
payload: {
|
||||
routes: routes,
|
||||
pagination: pagination,
|
||||
filtersAvailable: filtersAvailable,
|
||||
filtersApplied: filtersApplied,
|
||||
apiStatus: apiStatus
|
||||
}
|
||||
});
|
||||
@ -87,16 +92,18 @@ function fetchRoutes(type) {
|
||||
[ROUTES_NOT_EXPORTED]: 'not_exported',
|
||||
}[type];
|
||||
|
||||
return (rsId, pId, page, query) => {
|
||||
return (rsId, pId, page, query, filters) => {
|
||||
return (dispatch) => {
|
||||
dispatch(requestAction());
|
||||
|
||||
axios.get(routesUrl(type, rsId, pId, page, query))
|
||||
axios.get(routesUrl(type, rsId, pId, page, query, filters))
|
||||
.then(
|
||||
({data}) => {
|
||||
dispatch(successAction(
|
||||
data[rtype],
|
||||
data.pagination,
|
||||
data.filters_available,
|
||||
data.filters_applied,
|
||||
data.api));
|
||||
},
|
||||
(error) => {
|
||||
|
@ -31,6 +31,11 @@ import RoutesLoadingIndicator from './loading-indicator'
|
||||
|
||||
import {filterableColumnsText} from './utils'
|
||||
|
||||
import FiltersEditor from 'components/filters/editor'
|
||||
import {mergeFilters} from 'components/filters/state'
|
||||
|
||||
import {makeLinkProps} from './urls'
|
||||
|
||||
// Actions
|
||||
import {setFilterQueryValue}
|
||||
from './actions'
|
||||
@ -73,7 +78,7 @@ const RoutesViewEmpty = (props) => {
|
||||
if (!props.loadNotExported) {
|
||||
return null; // There may be routes matching the query in there!
|
||||
}
|
||||
|
||||
|
||||
const hasContent = props.routes.received.totalResults > 0 ||
|
||||
props.routes.filtered.totalResults > 0 ||
|
||||
props.routes.notExported.totalResults > 0;
|
||||
@ -95,7 +100,7 @@ const RoutesViewEmpty = (props) => {
|
||||
class RoutesPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
||||
// Create debounced dispatch, as we don't want to flood
|
||||
// the server with API queries
|
||||
this.debouncedDispatch = debounce(this.props.dispatch, 350);
|
||||
@ -125,7 +130,7 @@ class RoutesPage extends React.Component {
|
||||
cacheStatus = null;
|
||||
}
|
||||
|
||||
// We have to shift the layout a bit, to make room for
|
||||
// We have to shift the layout a bit, to make room for
|
||||
// the related peers tabs
|
||||
let pageClass = "routeservers-page";
|
||||
if (this.props.relatedPeers.length > 1) {
|
||||
@ -167,7 +172,7 @@ class RoutesPage extends React.Component {
|
||||
|
||||
<QuickLinks routes={this.props.routes} />
|
||||
|
||||
<RoutesViewEmpty routes={this.props.routes}
|
||||
<RoutesViewEmpty routes={this.props.routes}
|
||||
loadNotExported={this.props.loadNotExported} />
|
||||
|
||||
<RoutesView
|
||||
@ -193,6 +198,10 @@ class RoutesPage extends React.Component {
|
||||
<Status routeserverId={this.props.params.routeserverId}
|
||||
cacheStatus={cacheStatus} />
|
||||
</div>
|
||||
<FiltersEditor makeLinkProps={makeLinkProps}
|
||||
linkProps={this.props.linkProps}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
filtersAvailable={this.props.filtersAvailable} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -247,9 +256,31 @@ export default connect(
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
loadNotExported: state.routes.loadNotExported ||
|
||||
!state.config.noexport_load_on_demand,
|
||||
|
||||
|
||||
anyLoading: anyLoading,
|
||||
|
||||
filtersApplied: state.routes.filtersApplied,
|
||||
filtersAvailable: mergeFilters(
|
||||
state.routes.receivedFiltersAvailable,
|
||||
state.routes.filteredFiltersAvailable,
|
||||
state.routes.notExportedFiltersAvailable
|
||||
),
|
||||
|
||||
linkProps: {
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
|
||||
loadNotExported: state.routes.loadNotExported,
|
||||
|
||||
page: 0,
|
||||
pageReceived: 0, // Reset pagination on filter change
|
||||
pageFiltered: 0,
|
||||
pageNotExported: 0,
|
||||
|
||||
query: state.routes.filterValue,
|
||||
|
||||
filtersApplied: state.routes.filtersApplied,
|
||||
},
|
||||
|
||||
relatedPeers: relatedPeers
|
||||
});
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ const PageSelect = (props) => {
|
||||
if (active) {
|
||||
itemClassName = "active";
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<li className={itemClassName}>
|
||||
<select className="form-control pagination-select"
|
||||
@ -64,8 +64,8 @@ class RoutesPaginatorView extends React.Component {
|
||||
* select for a dropdown like access.
|
||||
*/
|
||||
makePaginationPages(numPages) {
|
||||
const MAX_ITEMS = 12;
|
||||
const pages = Array.from(Array(numPages), (_, i) => i);
|
||||
const MAX_ITEMS = 12;
|
||||
const pages = Array.from(Array(numPages), (_, i) => i);
|
||||
return {
|
||||
items: pages.slice(0, MAX_ITEMS),
|
||||
select: pages.slice(MAX_ITEMS)
|
||||
@ -104,6 +104,7 @@ class RoutesPaginatorView extends React.Component {
|
||||
loadNotExported={this.props.loadNotExported}
|
||||
pageReceived={this.props.pageReceived}
|
||||
pageFiltered={this.props.pageFiltered}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
pageNotExported={this.props.pageNotExported} />
|
||||
</li>
|
||||
);
|
||||
@ -129,6 +130,7 @@ class RoutesPaginatorView extends React.Component {
|
||||
disabled={this.props.page == 0}
|
||||
routing={this.props.routing}
|
||||
anchor={this.props.anchor}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
loadNotExported={this.props.loadNotExported}
|
||||
pageReceived={this.props.pageReceived}
|
||||
pageFiltered={this.props.pageFiltered}
|
||||
@ -137,6 +139,7 @@ class RoutesPaginatorView extends React.Component {
|
||||
{pageLinks}
|
||||
<PageSelect pages={pages.select}
|
||||
page={this.props.page}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
onChange={(page) => this.navigateToPage(page)} />
|
||||
|
||||
{pages.select.length == 0 &&
|
||||
@ -146,6 +149,7 @@ class RoutesPaginatorView extends React.Component {
|
||||
label="»"
|
||||
routing={this.props.routing}
|
||||
anchor={this.props.anchor}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
loadNotExported={this.props.loadNotExported}
|
||||
pageReceived={this.props.pageReceived}
|
||||
pageFiltered={this.props.pageFiltered}
|
||||
@ -166,6 +170,8 @@ export const RoutesPaginator = connect(
|
||||
|
||||
loadNotExported: state.routes.loadNotExported,
|
||||
|
||||
filtersApplied: state.routes.filtersApplied,
|
||||
|
||||
routing: state.routing.locationBeforeTransitions
|
||||
})
|
||||
)(RoutesPaginatorView);
|
||||
|
@ -17,10 +17,15 @@ import {ROUTES_RECEIVED,
|
||||
|
||||
import {SET_FILTER_QUERY_VALUE} from './actions'
|
||||
|
||||
import {cloneFilters, decodeFiltersApplied, initialFilterState}
|
||||
from 'components/filters/state'
|
||||
|
||||
const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
|
||||
|
||||
const initialState = {
|
||||
|
||||
filtersApplied: initialFilterState,
|
||||
|
||||
received: [],
|
||||
receivedLoading: false,
|
||||
receivedRequested: false,
|
||||
@ -30,6 +35,7 @@ const initialState = {
|
||||
receivedTotalPages: 0,
|
||||
receivedTotalResults: 0,
|
||||
receivedApiStatus: {},
|
||||
receivedFiltersAvailable: initialFilterState,
|
||||
|
||||
filtered: [],
|
||||
filteredLoading: false,
|
||||
@ -40,6 +46,7 @@ const initialState = {
|
||||
filteredTotalPages: 0,
|
||||
filteredTotalResults: 0,
|
||||
filteredApiStatus: {},
|
||||
filteredFiltersAvailable: initialFilterState,
|
||||
|
||||
notExported: [],
|
||||
notExportedLoading: false,
|
||||
@ -50,6 +57,7 @@ const initialState = {
|
||||
notExportedTotalPages: 0,
|
||||
notExportedTotalResults: 0,
|
||||
notExportedApiStatus: {},
|
||||
notExportedFiltersAvailable: initialFilterState,
|
||||
|
||||
// Derived state from location
|
||||
loadNotExported: false,
|
||||
@ -83,6 +91,9 @@ function _handleLocationChange(state, payload) {
|
||||
// Determine on demand loading state
|
||||
const loadNotExported = parseInt(query["ne"] || 0, 10) === 1 ? true : false;
|
||||
|
||||
// Restore filters applied from location
|
||||
const filtersApplied = decodeFiltersApplied(query);
|
||||
|
||||
const nextState = Object.assign({}, state, {
|
||||
filterQuery: filterQuery,
|
||||
filterValue: filterQuery, // location overrides form
|
||||
@ -92,6 +103,7 @@ function _handleLocationChange(state, payload) {
|
||||
notExportedPage: notExportedPage,
|
||||
|
||||
loadNotExported: loadNotExported,
|
||||
filtersApplied: filtersApplied,
|
||||
});
|
||||
|
||||
return nextState;
|
||||
@ -108,7 +120,6 @@ function _handleFetchRoutesRequest(type, state, payload) {
|
||||
return nextState;
|
||||
}
|
||||
|
||||
|
||||
function _handleFetchRoutesSuccess(type, state, payload) {
|
||||
const stype = _stateType(type);
|
||||
const pagination = payload.pagination;
|
||||
@ -124,7 +135,9 @@ function _handleFetchRoutesSuccess(type, state, payload) {
|
||||
|
||||
[stype+'ApiStatus']: apiStatus,
|
||||
|
||||
[stype+'Loading']: false
|
||||
[stype+'Loading']: false,
|
||||
|
||||
[stype+'FiltersAvailable']: payload.filtersAvailable,
|
||||
});
|
||||
|
||||
return nextState;
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
import {filtersUrlEncode} from 'components/filters/encoding'
|
||||
|
||||
export const makeLinkProps = function(props) {
|
||||
const linkPage = parseInt(props.page, 10);
|
||||
|
||||
@ -23,10 +25,19 @@ export const makeLinkProps = function(props) {
|
||||
break;
|
||||
}
|
||||
|
||||
const query = props.routing.query.q || "";
|
||||
let filtering = "";
|
||||
if (props.filtersApplied) {
|
||||
filtering = filtersUrlEncode(props.filtersApplied);
|
||||
}
|
||||
|
||||
const query = props.routing.query.q || "";
|
||||
const search = `?ne=${ne}&pr=${pr}&pf=${pf}&pn=${pn}&q=${query}${filtering}`;
|
||||
|
||||
let hash = null;
|
||||
if (props.anchor) {
|
||||
hash = `#${props.anchor}`;
|
||||
}
|
||||
|
||||
const search = `?ne=${ne}&pr=${pr}&pf=${pf}&pn=${pn}&q=${query}`;
|
||||
const hash = `#${props.anchor}`;
|
||||
const linkTo = {
|
||||
pathname: props.routing.pathname,
|
||||
hash: hash,
|
||||
|
@ -14,6 +14,8 @@ import {fetchRoutesReceived,
|
||||
|
||||
import {makeLinkProps} from './urls'
|
||||
|
||||
import {filtersEqual} from 'components/filters/groups'
|
||||
|
||||
// Constants
|
||||
import {ROUTES_RECEIVED,
|
||||
ROUTES_FILTERED,
|
||||
@ -52,6 +54,9 @@ class RoutesView extends React.Component {
|
||||
dispatchFetchRoutes() {
|
||||
const type = this.props.type;
|
||||
|
||||
// Get filters
|
||||
const filters = this.props.filtersApplied;
|
||||
|
||||
// Depending on the component's configuration, dispatch
|
||||
// routes fetching
|
||||
const fetchRoutes = {
|
||||
@ -75,11 +80,11 @@ class RoutesView extends React.Component {
|
||||
}
|
||||
|
||||
// Otherwise, just dispatch the request:
|
||||
this.props.dispatch(fetchRoutes(rsId, pId, params.page, query));
|
||||
this.props.dispatch(fetchRoutes(rsId, pId, params.page, query, filters));
|
||||
}
|
||||
|
||||
/*
|
||||
* Diff props and this.props to check if we need to
|
||||
* Diff props and this.props to check if we need to
|
||||
* dispatch another fetch routes
|
||||
*/
|
||||
routesNeedFetch(props) {
|
||||
@ -87,8 +92,9 @@ class RoutesView extends React.Component {
|
||||
const nextParams = this.props.routes[type];
|
||||
const params = props.routes[type]; // Previous props
|
||||
|
||||
if (this.props.filterQuery != props.filterQuery || // Pagination
|
||||
params.page != nextParams.page || // Query
|
||||
if (this.props.filterQuery != props.filterQuery || // Query
|
||||
params.page != nextParams.page || // Pagination
|
||||
!filtersEqual(this.props.filtersApplied, props.filtersApplied) || // Filters
|
||||
params.loadRoutes != nextParams.loadRoutes || // Defered loading
|
||||
props.protocolId != this.props.protocolId // Switch related peers
|
||||
) {
|
||||
@ -146,7 +152,7 @@ class RoutesView extends React.Component {
|
||||
if (state.totalResults == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Render the routes card
|
||||
return (
|
||||
<div className={`card routes-view ${name}`}>
|
||||
@ -196,7 +202,7 @@ class RoutesView extends React.Component {
|
||||
|
||||
anchor: "routes-not-exported",
|
||||
page: this.props.routes.notExported.page,
|
||||
|
||||
|
||||
pageReceived: this.props.routes.received.page,
|
||||
pageFiltered: this.props.routes.filtered.page,
|
||||
pageNotExported: this.props.routes.notExported.page,
|
||||
@ -214,7 +220,7 @@ class RoutesView extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<p className="help">
|
||||
Due to the potentially high amount of routes not exported,
|
||||
Due to the potentially high amount of routes not exported,
|
||||
they are only fetched on demand.
|
||||
</p>
|
||||
|
||||
@ -272,6 +278,7 @@ export default connect(
|
||||
[ROUTES_FILTERED]: filtered,
|
||||
[ROUTES_NOT_EXPORTED]: notExported
|
||||
},
|
||||
filtersApplied: state.routes.filtersApplied,
|
||||
routing: state.routing.locationBeforeTransitions
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user