Project

General

Profile

« Previous | Next » 

Revision 09cecfa0

Added by Avi Sharvit almost 6 years ago

Fixes #21648 - Red Hat Recommended Repositories

Adds recommended-repository-sets-toggler
to the available-repository-sets-list

View differences:

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