imported client from birdseye

This commit is contained in:
Matthias Hannig 2017-05-16 13:34:00 +02:00
parent 9916c4a7c1
commit 19e46c17c2
67 changed files with 3423 additions and 0 deletions

5
client/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
build/
*.log
node_modules/

50
client/Makefile Normal file
View File

@ -0,0 +1,50 @@
#
# Build Helper
# ------------
#
# Create a full build by just typing make.
# This will automatically install all dependencies from NPM and
# start the build process.
#
# While developing, you might want to use 'make watch'
# which will automatically restart gulp in case something went
# wrong.
#
VERSION=$(shell cat ../VERSION)
DIST_BUILDS=../../birdseye-static/builds
DIST=birdseye-ui-dist-$(VERSION).tar.gz
# == END CONFIGURATION ==
DIST_BUILD=$(addprefix $(DIST_BUILDS)/, $(DIST))
all: deps client
deps:
@echo "Installing dependencies"
npm install
client:
@echo "Building birdseye UI"
gulp
client_prod:
@echo "Building birdseye UI (production)"
DISABLE_LOGGING=1 gulp
watch:
while true; do gulp watch; done
$(DIST_BUILD): deps client_prod
@echo "Creating birdseye ui distribution"
tar cvzf $(DIST) build/
mv $(DIST) $(DIST_BUILDS)
@echo ""
@echo "Done. Don't forget to push the dist to github"
dist: $(DIST_BUILD)
echo $(DIST_BUILD)

102
client/app.jsx Normal file
View File

@ -0,0 +1,102 @@
/**
* Birdseye v.1.0.0
* ----------------
*
* @author Matthias Hannig <mha@ecix.net>
*/
import axios from 'axios'
import React from 'react'
import ReactDOM from 'react-dom'
import { Component } from 'react'
// Config
import { configureAxios } from './config'
// Redux
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
// Router
import { createHistory } from 'history'
import { Router,
Route,
IndexRoute,
IndexRedirect,
useRouterHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
// Components
import LayoutMain from 'layouts/main'
import WelcomePage
from 'components/welcome'
import RouteserverPage
from 'components/routeservers/page'
import RoutesPage
from 'components/routeservers/routes/page'
// Middlewares
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import { routerMiddleware as createRouterMiddleware }
from 'react-router-redux'
// Reducer
import combinedReducer from './reducer/app-reducer'
// Setup routing
const browserHistory = useRouterHistory(createHistory)({
basename: '/birdseye/app'
});
// Setup application
let store;
const routerMiddleware = createRouterMiddleware(browserHistory);
if (window.NO_LOG) {
store = createStore(combinedReducer, applyMiddleware(
routerMiddleware,
thunkMiddleware
));
} else {
const loggerMiddleware = createLogger();
store = createStore(combinedReducer, applyMiddleware(
routerMiddleware,
thunkMiddleware,
loggerMiddleware
));
}
const history = syncHistoryWithStore(browserHistory, store);
// Setup axios
configureAxios(axios);
// Create App
class Birdseye extends Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Route path="/" component={LayoutMain}>
<IndexRoute component={WelcomePage}/>
<Route path="/routeservers">
<Route path=":routeserverId" component={RouteserverPage} />
<Route path=":routeserverId/protocols/:protocolId/routes" component={RoutesPage} />
</Route>
</Route>
</Router>
</Provider>
);
}
}
var mount = document.getElementById('app');
ReactDOM.render(<Birdseye />, mount);

View File

@ -0,0 +1,9 @@
.card {
background: white;
margin: 10px 20px;
border-radius: 2px;
box-shadow: 0px 1px 3px #cccccc;
padding: 20px;
}

View File

@ -0,0 +1,36 @@
.error-notify {
min-width: 250px;
margin-left: -125px;
border-bottom: 7px solid #7c0002;
background-color: rgba(255,0,0,0.8);
color: #fff;
border-radius: 2px;
padding: 16px;
position: fixed;
display: table;
z-index: 2;
right: 30px;
top: 20px;
.error-icon {
display: table-cell;
width: 60px;
padding: 10px 10px 0 10px;
text-align: center;
vertical-align: top;
> i {
font-size: 2em;
border-radius: 100%;
}
}
.error-message{
display: table-cell;
> p {
line-height: 1.2;
margin-top: 6px;
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Show Modal
*/
.modal-show {
display: block;
.modal-content {
border-radius: 3px;
}
}
.bgp-attributes-modal {
.modal-header {
p {
margin: 0px;
font-size: 10px;
}
h4 {
font-size: 14px;
margin: 0px;
}
}
table {
th {
width: 142px;
}
}
}
.table-nolines {
td, th {
border: none !important;
}
}

View File

@ -0,0 +1,28 @@
.table-routes {
// Make pseudo links
td {
cursor: pointer;
color: #337ab7;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.reject-reason {
font-size: 90%;
color: #333;
margin-bottom: 1px;
}
}
.table-protocols {
.date-since {
white-space: nowrap;
}
}

View File

@ -0,0 +1,90 @@
/*
* HEADER
*/
.page-sidebar {
color: #f0f0f0;
h2 {
color: #b0b0b0;
margin-top: 15px;
font-size: 12px;
text-transform: uppercase;
padding-left: 15px;
}
.sidebar-header {
color: #f0f0f0;
padding: 5px;
display: flex;
flex-direction: row;
background: #222;
.logo {
flex: 0 0 45px;
padding: 10px 8px;
i {
font-size: 28px;
}
}
.title {
flex: 1;
h1 {
margin: 7px 0px 0px 0px;
padding: 3px 0px;
font-size: 13px;
color: #f0f0f0;
}
p {
font-size: 10px;
color: #f0f0f0;
}
}
}
.routeservers-list {
margin-top: 40px;
ul {
list-style: none;
padding: 0px;
margin: 0px;
}
li {
padding: 8px;
padding-left: 15px;
cursor: pointer;
}
li.active {
}
.routeserver-id {
font-size: 13px;
display: block;
}
.routeserver-status {
.bird-version {
font-size: 10px;
color: #888888;
}
}
}
}

View File

@ -0,0 +1,37 @@
.page-header {
padding: 0px 20px;
.status-name {
font-weight: bold;
}
.status-protocol {
font-weight: bold;
}
.spacer {
font-weight: bold;
padding: 0px 8px;
}
}
.routeserver-status {
ul {
margin: 0px;
padding: 0px;
list-style: none;
li {
padding: 10px;
i {
width: 25px;
}
}
}
}

View File

@ -0,0 +1,8 @@
.welcome-page {
.jumbotron {
padding: 20px;
}
}

View File

@ -0,0 +1,97 @@
/**
* Main Layout Scss File
*/
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:700,400,300);
body, html {
font-family: "Source Sans Pro";
margin: 0px;
padding: 0px;
}
body,
.page {
display: flex;
min-height: 100vh;
flex-direction: row;
}
#app {
width: 100%;
}
.page header {
margin-top: 50px;
}
.page-body {
display: flex;
flex: 1;
}
.page-content {
flex: 1 0;
background: #f4f5f7;
padding: 0px;
margin: 0px;
}
.page-nav {
flex: 0 0 250px;
order: -1;
background: #f8f8f8;
// border-right: 1px solid #ccc;
}
.page-sidebar {
flex: 0 0 255px;
background: #272634;
// border-left: 1px solid #ccc;
}
header, footer {
flex: none;
}
.navbar {
box-shadow: none;
border-bottom: none;
}
.page-header {
line-height: 59px;
background: white;
margin: 0px;
height: 59px;
box-shadow: 0px 1px 4px #eee;
}
.table-protocols {
.protocol-state-error {
display: block;
font-size: 11px;
color: orange;
}
}
.loading-indicator {
text-align: center;
width: 50px;
margin: 0px auto;
}
.details-main {
.help-block {
margin: 20px;
padding: 10px 5px;
}
margin-right: 0px;
margin-top: 20px;
}

View File

@ -0,0 +1,12 @@
@import 'node_modules/font-awesome/css/font-awesome';
@import 'layouts/main';
@import 'components/welcome';
@import 'components/sidebar';
@import 'components/status';
@import 'components/card';
@import 'components/modal';
@import 'components/routes';
@import 'components/error';

View File

@ -0,0 +1,27 @@
import axios from 'axios';
import {apiError} from 'components/errors/actions'
import {loadRejectReasonsSuccess} from 'components/routeservers/actions';
export const LOAD_CONFIG_SUCCESS = "@birdseye/LOAD_CONFIG_SUCCESS";
function loadConfigSuccess(routes_columns) {
return {
type: LOAD_CONFIG_SUCCESS,
routes_columns: routes_columns
}
}
export function loadConfig() {
return (dispatch) => {
axios.get(`/birdseye/api/config/`)
.then(({data}) => {
dispatch(
loadRejectReasonsSuccess(data.config.rejection.asn,
data.config.rejection.reject_id,
data.config.reject_reasons)
);
dispatch(loadConfigSuccess(data.config.routes_columns));
})
.catch(error => dispatch(apiError(error)));
}
}

View File

@ -0,0 +1,21 @@
import {LOAD_CONFIG_SUCCESS} from './actions'
const initialState = {
routes_columns: {
Gateway: "gateway",
Interface: "interface",
Metric: "metric",
}
};
export default function reducer(state = initialState, action) {
switch(action.type) {
case LOAD_CONFIG_SUCCESS:
return {routes_columns: action.routes_columns};
}
return state;
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import { connect } from 'react-redux'
import { loadConfig } from 'components/config/actions'
class Config extends React.Component {
componentDidMount() {
this.props.dispatch(loadConfig());
}
render() {
return null;
}
}
export default connect()(Config);

View File

@ -0,0 +1,27 @@
/**
* Datetime Component
*
* @author Matthias Hannig <mha@ecix.net>
*/
import React from 'react'
import moment from 'moment'
export default class Datetime extends React.Component {
render() {
let timefmt = this.props.format;
if (!timefmt) {
timefmt = 'LLLL';
}
let time = moment(this.props.value);
return (
<span>{time.format(timefmt)}</span>
);
}
}

View File

@ -0,0 +1,12 @@
export const API_ERROR = '@birdseye/API_ERROR';
export function apiError(error) {
return {
type: API_ERROR,
error,
};
}
export function resetApiError() {
return apiError(null);
}

View File

@ -0,0 +1,65 @@
import React from 'react'
import {connect} from 'react-redux'
import {resetApiError} from './actions'
class ErrorsPage extends React.Component {
resetApiError() {
this.props.dispatch(resetApiError());
}
render() {
if (!this.props.error) {
return null;
}
let status = null;
if (this.props.error.response) {
status = this.props.error.response.status;
}
if (!status || (status != 429 && status < 500)) {
return null;
}
let body = null;
if (status == 429) {
body = (
<div className="error-message">
<p>Birdseye reached the request limit.</p>
<p>We suggest you try at a less busy time.</p>
</div>
);
} else {
body = (
<div className="error-message">
<p>
Birdseye has trouble connecting to the API
{this.props.error.response &&
" (got HTTP " + this.props.error.response.status + ")"}
.
</p>
<p>If this problem persist, we suggest you try again later.</p>
</div>
);
}
return (
<div className="error-notify">
<div className="error-icon">
<i className="fa fa-times-circle" aria-hidden="true"></i>
</div>
{body}
</div>
);
}
}
export default connect(
(state) => ({
error: state.errors.error
})
)(ErrorsPage);

View File

@ -0,0 +1,17 @@
import {API_ERROR} from './actions'
const initialState = {
error: null,
};
export default function reducer(state = initialState, action) {
switch(action.type) {
case API_ERROR:
return {error: action.error};
}
return state;
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import Spinner from 'react-spinkit'
export default class Indicator extends React.Component {
render() {
if (this.props.show == false) {
return null;
}
return (
<div className="loading-indicator">
<Spinner spinnerName="circle" />
</div>
);
}
}

View File

@ -0,0 +1,105 @@
import axios from 'axios'
export const SET_QUERY_INPUT_VALUE = "@lookup/SET_QUERY_INPUT_VALUE";
export const SET_QUERY_VALUE = "@lookup/SET_QUERY_VALUE";
export const SET_QUERY_TYPE = "@lookup/SET_QUERY_TYPE";
export const RESET = "@lookup/RESET";
export const EXECUTE = "@lookup/EXECUTE";
export const LOOKUP_STARTED = "@lookup/LOOKUP_STARTED";
export const LOOKUP_RESULTS = "@lookup/LOOKUP_RESULTS";
/*
* Action Creators
*/
export function setQueryInputValue(q) {
if(!q) { q = ''; }
return {
type: SET_QUERY_INPUT_VALUE,
payload: {
queryInput: q
}
}
}
export function setQueryValue(q) {
return {
type: SET_QUERY_VALUE,
payload: {
query: q
}
}
}
export function setQueryType(type) {
return {
type: SET_QUERY_TYPE,
payload: {
queryType: type
}
}
}
export function reset() {
return {
type: RESET
}
}
export function execute() {
return {
type: EXECUTE
}
}
export function lookupStarted(routeserverId, query) {
return {
type: LOOKUP_STARTED,
payload: {
routeserverId: routeserverId,
query: query
}
}
}
export function lookupResults(routeserverId, query, results) {
return {
type: LOOKUP_RESULTS,
payload: {
routeserverId: routeserverId,
query: query,
results: results
}
}
}
export function routesSearch(routeserverId, q) {
return (dispatch) => {
dispatch(lookupStarted(routeserverId, q));
axios.get(`/birdseye/api/routeserver/${routeserverId}/routes/lookup?q=${q}`)
.then((result) => {
let routes = result.data.result.routes;
dispatch(lookupResults(
routeserverId,
q,
routes
));
})
.catch((error) => {
dispatch(lookupResults(
routeserverId,
q,
[]
));
});
}
}

View File

@ -0,0 +1,87 @@
import React from 'react'
import {connect} from 'react-redux'
import SearchInput
from 'components/search-input'
import LoadingIndicator
from 'components/loading-indicator/small'
import {setQueryInputValue,
execute,
routesSearch}
from './actions'
import QueryDispatcher
from './query-dispatcher'
import LookupResults
from './results'
import {queryParams}
from 'components/utils/query'
class LookupView extends React.Component {
setQuery(q) {
this.props.dispatch(
setQueryInputValue(q)
);
}
componentDidMount() {
// Initial mount: keep query from querystring
let params = queryParams();
this.props.dispatch(
setQueryInputValue(params.q)
);
}
handleFormSubmit(e) {
e.preventDefault();
this.props.dispatch(execute());
return false;
}
render() {
return (
<div className="routes-lookup">
<div className="card lookup-header">
<form className="form-lookup" onSubmit={(e) => this.handleFormSubmit(e)}>
<SearchInput placeholder="Search for routes by entering a network address"
name="q"
onChange={(e) => this.setQuery(e.target.value)}
disabled={this.props.isSearching}
value={this.props.queryInput} />
<QueryDispatcher />
</form>
</div>
<LoadingIndicator show={this.props.isRunning} />
<div className="lookup-results">
<LookupResults results={this.props.results}
finished={this.props.isFinished} />
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
isRunning: state.lookup.queryRunning,
isFinished: state.lookup.queryFinished,
queryInput: state.lookup.queryInput,
results: state.lookup.results,
search: state.lookup.search,
}
}
)(LookupView);

View File

@ -0,0 +1,114 @@
import React from 'react'
import {connect} from 'react-redux'
import {QUERY_TYPE_UNKNOWN,
QUERY_TYPE_PREFIX}
from './query'
import {setQueryType,
routesSearch}
from './actions'
class QueryDispatcher extends React.Component {
/*
* Check if given query is a valid network address
* with a lame regex if format resembles a network address.
*/
isNetwork(query) {
// IPv4:
if (query.match(/(\d+\.)(\d+\.)(\d+\.)(\d+)\/(\d+)/)) {
return true;
}
// IPv6:
if (query.match(/([0-9a-fA-F]+:+)+\/\d+/)) {
return true;
}
return false;
}
/*
* Check if our query is ready
*/
isQueryReady() {
if (this.props.isRunning ||
this.props.queryType == QUERY_TYPE_UNKNOWN) {
return false;
}
return true;
}
executeQuery() {
// Check if we should dispatch this query now
for (let rs of this.props.routeservers) {
// Debug: limit to rs20
if (rs.id != 20) { continue; }
console.log("DISPATCHING SEARCH FOR RS:", rs);
switch (this.props.queryType) {
case QUERY_TYPE_PREFIX:
this.props.dispatch(
routesSearch(rs.id, this.props.input)
);
default:
this.props.dispatch(
dummySearch(rs.id, this.props.input)
);
}
}
}
/*
* handle query input, dispatches queryies to
* all routeservers.
*/
componentWillReceiveProps(nextProps) {
console.log("RECV PROPS:", nextProps);
if (nextProps.isRunning) {
return null; // Do nothing while a query is being processed
}
if (nextProps.shouldExecute) {
this.executeQuery();
return null;
}
// Determine query type
let queryType = QUERY_TYPE_UNKNOWN;
if (this.isNetwork(nextProps.input)) {
queryType = QUERY_TYPE_PREFIX;
}
this.props.dispatch(setQueryType(queryType));
}
/*
* Render anything? Nope.
*/
render() {
return null;
}
}
export default connect(
(state) => {
return {
input: state.lookup.queryInput,
queryType: state.lookup.queryType,
isRunning: state.lookup.queryRunning,
isFinished: state.lookup.queryFinished,
shouldExecute: state.lookup.queryDispatch,
routeserversQueue: state.lookup.routeserversQueue,
routeservers: state.routeservers.all
};
}
)(QueryDispatcher);

View File

@ -0,0 +1,6 @@
export const QUERY_TYPE_UNKNOWN = 'unknown';
export const QUERY_TYPE_PREFIX = 'prefix';
export const QUERY_TYPE_ASN = 'asn';

View File

@ -0,0 +1,119 @@
import {SET_QUERY_TYPE,
SET_QUERY_VALUE,
SET_QUERY_INPUT_VALUE,
LOOKUP_STARTED,
LOOKUP_RESULTS,
RESET,
EXECUTE}
from './actions'
import {QUERY_TYPE_UNKNOWN} from './query'
const initialState = {
results: {},
queue: new Set(),
queryInput: "",
query: "",
queryType: QUERY_TYPE_UNKNOWN,
queryRunning: false,
queryFinished: false,
queryDispatch: false,
};
// Action handlers:
// Handle lookup start
function _lookupStarted(state, lookup) {
// Enqueue Routeserver
let queue = new Set(state.queue);
queue.add(lookup.routeserverId);
// Clear results
let results = Object.assign({}, state.results, {
[lookup.routeserverId]: []
});
// Make state update
return {
queue: queue,
results: results,
queryRunning: true,
queryFinished: false,
};
}
// Handle a finished lookup
function _lookupResults(state, lookup) {
// Dequeue routeserver
let queue = new Set(state.queue);
let currentQueueSize = queue.size;
queue.delete(lookup.routeserverId);
// Any routeservers left in the queue?
let isRunning = true;
if (queue.size == 0) {
isRunning = false;
}
let isFinished = false;
if (queue.size == 0 && currentQueueSize > 0) {
isFinished = true;
}
// Update results set
let results = Object.assign({}, state.results, {
[lookup.routeserverId]: lookup.results,
});
// Make state update
return {
results: results,
queue: queue,
queryRunning: isRunning,
queryFinished: isFinished
}
}
// Reducer
export default function reducer(state=initialState, action) {
let payload = action.payload;
switch(action.type) {
// Setup
case SET_QUERY_TYPE:
case SET_QUERY_VALUE:
case SET_QUERY_INPUT_VALUE:
return Object.assign({}, state, payload);
// Search
case LOOKUP_STARTED:
// Update state on lookup started
return Object.assign({}, state, _lookupStarted(state, payload), {
queryDispatch: false,
});
case LOOKUP_RESULTS:
// Update state when we receive results
return Object.assign({}, state, _lookupResults(state, payload));
case EXECUTE:
return Object.assign({}, state, {
queryDispatch: true,
});
case RESET:
return Object.assign({}, state, initialState);
}
return state;
}

View File

@ -0,0 +1,61 @@
import React from 'react'
export default class LookupResults extends React.Component {
_countResults() {
let count = 0;
for (let rs in this.props.results) {
let set = this.props.results[rs];
count += set.length;
}
return count;
}
_resultSetEmpty() {
let resultCount = this._countResults();
if (this.props.finished && resultCount == 0){
return true;
}
return false;
}
_awaitingResults() {
let resultCount = this._countResults();
if (!this.props.finished && resultCount == 0) {
return true;
}
return false;
}
/* No Results */
renderEmpty() {
return (
<div className="card card-results card-no-results">
The prefix could not be found.
Did you specify a network address?
</div>
);
}
render() {
if (this._resultSetEmpty()) {
return this.renderEmpty();
}
if (this._awaitingResults) {
return null;
}
// Render Results table
return (
<div className="card card-results">
ROUTES INCOMING!
</div>
);
}
}

View File

@ -0,0 +1,191 @@
import _ from 'underscore'
import React from 'react'
import {connect} from 'react-redux'
import {loadRouteserverRoutes, loadRouteserverRoutesFiltered} from '../actions'
import {showBgpAttributes} from './bgp-attributes-modal-actions'
import LoadingIndicator
from 'components/loading-indicator/small'
class FilterReason extends React.Component {
render() {
const route = this.props.route;
if (!this.props.reject_reasons || !route || !route.bgp ||
!route.bgp.large_communities) {
return null;
}
const reason = route.bgp.large_communities.filter(elem =>
elem[0] == this.props.asn && elem[1] == this.props.reject_id
);
if (!reason.length) {
return null;
}
return <p className="reject-reason">{this.props.reject_reasons[reason[0][2]]}</p>;
}
}
FilterReason = connect(
state => {
return {
reject_reasons: state.routeservers.reject_reasons,
asn: state.routeservers.asn,
reject_id: state.routeservers.reject_id,
}
}
)(FilterReason);
function _filteredRoutes(routes, filter) {
let filtered = [];
if(filter == "") {
return routes; // nothing to do here
}
filter = filter.toLowerCase();
// Filter protocols
filtered = _.filter(routes, (r) => {
return (r.network.toLowerCase().indexOf(filter) != -1 ||
r.gateway.toLowerCase().indexOf(filter) != -1 ||
r.interface.toLowerCase().indexOf(filter) != -1);
});
return filtered;
}
class RoutesTable extends React.Component {
showAttributesModal(route) {
this.props.dispatch(
showBgpAttributes(route)
);
}
render() {
let routes = this.props.routes;
const routes_columns = this.props.routes_columns;
routes = _filteredRoutes(routes, this.props.filter);
if (!routes || !routes.length) {
return null;
}
const _lookup = (r, path) => {
const split = path.split(".").reduce((acc, elem) => acc[elem], r);
if (Array.isArray(split)) {
return split.join(" ");
}
return split;
}
let routesView = routes.map((r) => {
return (
<tr key={r.network} onClick={() => this.showAttributesModal(r)}>
<td>{r.network}{this.props.display_filter && <FilterReason route={r}/>}</td>
{Object.keys(routes_columns).map(col => <td key={col}>{_lookup(r, col)}</td>)}
</tr>
);
});
return (
<div className="card">
{this.props.header}
<table className="table table-striped table-routes">
<thead>
<tr>
<th>Network</th>
{Object.values(routes_columns).map(col => <th key={col}>{col}</th>)}
</tr>
</thead>
<tbody>
{routesView}
</tbody>
</table>
</div>
);
}
}
RoutesTable = connect(
(state) => {
return {
filter: state.routeservers.routesFilterValue,
reject_reasons: state.routeservers.reject_reasons,
routes_columns: state.config.routes_columns,
}
}
)(RoutesTable);
class RoutesTables extends React.Component {
componentDidMount() {
this.props.dispatch(
loadRouteserverRoutes(this.props.routeserverId, this.props.protocolId)
);
this.props.dispatch(
loadRouteserverRoutesFiltered(this.props.routeserverId,
this.props.protocolId)
);
}
render() {
if(this.props.isLoading) {
return (
<LoadingIndicator />
);
}
const routes = this.props.routes[this.props.protocolId];
const filtered = this.props.filtered[this.props.protocolId] || [];
if((!routes || routes.length == 0) &&
(!filtered || filtered.length == 0)) {
return(
<p className="help-block">
No routes matched your filter.
</p>
);
}
const received = routes.filter(r => filtered.indexOf(r) < 0);
const mkHeader = (color, action) => (
<p style={{"color": color, "textTransform": "uppercase"}}>
Routes {action}
</p>
);
const filtdHeader = mkHeader("orange", "filtered");
const recvdHeader = mkHeader("green", "accepted");
return (
<div>
<RoutesTable header={filtdHeader} routes={filtered} display_filter={true}/>
<RoutesTable header={recvdHeader} routes={received} display_filter={false}/>
</div>
);
}
}
export default connect(
(state) => {
return {
isLoading: state.routeservers.routesAreLoading,
routes: state.routeservers.routes,
filtered: state.routeservers.filtered,
}
}
)(RoutesTables);

View File

@ -0,0 +1,70 @@
/**
* Bootstrap Modal React Component
*
* @author Matthias Hannig <mha@ecix.net>
*/
import React from 'react'
export class Header extends React.Component {
render() {
return(
<div className="modal-header">
<button type="button"
className="close"
aria-label="Close"
onClick={this.props.onClickClose}>
<span aria-hidden="true">&times;</span></button>
{this.props.children}
</div>
);
}
}
export class Body extends React.Component {
render() {
return (
<div className="modal-body">
{this.props.children}
</div>
);
}
}
export class Footer extends React.Component {
render() {
return(
<div className="modal-footer">
{this.props.children}
</div>
);
}
}
export default class Modal extends React.Component {
render() {
if(!this.props.show) {
return null;
}
return (
<div className={this.props.className}>
<div className="modal modal-open modal-show fade in" role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
{this.props.children}
</div>
</div>
</div>
<div className="modal-backdrop fade in"
onClick={this.props.onClickBackdrop}></div>
</div>
);
}
}

View File

@ -0,0 +1,12 @@
import { combineReducers } from 'redux'
import bgpAttributesModalReducer
from 'components/routeservers/routes/bgp-attributes-modal-reducer'
export default combineReducers({
bgpAttributes: bgpAttributesModalReducer
});

View File

@ -0,0 +1,14 @@
import React from 'react'
export default class PageHeader extends React.Component {
render() {
return (
<div className="page-header">
{this.props.children}
</div>
)
}
}

View File

@ -0,0 +1,20 @@
import moment from 'moment'
import React from 'react'
export default class RelativeTime extends React.Component {
render() {
let time = moment.utc(this.props.value);
return (
<span>{time.fromNow(this.props.suffix)}</span>
)
}
}

View File

@ -0,0 +1,253 @@
/**
* Routeservers Actions
*/
import axios from 'axios'
import {apiError} from 'components/errors/actions'
export const LOAD_ROUTESERVERS_REQUEST = '@birdseye/LOAD_ROUTESERVERS_REQUEST';
export const LOAD_ROUTESERVERS_SUCCESS = '@birdseye/LOAD_ROUTESERVERS_SUCCESS';
export const LOAD_ROUTESERVERS_ERROR = '@birdseye/LOAD_ROUTESERVERS_ERROR';
export const LOAD_ROUTESERVER_STATUS_REQUEST = '@birdseye/LOAD_ROUTESERVER_STATUS_REQUEST';
export const LOAD_ROUTESERVER_STATUS_SUCCESS = '@birdseye/LOAD_ROUTESERVER_STATUS_SUCCESS';
export const LOAD_ROUTESERVER_STATUS_ERROR = '@birdseye/LOAD_ROUTESERVER_STATUS_ERROR';
export const LOAD_ROUTESERVER_PROTOCOL_REQUEST = '@birdseye/LOAD_ROUTESERVER_PROTOCOL_REQUEST';
export const LOAD_ROUTESERVER_PROTOCOL_SUCCESS = '@birdseye/LOAD_ROUTESERVER_PROTOCOL_SUCCESS';
export const LOAD_ROUTESERVER_PROTOCOL_ERROR = '@birdseye/LOAD_ROUTESERVER_PROTOCOL_ERROR';
export const LOAD_ROUTESERVER_ROUTES_REQUEST = '@birdseye/LOAD_ROUTESERVER_ROUTES_REQUEST';
export const LOAD_ROUTESERVER_ROUTES_SUCCESS = '@birdseye/LOAD_ROUTESERVER_ROUTES_SUCCESS';
export const LOAD_ROUTESERVER_ROUTES_ERROR = '@birdseye/LOAD_ROUTESERVER_ROUTES_ERROR';
export const LOAD_ROUTESERVER_ROUTES_FILTERED_REQUEST = '@birdseye/LOAD_ROUTESERVER_ROUTES_FILTERED_REQUEST';
export const LOAD_ROUTESERVER_ROUTES_FILTERED_SUCCESS = '@birdseye/LOAD_ROUTESERVER_ROUTES_FILTERED_SUCCESS';
export const SET_PROTOCOLS_FILTER_VALUE = '@birdseye/SET_PROTOCOLS_FILTER_VALUE';
export const SET_ROUTES_FILTER_VALUE = '@birdseye/SET_ROUTES_FILTER_VALUE';
export const LOAD_REJECT_REASONS_REQUEST = '@birdseye/LOAD_REJECT_REASONS_REQUEST';
export const LOAD_REJECT_REASONS_SUCCESS = '@birdseye/LOAD_REJECT_REASONS_SUCCESS';
// Action Creators
export function loadRouteserversRequest() {
return {
type: LOAD_ROUTESERVERS_REQUEST
}
}
export function loadRouteserversSuccess(routeservers) {
return {
type: LOAD_ROUTESERVERS_SUCCESS,
payload: {
routeservers: routeservers
}
}
}
export function loadRouteserversError(error) {
return {
type: LOAD_ROUTESERVERS_ERROR,
payload: {
error: error
}
}
}
export function loadRouteservers() {
return (dispatch) => {
dispatch(loadRouteserversRequest())
axios.get('/birdseye/api/routeserver/')
.then(({data}) => {
dispatch(loadRouteserversSuccess(data["routeservers"]));
})
.catch((error) => {
dispatch(apiError(error));
dispatch(loadRouteserversError(error.data));
});
}
}
export function loadRouteserverStatusRequest(routeserverId) {
return {
type: LOAD_ROUTESERVER_STATUS_REQUEST,
payload: {
routeserverId: routeserverId
}
}
}
export function loadRouteserverStatusSuccess(routeserverId, status) {
return {
type: LOAD_ROUTESERVER_STATUS_SUCCESS,
payload: {
status: status,
routeserverId: routeserverId
}
}
}
export function loadRouteserverStatusError(routeserverId, error) {
return {
type: LOAD_ROUTESERVER_STATUS_ERROR,
payload: {
error: error,
routeserverId: routeserverId
}
}
}
export function loadRouteserverStatus(routeserverId) {
return (dispatch) => {
dispatch(loadRouteserverStatusRequest(routeserverId));
axios.get(`/birdseye/api/routeserver/${routeserverId}/status/`)
.then(({data}) => {
dispatch(loadRouteserverStatusSuccess(routeserverId, data.status));
})
.catch((error) => {
dispatch(apiError(error));
dispatch(loadRouteserverStatusError(routeserverId, error.data));
});
}
}
export function loadRouteserverProtocolRequest(routeserverId) {
return {
type: LOAD_ROUTESERVER_PROTOCOL_REQUEST,
payload: {
routeserverId: routeserverId,
}
}
}
export function loadRouteserverProtocolSuccess(routeserverId, protocol) {
return {
type: LOAD_ROUTESERVER_PROTOCOL_SUCCESS,
payload: {
routeserverId: routeserverId,
protocol: protocol
}
}
}
export function loadRouteserverProtocol(routeserverId) {
return (dispatch) => {
dispatch(loadRouteserverProtocolRequest(routeserverId));
axios.get(`/birdseye/api/routeserver/${routeserverId}/protocol/`)
.then(({data}) => {
dispatch(setProtocolsFilterValue(""));
dispatch(loadRouteserverProtocolSuccess(routeserverId, data.protocols));
})
.catch(error => dispatch(apiError(error)));
}
}
export function loadRouteserverRoutesRequest(routeserverId, protocolId) {
return {
type: LOAD_ROUTESERVER_ROUTES_REQUEST,
payload: {
routeserverId: routeserverId,
protocolId: protocolId,
}
}
}
export function loadRouteserverRoutesSuccess(routeserverId, protocolId, routes) {
return {
type: LOAD_ROUTESERVER_ROUTES_SUCCESS,
payload: {
routeserverId: routeserverId,
protocolId: protocolId,
routes: routes
}
}
}
export function loadRouteserverRoutes(routeserverId, protocolId) {
return (dispatch) => {
dispatch(loadRouteserverRoutesRequest(routeserverId, protocolId))
axios.get(`/birdseye/api/routeserver/${routeserverId}/routes/?protocol=${protocolId}`)
.then(({data}) => {
dispatch(
loadRouteserverRoutesSuccess(routeserverId, protocolId, data.routes)
);
dispatch(setRoutesFilterValue(""));
})
.catch(error => dispatch(apiError(error)));
}
}
export function loadRouteserverRoutesFilteredRequest(routeserverId, protocolId) {
return {
type: LOAD_ROUTESERVER_ROUTES_FILTERED_REQUEST,
payload: {
routeserverId: routeserverId,
protocolId: protocolId,
}
}
}
export function loadRouteserverRoutesFilteredSuccess(routeserverId, protocolId, routes) {
return {
type: LOAD_ROUTESERVER_ROUTES_FILTERED_SUCCESS,
payload: {
routeserverId: routeserverId,
protocolId: protocolId,
routes: routes
}
}
}
export function loadRouteserverRoutesFiltered(routeserverId, protocolId) {
return (dispatch) => {
dispatch(loadRouteserverRoutesFilteredRequest(routeserverId, protocolId))
axios.get(`/birdseye/api/routeserver/${routeserverId}/routes/filtered/?protocol=${protocolId}`)
.then(({data}) => {
dispatch(
loadRouteserverRoutesFilteredSuccess(routeserverId, protocolId, data.routes)
);
dispatch(setRoutesFilterValue(""));
})
.catch(error => dispatch(apiError(error)));
}
}
export function setProtocolsFilterValue(value) {
return {
type: SET_PROTOCOLS_FILTER_VALUE,
payload: {
protocolsFilterValue: value
}
}
}
export function setRoutesFilterValue(value) {
return {
type: SET_ROUTES_FILTER_VALUE,
payload: {
routesFilterValue: value
}
}
}
export function loadRejectReasonsSuccess(asn, reject_id, reject_reasons) {
return {
type: LOAD_REJECT_REASONS_SUCCESS,
payload: {asn, reject_id, reject_reasons}
};
}

View File

@ -0,0 +1,32 @@
import React from 'react'
import {connect} from 'react-redux'
class Details extends React.Component {
render() {
let rsStatus = this.props.details[this.props.routeserverId];
if (!rsStatus) {
return null;
}
// Get routeserver name
let rs = this.props.routeservers[parseInt(this.props.routeserverId)];
if (!rs) {
return null;
}
return (
<span className="status-name">{rs.name}</span>
);
}
}
export default connect(
(state) => {
return {
routeservers: state.routeservers.all,
details: state.routeservers.details
}
}
)(Details);

View File

@ -0,0 +1,61 @@
import React from 'react'
import {connect} from 'react-redux'
import PageHeader from 'components/page-header'
import Details from './details'
import Status from './status'
import SearchInput from 'components/search-input'
import Protocols from './protocols'
import {setProtocolsFilterValue} from './actions'
class RouteserversPage extends React.Component {
setFilter(value) {
this.props.dispatch(
setProtocolsFilterValue(value)
);
}
render() {
return(
<div className="routeservers-page">
<PageHeader>
<Details routeserverId={this.props.params.routeserverId} />
</PageHeader>
<div className="row details-main">
<div className="col-md-8">
<div className="card">
<SearchInput
value={this.props.protocolsFilterValue}
placeholder="Filter by Neighbour, ASN or Description"
onChange={(e) => this.setFilter(e.target.value)}
/>
</div>
<Protocols protocol="bgp" routeserverId={this.props.params.routeserverId} />
</div>
<div className="col-md-4">
<div className="card">
<Status routeserverId={this.props.params.routeserverId} />
</div>
</div>
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
protocolsFilterValue: state.routeservers.protocolsFilterValue
};
}
)(RouteserversPage);

View File

@ -0,0 +1,232 @@
import _ from 'underscore'
import React from 'react'
import {connect} from 'react-redux'
import {loadRouteserverProtocol}
from 'components/routeservers/actions'
import {Link} from 'react-router'
import RelativeTime
from 'components/relativetime'
import LoadingIndicator
from 'components/loading-indicator/small'
function _filteredProtocols(protocols, filter) {
let filtered = [];
if(filter == "") {
return protocols; // nothing to do here
}
filter = filter.toLowerCase();
// Filter protocols
filtered = _.filter(protocols, (p) => {
return (p.neighbor_address.toLowerCase().indexOf(filter) != -1 ||
p.description.toLowerCase().indexOf(filter) != -1);
});
return filtered;
}
class RoutesLink extends React.Component {
render() {
let url = `/routeservers/${this.props.routeserverId}/protocols/${this.props.protocol}/routes`;
if (this.props.state != 'up') {
return (<span>{this.props.children}</span>);
}
return (
<Link to={url}>
{this.props.children}
</Link>
)
}
}
class NeighboursTable extends React.Component {
render() {
let neighbours = this.props.neighbours.map( (n) => {
return (
<tr key={n.protocol}>
<td>
<RoutesLink routeserverId={this.props.routeserverId}
protocol={n.protocol}
state={n.state}>
{n.neighbor_address}
</RoutesLink>
</td>
<td>{n.neighbor_as}</td>
<td>{n.state}</td>
<td className="date-since">
<RelativeTime value={n.state_changed} suffix={true} />
</td>
<td>
<RoutesLink routeserverId={this.props.routeserverId}
protocol={n.protocol}
state={n.state}>
{n.description}
{n.state != "up" && n.last_error &&
<span className="protocol-state-error">
{n.last_error}
</span>}
</RoutesLink>
</td>
<td>
<RoutesLink routeserverId={this.props.routeserverId}
protocol={n.protocol}
state={n.state}>
{n.routes.imported}
</RoutesLink>
</td>
<td>
<RoutesLink routeserverId={this.props.routeserverId}
protocol={n.protocol}
state={n.state}>
{n.routes.filtered}
</RoutesLink>
</td>
</tr>
);
});
let uptimeTitle;
switch(this.props.state) {
case 'up':
uptimeTitle = 'Uptime'; break;
case 'down':
uptimeTitle = 'Downtime'; break;
case 'start':
uptimeTitle = 'Since'; break;
}
return (
<div className="card">
<table className="table table-striped table-protocols">
<thead>
<tr>
<th>Neighbour</th>
<th>ASN</th>
<th>State</th>
<th>{uptimeTitle}</th>
<th>Description</th>
<th>Routes Recv.</th>
<th>Routes Filtered</th>
</tr>
</thead>
<tbody>
{neighbours}
</tbody>
</table>
</div>
);
}
}
class Protocols extends React.Component {
componentDidMount() {
this.props.dispatch(
loadRouteserverProtocol(parseInt(this.props.routeserverId))
);
}
componentWillReceiveProps(nextProps) {
if(this.props.routeserverId != nextProps.routeserverId) {
this.props.dispatch(
loadRouteserverProtocol(parseInt(nextProps.routeserverId))
);
}
}
render() {
if(this.props.isLoading) {
return (
<div className="card">
<LoadingIndicator />
</div>
);
}
let protocol = this.props.protocols[parseInt(this.props.routeserverId)];
if(!protocol) {
return null;
}
protocol = _filteredProtocols(protocol, this.props.filter);
if(!protocol || protocol.length == 0) {
return (
<div className="card">
<p className="help-block">
No neighbours could be found.
</p>
</div>
);
}
// Filter neighbours
let neighboursUp = [];
let neighboursDown = [];
let neighboursIdle = [];
for (let id in protocol) {
let n = protocol[id];
switch(n.state) {
case 'up':
neighboursUp.push(n);
break;
case 'down':
neighboursDown.push(n);
break;
case 'start':
neighboursIdle.push(n);
break;
default:
neighboursUp.push(n);
console.error("Couldn't classify neighbour by state:", n);
}
}
// Render tables
let tables = [];
if (neighboursUp.length) {
tables.push(<NeighboursTable key="up" state="up"
neighbours={neighboursUp}
routeserverId={this.props.routeserverId} />);
}
if (neighboursDown.length) {
tables.push(<NeighboursTable key="down" state="down"
neighbours={neighboursDown}
routeserverId={this.props.routeserverId} />);
}
if (neighboursIdle.length) {
tables.push(<NeighboursTable key="start" state="start"
neighbours={neighboursIdle}
routeserverId={this.props.routeserverId} />);
}
return (
<div>{tables}</div>
);
}
}
export default connect(
(state) => {
return {
isLoading: state.routeservers.protocolsAreLoading,
protocols: state.routeservers.protocols,
filter: state.routeservers.protocolsFilterValue
}
}
)(Protocols);

View File

@ -0,0 +1,28 @@
import React from 'react'
import {connect} from 'react-redux'
/*
* Show current neighbour if selected
* This should help to create a breadcrumb style navigation
* in the header.
*/
class ProtocolName extends React.Component {
render() {
return (
<span className="status-protocol">
{this.props.protocol.description}
</span>
);
}
}
export default connect(
(state, props) => {
let rsProtocols = state.routeservers.protocols[props.routeserverId]||{};
let protocol = rsProtocols[props.protocolId]||{};
return {
protocol: protocol
};
}
)(ProtocolName);

View File

@ -0,0 +1,116 @@
// Routeserver Reducer
import {LOAD_ROUTESERVERS_REQUEST,
LOAD_ROUTESERVERS_SUCCESS,
LOAD_ROUTESERVER_STATUS_SUCCESS,
LOAD_ROUTESERVER_PROTOCOL_REQUEST,
LOAD_ROUTESERVER_PROTOCOL_SUCCESS,
LOAD_ROUTESERVER_ROUTES_REQUEST,
LOAD_ROUTESERVER_ROUTES_SUCCESS,
LOAD_ROUTESERVER_ROUTES_FILTERED_REQUEST,
LOAD_ROUTESERVER_ROUTES_FILTERED_SUCCESS,
SET_PROTOCOLS_FILTER_VALUE,
SET_ROUTES_FILTER_VALUE,
LOAD_REJECT_REASONS_REQUEST,
LOAD_REJECT_REASONS_SUCCESS}
from './actions'
const initialState = {
all: [],
filtered: {},
details: {},
protocols: {},
routes: {},
reject_reasons: {},
reject_id: 0,
asn: 0,
protocolsFilterValue: "",
routesFilterValue: "",
isLoading: false,
routesAreLoading: false,
protocolsAreLoading: false
};
export default function reducer(state = initialState, action) {
switch(action.type) {
case LOAD_ROUTESERVERS_REQUEST:
return Object.assign({}, state, {
isLoading: true
});
case LOAD_ROUTESERVERS_SUCCESS:
return Object.assign({}, state, {
all: action.payload.routeservers,
isLoading: false
});
case LOAD_ROUTESERVER_ROUTES_REQUEST:
case LOAD_ROUTESERVER_ROUTES_FILTERED_REQUEST:
return Object.assign({}, state, {
routesAreLoading: true
});
case LOAD_ROUTESERVER_PROTOCOL_REQUEST:
return Object.assign({}, state, {
protocolsAreLoading: true
})
case LOAD_ROUTESERVER_PROTOCOL_SUCCESS:
var protocols = Object.assign({}, state.protocols, {
[action.payload.routeserverId]: action.payload.protocol
});
return Object.assign({}, state, {
protocols: protocols,
protocolsAreLoading: false
});
case LOAD_ROUTESERVER_ROUTES_SUCCESS:
var routes = Object.assign({}, state.routes, {
[action.payload.protocolId]: action.payload.routes
});
return Object.assign({}, state, {
routes: routes,
routesAreLoading: false
});
case LOAD_ROUTESERVER_ROUTES_FILTERED_SUCCESS:
var filtered = Object.assign({}, state.filtered, {
[action.payload.protocolId]: action.payload.routes
});
return Object.assign({}, state, {
filtered: filtered,
routesAreLoading: false
});
case LOAD_REJECT_REASONS_SUCCESS:
return Object.assign({}, state, action.payload);
case LOAD_ROUTESERVER_STATUS_SUCCESS:
var details = Object.assign({}, state.details, {
[action.payload.routeserverId]: action.payload.status
});
return Object.assign({}, state, {
details: details
});
case SET_PROTOCOLS_FILTER_VALUE:
case SET_ROUTES_FILTER_VALUE:
return Object.assign({}, state, action.payload);
}
return state;
}

View File

@ -0,0 +1,40 @@
export const SHOW_BGP_ATTRIBUTES_MODAL = '@birdseye/SHOW_BGP_ATTRIBUTES_MODAL';
export const HIDE_BGP_ATTRIBUTES_MODAL = '@birdseye/HIDE_BGP_ATTRIBUTES_MODAL';
export const SET_BGP_ATTRIBUTES = '@birdseye/SET_BGP_ATTRIBUTES';
/**
* Action Creators
*/
export function showBgpAttributesModal() {
return {
type: SHOW_BGP_ATTRIBUTES_MODAL,
}
}
export function hideBgpAttributesModal() {
return {
type: HIDE_BGP_ATTRIBUTES_MODAL
}
}
export function setBgpAttributes(attributes) {
return {
type: SET_BGP_ATTRIBUTES,
payload: {
bgpAttributes: attributes
}
}
}
export function showBgpAttributes(attributes) {
return (dispatch) => {
dispatch(setBgpAttributes(attributes));
dispatch(showBgpAttributesModal());
}
}

View File

@ -0,0 +1,31 @@
/**
* Bgp Attributes Modal Reducer
*
* @author Matthias Hannig <mha@ecix.net>
*/
import {SHOW_BGP_ATTRIBUTES_MODAL,
HIDE_BGP_ATTRIBUTES_MODAL,
SET_BGP_ATTRIBUTES} from './bgp-attributes-modal-actions'
const initialState = {
show: false,
bgpAttributes: {}
};
export default function reducer(state = initialState, action) {
switch(action.type) {
case SHOW_BGP_ATTRIBUTES_MODAL:
return Object.assign({}, state, { show: true });
case HIDE_BGP_ATTRIBUTES_MODAL:
return Object.assign({}, state, { show: false });
case SET_BGP_ATTRIBUTES:
return Object.assign({}, state, action.payload);
}
return state;
}

View File

@ -0,0 +1,102 @@
/**
* Show BGP attributes as a modal dialog
*
* @author Matthias Hannig <mha@ecix.net>
*/
import React from 'react'
import {connect} from 'react-redux'
import Modal, {Header, Body, Footer} from 'components/modals/modal'
import {hideBgpAttributesModal}
from './bgp-attributes-modal-actions'
class BgpAttributesModal extends React.Component {
closeModal() {
this.props.dispatch(
hideBgpAttributesModal()
);
}
render() {
let attrs = this.props.bgpAttributes;
if (!attrs.bgp) {
return null;
}
let communities = [];
if (attrs.bgp.communities) {
communities = attrs.bgp.communities.map((c) => c.join(':'));
}
let large_communities = [];
if (attrs.bgp.large_communities) {
large_communities = attrs.bgp.large_communities.map((c) => c.join(':'));
}
return (
<Modal className="bgp-attributes-modal"
show={this.props.show}
onClickBackdrop={() => this.closeModal()}>
<Header onClickClose={() => this.closeModal()}>
<p>BGP Attributes for Network:</p>
<h4>{attrs.network}</h4>
</Header>
<Body>
<table className="table table-nolines">
<tbody>
<tr>
<th>Origin:</th><td>{attrs.bgp.origin}</td>
</tr>
<tr>
<th>Local Pref:</th><td>{attrs.bgp.local_pref}</td>
</tr>
<tr>
<th>Next Hop:</th><td>{attrs.bgp.next_hop}</td>
</tr>
{attrs.bgp && attrs.bgp.med &&
<tr>
<th>MED:</th><td>{attrs.bgp.med}</td>
</tr>}
{attrs.bgp && attrs.bgp.as_path &&
<tr>
<th>AS Path:</th><td>{attrs.bgp.as_path.join(' ')}</td>
</tr>
}
<tr>
<th>Communities:</th>
<td>{communities.join(' ')}</td>
</tr>
{large_communities.length > 0 &&
<tr>
<th>Large Communities:</th>
<td>{large_communities.join(' ')}</td>
</tr>}
</tbody>
</table>
</Body>
<Footer>
<button className="btn btn-default"
onClick={() => this.closeModal()}>Close</button>
</Footer>
</Modal>
);
}
}
export default connect(
(state) => {
return {
show: state.modals.bgpAttributes.show,
bgpAttributes: state.modals.bgpAttributes.bgpAttributes
}
}
)(BgpAttributesModal);

View File

@ -0,0 +1,89 @@
import React from 'react'
import {connect} from 'react-redux'
import {Link} from 'react-router'
import Details from '../details'
import Status from '../status'
import PageHeader from 'components/page-header'
import ProtocolName
from 'components/routeservers/protocols/name'
import Routes from './routes'
import SearchInput from 'components/search-input'
import BgpAttributesModal
from './bgp-attributes-modal'
// Actions
import {setRoutesFilterValue}
from '../actions'
import {loadRouteserverProtocol}
from 'components/routeservers/actions'
class RoutesPage extends React.Component {
setFilter(value) {
this.props.dispatch(
setRoutesFilterValue(value)
);
}
componentDidMount() {
// Assert protocols for RS are loaded
this.props.dispatch(
loadRouteserverProtocol(parseInt(this.props.params.routeserverId))
);
}
render() {
return(
<div className="routeservers-page">
<PageHeader>
<Link to={`/routeservers/${this.props.params.routeserverId}`}>
<Details routeserverId={this.props.params.routeserverId} />
</Link>
<span className="spacer">&raquo;</span>
<ProtocolName routeserverId={this.props.params.routeserverId}
protocolId={this.props.params.protocolId} />
</PageHeader>
<BgpAttributesModal />
<div className="row details-main">
<div className="col-md-8">
<div className="card">
<SearchInput
value={this.props.routesFilterValue}
placeholder="Filter by Network, Gateway or Interface"
onChange={(e) => this.setFilter(e.target.value)} />
</div>
<Routes routeserverId={this.props.params.routeserverId}
protocolId={this.props.params.protocolId} />
</div>
<div className="col-md-4">
<div className="card">
<Status routeserverId={this.props.params.routeserverId} />
</div>
</div>
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
routesFilterValue: state.routeservers.routesFilterValue
}
}
)(RoutesPage);

View File

@ -0,0 +1,191 @@
import _ from 'underscore'
import React from 'react'
import {connect} from 'react-redux'
import {loadRouteserverRoutes, loadRouteserverRoutesFiltered} from '../actions'
import {showBgpAttributes} from './bgp-attributes-modal-actions'
import LoadingIndicator
from 'components/loading-indicator/small'
class FilterReason extends React.Component {
render() {
const route = this.props.route;
if (!this.props.reject_reasons || !route || !route.bgp ||
!route.bgp.large_communities) {
return null;
}
const reason = route.bgp.large_communities.filter(elem =>
elem[0] == this.props.asn && elem[1] == this.props.reject_id
);
if (!reason.length) {
return null;
}
return <p className="reject-reason">{this.props.reject_reasons[reason[0][2]]}</p>;
}
}
FilterReason = connect(
state => {
return {
reject_reasons: state.routeservers.reject_reasons,
asn: state.routeservers.asn,
reject_id: state.routeservers.reject_id,
}
}
)(FilterReason);
function _filteredRoutes(routes, filter) {
let filtered = [];
if(filter == "") {
return routes; // nothing to do here
}
filter = filter.toLowerCase();
// Filter protocols
filtered = _.filter(routes, (r) => {
return (r.network.toLowerCase().indexOf(filter) != -1 ||
r.gateway.toLowerCase().indexOf(filter) != -1 ||
r.interface.toLowerCase().indexOf(filter) != -1);
});
return filtered;
}
class RoutesTable extends React.Component {
showAttributesModal(route) {
this.props.dispatch(
showBgpAttributes(route)
);
}
render() {
let routes = this.props.routes;
const routes_columns = this.props.routes_columns;
routes = _filteredRoutes(routes, this.props.filter);
if (!routes || !routes.length) {
return null;
}
const _lookup = (r, path) => {
const split = path.split(".").reduce((acc, elem) => acc[elem], r);
if (Array.isArray(split)) {
return split.join(" ");
}
return split;
}
let routesView = routes.map((r) => {
return (
<tr key={r.network} onClick={() => this.showAttributesModal(r)}>
<td>{r.network}{this.props.display_filter && <FilterReason route={r}/>}</td>
{Object.keys(routes_columns).map(col => <td key={col}>{_lookup(r, col)}</td>)}
</tr>
);
});
return (
<div className="card">
{this.props.header}
<table className="table table-striped table-routes">
<thead>
<tr>
<th>Network</th>
{Object.values(routes_columns).map(col => <th key={col}>{col}</th>)}
</tr>
</thead>
<tbody>
{routesView}
</tbody>
</table>
</div>
);
}
}
RoutesTable = connect(
(state) => {
return {
filter: state.routeservers.routesFilterValue,
reject_reasons: state.routeservers.reject_reasons,
routes_columns: state.config.routes_columns,
}
}
)(RoutesTable);
class RoutesTables extends React.Component {
componentDidMount() {
this.props.dispatch(
loadRouteserverRoutes(this.props.routeserverId, this.props.protocolId)
);
this.props.dispatch(
loadRouteserverRoutesFiltered(this.props.routeserverId,
this.props.protocolId)
);
}
render() {
if(this.props.isLoading) {
return (
<LoadingIndicator />
);
}
const routes = this.props.routes[this.props.protocolId];
const filtered = this.props.filtered[this.props.protocolId] || [];
if((!routes || routes.length == 0) &&
(!filtered || filtered.length == 0)) {
return(
<p className="help-block">
No routes matched your filter.
</p>
);
}
const received = routes.filter(r => filtered.indexOf(r) < 0);
const mkHeader = (color, action) => (
<p style={{"color": color, "textTransform": "uppercase"}}>
Routes {action}
</p>
);
const filtdHeader = mkHeader("orange", "filtered");
const recvdHeader = mkHeader("green", "accepted");
return (
<div>
<RoutesTable header={filtdHeader} routes={filtered} display_filter={true}/>
<RoutesTable header={recvdHeader} routes={received} display_filter={false}/>
</div>
);
}
}
export default connect(
(state) => {
return {
isLoading: state.routeservers.routesAreLoading,
routes: state.routeservers.routes,
filtered: state.routeservers.filtered,
}
}
)(RoutesTables);

View File

@ -0,0 +1,48 @@
import React from 'react'
import {connect} from 'react-redux'
import Datetime from 'components/datetime'
class Details extends React.Component {
render() {
let rsStatus = this.props.details[this.props.routeserverId];
if (!rsStatus) {
return null;
}
// Get routeserver name
let rs = this.props.routeservers[parseInt(this.props.routeserverId)];
if (!rs) {
return null;
}
return (
<div className="routeserver-status">
<ul>
{rsStatus.last_reboot &&
<li><i className="fa fa-clock-o"></i>
Last Reboot: <b><Datetime value={rsStatus.last_reboot} /></b>
</li>}
<li><i className="fa fa-clock-o"></i>
Last Reconfig: <b><Datetime value={rsStatus.last_reconfig} /></b>
</li>
<li><i className="fa fa-battery-full"></i>
<b>{rsStatus.message}</b></li>
</ul>
</div>
);
}
}
export default connect(
(state) => {
return {
routeservers: state.routeservers.all,
details: state.routeservers.details
}
}
)(Details);

View File

@ -0,0 +1,20 @@
import React from 'react'
export default class SearchInput extends React.Component {
render() {
return(
<div className="input-group">
<span className="input-group-addon">
<i className="fa fa-search"></i>
</span>
<input type="text"
className="form-control"
{...this.props} />
</div>
);
}
}

View File

@ -0,0 +1,22 @@
import React from 'react'
import {Link} from 'react-router'
export default class SidebarHeader extends React.Component {
render() {
return (
<div className="sidebar-header">
<div className="logo">
<Link to='/'>
<i className="fa fa-cloud"></i>
</Link>
</div>
<div className="title">
<h1>Birdseye</h1>
<p>Your friendly bird looking glass</p>
</div>
</div>
);
}
}

View File

@ -0,0 +1,26 @@
/**
* Main Sidebar Component (aka. Navigation)
*
*/
import React from 'react'
import Header from './header'
import Routeservers from './routeservers'
export default class Sidebar extends React.Component {
render() {
return (
<aside className="page-sidebar">
<Header />
<Routeservers />
</aside>
)
}
}

View File

@ -0,0 +1,60 @@
/**
* Routeservers List component
*/
import React from 'react'
import { connect } from 'react-redux'
import{ push } from 'react-router-redux'
import { loadRouteservers } from 'components/routeservers/actions'
// Components
import Status from './status'
class RouteserversList extends React.Component {
componentDidMount() {
this.props.dispatch(
loadRouteservers()
);
}
showRouteserver(id) {
this.props.dispatch(
push(`/routeservers/${id}`)
);
}
render() {
let routeservers = this.props.routeservers.map((rs) =>
<li key={rs.id} onClick={() => this.showRouteserver(rs.id)}>
<span className="routeserver-id">{rs.name}</span>
<Status routeserverId={rs.id} />
</li>
);
return (
<div className="routeservers-list">
<h2>Routeservers</h2>
<ul>
{routeservers}
</ul>
</div>
);
}
}
export default connect(
(state) => {
return {
routeservers: state.routeservers.all
};
}
)(RouteserversList);

View File

@ -0,0 +1,44 @@
/*
* Bird status
*/
import React from 'react'
import {connect} from 'react-redux'
// Actions
import {loadRouteserverStatus}
from 'components/routeservers/actions'
class Status extends React.Component {
componentDidMount() {
this.props.dispatch(
loadRouteserverStatus(this.props.routeserverId)
);
}
render() {
let rsStatus = this.props.details[this.props.routeserverId];
if (!rsStatus) {
return null;
}
return (
<div className="routeserver-status">
<div className="bird-version">
Bird {rsStatus.version}
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
details: state.routeservers.details
}
}
)(Status);

View File

@ -0,0 +1,20 @@
/*
* Fetch query params from location
*/
export function queryParams() {
if (!window && !window.location && !window.location.search) {
return {}
}
let search = window.location.search.slice(1); // omit ?
let tokens = search.split("&");
let params = {};
for (let t of tokens) {
let kv = t.split("=", 2)
params[kv[0]] = kv[1];
}
return params;
}

View File

@ -0,0 +1,28 @@
import React from 'react'
import PageHeader from 'components/page-header'
import Lookup from 'components/lookup'
export default class Welcome extends React.Component {
render() {
return (
<div className="welcome-page">
<PageHeader></PageHeader>
<div className="jumbotron">
<h1>Welcome to Birdseye!</h1>
<p>Your friendly bird looking glass</p>
</div>
<div className="col-md-8">
<Lookup />
</div>
</div>
)
}
}

8
client/config/index.jsx Normal file
View File

@ -0,0 +1,8 @@
export const configureAxios = function(axios) {
// Setup axios to use django xsrf token
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
};

47
client/gulp/build.js Normal file
View File

@ -0,0 +1,47 @@
'use strict';
/**
* Gulp main configuration
*/
var fs = require('fs');
var path = require('path');
var gulp = require('gulp');
var runSequence = require('run-sequence');
var tasks = fs.readdirSync('./gulp/tasks').filter(function(filename){
return path.extname(filename) === '.js';
});
// == Load configuration
global.config = JSON.parse(fs.readFileSync('./gulp/config.json'));
// == Import all tasks
tasks.forEach(function(task){
require('./tasks/' + task);
});
// == Register build task.
gulp.task('build', [
'bundle',
'html',
'stylesheets',
'assets',
'app'
], function() {
});
// == Production task
gulp.task('production', ['build', 'appmin'], function() {
});
// == Register default task
gulp.task('default', ['clean'], function() {
runSequence(
'build'
);
});

18
client/gulp/config.json Normal file
View File

@ -0,0 +1,18 @@
{
"bundle": {
"js": {
"libs": [
"node_modules/jquery/dist/jquery.js",
"node_modules/bootstrap/dist/js/bootstrap.js"
]
},
"css": {
"libs": [
"node_modules/bootstrap/dist/css/bootstrap.css",
"node_modules/bootstrap/dist/css/bootstrap-theme.css",
"node_modules/font-awesome/css/font-awesome.css"
]
}
}
}

54
client/gulp/tasks/app.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
/**
* Task: app
*
* Compile the main app
*/
var gulp = require('gulp');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var size = require('gulp-size');
var browserify = require('browserify');
var babelify = require('babelify');
var source = require('vinyl-source-stream');
// == Register task: app
gulp.task('app', function(){
var entries = ['./app.jsx'];
if (process.env.DISABLE_LOGGING) {
entries.unshift('./no_log.jsx');
}
var bundler = browserify({
entries: entries,
extensions: ['.jsx'],
paths: ['./node_modules', './']
});
return bundler.transform(
babelify.configure({
presets: ["es2015", "react"]
})
)
.bundle()
.pipe(source('app.js'))
.pipe(gulp.dest('build/js'))
.pipe(size());
});
gulp.task('appmin', ['app'], function() {
return gulp.src('build/js/app.js')
.pipe(uglify())
.pipe(size())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('build/js'));
});

View File

@ -0,0 +1,33 @@
'use strict';
/**
* Task: assets
*
* Copy / process all assets like images, fonts, etc.
*/
var gulp = require('gulp');
var flatten = require('gulp-flatten');
// == Register task: watch
gulp.task('assets', function(){
// Just copy all assets.
var assets = ['images'];
assets.forEach(function(asset){
gulp.src('app/assets/'+asset+'/**')
.pipe(gulp.dest('build/'+asset));
});
// Copy local fonts
gulp.src('app/assets/fonts/**')
.pipe(gulp.dest('build/fonts/'));
// Copy fonts from libraries
gulp.src('node_modules/**/*.{otf,eot,svg,ttf,woff,woff2}')
.pipe(flatten())
.pipe(gulp.dest('build/fonts/'));
});

View File

@ -0,0 +1,50 @@
'use strict';
/**
* Task: bundle
*
* Bundle dependencies into a single file.
*
* See: config.bundle
*/
var gulp = require('gulp');
var rename = require('gulp-rename');
var concat = require('gulp-concat');
var cssmin = require('gulp-cssmin');
var uglify = require('gulp-uglify');
gulp.task('bundle', function(){
// Get bundles (js and css) from config
var bundle = global.config.bundle;
// Process scripts
if ( bundle['js'] ) {
for(var name in bundle.js) {
var files = bundle.js[name];
gulp.src(files)
.pipe(concat(name + '.js'))
.pipe(gulp.dest('build/js'))
.pipe(uglify())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('build/js'));
}
}
// Process css
if ( bundle['css'] ) {
for(var name in bundle.css) {
var files = bundle.css[name];
gulp.src(files)
.pipe(concat(name + '.css'))
.pipe(gulp.dest('build/css'))
.pipe(cssmin())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('build/css'));
}
}
});

View File

@ -0,0 +1,14 @@
'use strict';
/**
* Task: Clean - Remove build.
*/
var gulp = require('gulp');
var clean = require('gulp-clean');
gulp.task('clean', function(){
return gulp.src('build/', {read: false})
.pipe(clean());
});

20
client/gulp/tasks/html.js Normal file
View File

@ -0,0 +1,20 @@
'use strict';
/**
* Task: html
*
* Copy all markup files.
*/
var gulp = require('gulp');
// == Register task
gulp.task('html', function(){
// Copy main app
gulp.src('*.html').pipe(gulp.dest('build/'));
});

19
client/gulp/tasks/lint.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
/**
* Task: lint
* Two words: code quality!
*/
var gulp = require('gulp');
var jshint = require('gulp-jshint');
gulp.task('lint', function(){
return gulp.src([
'app/**/*.js',
])
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});

View File

@ -0,0 +1,31 @@
'use strict';
/**
* Task: stylesheets
*
* Compile stylesheets from less source.
*/
var gulp = require('gulp');
var sass = require('gulp-sass');
var cssmin = require('gulp-cssmin');
var rename = require('gulp-rename');
var autoprefixer = require('gulp-autoprefixer');
// == Register task: stylesheets
gulp.task('stylesheets', function(){
// Compile less files
gulp.src('assets/scss/*.scss')
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('build/css/'))
.pipe(cssmin())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('build/css/'));
});

View File

@ -0,0 +1,24 @@
'use strict';
/**
* Task: watch
*
* Watch filesystem for changes in files and
* run the corresponding tasks.
*/
var gulp = require('gulp');
// == Register task: watch
gulp.task('watch', function(){
// Watch CSS, Scripts, HTML, and everything
// else in the source directory
gulp.watch('assets/html/**/*.html', ['html']);
gulp.watch('assets/js/**/*.js', ['scripts']);
gulp.watch('**/*.jsx', ['app']);
gulp.watch('assets/scss/**/*.scss', ['stylesheets']);
gulp.watch('assets/**/*', ['assets']);
});

16
client/gulpfile.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
/**
* The gulp configuration and tasks are splitted
* into multiple files.
*
* (c) 2015 Matthias Hannig
*/
// == Set environment: Choose between 'development' or 'production'
// TODO: Get build env from environment.
global.buildEnv = 'development';
// == Load gulp config
require('./gulp/build');

20
client/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Birdseye - The friendly bird looking glass</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="css/libs.min.css" />
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<!-- Application: Birdseye -->
<div id="app"></div>
<!-- Scripts -->
<script src="js/libs.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

23
client/layouts/main.jsx Normal file
View File

@ -0,0 +1,23 @@
import React from 'react'
import Sidebar from 'components/sidebar'
import ErrorsPage from 'components/errors/page'
import Config from 'components/config/view'
export default class LayoutMain extends React.Component {
render() {
return (
<div className="page">
<ErrorsPage />
<Sidebar />
<div className="page-body">
<main className="page-content">
{this.props.children}
</main>
</div>
<Config/>
</div>
);
}
}

1
client/no_log.jsx Normal file
View File

@ -0,0 +1 @@
window.NO_LOG = true;

54
client/package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "birdseye-app",
"version": "1.0.0",
"description": "",
"main": "gulpfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Matthias Hannig",
"license": "MIT",
"devDependencies": {
"axios": "^0.15.2",
"babel": "^6.5.2",
"babel-core": "^6.17.0",
"babel-eslint": "^7.0.0",
"babel-preset-es2015": "^6.16.0",
"babel-preset-es2016": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babelify": "^7.3.0",
"bootstrap": "^3.3.7",
"browserify": "^13.1.0",
"cssify": "^1.0.3",
"font-awesome": "^4.6.3",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.1",
"gulp-clean": "^0.3.2",
"gulp-concat": "^2.6.0",
"gulp-cssmin": "^0.1.7",
"gulp-flatten": "^0.3.1",
"gulp-jshint": "^2.0.1",
"gulp-rename": "^1.2.2",
"gulp-sass": "^2.3.2",
"gulp-size": "^2.1.0",
"gulp-uglify": "^2.0.0",
"history": "^2.1.2",
"jquery": "^3.1.1",
"jshint": "^2.9.4",
"moment": "^2.15.1",
"node-sass": "^3.10.1",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",
"react-router": "^2.8.1",
"react-router-redux": "^4.0.6",
"react-spinkit": "^1.1.11",
"redux": "^3.6.0",
"redux-logger": "^2.7.0",
"redux-thunk": "^2.1.0",
"run-sequence": "^1.2.2",
"underscore": "^1.8.3",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
}
}

View File

@ -0,0 +1,32 @@
import { combineReducers } from 'redux'
// Library Reducers
import { routerReducer } from 'react-router-redux'
// Application Reducers
import routeserversReducer
from 'components/routeservers/reducer'
import modalsReducer
from 'components/modals/reducer'
import errorsReducer
from 'components/errors/reducer'
import configReducer
from 'components/config/reducer'
import lookupReducer
from 'components/lookup/reducer'
export default combineReducers({
routeservers: routeserversReducer,
modals: modalsReducer,
routing: routerReducer,
lookup: lookupReducer,
errors: errorsReducer,
config: configReducer,
});