Revision a303c6bb
Added by matan over 7 years ago
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
fixes #18464 - Add Redux to statistics page
This is a first step to replacing Flux with Redux