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.
- checksums.yaml +4 -4
- data/app/controllers/katello/api/registry/registry_proxies_controller.rb +2 -18
- data/app/controllers/katello/api/v2/alternate_content_sources_controller.rb +7 -5
- data/app/controllers/katello/api/v2/repositories_controller.rb +3 -18
- data/app/helpers/katello/hosts_and_hostgroups_helper.rb +13 -8
- data/app/lib/actions/katello/alternate_content_source/create.rb +3 -1
- data/app/lib/actions/katello/alternate_content_source/update.rb +3 -1
- data/app/lib/actions/pulp3/orchestration/content_view_version/copy_version_units_to_library.rb +1 -1
- data/app/lib/actions/pulp3/orchestration/content_view_version/export.rb +11 -11
- data/app/lib/actions/pulp3/orchestration/content_view_version/syncable_export.rb +0 -2
- data/app/lib/actions/pulp3/repository/reclaim_space.rb +1 -1
- data/app/lib/katello/api/v2/error_handling.rb +12 -2
- data/app/lib/katello/concerns/base_template_scope_extensions.rb +7 -3
- data/app/models/katello/alternate_content_source.rb +54 -4
- data/app/models/katello/concerns/host_managed_extensions.rb +14 -0
- data/app/models/katello/glue/provider.rb +1 -1
- data/app/models/katello/host/content_facet.rb +2 -0
- data/app/services/katello/pulp3/content_view_version/export_validation_error.rb +1 -1
- data/app/services/katello/pulp3/content_view_version/export_validator.rb +16 -0
- data/app/views/foreman/smart_proxies/_content_sync.html.erb +1 -1
- data/db/migrate/20230203141353_set_new_acs_verify_ssl_default.rb +5 -0
- data/db/seeds.d/111-upgrade_tasks.rb +2 -1
- data/lib/katello/plugin.rb +0 -12
- data/lib/katello/tasks/upgrades/4.8/regenerate_imported_repository_metadata.rake +33 -0
- data/lib/katello/version.rb +1 -1
- data/webpack/components/Content/{ContentPage.js → GenericContentPage.js} +7 -4
- data/webpack/components/Content/__tests__/ContentTable.test.js +1 -1
- data/webpack/components/Content/__tests__/GenericContentPage.test.js +35 -0
- data/webpack/components/Search/SearchText.js +70 -0
- data/webpack/components/Table/EmptyStateMessage.js +2 -2
- data/webpack/components/Table/TableWrapper.js +4 -0
- data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ChangeHostCVModal.js +10 -72
- data/webpack/components/extensions/HostDetails/HostDetailsConstants.js +0 -1
- data/webpack/components/extensions/HostDetails/HostDetailsSelectors.js +0 -6
- data/webpack/components/extensions/HostDetails/Tabs/__tests__/moduleStreamsTab.test.js +76 -0
- data/webpack/components/extensions/SearchBar/SearchBarConstants.js +3 -0
- data/webpack/components/extensions/SearchBar/SearchBarHooks.js +50 -0
- data/webpack/components/extensions/SearchBar/SearchBarReducer.js +14 -0
- data/webpack/components/extensions/SearchBar/SearchBarSelectors.js +5 -0
- data/webpack/redux/actions/RedHatRepositories/helpers.js +5 -3
- data/webpack/redux/reducers/index.js +2 -2
- data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreate.test.js +1 -13
- data/webpack/scenes/AlternateContentSources/Details/ACSExpandableDetails.js +6 -5
- data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditCredentials.js +1 -0
- data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditDetails.js +3 -2
- data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditProducts.js +1 -0
- data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditSmartProxies.js +2 -0
- data/webpack/scenes/AlternateContentSources/Details/EditModals/ACSEditURLPaths.js +1 -0
- data/webpack/scenes/AlternateContentSources/MainTable/ACSTable.js +1 -0
- data/webpack/scenes/Content/{ContentPage.js → GenericContentPage.js} +2 -2
- data/webpack/scenes/Content/__tests__/contentTable.test.js +2 -2
- data/webpack/scenes/Content/index.js +2 -2
- data/webpack/scenes/ContentViews/Details/ContentViewDetails.js +1 -0
- data/webpack/scenes/ContentViews/Details/Filters/Rules/ContainerTag/AddEditContainerTagRuleModal.js +14 -17
- data/webpack/scenes/ContentViews/Details/Filters/Rules/Package/AddEditPackageRuleModal.js +24 -28
- data/webpack/scenes/ContentViews/Details/Filters/__tests__/CVContainerImageFilterContent.test.js +11 -18
- data/webpack/scenes/ContentViews/Details/Filters/__tests__/CVRpmFilterContent.test.js +10 -23
- data/webpack/scenes/ContentViews/Details/Filters/__tests__/ContentViewPackageGroupFilter.test.js +0 -2
- data/webpack/scenes/ContentViews/Details/Versions/__tests__/contentViewVersions.test.js +1 -7
- data/webpack/scenes/ContentViews/components/ContentViewSelect/ContentViewSelectOption.js +87 -0
- data/webpack/scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths.js +1 -1
- data/webpack/scenes/ContentViews/expansions/__tests__/contentViewComponentsModal.test.js +0 -2
- data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +153 -28
- data/webpack/scenes/Hosts/ChangeContentSource/index.js +14 -15
- data/webpack/scenes/Hosts/ChangeContentSource/selectors.js +4 -0
- data/webpack/scenes/Hosts/ChangeContentSource/styles.scss +4 -0
- data/webpack/scenes/ModuleStreams/ModuleStreamsPage.js +2 -2
- data/webpack/scenes/ModuleStreams/__tests__/ModuleStreamPage.test.js +2 -2
- data/webpack/scenes/ModuleStreams/__tests__/__snapshots__/ModuleStreamPage.test.js.snap +1 -1
- data/webpack/scenes/Settings/SettingsConstants.js +2 -3
- data/webpack/scenes/Settings/SettingsReducer.js +2 -16
- data/webpack/scenes/Settings/SettingsSelectors.js +2 -2
- data/webpack/test-utils/react-testing-lib-wrapper.js +0 -6
- metadata +16 -25
- data/webpack/components/Content/__tests__/ContentPage.test.js +0 -32
- data/webpack/components/Content/__tests__/__snapshots__/ContentPage.test.js.snap +0 -89
- data/webpack/components/Search/Search.js +0 -156
- data/webpack/components/Search/__tests__/search.test.js +0 -104
- data/webpack/components/Search/helpers.js +0 -6
- data/webpack/components/Search/index.js +0 -15
- data/webpack/components/TypeAhead/TypeAhead.js +0 -157
- data/webpack/components/TypeAhead/TypeAhead.scss +0 -7
- data/webpack/components/TypeAhead/helpers/commonPropTypes.js +0 -35
- data/webpack/components/TypeAhead/helpers/helpers.js +0 -32
- data/webpack/components/TypeAhead/index.js +0 -3
- data/webpack/components/TypeAhead/pf3Search/TypeAheadInput.js +0 -44
- data/webpack/components/TypeAhead/pf3Search/TypeAheadItems.js +0 -56
- data/webpack/components/TypeAhead/pf3Search/TypeAheadSearch.js +0 -53
- data/webpack/components/TypeAhead/pf4Search/TypeAheadInput.js +0 -66
- data/webpack/components/TypeAhead/pf4Search/TypeAheadInput.scss +0 -12
- data/webpack/components/TypeAhead/pf4Search/TypeAheadItems.js +0 -59
- 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
|
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
|
-
|
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
|
-
|
68
|
+
GenericContentPage.defaultProps = {
|
66
69
|
autocompleteEndpoint: undefined,
|
67
70
|
autocompleteQueryParams: undefined,
|
68
71
|
bookmarkController: undefined,
|
69
72
|
};
|
70
73
|
|
71
|
-
export default
|
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 {
|
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(
|
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
|
}
|
data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ChangeHostCVModal.js
CHANGED
@@ -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,
|
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:
|
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
|
197
|
-
|
198
|
-
|
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>
|
@@ -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,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
|
+
};
|
@@ -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.
|
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.
|
39
|
-
'satellite-utils-6.
|
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,
|
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 &&
|