imported client from birdseye
This commit is contained in:
parent
9916c4a7c1
commit
19e46c17c2
5
client/.gitignore
vendored
Normal file
5
client/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
build/
|
||||
*.log
|
||||
|
||||
node_modules/
|
50
client/Makefile
Normal file
50
client/Makefile
Normal 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
102
client/app.jsx
Normal 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);
|
||||
|
9
client/assets/scss/components/card.scss
Normal file
9
client/assets/scss/components/card.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
margin: 10px 20px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 1px 3px #cccccc;
|
||||
padding: 20px;
|
||||
}
|
||||
|
36
client/assets/scss/components/error.scss
Normal file
36
client/assets/scss/components/error.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
38
client/assets/scss/components/modal.scss
Normal file
38
client/assets/scss/components/modal.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
28
client/assets/scss/components/routes.scss
Normal file
28
client/assets/scss/components/routes.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
90
client/assets/scss/components/sidebar.scss
Normal file
90
client/assets/scss/components/sidebar.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
37
client/assets/scss/components/status.scss
Normal file
37
client/assets/scss/components/status.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
8
client/assets/scss/components/welcome.scss
Normal file
8
client/assets/scss/components/welcome.scss
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
.welcome-page {
|
||||
.jumbotron {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
97
client/assets/scss/layouts/main.scss
Normal file
97
client/assets/scss/layouts/main.scss
Normal 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;
|
||||
}
|
||||
|
12
client/assets/scss/main.scss
Normal file
12
client/assets/scss/main.scss
Normal 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';
|
||||
|
27
client/components/config/actions.jsx
Normal file
27
client/components/config/actions.jsx
Normal 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)));
|
||||
}
|
||||
}
|
21
client/components/config/reducer.jsx
Normal file
21
client/components/config/reducer.jsx
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
17
client/components/config/view.jsx
Normal file
17
client/components/config/view.jsx
Normal 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);
|
27
client/components/datetime/index.jsx
Normal file
27
client/components/datetime/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
client/components/errors/actions.jsx
Normal file
12
client/components/errors/actions.jsx
Normal 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);
|
||||
}
|
65
client/components/errors/page.jsx
Normal file
65
client/components/errors/page.jsx
Normal 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);
|
17
client/components/errors/reducer.jsx
Normal file
17
client/components/errors/reducer.jsx
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
19
client/components/loading-indicator/small.jsx
Normal file
19
client/components/loading-indicator/small.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
105
client/components/lookup/actions.jsx
Normal file
105
client/components/lookup/actions.jsx
Normal 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,
|
||||
[]
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
87
client/components/lookup/index.jsx
Normal file
87
client/components/lookup/index.jsx
Normal 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);
|
||||
|
114
client/components/lookup/query-dispatcher.jsx
Normal file
114
client/components/lookup/query-dispatcher.jsx
Normal 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);
|
||||
|
6
client/components/lookup/query.jsx
Normal file
6
client/components/lookup/query.jsx
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
export const QUERY_TYPE_UNKNOWN = 'unknown';
|
||||
export const QUERY_TYPE_PREFIX = 'prefix';
|
||||
export const QUERY_TYPE_ASN = 'asn';
|
||||
|
119
client/components/lookup/reducer.jsx
Normal file
119
client/components/lookup/reducer.jsx
Normal 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;
|
||||
}
|
||||
|
61
client/components/lookup/results.jsx
Normal file
61
client/components/lookup/results.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
191
client/components/lookup/routes.jsx
Normal file
191
client/components/lookup/routes.jsx
Normal 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);
|
70
client/components/modals/modal.jsx
Normal file
70
client/components/modals/modal.jsx
Normal 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">×</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>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
12
client/components/modals/reducer.jsx
Normal file
12
client/components/modals/reducer.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
import bgpAttributesModalReducer
|
||||
from 'components/routeservers/routes/bgp-attributes-modal-reducer'
|
||||
|
||||
export default combineReducers({
|
||||
bgpAttributes: bgpAttributesModalReducer
|
||||
});
|
||||
|
||||
|
14
client/components/page-header/index.jsx
Normal file
14
client/components/page-header/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
20
client/components/relativetime/index.jsx
Normal file
20
client/components/relativetime/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
253
client/components/routeservers/actions.jsx
Normal file
253
client/components/routeservers/actions.jsx
Normal 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}
|
||||
};
|
||||
}
|
32
client/components/routeservers/details/index.jsx
Normal file
32
client/components/routeservers/details/index.jsx
Normal 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);
|
||||
|
61
client/components/routeservers/page.jsx
Normal file
61
client/components/routeservers/page.jsx
Normal 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);
|
||||
|
||||
|
232
client/components/routeservers/protocols/index.jsx
Normal file
232
client/components/routeservers/protocols/index.jsx
Normal 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);
|
||||
|
28
client/components/routeservers/protocols/name.jsx
Normal file
28
client/components/routeservers/protocols/name.jsx
Normal 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);
|
116
client/components/routeservers/reducer.jsx
Normal file
116
client/components/routeservers/reducer.jsx
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
102
client/components/routeservers/routes/bgp-attributes-modal.jsx
Normal file
102
client/components/routeservers/routes/bgp-attributes-modal.jsx
Normal 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);
|
||||
|
89
client/components/routeservers/routes/page.jsx
Normal file
89
client/components/routeservers/routes/page.jsx
Normal 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">»</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);
|
||||
|
191
client/components/routeservers/routes/routes.jsx
Normal file
191
client/components/routeservers/routes/routes.jsx
Normal 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);
|
48
client/components/routeservers/status/index.jsx
Normal file
48
client/components/routeservers/status/index.jsx
Normal 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);
|
||||
|
20
client/components/search-input/index.jsx
Normal file
20
client/components/search-input/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
22
client/components/sidebar/header/index.jsx
Normal file
22
client/components/sidebar/header/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
26
client/components/sidebar/index.jsx
Normal file
26
client/components/sidebar/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
60
client/components/sidebar/routeservers/index.jsx
Normal file
60
client/components/sidebar/routeservers/index.jsx
Normal 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);
|
||||
|
||||
|
44
client/components/sidebar/routeservers/status/index.jsx
Normal file
44
client/components/sidebar/routeservers/status/index.jsx
Normal 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);
|
||||
|
||||
|
20
client/components/utils/query.jsx
Normal file
20
client/components/utils/query.jsx
Normal 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;
|
||||
}
|
||||
|
||||
|
28
client/components/welcome/index.jsx
Normal file
28
client/components/welcome/index.jsx
Normal 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
8
client/config/index.jsx
Normal 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
47
client/gulp/build.js
Normal 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
18
client/gulp/config.json
Normal 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
54
client/gulp/tasks/app.js
Normal 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'));
|
||||
});
|
||||
|
||||
|
33
client/gulp/tasks/assets.js
Normal file
33
client/gulp/tasks/assets.js
Normal 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/'));
|
||||
|
||||
});
|
||||
|
50
client/gulp/tasks/bundle.js
Normal file
50
client/gulp/tasks/bundle.js
Normal 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'));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
14
client/gulp/tasks/clean.js
Normal file
14
client/gulp/tasks/clean.js
Normal 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
20
client/gulp/tasks/html.js
Normal 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
19
client/gulp/tasks/lint.js
Normal 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'));
|
||||
|
||||
});
|
||||
|
31
client/gulp/tasks/stylesheets.js
Normal file
31
client/gulp/tasks/stylesheets.js
Normal 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/'));
|
||||
});
|
||||
|
24
client/gulp/tasks/watch.js
Normal file
24
client/gulp/tasks/watch.js
Normal 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
16
client/gulpfile.js
Normal 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
20
client/index.html
Normal 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
23
client/layouts/main.jsx
Normal 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
1
client/no_log.jsx
Normal file
@ -0,0 +1 @@
|
||||
window.NO_LOG = true;
|
54
client/package.json
Normal file
54
client/package.json
Normal 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"
|
||||
}
|
||||
}
|
32
client/reducer/app-reducer.jsx
Normal file
32
client/reducer/app-reducer.jsx
Normal 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,
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user