Revision 1291288b
Added by Tomer Brisker almost 3 years ago
package.json | ||
---|---|---|
"create-react-component": "yo react-domain"
|
||
},
|
||
"dependencies": {
|
||
"@theforeman/vendor": "^8.7.0",
|
||
"@theforeman/vendor": "^8.8.0",
|
||
"eslint-plugin-spellcheck": "0.0.17",
|
||
"intl": "~1.2.5",
|
||
"jed": "^1.1.1",
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChart.fixtures.js | ||
---|---|---|
import Immutable from 'seamless-immutable';
|
||
import { STATUS } from '../../constants';
|
||
import { noop } from '../../common/helpers';
|
||
import { FACT_CHART } from './FactChartConstants';
|
||
|
||
export const id = 1;
|
||
export const url = 'some/url';
|
||
export const key = `${FACT_CHART}_${id}`;
|
||
export const title = 'some_title';
|
||
export const search = 'some-search';
|
||
export const status = STATUS.RESOLVED;
|
||
export const hostsCount = 100;
|
||
export const modalToDisplay = { 1: true };
|
||
export const openModal = noop;
|
||
export const closeModal = noop;
|
||
export const chartData = [
|
||
['Debian 8', 1],
|
||
['Fedora 27', 2],
|
||
['Fedora 26', 1],
|
||
];
|
||
|
||
export const initialState = Immutable({
|
||
modalToDisplay: {},
|
||
});
|
||
|
||
export const modalOpenState = initialState.merge({ modalToDisplay });
|
||
|
||
export const modalSuccessState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
chartData,
|
||
});
|
||
|
||
export const modalLoadingState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
});
|
||
|
||
export const modalErrorState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
});
|
||
|
||
export const props = {
|
||
id,
|
||
title,
|
||
search,
|
||
status,
|
||
hostsCount,
|
||
chartData,
|
||
modalToDisplay: true,
|
||
openModal,
|
||
closeModal,
|
||
};
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChart.js | ||
---|---|---|
ngettext as n__,
|
||
translate as __,
|
||
} from '../../../react_app/common/I18n';
|
||
import './FactChart.scss';
|
||
import './style.scss';
|
||
|
||
const FactChart = ({
|
||
hostsCount,
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChart.scss | ||
---|---|---|
.fact-chart {
|
||
.modal-body {
|
||
min-height: 500px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChart.stories.js | ||
---|---|---|
import React from 'react';
|
||
import thunk from 'redux-thunk';
|
||
import configureMockStore from 'redux-mock-store';
|
||
import FactChart from '.';
|
||
import {
|
||
initialState,
|
||
modalSuccessState,
|
||
modalLoadingState,
|
||
modalErrorState,
|
||
} from './FactChart.fixtures';
|
||
import Story from '../../../../../stories/components/Story';
|
||
|
||
const mockStore = configureMockStore([thunk]);
|
||
|
||
const dataProp = { id: 1, title: 'test title' };
|
||
|
||
export default {
|
||
title: 'Page chunks/FactChartModal',
|
||
};
|
||
|
||
export const modalClosed = () => (
|
||
<Story>
|
||
<FactChart store={mockStore({ factChart: initialState })} data={dataProp} />
|
||
</Story>
|
||
);
|
||
|
||
modalClosed.story = {
|
||
name: 'ModalClosed',
|
||
};
|
||
|
||
export const modalOpen = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalSuccessState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
modalOpen.story = {
|
||
name: 'ModalOpen',
|
||
};
|
||
|
||
export const loading = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalLoadingState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
export const noData = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalErrorState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
noData.story = {
|
||
name: 'No data',
|
||
};
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChartActions.js | ||
---|---|---|
import {
|
||
FACT_CHART_MODAL_OPEN,
|
||
FACT_CHART_MODAL_CLOSE,
|
||
} from './FactChartConstants';
|
||
import { get } from '../../redux/API';
|
||
|
||
export const openModal = ({ id, title, apiKey, apiUrl }) => dispatch => {
|
||
dispatch(get({ key: apiKey, url: apiUrl }));
|
||
dispatch({
|
||
type: FACT_CHART_MODAL_OPEN,
|
||
payload: { id, title },
|
||
});
|
||
};
|
||
|
||
export const closeModal = id => ({
|
||
type: FACT_CHART_MODAL_CLOSE,
|
||
payload: { id },
|
||
});
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChartConstants.js | ||
---|---|---|
export const FACT_CHART = 'FACT_CHART';
|
||
export const FACT_CHART_MODAL_OPEN = 'FACT_CHART_MODAL_OPEN';
|
||
export const FACT_CHART_MODAL_CLOSE = 'FACT_CHART_MODAL_CLOSE';
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChartReducer.js | ||
---|---|---|
import Immutable from 'seamless-immutable';
|
||
import {
|
||
FACT_CHART_MODAL_CLOSE,
|
||
FACT_CHART_MODAL_OPEN,
|
||
} from './FactChartConstants';
|
||
|
||
const initialState = Immutable({
|
||
modalToDisplay: {},
|
||
});
|
||
|
||
// should be removed when the modals infrastructure will get merged.
|
||
export default (state = initialState, { type, payload }) => {
|
||
switch (type) {
|
||
case FACT_CHART_MODAL_OPEN:
|
||
return state
|
||
.set('title', payload.title)
|
||
.set('modalToDisplay', { [payload.id]: true });
|
||
case FACT_CHART_MODAL_CLOSE:
|
||
return state.set('modalToDisplay', {});
|
||
default:
|
||
return state;
|
||
}
|
||
};
|
webpack/assets/javascripts/react_app/components/FactCharts/FactChartSelectors.js | ||
---|---|---|
import { createSelector } from 'reselect';
|
||
import {
|
||
selectAPIStatus,
|
||
selectAPIResponse,
|
||
} from '../../redux/API/APISelectors';
|
||
|
||
export const selectFactChartData = (state, key) =>
|
||
selectAPIResponse(state, key).values || [];
|
||
|
||
export const selectFactChartStatus = (state, key) =>
|
||
selectAPIStatus(state, key);
|
||
|
||
const hostCounter = (accumulator, currentValue) => accumulator + currentValue;
|
||
|
||
export const selectHostCount = createSelector(selectFactChartData, chartData =>
|
||
chartData.length ? chartData.map(item => item[1]).reduce(hostCounter) : 0
|
||
);
|
||
|
||
export const selectFactChart = state => state.factChart;
|
||
|
||
export const selectDisplayModal = (state, id) =>
|
||
selectFactChart(state).modalToDisplay[id] || false;
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/FactChart.test.js | ||
---|---|---|
import { shallow } from '@theforeman/test';
|
||
import React from 'react';
|
||
import FactChart from '../FactChart';
|
||
import { props } from '../FactChart.fixtures';
|
||
import { props } from '../fixtures';
|
||
|
||
describe('factCharts', () => {
|
||
it('should render open', () => {
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/FactChartActions.test.js | ||
---|---|---|
import { testActionSnapshotWithFixtures } from '../../../common/testHelpers';
|
||
import { openModal, closeModal } from '../FactChartActions';
|
||
import { key, url, id, title } from '../FactChart.fixtures';
|
||
|
||
jest.unmock('../FactChartActions');
|
||
|
||
const fixtures = {
|
||
'should open modal': () => openModal({ apiKey: key, apiUrl: url, id, title }),
|
||
'should close modal': () => closeModal(id),
|
||
};
|
||
|
||
describe('FactCharts actions', () => testActionSnapshotWithFixtures(fixtures));
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/FactChartReducer.test.js | ||
---|---|---|
import { testReducerSnapshotWithFixtures } from '../../../common/testHelpers';
|
||
|
||
import reducer from '../FactChartReducer';
|
||
import { FACT_CHART_MODAL_CLOSE } from '../FactChartConstants';
|
||
|
||
const fixtures = {
|
||
'initial state': {},
|
||
'should not display modal': {
|
||
action: {
|
||
type: FACT_CHART_MODAL_CLOSE,
|
||
payload: {},
|
||
},
|
||
},
|
||
};
|
||
|
||
describe('FactChart reducer', () =>
|
||
testReducerSnapshotWithFixtures(reducer, fixtures));
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/FactChartSelectors.test.js | ||
---|---|---|
import { STATUS } from '../../../constants';
|
||
import { chartData, modalToDisplay, id, key } from '../FactChart.fixtures';
|
||
import {
|
||
selectHostCount,
|
||
selectFactChart,
|
||
selectDisplayModal,
|
||
selectFactChartData,
|
||
selectFactChartStatus,
|
||
} from '../FactChartSelectors';
|
||
|
||
describe('Fact Chart Selector', () => {
|
||
const factChartState = {
|
||
factChart: { modalToDisplay },
|
||
API: {
|
||
[key]: {
|
||
response: { values: chartData },
|
||
status: STATUS.PENDING,
|
||
},
|
||
},
|
||
};
|
||
|
||
it('should count hosts', () => {
|
||
const selected = selectHostCount(factChartState, key);
|
||
expect(selected).toMatchSnapshot();
|
||
});
|
||
|
||
it('should return factChart object', () => {
|
||
const chart = selectFactChart(factChartState);
|
||
|
||
expect(chart).toMatchSnapshot();
|
||
});
|
||
|
||
it('should return true for rendering modal', () => {
|
||
const chart = selectDisplayModal(factChartState, id);
|
||
|
||
expect(chart).toMatchSnapshot();
|
||
});
|
||
|
||
it('should return factChart data', () => {
|
||
const data = selectFactChartData(factChartState, key);
|
||
|
||
expect(data).toMatchSnapshot();
|
||
});
|
||
|
||
it('should return factChart status', () => {
|
||
const data = selectFactChartStatus(factChartState, key);
|
||
|
||
expect(data).toMatchSnapshot();
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/__snapshots__/FactChartActions.test.js.snap | ||
---|---|---|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||
|
||
exports[`FactCharts actions should close modal 1`] = `
|
||
Object {
|
||
"payload": Object {
|
||
"id": 1,
|
||
},
|
||
"type": "FACT_CHART_MODAL_CLOSE",
|
||
}
|
||
`;
|
||
|
||
exports[`FactCharts actions should open modal 1`] = `
|
||
Array [
|
||
Array [
|
||
Object {
|
||
"payload": Object {
|
||
"key": "FACT_CHART_1",
|
||
"url": "some/url",
|
||
},
|
||
"type": "API_GET",
|
||
},
|
||
],
|
||
Array [
|
||
Object {
|
||
"payload": Object {
|
||
"id": 1,
|
||
"title": "some_title",
|
||
},
|
||
"type": "FACT_CHART_MODAL_OPEN",
|
||
},
|
||
],
|
||
]
|
||
`;
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/__snapshots__/FactChartReducer.test.js.snap | ||
---|---|---|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||
|
||
exports[`FactChart reducer initial state 1`] = `
|
||
Object {
|
||
"modalToDisplay": Object {},
|
||
}
|
||
`;
|
||
|
||
exports[`FactChart reducer should not display modal 1`] = `
|
||
Object {
|
||
"modalToDisplay": Object {},
|
||
}
|
||
`;
|
webpack/assets/javascripts/react_app/components/FactCharts/__test__/__snapshots__/FactChartSelectors.test.js.snap | ||
---|---|---|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||
|
||
exports[`Fact Chart Selector should count hosts 1`] = `4`;
|
||
|
||
exports[`Fact Chart Selector should return factChart data 1`] = `
|
||
Array [
|
||
Array [
|
||
"Debian 8",
|
||
1,
|
||
],
|
||
Array [
|
||
"Fedora 27",
|
||
2,
|
||
],
|
||
Array [
|
||
"Fedora 26",
|
||
1,
|
||
],
|
||
]
|
||
`;
|
||
|
||
exports[`Fact Chart Selector should return factChart object 1`] = `
|
||
Object {
|
||
"modalToDisplay": Object {
|
||
"1": true,
|
||
},
|
||
}
|
||
`;
|
||
|
||
exports[`Fact Chart Selector should return factChart status 1`] = `"PENDING"`;
|
||
|
||
exports[`Fact Chart Selector should return true for rendering modal 1`] = `true`;
|
webpack/assets/javascripts/react_app/components/FactCharts/fixtures.js | ||
---|---|---|
import Immutable from 'seamless-immutable';
|
||
import { STATUS } from '../../constants';
|
||
import { noop } from '../../common/helpers';
|
||
|
||
export const id = 1;
|
||
export const url = 'some/url';
|
||
export const key = `FACT_CHART_${id}`;
|
||
export const title = 'some_title';
|
||
export const search = 'some-search';
|
||
export const status = STATUS.RESOLVED;
|
||
export const hostsCount = 100;
|
||
export const modalToDisplay = { 1: true };
|
||
export const openModal = noop;
|
||
export const closeModal = noop;
|
||
export const chartData = [
|
||
['Debian 8', 1],
|
||
['Fedora 27', 2],
|
||
['Fedora 26', 1],
|
||
];
|
||
|
||
export const initialState = Immutable({
|
||
modalToDisplay: {},
|
||
});
|
||
|
||
export const modalOpenState = initialState.merge({ modalToDisplay });
|
||
|
||
export const modalSuccessState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
chartData,
|
||
});
|
||
|
||
export const modalLoadingState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
});
|
||
|
||
export const modalErrorState = Immutable.merge(initialState, {
|
||
modalToDisplay,
|
||
});
|
||
|
||
export const props = {
|
||
id,
|
||
title,
|
||
search,
|
||
status,
|
||
hostsCount,
|
||
chartData,
|
||
modalToDisplay: true,
|
||
openModal,
|
||
closeModal,
|
||
};
|
webpack/assets/javascripts/react_app/components/FactCharts/index.js | ||
---|---|---|
import React from 'react';
|
||
import { useSelector, useDispatch } from 'react-redux';
|
||
import PropTypes from 'prop-types';
|
||
import { get } from '../../redux/API';
|
||
|
||
import FactChart from './FactChart';
|
||
import reducer from './FactChartReducer';
|
||
import { openModal, closeModal } from './FactChartActions';
|
||
import { FACT_CHART } from './FactChartConstants';
|
||
import { openModal, closeModal } from './slice';
|
||
|
||
import {
|
||
selectHostCount,
|
||
selectDisplayModal,
|
||
selectFactChartStatus,
|
||
selectFactChartData,
|
||
} from './FactChartSelectors';
|
||
} from './selectors';
|
||
|
||
const ConnectedFactChart = ({ id, path, title, search }) => {
|
||
const key = `${FACT_CHART}_${id}`;
|
||
const key = `FACT_CHART_${id}`;
|
||
const hostsCount = useSelector(state => selectHostCount(state, key));
|
||
const status = useSelector(state => selectFactChartStatus(state, key));
|
||
const chartData = useSelector(state => selectFactChartData(state, key));
|
||
const modalToDisplay = useSelector(state => selectDisplayModal(state, id));
|
||
const dispatch = useDispatch();
|
||
const dispatchCloseModal = () => dispatch(closeModal(id));
|
||
const dispatchOpenModal = () =>
|
||
dispatch(openModal({ id, title, apiKey: key, apiUrl: path }));
|
||
const dispatchCloseModal = () => dispatch(closeModal());
|
||
const dispatchOpenModal = () => {
|
||
dispatch(get({ key, url: path }));
|
||
dispatch(openModal({ id, title }));
|
||
};
|
||
|
||
return (
|
||
<FactChart
|
||
... | ... | |
};
|
||
|
||
export default ConnectedFactChart;
|
||
|
||
export const reducers = { factChart: reducer };
|
webpack/assets/javascripts/react_app/components/FactCharts/selectors.js | ||
---|---|---|
import { createSelector } from 'reselect';
|
||
import {
|
||
selectAPIStatus,
|
||
selectAPIResponse,
|
||
} from '../../redux/API/APISelectors';
|
||
|
||
export const selectFactChartData = (state, key) =>
|
||
selectAPIResponse(state, key).values || [];
|
||
|
||
export const selectFactChartStatus = (state, key) =>
|
||
selectAPIStatus(state, key);
|
||
|
||
const hostCounter = (accumulator, currentValue) => accumulator + currentValue;
|
||
|
||
export const selectHostCount = createSelector(selectFactChartData, chartData =>
|
||
chartData.length ? chartData.map(item => item[1]).reduce(hostCounter) : 0
|
||
);
|
||
|
||
export const selectFactChart = state => state.factChart;
|
||
|
||
export const selectDisplayModal = (state, id) =>
|
||
selectFactChart(state).modalToDisplay[id] || false;
|
webpack/assets/javascripts/react_app/components/FactCharts/slice.js | ||
---|---|---|
import { createSlice } from '@reduxjs/toolkit';
|
||
|
||
const initialState = {
|
||
modalToDisplay: {},
|
||
};
|
||
|
||
const factChartSlice = createSlice({
|
||
name: 'factChart',
|
||
initialState,
|
||
reducers: {
|
||
openModal(state, { payload }) {
|
||
state.title = payload.title;
|
||
state.modalToDisplay = { [payload.id]: true };
|
||
},
|
||
closeModal(state) {
|
||
state.modalToDisplay = {};
|
||
},
|
||
},
|
||
});
|
||
|
||
export const { openModal, closeModal } = factChartSlice.actions;
|
||
export default factChartSlice.reducer;
|
webpack/assets/javascripts/react_app/components/FactCharts/stories.js | ||
---|---|---|
import React from 'react';
|
||
import thunk from 'redux-thunk';
|
||
import configureMockStore from 'redux-mock-store';
|
||
import FactChart from '.';
|
||
import {
|
||
initialState,
|
||
modalSuccessState,
|
||
modalLoadingState,
|
||
modalErrorState,
|
||
} from './fixtures';
|
||
import Story from '../../../../../stories/components/Story';
|
||
|
||
const mockStore = configureMockStore([thunk]);
|
||
|
||
const dataProp = { id: 1, title: 'test title' };
|
||
|
||
export default {
|
||
title: 'Page chunks/FactChartModal',
|
||
};
|
||
|
||
export const modalClosed = () => (
|
||
<Story>
|
||
<FactChart store={mockStore({ factChart: initialState })} data={dataProp} />
|
||
</Story>
|
||
);
|
||
|
||
modalClosed.story = {
|
||
name: 'ModalClosed',
|
||
};
|
||
|
||
export const modalOpen = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalSuccessState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
modalOpen.story = {
|
||
name: 'ModalOpen',
|
||
};
|
||
|
||
export const loading = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalLoadingState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
export const noData = () => (
|
||
<Story>
|
||
<FactChart
|
||
store={mockStore({ factChart: modalErrorState })}
|
||
data={dataProp}
|
||
/>
|
||
</Story>
|
||
);
|
||
|
||
noData.story = {
|
||
name: 'No data',
|
||
};
|
webpack/assets/javascripts/react_app/components/FactCharts/style.scss | ||
---|---|---|
.fact-chart {
|
||
.modal-body {
|
||
min-height: 500px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
webpack/assets/javascripts/react_app/redux/reducers/index.js | ||
---|---|---|
import { reducers as diffModalReducers } from '../../components/ConfigReports/DiffModal';
|
||
import { reducers as editorReducers } from '../../components/Editor';
|
||
import { reducers as templateGenerationReducers } from '../../components/TemplateGenerator';
|
||
import { reducers as factChartReducers } from '../../components/FactCharts';
|
||
import factChart from '../../components/FactCharts/slice';
|
||
import { reducers as fillReducers } from '../../components/common/Fill';
|
||
import { reducers as typeAheadSelectReducers } from '../../components/common/TypeAheadSelect';
|
||
import { reducers as auditsPageReducers } from '../../routes/Audits/AuditsPage';
|
||
... | ... | |
...diffModalReducers,
|
||
...editorReducers,
|
||
...templateGenerationReducers,
|
||
...factChartReducers,
|
||
factChart,
|
||
...typeAheadSelectReducers,
|
||
...settingRecordsReducers,
|
||
...personalAccessTokensReducers,
|
Also available in: Unified diff
Fixes #33003 - Refactor FactChart to use slice pattern (#8657)
Following the discussion in
https://community.theforeman.org/t/rethinking-react-redux-folder-structure/24183
to use the slice pattern, updating this POC into actual changes to match
the agreed structure.