initial port of neighbors table

This commit is contained in:
Annika Hannig 2022-07-13 16:31:58 +02:00
parent 96454440ac
commit 70fab38365
4 changed files with 303 additions and 142 deletions

View File

@ -1,15 +1,9 @@
import bigInt from 'big-integer';
import { useRef }
import { useRef
, useMemo
}
from 'react';
import { useLocation }
from 'react-router-dom';
import { ipToNumeric }
from 'app/utils/ip'
import { useNeighbors }
from 'app/components/neighbors/Provider';
import NeighborsTable
@ -34,73 +28,6 @@ const getFilterAsn = (filter) => {
return asn;
}
/**
* Sort alphanumeric
*/
const sortAnum = (sort) => {
return (a, b) => {
const va = a[sort];
const vb = b[sort];
if (va < vb ) { return -1; }
if (va > vb ) { return 1; }
return 0;
}
}
/**
* Sort by IPAddress
*/
const sortIpAddr = (sort) => {
return (a, b) => {
const va = ipToNumeric(a[sort]);
const vb = ipToNumeric(b[sort]);
// Handle ipv6 case
if (va instanceof bigInt) {
return va.compareTo(vb);
}
if (va < vb ) { return -1; }
if (va > vb ) { return 1; }
return 0;
}
}
/**
* Sort with order
*/
const sortOrder = (cmp, order) => {
return (a, b) => {
const res = cmp(a, b);
if (order === 'desc') {
return res * -1;
}
return res;
}
}
/**
* Sort neighbors
*/
const sortNeighbors = (neighbors, sort, order) => {
// Make compare function
let cmp = sortAnum(sort);
if (sort === "address") {
cmp = sortIpAddr(sort);
}
return neighbors.sort(sortOrder(cmp, order));
}
/**
* Check if state is up or established
*/
const isUpState = (s) => {
if (!s) { return false; }
s = s.toLowerCase();
return (s.includes("up") || s.includes("established"));
}
/**
* Filter neighbors
@ -137,11 +64,14 @@ const Neighbors = ({filter}) => {
const refDown = useRef();
const {isLoading, neighbors} = useNeighbors();
const filtered = useMemo(
() => filterNeighbors(neighbors, filter),
[neighbors, filter]);
if (isLoading) {
return <LoadingIndicator show={true} />;
}
const filtered = filterNeighbors(neighbors, filter);
if (!filtered || filtered.length === 0) {
// Empty Neighbors List
return (

View File

@ -1,41 +1,311 @@
import bigInt from 'big-integer';
import { FontAwesomeIcon }
from '@fortawesome/react-fontawesome';
import { faCircleArrowUp
, faCircleArrowDown
}
from '@fortawesome/free-solid-svg-icons';
import { useMemo
}
from 'react';
import { useParams }
from 'react-router-dom';
import { Link
}
from 'react-router-dom';
import { ipToNumeric }
from 'app/utils/ip'
import { useConfig }
from 'app/components/config/Provider';
import { useSelectedRouteServer }
from 'app/components/routeservers/Provider';
import { useQuery
, useQueryLink
}
from 'app/components/query';
import RelativeTimestamp
from 'app/components/datetime/RelativeTimestamp';
/**
* Default: Sort by ASN, ascending order.
*/
const querySortDefault = {
s: 'asn',
o: 'asc',
};
const lookupProperty = (obj, path) => {
let property = path.split(".").reduce((acc, part) => acc[part], obj);
if (typeof(property) == "undefined") {
property = `Property "${path}" not found in object.`;
}
return property;
}
/**
* Check if state is up or established
*/
const isUpState = (s) => {
if (!s) { return false; }
s = s.toLowerCase();
return (s.includes("up") || s.includes("established"));
}
/**
* Sort alphanumeric
*/
const sortAnum = (sort) => {
return (a, b) => {
const va = a[sort];
const vb = b[sort];
if (va < vb ) { return -1; }
if (va > vb ) { return 1; }
return 0;
}
}
/**
* Sort by IPAddress
*/
const sortIpAddr = (sort) => {
return (a, b) => {
const va = ipToNumeric(a[sort]);
const vb = ipToNumeric(b[sort]);
// Handle ipv6 case
if (va instanceof bigInt) {
return va.compareTo(vb);
}
if (va < vb ) { return -1; }
if (va > vb ) { return 1; }
return 0;
}
}
/**
* Sort with order
*/
const sortOrder = (cmp, order) => {
return (a, b) => {
const res = cmp(a, b);
if (order === 'desc') {
return res * -1;
}
return res;
}
}
/**
* Sort neighbors
*/
const sortNeighbors = (neighbors, sort, order) => {
// Make compare function
let cmp = sortAnum(sort);
if (sort === "address") {
cmp = sortIpAddr(sort);
}
return neighbors.sort(sortOrder(cmp, order));
}
/**
* Section renders the sections title
*/
const Section = ({state}) => {
let sectionTitle = '';
let sectionCls = 'card-header card-header-neighbors ';
switch(state) {
case 'up':
sectionTitle = 'BGP Sessions Established';
sectionCls += 'established ';
break;
case 'down':
sectionTitle = 'BGP Sessions Down';
sectionCls += 'down ';
break;
case 'start':
sectionTitle = 'BGP Sessions Start';
sectionCls += '';
break;
default:
}
return (<p className={sectionCls}>{sectionTitle}</p>);
}
/**
* RoutesLink is a link to the routes of the neighbor
*/
const RoutesLink = ({neighbor, children}) => {
const { routeServerId } = useParams();
if (!isUpState(neighbor.state)) {
return <>{children}</>;
};
const url = `/routeservers/${routeServerId}/protocols/${neighbor.id}/routes`;
return (
<Link to={url}>{children}</Link>
);
}
/**
* Sort indicator indicates the sorting order of the column
*/
const SortIndicator = ({order, active}) => {
if (!active) {
return null;
}
let icon = faCircleArrowUp;
if (order === 'desc') {
icon = faCircleArrowDown;
}
return <FontAwesomeIcon icon={icon} />;
}
const ColumnHeader = ({title, id}) => {
const [query, makeLink] = useQueryLink(querySortDefault);
const sort = id.toLowerCase();
const active = query.s === sort;
let cls = `col-neighbor-attr col-neighbor-${id} `;
let link = makeLink({s: sort}); // s: Sort column
if (active) {
cls += 'col-neighbor-active ';
link = makeLink({o: query.o === 'asc' ? 'desc' : 'asc'}); // o: Toggle order
}
return (
<th className={cls}>
<Link to={link}>{title} <SortIndicator active={active} order={query.o} /></Link>
</th>
);
}
// Column Widgets:
const ColDescription = ({neighbor}) => {
return (
<td>
<RoutesLink neighbor={neighbor}>
{neighbor.description}
{!isUpState(neighbor.state) &&
neighbor.last_error &&
<span className="protocol-state-error">
{neighbor.last_error}
</span>}
</RoutesLink>
</td>
);
}
const ColUptime = ({neighbor}) => {
return (
<td className="date-since">
<RelativeTimestamp value={neighbor.uptime} suffix={true} />
</td>
);
}
const ColLinked = ({neighbor, column}) => {
// Access neighbor property by path
const property = lookupProperty(neighbor, column);
return (
<td>
<RoutesLink neighbor={neighbor}>
{property}
</RoutesLink>
</td>
);
}
const ColPlain = ({neighbor, column}) => {
// Access neighbor property by path
const property = lookupProperty(neighbor, column);
return (
<td>{property}</td>
);
}
const ColNotAvailable = () => {
return <td>-</td>;
}
const NeighborColumn = ({neighbor, column}) => {
const rs = useSelectedRouteServer();
const widgets = {
// Special cases
"asn": ColPlain,
"state": ColPlain,
"Uptime": ColUptime,
"Description": ColDescription,
};
// For openbgpd the value is ommitted
if (rs.type === "openbgpd") {
widgets["routes_not_exported"] = ColNotAvailable;
}
// Get render function
let Widget = widgets[column] || ColLinked;
return (
<Widget neighbor={neighbor} column={column} />
);
}
/**
* NeighborsTable renders the table of neighbors
*/
const NeighborsTable = ({neighbors, state, ref}) => {
const config = useConfig();
const [query] = useQuery();
const columns = config.neighbors_columns;
const columnsOrder = config.neighbors_columns_order;
const sortColumn = query.s;
const sortOrder = query.o;
const sortedNeighbors = useMemo(
() => sortNeighbors(neighbors, sortColumn, sortOrder),
[neighbors, sortColumn, sortOrder]);
if (!neighbors || neighbors.length === 0) {
return null; // nothing to do here
}
let sectionTitle = '';
let sectionAnchor = 'sessions-unknown';
let sectionCls = 'card-header card-header-neighbors ';
const header = columnsOrder.map((col) =>
<ColumnHeader
key={col}
id={col}
title={columns[col]} />);
switch(state) {
case 'up':
sectionAnchor = 'sessions-up';
sectionTitle = 'BGP Sessions Established';
sectionCls += 'established ';
break;
case 'down':
sectionAnchor = 'sessions-down';
break;
case 'start':
sectionAnchor = 'sessions-down';
sectionTitle = 'BGP Sessions Down';
sectionCls += 'down ';
break;
default:
}
let header = <td>Header</td>;
let rows = <tr><td>Row</td></tr>;
const rows = sortedNeighbors.map((neighbor) => {
const columns = columnsOrder.map(
(col) =>
<NeighborColumn
key={col}
column={col}
neighbor={neighbor} />);
return <tr key={neighbor.id}>{columns}</tr>;
});
return (
<div className="card" ref={ref}>
<p className={sectionCls}>{sectionTitle}</p>
<Section state={state} />
<table className="table table-striped table-protocols">
<thead>
<tr>

View File

@ -1,39 +0,0 @@
import { useMemo
, useCallback
}
from 'react';
import { useSearchParams
}
from 'react-router-dom';
/**
* useQuery is an extension to useLocation to handle
* query parameters. Internally this uses URLSearchParams
* for decoding but returns an object merged with the defaults.
* To prevent loops, the search parameters are only updated
* if they differ.
*/
export const useQuery = (defaults={}) => {
const [query, setQuery] = useSearchParams(defaults);
const params = useMemo(() => {
// For convenient access convert params to object
let q = {};
for (const [k, v] of query) {
q[k] = v;
}
return q;
}, [query]);
const update = useCallback((q) => {
// Only update if query differs
const next = new URLSearchParams({...params, ...q});
if (next.toString() !== query.toString()) {
setQuery(next);
}
}, [params, query, setQuery]);
return [params, update];
}

View File

@ -8,7 +8,7 @@ import { useSearchParams }
from 'react-router-dom';
import { useQuery }
from 'app/components/search/query';
from 'app/components/query';
import { useSelectedRouteServer }
from 'app/components/routeservers/Provider';
@ -61,7 +61,7 @@ const NeighborsPage = () => {
/>
</div>
<QuickLinks />
<Neighbors filter={""} />
<Neighbors filter={query.q} />
</div>
<div className="col-lg-3 col-md-12 col-aside-details">
<div className="card">