katello 4.8.0.rc1 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +2 -18
  3. data/app/controllers/katello/api/v2/alternate_content_sources_controller.rb +7 -5
  4. data/app/controllers/katello/api/v2/repositories_controller.rb +3 -18
  5. data/app/helpers/katello/hosts_and_hostgroups_helper.rb +13 -8
  6. data/app/lib/actions/katello/alternate_content_source/create.rb +3 -1
  7. data/app/lib/actions/katello/alternate_content_source/update.rb +3 -1
  8. data/app/lib/actions/pulp3/orchestration/content_view_version/copy_version_units_to_library.rb +1 -1
  9. data/app/lib/actions/pulp3/orchestration/content_view_version/export.rb +11 -11
  10. data/app/lib/actions/pulp3/orchestration/content_view_version/syncable_export.rb +0 -2
  11. data/app/lib/actions/pulp3/repository/reclaim_space.rb +1 -1
  12. data/app/lib/katello/api/v2/error_handling.rb +12 -2
  13. data/app/lib/katello/concerns/base_template_scope_extensions.rb +7 -3
  14. data/app/models/katello/alternate_content_source.rb +54 -4
  15. data/app/models/katello/concerns/host_managed_extensions.rb +14 -0
  16. data/app/models/katello/glue/provider.rb +1 -1
  17. data/app/models/katello/host/content_facet.rb +2 -0
  18. data/app/services/katello/pulp3/content_view_version/export_validation_error.rb +1 -1
  19. data/app/services/katello/pulp3/content_view_version/export_validator.rb +16 -0
  20. data/app/views/foreman/smart_proxies/_content_sync.html.erb +1 -1
  21. data/db/migrate/20230203141353_set_new_acs_verify_ssl_default.rb +5 -0
  22. data/db/seeds.d/111-upgrade_tasks.rb +2 -1
  23. data/lib/katello/plugin.rb +0 -12
  24. data/lib/katello/tasks/upgrades/4.8/regenerate_imported_repository_metadata.rake +33 -0
  25. data/lib/katello/version.rb +1 -1
  26. data/webpack/components/Content/{ContentPage.js → GenericContentPage.js} +7 -4
  27. data/webpack/components/Content/__tests__/ContentTable.test.js +1 -1
  28. data/webpack/components/Content/__tests__/GenericContentPage.test.js +35 -0
  29. data/webpack/components/Search/SearchText.js +70 -0
  30. data/webpack/components/Table/EmptyStateMessage.js +2 -2
  31. data/webpack/components/Table/TableWrapper.js +4 -0
  32. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ChangeHostCVModal.js +10 -72
  33. data/webpack/components/extensions/HostDetails/HostDetailsConstants.js +0 -1
  34. data/webpack/components/extensions/HostDetails/HostDetailsSelectors.js +0 -6
  35. data/webpack/components/extensions/HostDetails/Tabs/__tests__/moduleStreamsTab.test.js +76 -0
  36. data/webpack/components/extensions/SearchBar/SearchBarConstants.js +3 -0
  37. data/webpack/components/extensions/SearchBar/SearchBarHooks.js +50 -0
  38. data/webpack/components/extensions/SearchBar/SearchBarReducer.js +14 -0
  39. data/webpack/components/extensions/SearchBar/SearchBarSelectors.js +5 -0
  40. data/webpack/redux/actions/RedHatRepositories/helpers.js +5 -3
  41. data/webpack/redux/reducers/index.js +2 -2
  42. data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreate.test.js +1 -13
  43. data/webpack/scenes/AlternateContentSources/Details/ACSExpandableDetails.js +6 -5
  44. data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditCredentials.js +1 -0
  45. data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditDetails.js +3 -2
  46. data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditProducts.js +1 -0
  47. data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditSmartProxies.js +2 -0
  48. data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditURLPaths.js +1 -0
  49. data/webpack/scenes/AlternateContentSources/MainTable/ACSTable.js +1 -0
  50. data/webpack/scenes/Content/{ContentPage.js → GenericContentPage.js} +2 -2
  51. data/webpack/scenes/Content/__tests__/contentTable.test.js +2 -2
  52. data/webpack/scenes/Content/index.js +2 -2
  53. data/webpack/scenes/ContentViews/Details/ContentViewDetails.js +1 -0
  54. data/webpack/scenes/ContentViews/Details/Filters/Rules/ContainerTag/AddEditContainerTagRuleModal.js +14 -17
  55. data/webpack/scenes/ContentViews/Details/Filters/Rules/Package/AddEditPackageRuleModal.js +24 -28
  56. data/webpack/scenes/ContentViews/Details/Filters/__tests__/CVContainerImageFilterContent.test.js +11 -18
  57. data/webpack/scenes/ContentViews/Details/Filters/__tests__/CVRpmFilterContent.test.js +10 -23
  58. data/webpack/scenes/ContentViews/Details/Filters/__tests__/ContentViewPackageGroupFilter.test.js +0 -2
  59. data/webpack/scenes/ContentViews/Details/Versions/__tests__/contentViewVersions.test.js +1 -7
  60. data/webpack/scenes/ContentViews/components/ContentViewSelect/ContentViewSelectOption.js +87 -0
  61. data/webpack/scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths.js +1 -1
  62. data/webpack/scenes/ContentViews/expansions/__tests__/contentViewComponentsModal.test.js +0 -2
  63. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +153 -28
  64. data/webpack/scenes/Hosts/ChangeContentSource/index.js +14 -15
  65. data/webpack/scenes/Hosts/ChangeContentSource/selectors.js +4 -0
  66. data/webpack/scenes/Hosts/ChangeContentSource/styles.scss +4 -0
  67. data/webpack/scenes/ModuleStreams/ModuleStreamsPage.js +2 -2
  68. data/webpack/scenes/ModuleStreams/__tests__/ModuleStreamPage.test.js +2 -2
  69. data/webpack/scenes/ModuleStreams/__tests__/__snapshots__/ModuleStreamPage.test.js.snap +1 -1
  70. data/webpack/scenes/Settings/SettingsConstants.js +2 -3
  71. data/webpack/scenes/Settings/SettingsReducer.js +2 -16
  72. data/webpack/scenes/Settings/SettingsSelectors.js +2 -2
  73. data/webpack/test-utils/react-testing-lib-wrapper.js +0 -6
  74. metadata +16 -25
  75. data/webpack/components/Content/__tests__/ContentPage.test.js +0 -32
  76. data/webpack/components/Content/__tests__/__snapshots__/ContentPage.test.js.snap +0 -89
  77. data/webpack/components/Search/Search.js +0 -156
  78. data/webpack/components/Search/__tests__/search.test.js +0 -104
  79. data/webpack/components/Search/helpers.js +0 -6
  80. data/webpack/components/Search/index.js +0 -15
  81. data/webpack/components/TypeAhead/TypeAhead.js +0 -157
  82. data/webpack/components/TypeAhead/TypeAhead.scss +0 -7
  83. data/webpack/components/TypeAhead/helpers/commonPropTypes.js +0 -35
  84. data/webpack/components/TypeAhead/helpers/helpers.js +0 -32
  85. data/webpack/components/TypeAhead/index.js +0 -3
  86. data/webpack/components/TypeAhead/pf3Search/TypeAheadInput.js +0 -44
  87. data/webpack/components/TypeAhead/pf3Search/TypeAheadItems.js +0 -56
  88. data/webpack/components/TypeAhead/pf3Search/TypeAheadSearch.js +0 -53
  89. data/webpack/components/TypeAhead/pf4Search/TypeAheadInput.js +0 -66
  90. data/webpack/components/TypeAhead/pf4Search/TypeAheadInput.scss +0 -12
  91. data/webpack/components/TypeAhead/pf4Search/TypeAheadItems.js +0 -59
  92. data/webpack/components/TypeAhead/pf4Search/TypeAheadSearch.js +0 -81
@@ -4,8 +4,9 @@ import { Grid, Col, Row, Form, FormGroup } from 'react-bootstrap';
4
4
  import SearchBar from 'foremanReact/components/SearchBar';
5
5
  import { getControllerSearchProps } from 'foremanReact/constants';
6
6
  import ContentTable from './ContentTable';
7
+ import { useClearSearch } from '../extensions/SearchBar/SearchBarHooks';
7
8
 
8
- const ContentPage = ({
9
+ const GenericContentPage = ({
9
10
  header, onSearch, bookmarkController,
10
11
  autocompleteEndpoint, autocompleteQueryParams,
11
12
  updateSearchQuery, initialInputValue,
@@ -15,6 +16,7 @@ const ContentPage = ({
15
16
  ...getControllerSearchProps(autocompleteEndpoint, `searchBar-content-page-${header}`, true, autocompleteQueryParams),
16
17
  controller: bookmarkController,
17
18
  };
19
+ const searchBarKey = useClearSearch({ updateSearchQuery });
18
20
  return (
19
21
  <Grid bsClass="container-fluid">
20
22
  <Row>
@@ -27,6 +29,7 @@ const ContentPage = ({
27
29
  <Form className="toolbar-pf-actions">
28
30
  <FormGroup className="toolbar-pf toolbar-pf-filter">
29
31
  <SearchBar
32
+ key={searchBarKey}
30
33
  data={searchDataProp}
31
34
  onSearch={onSearch}
32
35
  onSearchChange={updateSearchQuery}
@@ -49,7 +52,7 @@ const ContentPage = ({
49
52
  );
50
53
  };
51
54
 
52
- ContentPage.propTypes = {
55
+ GenericContentPage.propTypes = {
53
56
  header: PropTypes.string.isRequired,
54
57
  content: PropTypes.shape({}).isRequired,
55
58
  tableSchema: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
@@ -62,10 +65,10 @@ ContentPage.propTypes = {
62
65
  bookmarkController: PropTypes.string,
63
66
  };
64
67
 
65
- ContentPage.defaultProps = {
68
+ GenericContentPage.defaultProps = {
66
69
  autocompleteEndpoint: undefined,
67
70
  autocompleteQueryParams: undefined,
68
71
  bookmarkController: undefined,
69
72
  };
70
73
 
71
- export default ContentPage;
74
+ export default GenericContentPage;
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
2
  import { shallow } from 'enzyme';
3
3
  import toJson from 'enzyme-to-json';
4
- import ContentTable from '../ContentTable';
5
4
  import { LoadingState } from '../../../components/LoadingState';
6
5
  import { Table } from '../../../components/pf3Table';
6
+ import ContentTable from '../ContentTable';
7
7
 
8
8
  describe('Content Table', () => {
9
9
  it('should render and contain appropriate components', async () => {
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { act } from 'react-test-renderer';
3
+ import { renderWithRedux, patientlyWaitFor } from 'react-testing-lib-wrapper';
4
+ import { CONTENT_KEY } from '../../../scenes/Content/ContentConstants';
5
+ import GenericContentPage from '../GenericContentPage';
6
+
7
+ const renderOptions = () => ({
8
+ apiNamespace: CONTENT_KEY,
9
+ });
10
+
11
+ test('Can render the basic component with no data', async (done) => {
12
+ const contentHeader = 'Content Header';
13
+ const content = { results: [] };
14
+ const onSearch = jest.fn();
15
+ const updateSearchQuery = jest.fn();
16
+ const searchQuery = '';
17
+ const onPaginationChange = jest.fn();
18
+ const TableSchema = [];
19
+ const bookmarkController = 'module_streams';
20
+
21
+ const { getByText } = renderWithRedux(<GenericContentPage
22
+ header={contentHeader}
23
+ content={content}
24
+ tableSchema={TableSchema}
25
+ onSearch={onSearch}
26
+ updateSearchQuery={updateSearchQuery}
27
+ initialInputValue={searchQuery}
28
+ onPaginationChange={onPaginationChange}
29
+ bookmarkController={bookmarkController}
30
+ />, renderOptions());
31
+
32
+ await patientlyWaitFor(() =>
33
+ expect(getByText(contentHeader)).toBeInTheDocument());
34
+ act(done);
35
+ });
@@ -0,0 +1,70 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { SearchAutocomplete } from 'foremanReact/components/SearchBar/SearchAutocomplete';
4
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
5
+ import { STATUS } from 'foremanReact/constants';
6
+ import { noop } from 'foremanReact/common/helpers';
7
+
8
+ const SearchText = ({
9
+ data: {
10
+ autocomplete: { url, apiParams } = { url: '' },
11
+ disabled,
12
+ },
13
+ initialQuery,
14
+ onSearchChange,
15
+ name,
16
+ }) => {
17
+ const [search, setSearch] = useState(initialQuery || '');
18
+ const getAPIparams = input => ({ ...apiParams(input) });
19
+ const { response, status, setAPIOptions } = useAPI('get', url, {
20
+ params: getAPIparams(search),
21
+ });
22
+ const onChange = (newValue) => {
23
+ onSearchChange(newValue);
24
+ setSearch(newValue);
25
+ setAPIOptions({ params: { ...getAPIparams(newValue) } });
26
+ };
27
+ const error =
28
+ status === STATUS.ERROR || response?.[0]?.error
29
+ ? response?.[0]?.error || response.message
30
+ : null;
31
+
32
+ let results = [];
33
+ if (Array.isArray(response) && !error) {
34
+ results = response.map(item => ({ label: item, category: '' }));
35
+ }
36
+
37
+ return (
38
+ <div className="foreman-search-text">
39
+ <SearchAutocomplete
40
+ results={results}
41
+ onSearchChange={onChange}
42
+ value={search}
43
+ disabled={disabled}
44
+ error={error}
45
+ name={name}
46
+ />
47
+ </div>
48
+ );
49
+ };
50
+
51
+ SearchText.propTypes = {
52
+ data: PropTypes.shape({
53
+ autocomplete: PropTypes.shape({
54
+ url: PropTypes.string.isRequired,
55
+ apiParams: PropTypes.func,
56
+ }).isRequired,
57
+ disabled: PropTypes.bool,
58
+ }).isRequired,
59
+ initialQuery: PropTypes.string,
60
+ onSearchChange: PropTypes.func,
61
+ name: PropTypes.string,
62
+ };
63
+
64
+ SearchText.defaultProps = {
65
+ initialQuery: '',
66
+ onSearchChange: noop,
67
+ name: null,
68
+ };
69
+
70
+ export default SearchText;
@@ -14,7 +14,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
14
14
  import { CubeIcon, ExclamationCircleIcon, SearchIcon, CheckCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
15
15
  import { global_danger_color_200 as dangerColor, global_success_color_100 as successColor } from '@patternfly/react-tokens';
16
16
  import { useDispatch, useSelector } from 'react-redux';
17
- import { selectHostDetailsClearSearch } from '../extensions/HostDetails/HostDetailsSelectors';
17
+ import { selectSearchBarClearSearch } from '../extensions/SearchBar/SearchBarSelectors';
18
18
 
19
19
  const KatelloEmptyStateIcon = ({
20
20
  error, search, customIcon, happyIcon,
@@ -51,7 +51,7 @@ const EmptyStateMessage = ({
51
51
  const defaultSecondaryActionText = searchIsActive ? __('Clear search') : __('Clear filters');
52
52
  const secondaryActionText = secondaryActionTextOverride || defaultSecondaryActionText;
53
53
  const dispatch = useDispatch();
54
- const clearSearch = useSelector(selectHostDetailsClearSearch);
54
+ const clearSearch = useSelector(selectSearchBarClearSearch);
55
55
  const showSecondaryActionAnchor = showSecondaryAction && secondaryActionLink;
56
56
  const handleClick = () => {
57
57
  if (searchIsActive) {
@@ -14,6 +14,7 @@ import MainTable from './MainTable';
14
14
  import { getPageStats } from './helpers';
15
15
  import SelectAllCheckbox from '../SelectAllCheckbox';
16
16
  import { orgId } from '../../services/api';
17
+ import { useClearSearch } from '../extensions/SearchBar/SearchBarHooks';
17
18
 
18
19
  /* Patternfly 4 table wrapper */
19
20
  const TableWrapper = ({
@@ -132,6 +133,8 @@ const TableWrapper = ({
132
133
  spawnFetch();
133
134
  }, [searchQuery, spawnFetch, additionalListeners]);
134
135
 
136
+ const searchBarKey = useClearSearch({ updateSearchQuery });
137
+
135
138
  // If the new page wouldn't exist because of a perPage change,
136
139
  // we should set the current page to the last page.
137
140
  const validatePagination = (data) => {
@@ -194,6 +197,7 @@ const TableWrapper = ({
194
197
  data={searchDataProp}
195
198
  initialQuery={searchQuery}
196
199
  onSearch={search => updateSearchQuery(search)}
200
+ key={searchBarKey}
197
201
  />
198
202
  </FlexItem>
199
203
  }
@@ -1,11 +1,7 @@
1
1
  import React, { useState, useCallback } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { FormattedMessage } from 'react-intl';
4
3
  import { useDispatch, useSelector } from 'react-redux';
5
- import { Modal, Button, SelectOption, Alert, Flex } from '@patternfly/react-core';
6
- import {
7
- global_palette_black_600 as pfDescriptionColor,
8
- } from '@patternfly/react-tokens';
4
+ import { Modal, Button, Alert } from '@patternfly/react-core';
9
5
  import { translate as __ } from 'foremanReact/common/I18n';
10
6
  import { STATUS } from 'foremanReact/constants';
11
7
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
@@ -15,42 +11,15 @@ import { ENVIRONMENT_PATHS_KEY } from '../../../../../scenes/ContentViews/compon
15
11
  import api from '../../../../../services/api';
16
12
  import getContentViews from '../../../../../scenes/ContentViews/ContentViewsActions';
17
13
  import { selectContentViews, selectContentViewStatus } from '../../../../../scenes/ContentViews/ContentViewSelectors';
18
- import { uniq } from '../../../../../utils/helpers';
19
- import ContentViewIcon from '../../../../../scenes/ContentViews/components/ContentViewIcon';
20
14
  import updateHostContentViewAndEnvironment from './HostContentViewActions';
21
15
  import HOST_CV_AND_ENV_KEY from './HostContentViewConstants';
22
16
  import { getHostDetails } from '../../HostDetailsActions';
23
17
  import ContentViewSelect from '../../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelect';
18
+ import ContentViewSelectOption
19
+ from '../../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelectOption';
24
20
 
25
21
  const ENV_PATH_OPTIONS = { key: ENVIRONMENT_PATHS_KEY };
26
22
 
27
- const ContentViewDescription = ({ cv, versionNumber }) => {
28
- const descriptionStyle = {
29
- fontSize: '12px',
30
- fontWeight: 400,
31
- color: pfDescriptionColor.value,
32
- };
33
- if (cv.default) return <span style={descriptionStyle}>{__('Library')}</span>;
34
- return (
35
- <span style={descriptionStyle}>
36
- <FormattedMessage
37
- id={`content-view-${cv.id}-version-${cv.latest_version}`}
38
- defaultMessage="Version {versionNumber}"
39
- values={{ versionNumber }}
40
- />
41
- </span>
42
- );
43
- };
44
-
45
- ContentViewDescription.propTypes = {
46
- cv: PropTypes.shape({
47
- default: PropTypes.bool.isRequired,
48
- id: PropTypes.number.isRequired,
49
- latest_version: PropTypes.string.isRequired,
50
- }).isRequired,
51
- versionNumber: PropTypes.string.isRequired,
52
- };
53
-
54
23
  const ChangeHostCVModal = ({
55
24
  isOpen,
56
25
  closeModal,
@@ -66,6 +35,7 @@ const ChangeHostCVModal = ({
66
35
  const [cvSelectOpen, setCVSelectOpen] = useState(false);
67
36
  const dispatch = useDispatch();
68
37
  const contentViewsInEnvResponse = useSelector(state => selectContentViews(state, `FOR_ENV_${hostEnvId}`));
38
+ const { results } = contentViewsInEnvResponse;
69
39
  const contentViewsInEnvStatus = useSelector(state => selectContentViewStatus(state, `FOR_ENV_${hostEnvId}`));
70
40
  const hostUpdateStatus = useSelector(state => selectAPIStatus(state, HOST_CV_AND_ENV_KEY));
71
41
  useAPI( // No TableWrapper here, so we can useAPI from Foreman
@@ -73,6 +43,7 @@ const ChangeHostCVModal = ({
73
43
  api.getApiUrl(`/organizations/${orgId}/environments/paths?permission_type=promotable`),
74
44
  ENV_PATH_OPTIONS,
75
45
  );
46
+ const selectedCVForHostId = results?.find(cv => cv.name === selectedCVForHost)?.id;
76
47
 
77
48
  const handleModalClose = () => {
78
49
  setCVSelectOpen(false);
@@ -102,13 +73,6 @@ const ChangeHostCVModal = ({
102
73
  const { results: contentViewsInEnv = [] } = contentViewsInEnvResponse;
103
74
  const canSave = !!(selectedCVForHost && selectedEnvForHost.length);
104
75
 
105
- const relevantVersionObjFromCv = (cv, env) => { // returns the entire version object
106
- const versions = cv.versions.filter(version => new Set(version.environment_ids).has(env.id));
107
- return uniq(versions)?.[0];
108
- };
109
- const relevantVersionFromCv = (cv, env) =>
110
- relevantVersionObjFromCv(cv, env)?.version; // returns the version text e.g. "1.0"
111
-
112
76
  const refreshHostDetails = () => {
113
77
  handleModalClose();
114
78
  return dispatch(getHostDetails({ hostname: hostName }));
@@ -119,7 +83,7 @@ const ChangeHostCVModal = ({
119
83
  id: hostId,
120
84
  host: {
121
85
  content_facet_attributes: {
122
- content_view_id: selectedCVForHost,
86
+ content_view_id: selectedCVForHostId,
123
87
  lifecycle_environment_id: selectedEnvId,
124
88
  },
125
89
  },
@@ -183,7 +147,7 @@ const ChangeHostCVModal = ({
183
147
  headerText={__('Select environment')}
184
148
  isDisabled={hostUpdateStatus === STATUS.PENDING}
185
149
  />
186
- {selectedEnvForHost.length > 0 &&
150
+ {selectedEnvForHost.length > 0 && contentViewsInEnvStatus !== STATUS.PENDING &&
187
151
  <ContentViewSelect
188
152
  selections={selectedCVForHost}
189
153
  onClear={() => setSelectedCVForHost(null)}
@@ -193,35 +157,9 @@ const ChangeHostCVModal = ({
193
157
  onToggle={isExpanded => setCVSelectOpen(isExpanded)}
194
158
  placeholderText={cvPlaceholderText()}
195
159
  >
196
- {contentViewsInEnv?.map(cv => (
197
- <SelectOption
198
- key={cv.id}
199
- value={cv.id}
200
- >
201
- <Flex
202
- direction={{ default: 'row', sm: 'row' }}
203
- flexWrap={{ default: 'nowrap' }}
204
- alignItems={{ default: 'alignItemsCenter', sm: 'alignItemsCenter' }}
205
- >
206
- <ContentViewIcon
207
- composite={cv.composite}
208
- size="sm"
209
- />
210
- <Flex
211
- direction={{ default: 'column', sm: 'column' }}
212
- flexWrap={{ default: 'nowrap' }}
213
- alignItems={{ default: 'alignItemsFlexStart', sm: 'alignItemsFlexStart' }}
214
- >
215
- {cv.name}
216
- <ContentViewDescription
217
- cv={cv}
218
- versionNumber={relevantVersionFromCv(cv, selectedEnv)}
219
- />
220
- </Flex>
221
- </Flex>
222
- </SelectOption>
223
- ))
224
- }
160
+ {(contentViewsInEnv.length !== 0) &&
161
+ contentViewsInEnv?.map(cv =>
162
+ <ContentViewSelectOption key={cv.id} cv={cv} env={selectedEnvForHost[0]} />)}
225
163
  </ContentViewSelect>
226
164
  }
227
165
  </Modal>
@@ -1,3 +1,2 @@
1
- export const SET_CLEAR_SEARCH = 'SET_CLEAR_SEARCH';
2
1
  const HOST_DETAILS = 'HOST_DETAILS';
3
2
  export default HOST_DETAILS;
@@ -14,9 +14,3 @@ export const selectHostDetailsStatus = state =>
14
14
 
15
15
  export const selectHostDetailsError = state =>
16
16
  selectAPIError(state, HOST_DETAILS_KEY);
17
-
18
- export const selectHostDetailsState = state =>
19
- state.katello.hostDetails;
20
-
21
- export const selectHostDetailsClearSearch = state =>
22
- selectHostDetailsState(state).clearSearch;
@@ -93,6 +93,82 @@ test('Can handle no Module streams being present', async (done) => {
93
93
  act(done);
94
94
  });
95
95
 
96
+ test('When there are no search results, can display an empty state with a clear search link that works', async (done) => {
97
+ // Setup autocomplete with mockForemanAutoComplete since we aren't adding /katello
98
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
99
+ const noResults = {
100
+ total: 0,
101
+ subtotal: 0,
102
+ page: 1,
103
+ search: 'bad search',
104
+ per_page: 20,
105
+ results: [],
106
+ };
107
+
108
+ const initialScope = nockInstance
109
+ .get(hostModuleStreams)
110
+ .query(true)
111
+ .times(1)
112
+ .reply(200, mockModuleStreams);
113
+
114
+ const badSearchScope = nockInstance
115
+ .get(hostModuleStreams)
116
+ .query({
117
+ sort_by: 'name',
118
+ sort_order: 'asc',
119
+ per_page: '20',
120
+ page: '1',
121
+ search: 'bad search',
122
+ })
123
+ .reply(200, noResults);
124
+
125
+ const badAutoCompleteScope =
126
+ mockForemanAutocomplete(
127
+ nockInstance,
128
+ autocompleteUrl,
129
+ true,
130
+ [],
131
+ 2, // times
132
+ );
133
+
134
+ const scopeWithoutSearch = nockInstance
135
+ .get(hostModuleStreams)
136
+ .query(true)
137
+ .reply(200, mockModuleStreams);
138
+
139
+ const { queryByText, getByRole } = renderWithRedux(<ModuleStreamsTab />, renderOptions());
140
+
141
+
142
+ await patientlyWaitFor(() => expect(queryByText(firstModuleStreams.name)).toBeInTheDocument());
143
+
144
+ const searchInput = getByRole('textbox', { name: 'Search input' });
145
+ // Foreman SearchAutocomplete doesn't run onSearchChange unless the element is focused!
146
+ searchInput.focus();
147
+
148
+ fireEvent.change(searchInput, { target: { value: 'bad search' } });
149
+ expect(searchInput.value).toBe('bad search');
150
+ const searchButton = getByRole('button', { name: 'Search' });
151
+ expect(searchButton).not.toHaveAttribute('aria-disabled', true);
152
+ fireEvent.click(searchButton);
153
+
154
+ await patientlyWaitFor(() => expect(queryByText('Your search returned no matching Module streams.')).toBeInTheDocument());
155
+ // Now click the clear search link and assert that the search is cleared and the results are back
156
+ const clearSearchLink = getByRole('button', { name: 'Clear search' });
157
+ fireEvent.click(clearSearchLink);
158
+
159
+ await patientlyWaitFor(() => {
160
+ expect(queryByText(firstModuleStreams.name)).toBeInTheDocument();
161
+ expect(getByRole('textbox', { name: 'Search input' }).value).toBe('');
162
+ expect(queryByText('Clear search')).not.toBeInTheDocument();
163
+ });
164
+
165
+ assertNockRequest(initialScope);
166
+ assertNockRequest(badAutoCompleteScope);
167
+ assertNockRequest(badSearchScope);
168
+ assertNockRequest(scopeWithoutSearch);
169
+ assertNockRequest(autocompleteScope, done);
170
+ });
171
+
96
172
  test('Can filter results based on status', async (done) => {
97
173
  const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
98
174
  const scope = nockInstance
@@ -0,0 +1,3 @@
1
+ export const SET_CLEAR_SEARCH = 'SET_CLEAR_SEARCH';
2
+
3
+ export default SET_CLEAR_SEARCH;
@@ -0,0 +1,50 @@
1
+ import { useEffect, useCallback, useRef } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { selectSearchBarClearSearch } from './SearchBarSelectors';
4
+
5
+ export const useClearSearch = ({
6
+ updateSearchQuery,
7
+ }) => {
8
+ const dispatch = useDispatch();
9
+ // We keep the clearSearch function in Redux to avoid prop drilling and make EmptyStateMessage
10
+ // more reusable both with and without TableWrapper.
11
+ const existingClearSearch = useSelector(selectSearchBarClearSearch);
12
+ // In Katello we don't have access to Foreman <SearchBar /> component's internal state,
13
+ // so we don't have an easy way to clear the search input. We can use a counter to
14
+ // pass as a key prop to the <SearchBar /> component, which will force it to reset
15
+ // its internal state when clearSearch is called.
16
+ const counter = useRef(0);
17
+
18
+ const clearSearch = useCallback(() => {
19
+ counter.current += 1; // reset the text input
20
+ if (typeof updateSearchQuery !== 'function') {
21
+ // eslint-disable-next-line no-console
22
+ console.error('You must pass the updateSearchQuery function to useClearSearch');
23
+ return;
24
+ }
25
+ updateSearchQuery(''); // make a new API call with blank search query
26
+ }, [updateSearchQuery]);
27
+
28
+ useEffect(() => {
29
+ if (typeof existingClearSearch !== 'function') {
30
+ dispatch({
31
+ type: 'SET_CLEAR_SEARCH',
32
+ payload: clearSearch,
33
+ });
34
+ }
35
+ }, [dispatch, existingClearSearch, clearSearch]);
36
+
37
+ // eslint-disable-next-line arrow-body-style
38
+ useEffect(() => {
39
+ return function cleanupClearSearch() {
40
+ dispatch({
41
+ type: 'SET_CLEAR_SEARCH',
42
+ payload: {},
43
+ });
44
+ };
45
+ }, [dispatch]);
46
+
47
+ return `search-bar-${counter.current}`;
48
+ };
49
+
50
+ export default useClearSearch;
@@ -0,0 +1,14 @@
1
+ import Immutable from 'seamless-immutable';
2
+
3
+ import { SET_CLEAR_SEARCH } from './SearchBarConstants';
4
+
5
+ const initialState = Immutable({ clearSearch: undefined });
6
+
7
+ export default (state = initialState, action) => {
8
+ switch (action.type) {
9
+ case SET_CLEAR_SEARCH:
10
+ return state.set('clearSearch', action.payload);
11
+ default:
12
+ return state;
13
+ }
14
+ };
@@ -0,0 +1,5 @@
1
+ export const selectSearchBarState = state =>
2
+ state.katello.searchBar;
3
+
4
+ export const selectSearchBarClearSearch = state =>
5
+ selectSearchBarState(state)?.clearSearch;
@@ -11,7 +11,9 @@ const repoTypeSearchQueryMap = {
11
11
 
12
12
  const recommendedRepositoriesRHEL = [
13
13
  'rhel-9-for-x86_64-baseos-rpms',
14
+ 'rhel-9-for-x86_64-baseos-kickstart',
14
15
  'rhel-9-for-x86_64-appstream-rpms',
16
+ 'rhel-9-for-x86_64-appstream-kickstart',
15
17
  'rhel-8-for-x86_64-baseos-rpms',
16
18
  'rhel-8-for-x86_64-baseos-kickstart',
17
19
  'rhel-8-for-x86_64-appstream-rpms',
@@ -33,10 +35,10 @@ const recommendedRepositoriesSatTools = [
33
35
  ];
34
36
 
35
37
  const recommendedRepositoriesMisc = [
36
- 'satellite-capsule-6.12-for-rhel-8-x86_64-rpms',
38
+ 'satellite-capsule-6.13-for-rhel-8-x86_64-rpms',
37
39
  'ansible-2-for-rhel-8-x86_64-rpms',
38
- 'satellite-maintenance-6.12-for-rhel-8-x86_64-rpms',
39
- 'satellite-utils-6.12-for-rhel-8-x86_64-rpms',
40
+ 'satellite-maintenance-6.13-for-rhel-8-x86_64-rpms',
41
+ 'satellite-utils-6.13-for-rhel-8-x86_64-rpms',
40
42
  ];
41
43
 
42
44
  const recommendedRepositorySetLables = recommendedRepositoriesRHEL
@@ -4,7 +4,6 @@ import redHatRepositories from './RedHatRepositories';
4
4
  import { subscriptions } from '../../scenes/Subscriptions';
5
5
  import { upstreamSubscriptions } from '../../scenes/Subscriptions/UpstreamSubscriptions';
6
6
  import { manifestHistory } from '../../scenes/Subscriptions/Manifest';
7
- import settings from '../../scenes/Settings';
8
7
  import { subscriptionDetails } from '../../scenes/Subscriptions/Details';
9
8
  import { setOrganization } from '../../components/SelectOrg/SetOrganization';
10
9
  import { moduleStreams } from '../../scenes/ModuleStreams';
@@ -13,6 +12,7 @@ import { moduleStreamDetails } from '../../scenes/ModuleStreams/Details';
13
12
  import { reducers as systemStatuses } from '../../components/extensions/about';
14
13
  import { contentViewDetails } from '../../scenes/ContentViews/Details';
15
14
  import hostDetails from '../../components/extensions/HostDetails/HostDetailsReducer';
15
+ import searchBar from '../../components/extensions/SearchBar/SearchBarReducer';
16
16
 
17
17
  export default combineReducers({
18
18
  organization,
@@ -20,13 +20,13 @@ export default combineReducers({
20
20
  subscriptions,
21
21
  upstreamSubscriptions,
22
22
  manifestHistory,
23
- settings,
24
23
  subscriptionDetails,
25
24
  setOrganization,
26
25
  moduleStreams,
27
26
  moduleStreamDetails,
28
27
  contentViewDetails,
29
28
  hostDetails,
29
+ searchBar,
30
30
  ...organizationProductsReducers,
31
31
  ...systemStatuses,
32
32
  });
@@ -3,7 +3,7 @@ import * as reactRedux from 'react-redux';
3
3
  import { Route } from 'react-router-dom';
4
4
  import { act, fireEvent, patientlyWaitFor, renderWithRedux } from 'react-testing-lib-wrapper';
5
5
  import api, { foremanApi } from '../../../../services/api';
6
- import { assertNockRequest, mockAutocomplete, mockSetting, nockInstance } from '../../../../test-utils/nockWrapper';
6
+ import { assertNockRequest, mockAutocomplete, nockInstance } from '../../../../test-utils/nockWrapper';
7
7
  import ACSTable from '../../MainTable/ACSTable';
8
8
  import contentCredentialResult from './contentCredentials.fixtures';
9
9
  import smartProxyResult from './smartProxy.fixtures';
@@ -70,18 +70,6 @@ const renderOptions = {
70
70
  },
71
71
  };
72
72
 
73
- let searchDelayScope;
74
- let autoSearchScope;
75
- beforeEach(() => {
76
- searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 0);
77
- autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing');
78
- });
79
-
80
- afterEach(() => {
81
- assertNockRequest(searchDelayScope);
82
- assertNockRequest(autoSearchScope);
83
- });
84
-
85
73
  test('Can show add ACS button if can_create is true', async (done) => {
86
74
  const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl);
87
75
  const scope = nockInstance
@@ -19,6 +19,7 @@ import {
19
19
  TextListItem,
20
20
  TextListItemVariants,
21
21
  TextListVariants,
22
+ Text,
22
23
  Flex,
23
24
  FlexItem,
24
25
  } from '@patternfly/react-core';
@@ -104,7 +105,7 @@ const ACSExpandableDetails = ({ expandedId }) => {
104
105
  }}
105
106
  contentId="showDetails"
106
107
  >
107
- {__('Details')}
108
+ <Text ouiaId="expandable-details-text">{__('Details')}</Text>
108
109
  </ExpandableSectionToggle>
109
110
  </SplitItem>
110
111
  {canEdit &&
@@ -184,7 +185,7 @@ const ACSExpandableDetails = ({ expandedId }) => {
184
185
  }}
185
186
  contentId="showSmartProxies"
186
187
  >
187
- {__('Smart proxies')}
188
+ <Text ouiaId="expandable-smart-proxies-text">{__('Smart proxies')}</Text>
188
189
  </ExpandableSectionToggle>
189
190
  </SplitItem>
190
191
  {canEdit &&
@@ -255,7 +256,7 @@ const ACSExpandableDetails = ({ expandedId }) => {
255
256
  isExpanded={showProducts}
256
257
  contentId="showProducts"
257
258
  >
258
- {__('Products')}
259
+ <Text ouiaId="expandable-products-text">{__('Products')}</Text>
259
260
  </ExpandableSectionToggle>
260
261
  </SplitItem>
261
262
  {canEdit &&
@@ -306,7 +307,7 @@ const ACSExpandableDetails = ({ expandedId }) => {
306
307
  isExpanded={showUrlPaths}
307
308
  contentId="showUrlPaths"
308
309
  >
309
- {__('URL and subpaths')}
310
+ <Text ouiaId="expandable-url-paths-text">{__('URL and subpaths')}</Text>
310
311
  </ExpandableSectionToggle>
311
312
  </SplitItem>
312
313
  {canEdit &&
@@ -367,7 +368,7 @@ const ACSExpandableDetails = ({ expandedId }) => {
367
368
  isExpanded={showCredentials}
368
369
  contentId="showCredentials"
369
370
  >
370
- {__('Credentials')}
371
+ <Text ouiaId="expandable-credentials-text">{__('Credentials')}</Text>
371
372
  </ExpandableSectionToggle>
372
373
  </SplitItem>
373
374
  {canEdit &&