Merge branch 'feature/community-filter' into develop

This commit is contained in:
Matthias Hannig 2018-10-23 23:57:56 +02:00
commit 8d3188351a
25 changed files with 931 additions and 495 deletions

View File

@ -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"`
}

View 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

View 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"`
}

View File

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

View File

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

View File

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

View File

@ -75,7 +75,7 @@ func apiLookupPrefixGlobal(
break
}
filtersAvailable.UpdateFromRoute(r)
filtersAvailable.UpdateFromLookupRoute(r)
}
// Homogenize results

View 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);

View File

@ -5,7 +5,7 @@ import {
FILTER_GROUP_COMMUNITIES,
FILTER_GROUP_EXT_COMMUNITIES,
FILTER_GROUP_LARGE_COMMUNITIES,
} from './filter-groups'
} from './groups'
function _makeFilter(value) {

View 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;
}

View File

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

View File

@ -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) => {

View File

@ -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,
}
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

@ -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
});
}

View File

@ -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="&raquo;"
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);

View File

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

View File

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

View File

@ -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
});
}