Project

General

Profile

« Previous | Next » 

Revision 1291288b

Added by Tomer Brisker almost 3 years ago

Fixes #33003 - Refactor FactChart to use slice pattern (#8657)

  • Fixes #33003, #32899 - Refactor FactChart to use slice pattern

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.

View differences:

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