Connect ara-web with ara-server

* install react-router and redux
 * create redux store and start fetching some playbooks from ara-server
 * create playbooks and config states in redux

Change-Id: I455f217797fc69d722bedd573eaed2cea70ede6b
This commit is contained in:
Guillaume Vincent 2018-04-23 16:48:09 +02:00
parent fc81f257d6
commit 722ea92916
26 changed files with 4992 additions and 4513 deletions

9099
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,25 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"patternfly": "^3.42.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-scripts": "1.1.1"
"axios": "^0.18.0",
"patternfly": "^3.54.2",
"patternfly-react": "^2.17.3",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-scripts": "^1.1.5",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"axios-mock-adapter": "^1.15.0",
"redux-mock-store": "^1.5.3"
}
}

35
scripts/faker.js Normal file
View File

@ -0,0 +1,35 @@
const results = [];
for (let i = 0; i < 200; i++) {
const random = Math.random();
if (random < 0.001) {
results.push({
id: `${i}`,
status: "changed",
duration: 55
});
} else if (random < 0.01) {
results.push({
id: `${i}`,
status: "skipped",
duration: 20
});
} else if (random < 0.02) {
results.push({
id: `${i}`,
status: "changed",
duration: 20
});
} else {
results.push({
id: `${i}`,
status: "ok",
duration: Math.random() * 20 + 20
});
}
}
results.push({
id: "200",
status: "failed",
duration: Math.random() * 20 + 20
});
console.log(results);

View File

@ -1,3 +1,3 @@
.App-intro {
text-align: center;
body {
background-color: #f5f5f5;
}

View File

@ -1,13 +1,50 @@
import React from "react";
import "./App.css";
import Navbar from "./components/Header/Navbar";
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import { Provider } from "react-redux";
import "./App.css";
import store from "./store";
import { setConfig } from "./config/configActions";
import * as Containers from "./containers";
class App extends Component {
constructor(props) {
super(props);
this.state = {
loading: true
};
}
componentDidMount() {
store.dispatch(setConfig({ apiURL: "http://localhost:8000" }));
this.setState({ loading: false });
}
class App extends React.Component {
render() {
return (
<div className="App">
<Navbar />
<p className="App-intro">...</p>
{this.state.loading ? (
<Containers.LoadingContainer />
) : (
<Provider store={store}>
<BrowserRouter>
<Switch>
<Redirect from="/" exact to="/playbooks" />
<Route
path="/playbooks"
exact
component={Containers.PlaybooksContainer}
/>
<Route
path="/about"
exact
component={Containers.AboutContainer}
/>
<Route component={Containers.Container404} />
</Switch>
</BrowserRouter>
</Provider>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
import React, { Component } from "react";
import { MainContainer } from "../containers";
export default class AboutContainer extends Component {
render() {
return (
<MainContainer>
<p>AboutContainer</p>
</MainContainer>
);
}
}

View File

@ -0,0 +1,8 @@
import * as types from "./configActionsTypes";
export function setConfig(config) {
return {
type: types.SET_CONFIG,
config
};
}

View File

@ -0,0 +1,13 @@
import * as actions from "./configActions";
import * as types from "./playbooksActionsTypes";
it("setConfig", () => {
const config = { apiURL: "http://example.org" };
const expectedActions = [
{
type: types.SET_CONFIG,
config
}
];
expect(actions.setConfig(config)).toEqual(expectedActions);
});

View File

@ -0,0 +1 @@
export const SET_CONFIG = "SET_CONFIG";

View File

@ -0,0 +1,14 @@
import * as types from "./configActionsTypes";
const initialState = {};
export default function(state = initialState, action) {
switch (action.type) {
case types.SET_CONFIG:
return {
...action.config
};
default:
return state;
}
}

View File

@ -0,0 +1,19 @@
import reducer from "./configReducer";
import * as types from "./configActionsTypes";
it("returns the initial state", () => {
expect(reducer(undefined, {})).toEqual({});
});
it("SET_CONFIG", () => {
const state = reducer(
{},
{
type: types.SET_CONFIG,
config: {
apiURL: "http://example.org"
}
}
);
expect(state.apiURL).toBe("http://example.org");
});

5
src/containers.js Normal file
View File

@ -0,0 +1,5 @@
export { default as Container404 } from "./layout/Container404";
export { default as LoadingContainer } from "./layout/LoadingContainer";
export { default as MainContainer } from "./layout/MainContainer";
export { default as PlaybooksContainer } from "./playbooks/PlaybooksContainer";
export { default as AboutContainer } from "./about/AboutContainer";

View File

@ -1,8 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import 'patternfly/dist/css/patternfly.min.css';
import 'patternfly/dist/css/patternfly-additions.min.css';
import './index.css';
import App from './App';
import React from "react";
import ReactDOM from "react-dom";
import "patternfly/dist/css/patternfly.min.css";
import "patternfly/dist/css/patternfly-additions.min.css";
import "./index.css";
import App from "./App";
ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -0,0 +1,12 @@
import React, { Component } from "react";
import MainContainer from "./MainContainer";
export default class Container404 extends Component {
render() {
return (
<MainContainer>
<p>404</p>
</MainContainer>
);
}
}

View File

@ -0,0 +1,7 @@
import React, { Component } from "react";
export default class LoadingContainer extends Component {
render() {
return <div>loading...</div>;
}
}

View File

@ -0,0 +1,19 @@
import React, { Component } from "react";
import Navbar from "./navigation/Navbar";
import { Grid, Row, Col } from "patternfly-react";
export default class MainContainer extends Component {
render() {
const { children } = this.props;
return (
<div className="MainContent">
<Navbar />
<Grid fluid>
<Row>
<Col xs={12}>{children}</Col>
</Row>
</Grid>
</div>
);
}
}

View File

@ -0,0 +1,15 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
export default class NavLink extends Component {
render() {
const { id, to, location, children, ...rest } = this.props;
return (
<li className={location.pathname === to ? "active" : ""}>
<Link to={to} id={`navbar-navbar-primary__${id}-link`} {...rest}>
{children}
</Link>
</li>
);
}
}

View File

@ -1,8 +1,13 @@
import React from "react";
import logo from "./logo.svg";
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { withRouter } from "react-router";
export default class Navbar extends React.Component {
import logo from "./logo.svg";
import NavLink from "./NavLink";
export class Navbar extends Component {
render() {
const { location } = this.props;
return (
<nav className="navbar navbar-default navbar-pf">
<div className="navbar-header">
@ -17,14 +22,18 @@ export default class Navbar extends React.Component {
<span className="icon-bar" />
<span className="icon-bar" />
</button>
<a className="navbar-brand" href="/reports/">
<Link
to="/playbooks"
id="navbar-navbar-header__playbooks-link"
className="navbar-brand"
>
<img
src={logo}
alt="ARA: Ansible Run Analysis"
width="81"
height="32"
/>
</a>
</Link>
</div>
<div className="collapse navbar-collapse navbar-collapse-1">
<ul className="nav navbar-nav navbar-utility">
@ -66,15 +75,17 @@ export default class Navbar extends React.Component {
</li>
</ul>
<ul className="nav navbar-nav navbar-primary">
<li className="active">
<a href="/reports/">Playbook reports</a>
</li>
<li>
<a href="/about/">About</a>
</li>
<NavLink id="playbooks" to="/playbooks" location={location}>
Playbooks reports
</NavLink>
<NavLink id="about" to="/about" location={location}>
About
</NavLink>
</ul>
</div>
</nav>
);
}
}
export default withRouter(Navbar);

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,40 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { MainContainer } from "../containers";
import { getPlaybooks } from "./playbooksActions";
export class PlaybooksContainer extends Component {
componentDidMount() {
this.props.getPlaybooks();
}
render() {
const { playbooks } = this.props;
return (
<MainContainer>
<p>playbooks</p>
<ul>
{playbooks.map(playbook => (
<li key={playbook.id}>{playbook.id}</li>
))}
</ul>
</MainContainer>
);
}
}
function mapStateToProps(state) {
return {
playbooks: Object.values(state.playbooks)
};
}
function mapDispatchToProps(dispatch) {
return {
getPlaybooks: () => dispatch(getPlaybooks())
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PlaybooksContainer);

View File

@ -0,0 +1,15 @@
import axios from "axios";
import * as types from "./playbooksActionsTypes";
export function getPlaybooks() {
return (dispatch, getState) => {
const { apiURL } = getState().config;
return axios.get(`${apiURL}/api/v1/playbooks`).then(response => {
dispatch({
type: types.FETCH_PLAYBOOKS,
playbooks: response.data.results
});
return response;
});
};
}

View File

@ -0,0 +1,31 @@
import axios from "axios";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import axiosMockAdapter from "axios-mock-adapter";
import { getPlaybooks } from "./playbooksActions";
import * as types from "./playbooksActionsTypes";
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const axiosMock = new axiosMockAdapter(axios);
it("getPlaybooks", () => {
axiosMock.onGet("https://api.example.org/api/v1/playbooks").reply(200, {
count: 1,
next: null,
previous: null,
results: [{ id: "p1" }]
});
const expectedActions = [
{
type: types.FETCH_PLAYBOOKS,
playbooks: [{ id: "p1" }]
}
];
const store = mockStore({ config: { apiURL: "https://api.example.org" } });
return store.dispatch(getPlaybooks()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

View File

@ -0,0 +1 @@
export const FETCH_PLAYBOOKS = "FETCH_PLAYBOOKS";

View File

@ -0,0 +1,15 @@
import * as types from "./playbooksActionsTypes";
const initialState = {};
export default function(state = initialState, action) {
switch (action.type) {
case types.FETCH_PLAYBOOKS:
return action.playbooks.reduce((accumulator, playbook) => {
accumulator[playbook.id] = playbook;
return accumulator;
}, {});
default:
return state;
}
}

View File

@ -0,0 +1,12 @@
import reducer from "./playbooksReducer";
import * as types from "./playbooksActionsTypes";
it("FETCH_PLAYBOOKS", () => {
const newState = reducer(undefined, {
type: types.FETCH_PLAYBOOKS,
playbooks: [{ id: "p1" }]
});
expect(newState).toEqual({
p1: { id: "p1" }
});
});

14
src/store.js Normal file
View File

@ -0,0 +1,14 @@
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import configReducer from "./config/configReducer";
import playbooksReducer from "./playbooks/playbooksReducer";
const store = createStore(
combineReducers({
config: configReducer,
playbooks: playbooksReducer
}),
applyMiddleware(thunk)
);
export default store;