Project

General

Profile

« Previous | Next » 

Revision a303c6bb

Added by matan over 7 years ago

fixes #18464 - Add Redux to statistics page

This is a first step to replacing Flux with Redux

View differences:

package.json
"description": "Foreman isn't really a node module, these are just dependencies needed to build the webpack bundle. 'dependencies' are the asset libraries in use and 'devDependencies' are used for the build process.",
"private": true,
"devDependencies": {
"@kadira/storybook": "^2.5.2",
"babel-cli": "^6.10.1",
"babel-core": "~6.7.2",
"babel-eslint": "^6.1.2",
......
"jest-cli": "^16.0.1",
"jsdom": "^9.5.0",
"react-addons-test-utils": "^15.3.1",
"react-redux": "^5.0.2",
"redux": "^3.6.0",
"redux-create-reducer": "^1.1.1",
"redux-logger": "^2.8.1",
"redux-mock-store": "^1.2.2",
"redux-thunk": "^2.2.0",
"seamless-immutable": "^7.0.1",
"stats-webpack-plugin": "^0.2.1",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0",
"@kadira/storybook": "^2.5.2"
"webpack-dev-server": "^1.9.0"
},
"optionalDependencies": {
"phantomjs-prebuilt": "^2.1.0"
webpack/assets/javascripts/react_app/API.js
});
return $.getJSON(url);
},
getStatisticsData(url) {
this.get(url)
.success(
(rawStatistics, textStatus, jqXHR) => {
ServerActions.receivedStatistics(rawStatistics, textStatus, jqXHR);
})
.error((jqXHR, textStatus, errorThrown) => {
ServerActions.statisticsRequestError(jqXHR, textStatus, errorThrown);
});
},
getHostPowerData(url) {
this.get(url)
.success(
webpack/assets/javascripts/react_app/actions/ServerActions.js
import NotificationActions from './NotificationActions';
export default {
receivedStatistics(rawStatistics, textStatus, jqXHR) {
AppDispatcher.dispatch({
actionType: ACTIONS.RECEIVED_STATISTICS,
rawStatistics
});
},
statisticsRequestError(jqXHR, textStatus, errorThrown) {
AppDispatcher.dispatch({
actionType: ACTIONS.STATISTICS_REQUEST_ERROR, info: {
jqXHR: jqXHR,
textStatus: textStatus,
errorThrown: errorThrown
}
});
},
receivedHostsPowerState(response, textStatus, jqXHR) {
AppDispatcher.dispatch({
actionType: ACTIONS.RECEIVED_HOSTS_POWER_STATE,
webpack/assets/javascripts/react_app/actions/StatisticsChartActions.js
import API from '../API';
export default {
getStatisticsData(url) {
API.getStatisticsData(url);
}
};
webpack/assets/javascripts/react_app/common/MountingService.js
import NotificationDrawerToggle from '../components/notifications/NotificationDrawerToggle';
import NotificationDrawer from '../components/notifications/NotificationDrawer';
import ReactDOM from 'react-dom';
import store from '../redux';
export function mount(component, selector, data) {
const components = {
StatisticsChartsList: {
type: StatisticsChartsList,
markup: <StatisticsChartsList data={data}/>
markup: <StatisticsChartsList store={store} data={data}/>
},
PowerStatusContainer: {
type: PowerStatusContainer,
webpack/assets/javascripts/react_app/components/charts/StatisticsChartsList.fixtures.js
export const statisticsData = [
{
id: 'operatingsystem',
title: 'OS Distribution',
url: 'statistics/operatingsystem',
search: '/hosts?search=os_title=~VAL~'
},
{
id: 'architecture',
title: 'Architecture Distribution',
url: 'statistics/architecture',
search: '/hosts?search=facts.architecture=~VAL~'
}
];
webpack/assets/javascripts/react_app/components/charts/StatisticsChartsList.js
import React, {PropTypes} from 'react';
import helpers from '../../common/helpers';
import React, { PropTypes } from 'react';
import chartService from '../../../services/statisticsChartService';
import ChartBox from './ChartBox';
import { STATUS } from '../../constants';
import StatisticsStore from '../../stores/StatisticsStore';
import StatisticsChartActions from '../../actions/StatisticsChartActions';
import './StatisticsChartsListStyles.css';
import { connect } from 'react-redux';
import * as StatisticsChartActions from '../../redux/actions/statistics';
import { STATUS } from '../../constants';
class StatisticsChartsList extends React.Component {
constructor(props) {
super(props);
this.state = {charts: this.stateSetup(this.props.data)};
helpers.bindMethods(this, [
'onChange',
'onError']
);
const getStatusFromChart = (chart) => {
if (chart.data) {
return STATUS.RESOLVED;
}
stateSetup(data) {
let chartStates = {};
data.forEach(chart => {
chartStates[chart.id] = {};
});
return chartStates;
if (chart.error) {
return STATUS.ERROR;
}
return STATUS.PENDING;
};
class StatisticsChartsList extends React.Component {
componentDidMount() {
StatisticsStore.addChangeListener(this.onChange);
StatisticsStore.addErrorListener(this.onError);
let chartStates = this.cloneChartStates();
this.props.data.forEach(chart => {
StatisticsChartActions.getStatisticsData(chart.url);
chartStates[chart.id].status = STATUS.PENDING;
});
this.updateStateCharts(chartStates);
}
const { getStatisticsData, data } = this.props;
cloneChartStates() {
return Object.assign({}, this.state.charts);
}
updateStateCharts(chartStates) {
this.setState({ charts: chartStates });
}
componentWillUnmount() {
StatisticsStore.removeChangeListener(this.onChange);
StatisticsStore.removeErrorListener(this.onError);
}
onChange(event) {
const id = event.id;
const statistics = StatisticsStore.getStatisticsData(id);
let chartStates = this.cloneChartStates();
chartStates[id] = Object.assign({}, chartStates[id], {
status: STATUS.RESOLVED,
data: statistics.data
});
this.updateStateCharts(chartStates);
}
onError(info) {
const xhr = info.jqXHR;
const id = xhr.originalRequestOptions.url.split('/')[1];
let chartStates = this.cloneChartStates();
chartStates[id] = Object.assign({}, chartStates[id], {
status: STATUS.ERROR,
errorText: info.errorThrown
});
this.updateStateCharts(chartStates);
getStatisticsData(data);
}
render() {
const noDataMsg = __('No data available').toString();
let charts = [];
const tip = __('Expand the chart').toString();
const charts = this.props.charts.map(chart => {
const config = chartService.getChartConfig(chart);
this.props.data.forEach(chart => {
let config, modalConfig;
chartService.syncConfigData(config, chart.data);
const modalConfig = chartService.getModalChartConfig(chart);
config = chartService.getChartConfig(chart);
chartService.syncConfigData(config, this.state.charts[chart.id].data);
modalConfig = chartService.getModalChartConfig(chart);
chartService.syncConfigData(modalConfig, this.state.charts[chart.id].data);
chartService.syncConfigData(modalConfig, chart.data);
charts.push(
return (
<ChartBox
key={chart.id}
config={config}
modalConfig={modalConfig}
noDataMsg={noDataMsg}
tip={tip}
status={this.state.charts[chart.id].status || STATUS.PENDING}
errorText={this.state.charts[chart.id].errorText}
errorText={chart.error}
id={chart.id}
status={ getStatusFromChart(chart) }
title={chart.title}
search={chart.search}
/>
......
return (
<div className="statistics-charts-list-root">
{charts}
{this.props.charts && charts}
</div>
);
}
......
data: PropTypes.array.isRequired
};
export default StatisticsChartsList;
const mapStateToProps = state => ({
charts: state.statistics.charts
});
export default connect(mapStateToProps, StatisticsChartActions)(
StatisticsChartsList
);
webpack/assets/javascripts/react_app/components/charts/StatisticsChartsList.spec.js
import React from 'react';
import { shallow } from 'enzyme';
import StatisticsChartsList from './StatisticsChartsList';
import { statisticsData } from './StatisticsChartsList.fixtures';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import immutable from 'seamless-immutable';
const mockStore = configureMockStore([thunk]);
describe('StatisticsChartsList', () => {
beforeEach(() => {
global.__ = str => str;
});
it('should render no panels for empty data', () => {
const store = mockStore({
statistics: immutable({ charts: [] })
});
const wrapper = shallow(
<StatisticsChartsList store={store} data={statisticsData} />
);
expect(
wrapper.render().find('.statistics-charts-list-panel').length
).toEqual(0);
});
it('should render two panels for fixtures data', () => {
const store = mockStore({
statistics: immutable({ charts: statisticsData })
});
const wrapper = shallow(
<StatisticsChartsList store={store} data={statisticsData} />
);
expect(
wrapper.render().find('.statistics-charts-list-panel').length
).toEqual(2);
});
});
webpack/assets/javascripts/react_app/constants.js
export const ACTIONS = {
RECEIVED_STATISTICS: 'RECEIVED_STATISTICS',
STATISTICS_REQUEST_ERROR: 'STATISTICS_REQUEST_ERROR',
RECEIVED_HOSTS_POWER_STATE: 'RECEIVED_HOSTS_POWER_STATE',
HOSTS_REQUEST_ERROR: 'HOSTS_REQUEST_ERROR',
RECEIVED_NOTIFICATIONS: 'RECEIVED_NOTIFICATIONS',
webpack/assets/javascripts/react_app/constants.test.js
jest.unmock('./constants');
import {ACTIONS} from './constants';
describe('exists', () => {
it('RECEIVED_STATISTICS action exists', () => {
expect(ACTIONS.RECEIVED_STATISTICS).not.toBeUndefined();
});
});
webpack/assets/javascripts/react_app/redux/actions/statistics/index.js
import API from '../../../API';
import {
STATISTICS_DATA_REQUEST,
STATISTICS_DATA_SUCCESS,
STATISTICS_DATA_FAILURE
} from '../../consts';
export const getStatisticsData = charts => dispatch => {
dispatch({ type: STATISTICS_DATA_REQUEST, payload: charts });
charts.forEach(chart => {
API.get(chart.url).then(
result => dispatch({ type: STATISTICS_DATA_SUCCESS, payload: result }),
(jqXHR, textStatus, error) => dispatch(
{ type: STATISTICS_DATA_FAILURE, payload: { error, id: chart.id } }
)
);
});
};
webpack/assets/javascripts/react_app/redux/actions/statistics/statistics.fixtures.js
export const requestData = [
{
id: 'operatingsystem',
title: 'OS Distribution',
url: 'statistics/operatingsystem',
search: '/hosts?search=os_title=~VAL~'
},
{
id: 'architecture',
title: 'Architecture Distribution',
url: 'statistics/architecture',
search: '/hosts?search=facts.architecture=~VAL~'
}
];
export const onFailureActions = [
{
type: 'STATISTICS_DATA_REQUEST',
payload: [
{
id: 'operatingsystem',
title: 'OS Distribution',
url: 'statistics/operatingsystem',
search: '/hosts?search=os_title=~VAL~'
},
{
id: 'architecture',
title: 'Architecture Distribution',
url: 'statistics/architecture',
search: '/hosts?search=facts.architecture=~VAL~'
}
]
},
{
type: 'STATISTICS_DATA_FAILURE',
payload: { error: {}, id: 'operatingsystem' }
},
{
type: 'STATISTICS_DATA_FAILURE',
payload: { error: {}, id: 'architecture' }
}
];
webpack/assets/javascripts/react_app/redux/actions/statistics/statistics.spec.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from './index';
import immutable from 'seamless-immutable';
import { requestData, onFailureActions } from './statistics.fixtures';
const mockStore = configureMockStore([thunk]);
describe('statistics actions', () => {
it(
'creates STATISTICS_DATA_REQUEST and fails when nock is not applied',
() => {
const store = mockStore({ statistics: immutable({}) });
store.dispatch(actions.getStatisticsData(requestData));
expect(store.getActions()).toEqual(onFailureActions);
}
);
});
webpack/assets/javascripts/react_app/redux/consts.js
export const STATISTICS_DATA_REQUEST = 'STATISTICS_DATA_REQUEST';
export const STATISTICS_DATA_SUCCESS = 'STATISTICS_DATA_SUCCESS';
export const STATISTICS_DATA_FAILURE = 'STATISTICS_DATA_FAILURE';
webpack/assets/javascripts/react_app/redux/index.js
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import reducer from './reducers';
const logger = createLogger();
export default createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(thunk, logger)
);
webpack/assets/javascripts/react_app/redux/reducers/index.js
import { combineReducers } from 'redux';
import statistics from './statistics';
export default combineReducers({
statistics: statistics
});
webpack/assets/javascripts/react_app/redux/reducers/statistics/index.js
import {
STATISTICS_DATA_REQUEST,
STATISTICS_DATA_SUCCESS,
STATISTICS_DATA_FAILURE
} from '../../consts';
import Immutable from 'seamless-immutable';
const initialState = Immutable({
charts: []
});
export default (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case STATISTICS_DATA_REQUEST: return state.set('charts', payload);
case STATISTICS_DATA_SUCCESS:
return state.set('charts', state.charts.map(chart => chart.id === payload.id ?
{ ...chart, data: payload.data } :
chart
));
case STATISTICS_DATA_FAILURE:
return state.set('charts', state.charts.map(chart => chart.id === payload.id ?
{ ...chart, error: payload.error } :
chart
));
default: return state;
}
};
webpack/assets/javascripts/react_app/redux/reducers/statistics/statistics.fixtures.js
import Immutable from 'seamless-immutable';
export const initialState = Immutable({
charts: []
});
export const request = [
{
id: 'operatingsystem',
title: 'OS Distribution',
url: 'statistics/operatingsystem',
search: '/hosts?search=os_title=~VAL~'
}
];
export const response = {
id: 'operatingsystem',
data: [['RedHat 3', 2]]
};
export const error = 'some error happened';
export const stateBeforeResponse = Immutable({
charts: request
});
export const stateAfterSuccess = Immutable({
charts: request.map(chart => ({
...chart,
data: response.data
}))
});
export const stateAfterFailure = Immutable({
charts: request.map(chart => ({
...chart,
error
}))
});
webpack/assets/javascripts/react_app/redux/reducers/statistics/statistics.spec.js
import reducer from './index';
import * as types from '../../consts';
import {
initialState,
request,
stateBeforeResponse,
response,
stateAfterSuccess,
stateAfterFailure,
error
} from './statistics.fixtures';
describe('statistics reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
it('should handle STATISTICS_DATA_REQUEST', () => {
expect(
reducer(initialState, {
type: types.STATISTICS_DATA_REQUEST,
payload: request
})
).toEqual(stateBeforeResponse);
});
it('should handle STATISTICS_DATA_SUCCESS', () => {
expect(
reducer(stateBeforeResponse, {
type: types.STATISTICS_DATA_SUCCESS,
payload: response
})
).toEqual(stateAfterSuccess);
});
it('should handle STATISTICS_DATA_FAILURE', () => {
expect(
reducer(stateBeforeResponse, {
type: types.STATISTICS_DATA_FAILURE,
payload: { error, id: request[0].id}
})
).toEqual(stateAfterFailure);
});
});
webpack/assets/javascripts/react_app/stores/StatisticsStore.js
import AppDispatcher from '../dispatcher';
import {ACTIONS} from '../constants';
import AppEventEmitter from './AppEventEmitter';
const _statistics = {};
class StatisticsEventEmitter extends AppEventEmitter {
constructor() {
super();
}
getStatisticsData(id) {
_statistics[id] = _statistics[id] || { data: [] };
return _statistics[id];
}
}
const StatisticsStore = new StatisticsEventEmitter();
AppDispatcher.register(action => {
switch (action.actionType) {
case ACTIONS.RECEIVED_STATISTICS: {
const item = action.rawStatistics;
_statistics[item.id] = _statistics[item.id] || {};
_statistics[item.id].data = item.data || [];
_statistics[item.id].isLoaded = true;
StatisticsStore.emitChange({id: item.id});
break;
}
case ACTIONS.STATISTICS_REQUEST_ERROR: {
StatisticsStore.emitError(action.info);
break;
}
default:
// no op
break;
}
});
export default StatisticsStore;

Also available in: Unified diff