Revision 09cecfa0
Added by Avi Sharvit almost 6 years ago
package.json | ||
---|---|---|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||
"babel-preset-env": "^1.6.0",
|
||
"babel-polyfill": "^6.26.0",
|
||
"babel-preset-react": "^6.24.1",
|
||
"enzyme": "^3.2.0",
|
||
"enzyme-adapter-react-16": "^1.1.0",
|
webpack/move_to_pf/test-utils/testHelpers.js | ||
---|---|---|
import React from 'react';
|
||
import { shallow } from 'enzyme';
|
||
import toJson from 'enzyme-to-json';
|
||
|
||
|
||
// a helper method for invoking a class method (for unit tests)
|
||
// obj = a class
|
||
// func = a tested function
|
||
// objThis = an object's this
|
||
// arg = function args
|
||
export const classFunctionUnitTest = (obj, func, objThis, args) =>
|
||
obj.prototype[func].apply(objThis, args);
|
||
|
||
/**
|
||
* Shallow render a component multipile times with fixtures
|
||
* @param {ReactComponent} Component Component to shallow-render
|
||
* @param {Object} fixtures key=fixture description, value=props to apply
|
||
* @return {Object} key=fixture description, value=shallow-rendered component
|
||
*/
|
||
export const shallowRenderComponentWithFixtures = (Component, fixtures) =>
|
||
Object.entries(fixtures).map(([description, props]) => ({
|
||
description,
|
||
component: shallow(<Component {...props} />),
|
||
}));
|
||
|
||
/**
|
||
* Test a component with fixtures and snapshots
|
||
* @param {ReactComponent} Component Component to test
|
||
* @param {Object} fixtures key=fixture description, value=props to apply
|
||
*/
|
||
export const testComponentSnapshotsWithFixtures = (Component, fixtures) =>
|
||
shallowRenderComponentWithFixtures(Component, fixtures).forEach(({ description, component }) =>
|
||
it(description, () => expect(toJson(component)).toMatchSnapshot()));
|
||
|
||
/**
|
||
* run an action (sync or async) and except the results to much snapshot
|
||
* @param {Function} runAction Action runner function
|
||
* @return {Promise}
|
||
*/
|
||
export const testActionSnapshot = async (runAction) => {
|
||
const actionResults = runAction();
|
||
|
||
// if it's an async action
|
||
if (typeof actionResults === 'function') {
|
||
const dispatch = jest.fn();
|
||
await actionResults(dispatch);
|
||
|
||
expect(dispatch.mock.calls).toMatchSnapshot();
|
||
} else {
|
||
expect(actionResults).toMatchSnapshot();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Test actions with fixtures and snapshots
|
||
* @param {Object} fixtures key=fixture description, value=action runner function
|
||
*/
|
||
export const testActionSnapshotWithFixtures = fixtures =>
|
||
Object.entries(fixtures).forEach(([description, runAction]) =>
|
||
it(description, () => testActionSnapshot(runAction)));
|
||
|
||
/**
|
||
* Test a reducer with fixtures and snapshots
|
||
* @param {Function} reducer reducer to test
|
||
* @param {Object} fixtures key=fixture description, value=props to apply
|
||
*/
|
||
export const testReducerSnapshotWithFixtures = (reducer, fixtures) => {
|
||
const reduce = ({ state, action = {} } = {}) => reducer(state, action);
|
||
Object.entries(fixtures).forEach(([description, action]) =>
|
||
it(description, () => expect(reduce(action)).toMatchSnapshot()));
|
||
};
|
webpack/redux/actions/RedHatRepositories/helpers.js | ||
---|---|---|
export function normalizeRepositorySets(data) {
|
||
data.results.forEach((repositorySet) => {
|
||
/* eslint no-param-reassign: ["error", { "ignorePropertyModificationsFor": ["id"] }] */
|
||
repositorySet.id = parseInt(repositorySet.id, 10);
|
||
});
|
||
return data;
|
||
}
|
||
|
||
const repoTypeSearchQueryMap = {
|
||
rpm: '(name ~ rpms) and (name !~ source rpm) and (name !~ debug rpm)',
|
||
sourceRpm: 'name ~ source rpm',
|
||
... | ... | |
beta: 'name ~ beta',
|
||
};
|
||
|
||
const recommendedRepositorySetLables = [
|
||
'rhel-7-server-rpms',
|
||
'rhel-6-server-rpms',
|
||
'rhel-6-server-satellite-tools-6.3-rpms',
|
||
'rhel-server-rhscl-7-rpms',
|
||
'rhel-7-server-satellite-capsule-6.3-rpms',
|
||
'rhel-7-server-satellite-capsule-6.4-rpms',
|
||
'rhel-7-server-satellite-tools-6.3-rpms',
|
||
'rhel-6-server-satellite-tools-6.3-rpms',
|
||
'rhel-7-server-ansible-2.5-rpms',
|
||
'rhel-7-server-optional-rpms',
|
||
'rhel-7-server-extras-rpms',
|
||
'rhel-5-server-els-rpms',
|
||
'rhel-7-server-eus-rpms',
|
||
];
|
||
|
||
const createLablesQuery = lables =>
|
||
lables.map(label => `label = ${label}`).join(' or ');
|
||
|
||
const isRecommendedRepositorySet = ({ label }) => recommendedRepositorySetLables.includes(label);
|
||
|
||
export const normalizeRepositorySets = (data) => {
|
||
data.results.forEach((repositorySet) => {
|
||
/* eslint no-param-reassign: ["error", { "ignorePropertyModificationsFor": ["id"] }] */
|
||
repositorySet.id = parseInt(repositorySet.id, 10);
|
||
repositorySet.recommended = isRecommendedRepositorySet(repositorySet);
|
||
});
|
||
|
||
return data;
|
||
};
|
||
|
||
const maptToSearchQuery = (filter) => {
|
||
if (filter === 'other') {
|
||
const joined = Object.keys(repoTypeSearchQueryMap)
|
||
... | ... | |
.map(v => `(${v})`)
|
||
.join(' and ');
|
||
|
||
export const recommendedRepositorySetsQuery = createLablesQuery(recommendedRepositorySetLables);
|
||
|
||
export default normalizeRepositorySets;
|
webpack/redux/actions/RedHatRepositories/sets.js | ||
---|---|---|
import api, { orgId } from '../../../services/api';
|
||
import { normalizeRepositorySets, repoTypeFilterToSearchQuery, joinSearchQueries } from './helpers';
|
||
import {
|
||
normalizeRepositorySets,
|
||
repoTypeFilterToSearchQuery,
|
||
joinSearchQueries,
|
||
recommendedRepositorySetsQuery,
|
||
} from './helpers';
|
||
|
||
import {
|
||
REPOSITORY_SETS_REQUEST,
|
||
REPOSITORY_SETS_SUCCESS,
|
||
REPOSITORY_SETS_FAILURE,
|
||
REPOSITORY_SETS_UPDATE_RECOMMENDED,
|
||
} from '../../consts';
|
||
import { propsToSnakeCase } from '../../../services/index';
|
||
|
||
// eslint-disable-next-line import/prefer-default-export
|
||
export const loadRepositorySets = (extendedParams = {}) => (dispatch) => {
|
||
export const loadRepositorySets = (extendedParams = {}) => (dispatch, getState) => {
|
||
const { recommended } = getState().katello.redHatRepositories.sets;
|
||
|
||
dispatch({ type: REPOSITORY_SETS_REQUEST, params: extendedParams });
|
||
|
||
const searchParams = extendedParams.search || {};
|
||
const search = joinSearchQueries([
|
||
repoTypeFilterToSearchQuery(searchParams.filters || []),
|
||
searchParams.query,
|
||
recommended ? recommendedRepositorySetsQuery : '',
|
||
]);
|
||
|
||
const params = {
|
||
... | ... | |
.then(({ data }) => {
|
||
dispatch({
|
||
type: REPOSITORY_SETS_SUCCESS,
|
||
response: normalizeRepositorySets(data),
|
||
search: searchParams,
|
||
payload: {
|
||
response: normalizeRepositorySets(data),
|
||
search: searchParams,
|
||
},
|
||
});
|
||
})
|
||
.catch((result) => {
|
||
dispatch({
|
||
type: REPOSITORY_SETS_FAILURE,
|
||
result,
|
||
payload: result,
|
||
});
|
||
});
|
||
};
|
||
|
||
export const updateRecommendedRepositorySets = value => (dispatch, getState) => {
|
||
const { search } = getState().katello.redHatRepositories.sets;
|
||
|
||
dispatch({
|
||
type: REPOSITORY_SETS_UPDATE_RECOMMENDED,
|
||
payload: value,
|
||
});
|
||
|
||
dispatch(loadRepositorySets({ search }));
|
||
};
|
webpack/redux/consts.js | ||
---|---|---|
export const REPOSITORY_SET_REPOSITORIES_REQUEST = 'REPOSITORY_SET_REPOSITORIES_REQUEST';
|
||
export const REPOSITORY_SET_REPOSITORIES_SUCCESS = 'REPOSITORY_SET_REPOSITORIES_SUCCESS';
|
||
export const REPOSITORY_SET_REPOSITORIES_FAILURE = 'REPOSITORY_SET_REPOSITORIES_FAILURE';
|
||
export const REPOSITORY_SETS_UPDATE_RECOMMENDED = 'REPOSITORY_SETS_UPDATE_RECOMMENDED';
|
||
|
||
export const REPOSITORY_ENABLED = 'REPOSITORY_ENABLED';
|
||
export const REPOSITORY_DISABLED = 'REPOSITORY_DISABLED';
|
webpack/redux/reducers/RedHatRepositories/sets.fixtures.js | ||
---|---|---|
|
||
export const initialState = Immutable({
|
||
loading: true,
|
||
recommended: false,
|
||
results: [],
|
||
pagination: {},
|
||
});
|
||
|
||
export const recommendedState = Immutable({
|
||
loading: true,
|
||
recommended: true,
|
||
results: [],
|
||
pagination: {},
|
||
});
|
||
|
||
export const loadingState = Immutable({
|
||
loading: true,
|
||
recommended: false,
|
||
results: [],
|
||
pagination: {},
|
||
});
|
||
... | ... | |
|
||
export const successState = Immutable({
|
||
loading: false,
|
||
recommended: false,
|
||
results: requestSuccessResponse.results,
|
||
searchIsActive: false,
|
||
search: undefined,
|
||
searchIsActive: true,
|
||
search: requestSuccessResponse.search,
|
||
pagination: {
|
||
page: 1,
|
||
perPage: 5,
|
webpack/redux/reducers/RedHatRepositories/sets.js | ||
---|---|---|
REPOSITORY_SETS_REQUEST,
|
||
REPOSITORY_SETS_SUCCESS,
|
||
REPOSITORY_SETS_FAILURE,
|
||
REPOSITORY_SETS_UPDATE_RECOMMENDED,
|
||
} from '../../consts';
|
||
|
||
import { initialState } from './sets.fixtures.js';
|
||
|
||
export default (state = initialState, action) => {
|
||
if (action.type === REPOSITORY_SETS_REQUEST) {
|
||
return state.set('loading', true);
|
||
} else if (action.type === REPOSITORY_SETS_SUCCESS) {
|
||
const {
|
||
page, per_page, subtotal, results, // eslint-disable-line camelcase
|
||
} = action.response;
|
||
|
||
return Immutable({
|
||
results,
|
||
pagination: {
|
||
page: Number(page),
|
||
// server can return per_page: null when there's error in the search query,
|
||
// don't store it in such case
|
||
// eslint-disable-next-line camelcase
|
||
perPage: Number(per_page || state.pagination.perPage),
|
||
},
|
||
itemCount: Number(subtotal),
|
||
loading: false,
|
||
searchIsActive: !isEmpty(action.search),
|
||
search: action.search,
|
||
});
|
||
} else if (action.type === REPOSITORY_SETS_FAILURE) {
|
||
return Immutable({
|
||
error: action.error,
|
||
loading: false,
|
||
});
|
||
const { payload } = action;
|
||
|
||
switch (action.type) {
|
||
case REPOSITORY_SETS_REQUEST:
|
||
return state.set('loading', true);
|
||
|
||
case REPOSITORY_SETS_UPDATE_RECOMMENDED:
|
||
return state
|
||
.set('recommended', payload);
|
||
|
||
case REPOSITORY_SETS_SUCCESS:
|
||
return state
|
||
.set('results', payload.response.results)
|
||
.set('pagination', {
|
||
page: Number(payload.response.page),
|
||
// server can return per_page: null when there's error in the search query,
|
||
// don't store it in such case
|
||
// eslint-disable-next-line camelcase
|
||
perPage: Number(payload.response.per_page || state.pagination.perPage),
|
||
})
|
||
.set('itemCount', Number(payload.response.subtotal))
|
||
.set('loading', false)
|
||
.set('searchIsActive', !isEmpty(payload.search))
|
||
.set('search', payload.search);
|
||
|
||
case REPOSITORY_SETS_FAILURE:
|
||
return Immutable({
|
||
error: payload,
|
||
loading: false,
|
||
});
|
||
|
||
default:
|
||
return state;
|
||
}
|
||
return state;
|
||
};
|
webpack/redux/reducers/RedHatRepositories/sets.test.js | ||
---|---|---|
|
||
import {
|
||
initialState,
|
||
recommendedState,
|
||
loadingState,
|
||
requestSuccessResponse,
|
||
successState,
|
||
... | ... | |
expect(reducer(undefined, {})).toEqual(initialState);
|
||
});
|
||
|
||
it('should update the recommended value on REPOSITORY_SETS_UPDATE_RECOMMENDED', () => {
|
||
expect(reducer(initialState, {
|
||
type: types.REPOSITORY_SETS_UPDATE_RECOMMENDED,
|
||
payload: true,
|
||
})).toEqual(recommendedState);
|
||
});
|
||
|
||
it('should keep loading state on REPOSITORY_SETS_REQUEST', () => {
|
||
expect(reducer(initialState, {
|
||
type: types.REPOSITORY_SETS_REQUEST,
|
||
... | ... | |
it('should flatten repositories response REPOSITORY_SETS_SUCCESS', () => {
|
||
expect(reducer(initialState, {
|
||
type: types.REPOSITORY_SETS_SUCCESS,
|
||
response: requestSuccessResponse,
|
||
payload: { response: requestSuccessResponse, search: requestSuccessResponse.search },
|
||
})).toEqual(successState);
|
||
});
|
||
|
||
it('should have error on REPOSITORY_SETS_FAILURE', () => {
|
||
expect(reducer(initialState, {
|
||
type: types.REPOSITORY_SETS_FAILURE,
|
||
error: 'Unable to process request.',
|
||
payload: 'Unable to process request.',
|
||
})).toEqual(errorState);
|
||
});
|
||
});
|
webpack/scenes/RedHatRepositories/components/RecommendedRepositorySetsToggler.js | ||
---|---|---|
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import classNames from 'classnames';
|
||
import { Switch, Icon, FieldLevelHelp } from 'patternfly-react';
|
||
|
||
import './RecommendedRepositorySetsToggler.scss';
|
||
|
||
const RecommendedRepositorySetsToggler = ({
|
||
enabled,
|
||
className,
|
||
children,
|
||
help,
|
||
onChange,
|
||
...props
|
||
}) => {
|
||
const classes = classNames('recommended-repositories-toggler-container', className);
|
||
|
||
return (
|
||
<div className={classes} {...props}>
|
||
<Switch bsSize="mini" value={enabled} onChange={() => onChange(!enabled)} />
|
||
<Icon type="fa" name="star" />
|
||
{children}
|
||
<FieldLevelHelp content={help} />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
RecommendedRepositorySetsToggler.propTypes = {
|
||
enabled: PropTypes.bool,
|
||
className: PropTypes.string,
|
||
children: PropTypes.node,
|
||
help: PropTypes.node,
|
||
onChange: PropTypes.func,
|
||
};
|
||
|
||
RecommendedRepositorySetsToggler.defaultProps = {
|
||
enabled: false,
|
||
className: '',
|
||
children: __('Recommended Repositories'),
|
||
help: __('This shows repositories that are used in a typical setup.'),
|
||
onChange: () => null,
|
||
};
|
||
|
||
export default RecommendedRepositorySetsToggler;
|
webpack/scenes/RedHatRepositories/components/RecommendedRepositorySetsToggler.scss | ||
---|---|---|
@import '~patternfly/dist/sass/patternfly/_color-variables';
|
||
|
||
.recommended-repositories-toggler-container {
|
||
|
||
> * {
|
||
margin: 0 3px;
|
||
|
||
&.fa.fa-star {
|
||
color: $color-pf-gold-300;
|
||
}
|
||
|
||
&:last-child {
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
webpack/scenes/RedHatRepositories/components/RepositorySet.js | ||
---|---|---|
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { ListView } from 'patternfly-react';
|
||
import { ListView, Icon } from 'patternfly-react';
|
||
|
||
import RepositoryTypeIcon from './RepositoryTypeIcon';
|
||
import RepositorySetRepositories from './RepositorySetRepositories';
|
||
|
||
const RepositorySet = ({
|
||
type, id, name, label, product,
|
||
type, id, name, label, product, recommended,
|
||
}) => (
|
||
<ListView.Item
|
||
id={id}
|
||
... | ... | |
heading={name}
|
||
leftContent={<RepositoryTypeIcon id={id} type={type} />}
|
||
stacked
|
||
actions={recommended ? <Icon type="fa" name="star" className="recommended-repository-set-icon" /> : ''}
|
||
hideCloseIcon
|
||
>
|
||
<RepositorySetRepositories contentId={id} productId={product.id} />
|
||
... | ... | |
name: PropTypes.string.isRequired,
|
||
id: PropTypes.number.isRequired,
|
||
}).isRequired,
|
||
recommended: PropTypes.bool,
|
||
};
|
||
|
||
RepositorySet.defaultProps = {
|
||
recommended: false,
|
||
};
|
||
|
||
export default RepositorySet;
|
webpack/scenes/RedHatRepositories/components/__tests__/RecommendedRepositorySetsToggler.test.js | ||
---|---|---|
import { testComponentSnapshotsWithFixtures } from '../../../../move_to_pf/test-utils/testHelpers';
|
||
|
||
import RecommendedRepositorySetsToggler from '../RecommendedRepositorySetsToggler';
|
||
|
||
const fixtures = {
|
||
'renders recommended-repository-sets-toggler': {
|
||
enabled: true,
|
||
className: 'some-class-name',
|
||
children: 'some-children',
|
||
help: 'some-help',
|
||
onChange: () => null,
|
||
},
|
||
};
|
||
|
||
describe('RecommendedRepositorySetsToggler', () => {
|
||
describe('rendering', () => testComponentSnapshotsWithFixtures(RecommendedRepositorySetsToggler, fixtures));
|
||
});
|
webpack/scenes/RedHatRepositories/components/__tests__/__snapshots__/RecommendedRepositorySetsToggler.test.js.snap | ||
---|---|---|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||
|
||
exports[`RecommendedRepositorySetsToggler rendering renders recommended-repository-sets-toggler 1`] = `
|
||
<div
|
||
className="recommended-repositories-toggler-container some-class-name"
|
||
>
|
||
<Switch
|
||
animate={true}
|
||
baseClass="bootstrap-switch"
|
||
bsSize="mini"
|
||
defaultValue={true}
|
||
disabled={false}
|
||
handleWidth="auto"
|
||
inverse={false}
|
||
labelText=" "
|
||
labelWidth="auto"
|
||
offColor="default"
|
||
offText="OFF"
|
||
onChange={[Function]}
|
||
onColor="primary"
|
||
onText="ON"
|
||
readonly={false}
|
||
tristate={false}
|
||
value={true}
|
||
wrapperClass="wrapper"
|
||
/>
|
||
<Icon
|
||
name="star"
|
||
type="fa"
|
||
/>
|
||
some-children
|
||
<FieldLevelHelp
|
||
close="true"
|
||
content="some-help"
|
||
/>
|
||
</div>
|
||
`;
|
webpack/scenes/RedHatRepositories/index.js | ||
---|---|---|
import { Spinner } from 'patternfly-react';
|
||
|
||
import { createEnabledRepoParams, loadEnabledRepos } from '../../redux/actions/RedHatRepositories/enabled';
|
||
import { loadRepositorySets } from '../../redux/actions/RedHatRepositories/sets';
|
||
import { loadRepositorySets, updateRecommendedRepositorySets } from '../../redux/actions/RedHatRepositories/sets';
|
||
import SearchBar from './components/SearchBar';
|
||
import RecommendedRepositorySetsToggler from './components/RecommendedRepositorySetsToggler';
|
||
import { getSetsComponent, getEnabledComponent } from './helpers';
|
||
|
||
class RedHatRepositoriesPage extends Component {
|
||
... | ... | |
|
||
<Row className="row-eq-height">
|
||
<Col sm={6} className="available-repositories-container">
|
||
<h2>{__('Available Repositories')}</h2>
|
||
<div className="available-repositories-header">
|
||
<h2>{__('Available Repositories')}</h2>
|
||
<RecommendedRepositorySetsToggler
|
||
enabled={repositorySets.recommended}
|
||
onChange={value => this.props.updateRecommendedRepositorySets(value)}
|
||
className="recommended-repositories-toggler"
|
||
/>
|
||
</div>
|
||
<Spinner loading={repositorySets.loading}>
|
||
{getSetsComponent(
|
||
repositorySets,
|
||
... | ... | |
RedHatRepositoriesPage.propTypes = {
|
||
loadEnabledRepos: PropTypes.func.isRequired,
|
||
loadRepositorySets: PropTypes.func.isRequired,
|
||
updateRecommendedRepositorySets: PropTypes.func.isRequired,
|
||
enabledRepositories: PropTypes.shape({}).isRequired,
|
||
repositorySets: PropTypes.shape({}).isRequired,
|
||
};
|
||
... | ... | |
export default connect(mapStateToProps, {
|
||
loadEnabledRepos,
|
||
loadRepositorySets,
|
||
updateRecommendedRepositorySets,
|
||
})(RedHatRepositoriesPage);
|
webpack/scenes/RedHatRepositories/index.scss | ||
---|---|---|
background-color: inherit;
|
||
}
|
||
|
||
.recommended-repository-set-icon {
|
||
color: $color-pf-gold-300;
|
||
font-size: 2em;
|
||
}
|
||
|
||
.available-repositories-container {
|
||
background-color: $color-pf-white;
|
||
border-right: $color-pf-black-300 2px solid;
|
||
|
||
.available-repositories-header {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
padding-top: 20px;
|
||
|
||
h2 {
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.content-view-pf-pagination {
|
webpack/test_setup.js | ||
---|---|---|
// Setup file for enzyme
|
||
// See http://airbnb.io/enzyme/docs/installation/react-16.html
|
||
import 'babel-polyfill';
|
||
import { configure } from 'enzyme';
|
||
import Adapter from 'enzyme-adapter-react-16';
|
||
|
Also available in: Unified diff
Fixes #21648 - Red Hat Recommended Repositories
Adds recommended-repository-sets-toggler
to the available-repository-sets-list