katello 4.5.0.rc1 → 4.5.0.rc2

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/katello/hosts/activation_key_edit.js +9 -2
  3. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +3 -0
  4. data/app/controllers/katello/api/v2/alternate_content_sources_bulk_actions_controller.rb +44 -0
  5. data/app/controllers/katello/api/v2/alternate_content_sources_controller.rb +29 -6
  6. data/app/controllers/katello/api/v2/content_view_components_controller.rb +1 -1
  7. data/app/controllers/katello/api/v2/content_view_repositories_controller.rb +1 -1
  8. data/app/controllers/katello/api/v2/repositories_controller.rb +2 -8
  9. data/app/lib/actions/katello/alternate_content_source/refresh.rb +27 -0
  10. data/app/lib/actions/katello/cdn_configuration/update.rb +1 -1
  11. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  12. data/app/lib/actions/katello/organization/manifest_refresh.rb +1 -1
  13. data/app/lib/actions/pulp3/alternate_content_source/delete.rb +2 -2
  14. data/app/lib/actions/pulp3/alternate_content_source/delete_remote.rb +2 -2
  15. data/app/lib/actions/pulp3/alternate_content_source/refresh.rb +23 -0
  16. data/app/lib/actions/pulp3/alternate_content_source/update.rb +2 -2
  17. data/app/lib/actions/pulp3/alternate_content_source/update_remote.rb +2 -2
  18. data/app/lib/actions/pulp3/orchestration/alternate_content_source/create.rb +0 -2
  19. data/app/lib/actions/pulp3/orchestration/alternate_content_source/refresh.rb +15 -0
  20. data/app/lib/actions/pulp3/orchestration/alternate_content_source/update.rb +0 -2
  21. data/app/lib/actions/pulp3/repository/refresh_distribution.rb +1 -4
  22. data/app/lib/actions/pulp3/repository/save_artifact.rb +1 -1
  23. data/app/lib/actions/pulp3/repository/save_distribution_references.rb +0 -2
  24. data/app/models/katello/alternate_content_source.rb +5 -0
  25. data/app/services/katello/pulp3/alternate_content_source.rb +6 -0
  26. data/app/services/katello/pulp3/content_view_version/metadata_map.rb +1 -1
  27. data/app/services/katello/pulp3/repository.rb +29 -1
  28. data/app/views/katello/api/v2/alternate_content_sources/base.json.rabl +10 -1
  29. data/app/views/katello/api/v2/content_facet/show.json.rabl +12 -0
  30. data/app/views/katello/api/v2/repository_sets/show.json.rabl +4 -0
  31. data/config/routes/api/v2.rb +16 -4
  32. data/db/migrate/20220303160220_remove_duplicate_errata.rb +1 -1
  33. data/db/migrate/20220428203334_add_last_refreshed_to_katello_alternate_content_sources.rb +5 -0
  34. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/capsule-content/capsule-content.controller.js +1 -1
  35. data/lib/katello/permission_creator.rb +4 -2
  36. data/lib/katello/tasks/refresh_alternate_content_sources.rake +10 -0
  37. data/lib/katello/version.rb +1 -1
  38. data/webpack/components/Bookmark/index.js +22 -14
  39. data/webpack/components/Search/Search.js +4 -0
  40. data/webpack/components/Table/MainTable.scss +5 -1
  41. data/webpack/components/Table/TableWrapper.js +5 -1
  42. data/webpack/components/TypeAhead/TypeAhead.js +4 -0
  43. data/webpack/components/TypeAhead/pf4Search/TypeAheadSearch.js +2 -0
  44. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ChangeHostCVModal.js +2 -8
  45. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ContentViewDetailsCard.js +41 -11
  46. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/HostCollectionsActions.js +2 -2
  47. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/HostCollectionsCard.js +32 -13
  48. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/__tests__/hostCollectionsCard.test.js +8 -0
  49. data/webpack/components/extensions/HostDetails/DetailsTabCards/RecentCommunicationCardExtensions.js +37 -0
  50. data/webpack/components/extensions/HostDetails/HostDetailsActions.js +11 -0
  51. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/SecondaryTabsRoutes.js +4 -0
  52. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/constants.js +2 -0
  53. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/index.js +6 -1
  54. data/webpack/components/extensions/HostDetails/Tabs/ErrataTab/ErrataTab.js +120 -51
  55. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsTab.js +71 -37
  56. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackageInstallModal.js +4 -3
  57. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js +117 -40
  58. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js +25 -3
  59. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js +85 -0
  60. data/webpack/components/extensions/HostDetails/Tabs/RepositorySetsTab/RepositorySetsTab.js +87 -33
  61. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/EnableTracerModal.js +14 -7
  62. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/HostTracesActions.js +2 -1
  63. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/TracesEnabler.js +104 -0
  64. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/TracesTab.js +92 -51
  65. data/webpack/components/extensions/HostDetails/Tabs/__tests__/errataTab.test.js +13 -23
  66. data/webpack/components/extensions/HostDetails/Tabs/{ModuleStreamsTab/__tests__/modules.fixtures.json → __tests__/moduleStreams.fixtures.json} +0 -0
  67. data/webpack/components/extensions/HostDetails/Tabs/{ModuleStreamsTab/__tests__ → __tests__}/moduleStreamsTab.test.js +13 -6
  68. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packageInstallModal.test.js +21 -15
  69. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packagesTab.test.js +8 -0
  70. data/webpack/components/extensions/HostDetails/Tabs/__tests__/repositorySets.fixtures.json +4 -1
  71. data/webpack/components/extensions/HostDetails/Tabs/__tests__/repositorySetsTab.test.js +26 -0
  72. data/webpack/components/extensions/HostDetails/Tabs/__tests__/tracesTab.test.js +7 -4
  73. data/webpack/components/extensions/HostDetails/hostDetailsHelpers.js +18 -0
  74. data/webpack/global_index.js +2 -2
  75. data/webpack/redux/actions/RedHatRepositories/helpers.js +5 -1
  76. data/webpack/scenes/AlternateContentSources/ACSActions.js +13 -1
  77. data/webpack/scenes/AlternateContentSources/ACSConstants.js +14 -0
  78. data/webpack/scenes/AlternateContentSources/ACSSelectors.js +10 -1
  79. data/webpack/scenes/AlternateContentSources/Create/ACSCreateContext.js +4 -0
  80. data/webpack/scenes/AlternateContentSources/Create/ACSCreateWizard.js +160 -0
  81. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSCreateFinish.js +79 -0
  82. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSCredentials.js +199 -0
  83. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSReview.js +104 -0
  84. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSSmartProxies.js +41 -0
  85. data/webpack/scenes/AlternateContentSources/Create/Steps/AcsUrlPaths.js +71 -0
  86. data/webpack/scenes/AlternateContentSources/Create/Steps/NameACS.js +57 -0
  87. data/webpack/scenes/AlternateContentSources/Create/Steps/SelectSource.js +77 -0
  88. data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreate.test.js +149 -0
  89. data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreateData.fixtures.json +3 -0
  90. data/webpack/scenes/AlternateContentSources/Create/__tests__/contentCredentials.fixtures.json +69 -0
  91. data/webpack/scenes/AlternateContentSources/Create/__tests__/smartProxy.fixtures.json +65 -0
  92. data/webpack/scenes/AlternateContentSources/MainTable/ACSTable.js +33 -23
  93. data/webpack/scenes/ContentCredentials/ContentCredentialSelectors.js +4 -1
  94. data/webpack/scenes/ContentViews/Create/CreateContentViewForm.js +2 -2
  95. data/webpack/scenes/ContentViews/Create/__tests__/createContentView.test.js +1 -1
  96. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js +1 -1
  97. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js +2 -2
  98. data/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js +4 -4
  99. data/webpack/scenes/ContentViews/Details/ContentViewInfo.js +1 -1
  100. data/webpack/scenes/ContentViews/__tests__/contentViewPage.test.js +4 -4
  101. data/webpack/scenes/ContentViews/components/ContentViewIcon.js +1 -1
  102. data/webpack/scenes/ContentViews/components/ContentViewsCounter.js +1 -1
  103. data/webpack/scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths.js +1 -1
  104. data/webpack/scenes/ContentViews/expansions/DetailsExpansion.js +2 -2
  105. data/webpack/scenes/ContentViews/expansions/RelatedContentViewComponentsModal.js +2 -2
  106. data/webpack/scenes/ContentViews/expansions/__tests__/contentViewComponentsModal.test.js +1 -1
  107. data/webpack/scenes/RedHatRepositories/components/Search.js +4 -4
  108. data/webpack/scenes/SmartProxy/SmartProxyContentActions.js +9 -2
  109. data/webpack/scenes/SmartProxy/SmartProxyContentConstants.js +1 -1
  110. data/webpack/scenes/SmartProxy/SmartProxyContentSelectors.js +10 -1
  111. data/webpack/scenes/Tasks/helpers.js +30 -3
  112. metadata +34 -14
  113. data/db/seeds.d/107-enable_dynflow.rb +0 -8
  114. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/EnableTracerEmptyState.js +0 -42
@@ -0,0 +1,149 @@
1
+ import React from 'react';
2
+ import * as reactRedux from 'react-redux';
3
+ import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper';
4
+ import api, { foremanApi } from '../../../../services/api';
5
+ import nock, { nockInstance, assertNockRequest, mockAutocomplete, mockSetting } from '../../../../test-utils/nockWrapper';
6
+ import ACSTable from '../../MainTable/ACSTable';
7
+ import contentCredentialResult from './contentCredentials.fixtures';
8
+ import smartProxyResult from './smartProxy.fixtures';
9
+
10
+ const ACSIndexPath = api.getApiUrl('/alternate_content_sources');
11
+ const ACSCreatePath = api.getApiUrl('/alternate_content_sources');
12
+ const contentCredentialPath = api.getApiUrl('/content_credentials');
13
+ const smartProxyPath = foremanApi.getApiUrl('/smart_proxies');
14
+ const autocompleteUrl = '/alternate_content_sources/auto_complete_search';
15
+
16
+ const createACSDetails = {
17
+ upstream_username: 'username',
18
+ upstream_password: 'password',
19
+ name: 'acs_test',
20
+ description: '',
21
+ base_url: 'https://test_url.com/',
22
+ subpaths: ['test/repo1/', 'test/repo2/'],
23
+ smart_proxy_names: ['centos7-katello-devel-stable.example.com'],
24
+ content_type: 'yum',
25
+ alternate_content_source_type: 'custom',
26
+ verify_ssl: false,
27
+ ssl_ca_cert_id: '',
28
+ };
29
+
30
+ const noResults = {
31
+ total: 0,
32
+ subtotal: 0,
33
+ page: 1,
34
+ per_page: 20,
35
+ results: [],
36
+ };
37
+
38
+ let searchDelayScope;
39
+ let autoSearchScope;
40
+ beforeEach(() => {
41
+ searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 0);
42
+ autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing');
43
+ });
44
+
45
+ afterEach(() => {
46
+ nock.cleanAll();
47
+ assertNockRequest(searchDelayScope);
48
+ assertNockRequest(autoSearchScope);
49
+ });
50
+
51
+ test('Can show add ACS button', async (done) => {
52
+ const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl);
53
+ const scope = nockInstance
54
+ .get(ACSIndexPath)
55
+ .query(true)
56
+ .reply(200, noResults);
57
+
58
+ const { queryByText } = renderWithRedux(<ACSTable />);
59
+
60
+ expect(queryByText("You currently don't have any alternate content sources.")).toBeNull();
61
+ await patientlyWaitFor(() => expect(queryByText("You currently don't have any alternate content sources.")).toBeInTheDocument());
62
+ expect(queryByText('Add source')).toBeInTheDocument();
63
+ assertNockRequest(autocompleteScope);
64
+ assertNockRequest(scope, done);
65
+ });
66
+
67
+ test('Can display create wizard and create ACS', async (done) => {
68
+ const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl);
69
+ const scope = nockInstance
70
+ .get(ACSIndexPath)
71
+ .query(true)
72
+ .reply(200, noResults);
73
+
74
+ const contentCredentialScope = nockInstance
75
+ .get(contentCredentialPath)
76
+ .query(true)
77
+ .reply(200, contentCredentialResult);
78
+
79
+ const smartProxyScope = nockInstance
80
+ .get(smartProxyPath)
81
+ .query(true)
82
+ .reply(200, smartProxyResult);
83
+
84
+ const createScope = nockInstance
85
+ .post(ACSCreatePath, createACSDetails)
86
+ .reply(201, { id: 22 });
87
+
88
+ const {
89
+ getByLabelText, getByText, getAllByRole, queryByText,
90
+ } = renderWithRedux(<ACSTable />);
91
+
92
+ expect(queryByText("You currently don't have any alternate content sources.")).toBeNull();
93
+ await patientlyWaitFor(() => expect(queryByText("You currently don't have any alternate content sources.")).toBeInTheDocument());
94
+ expect(queryByText('Add source')).toBeInTheDocument();
95
+ fireEvent.click(getByText('Add source'));
96
+
97
+ // First step: Select source
98
+ await patientlyWaitFor(() => {
99
+ expect(getByText('Add an alternate content source')).toBeInTheDocument();
100
+ expect(queryByText('Indicate the source type.')).toBeInTheDocument();
101
+ });
102
+
103
+ // Choose ACS type, content_type defaults to yum
104
+ fireEvent.click(getByText('Custom'));
105
+
106
+ // Go to next step: Name source
107
+ fireEvent.click(getByText('Next'));
108
+
109
+ await patientlyWaitFor(() => {
110
+ expect(getByText('Enter a name for your source.')).toBeInTheDocument();
111
+ });
112
+ // Enter Name
113
+ fireEvent.change(getByLabelText('acs_name_field'), { target: { value: 'acs_test' } });
114
+
115
+ // Mock smart proxy selector to go to next page
116
+ const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
117
+ useSelectorMock.mockReturnValue(smartProxyResult);
118
+ fireEvent.click(getByText('Next'));
119
+ fireEvent.click(getByLabelText('Add all'));
120
+
121
+ // Go to URL and subpath step
122
+ fireEvent.click(getByText('Next'));
123
+
124
+ fireEvent.change(getByLabelText('acs_base_url_field'), { target: { value: 'https://test_url.com/' } });
125
+ expect(getByLabelText('acs_base_url_field')).toHaveAttribute('value', 'https://test_url.com/');
126
+ fireEvent.change(getByLabelText('acs_subpath_field'), { target: { value: 'test/repo1/,test/repo2/' } });
127
+
128
+ // Mock content credential data
129
+ useSelectorMock.mockReturnValue(contentCredentialResult.results);
130
+ fireEvent.click(getByText('Next'));
131
+ const manualAuthRadio = getAllByRole('radio', { name: 'Manual authentication' })[0];
132
+ fireEvent.click(manualAuthRadio);
133
+ await patientlyWaitFor(() => {
134
+ expect(getByText('Username')).toBeInTheDocument();
135
+ expect(getByText('Password')).toBeInTheDocument();
136
+ });
137
+ fireEvent.change(getByLabelText('acs_username_field'), { target: { value: 'username' } });
138
+ fireEvent.change(getByLabelText('acs_password_field'), { target: { value: 'password' } });
139
+
140
+ fireEvent.click(getByText('Next'));
141
+ fireEvent.click(getByText('Add'));
142
+
143
+ useSelectorMock.mockClear();
144
+ assertNockRequest(autocompleteScope);
145
+ assertNockRequest(scope);
146
+ assertNockRequest(contentCredentialScope);
147
+ assertNockRequest(smartProxyScope);
148
+ assertNockRequest(createScope, done);
149
+ });
@@ -0,0 +1,69 @@
1
+ {
2
+ "total": 2,
3
+ "subtotal": 2,
4
+ "selectable": 2,
5
+ "page": 1,
6
+ "per_page": 20,
7
+ "error": null,
8
+ "search": null,
9
+ "sort": {
10
+ "by": "name",
11
+ "order": "asc"
12
+ },
13
+ "results": [
14
+ {
15
+ "name": "test",
16
+ "content_type": "cert",
17
+ "content": "abcdef",
18
+ "id": 1,
19
+ "organization_id": 1,
20
+ "organization": {
21
+ "name": "Default Organization",
22
+ "label": "Default_Organization",
23
+ "id": 1
24
+ },
25
+ "created_at": "2022-05-02 16:21:58 -0400",
26
+ "updated_at": "2022-05-02 16:21:58 -0400",
27
+ "gpg_key_products": [],
28
+ "gpg_key_repos": [],
29
+ "ssl_ca_products": [],
30
+ "ssl_ca_root_repos": [],
31
+ "ssl_client_products": [],
32
+ "ssl_client_root_repos": [],
33
+ "ssl_key_products": [],
34
+ "ssl_key_root_repos": [],
35
+ "permissions": {
36
+ "view_content_credenials": true,
37
+ "edit_content_credenials": true,
38
+ "destroy_content_credenials": true
39
+ }
40
+ },
41
+ {
42
+ "name": "test1",
43
+ "content_type": "gpg_key",
44
+ "content": "abcdef",
45
+ "id": 2,
46
+ "organization_id": 1,
47
+ "organization": {
48
+ "name": "Default Organization",
49
+ "label": "Default_Organization",
50
+ "id": 1
51
+ },
52
+ "created_at": "2022-05-02 16:22:30 -0400",
53
+ "updated_at": "2022-05-02 16:22:30 -0400",
54
+ "gpg_key_products": [],
55
+ "gpg_key_repos": [],
56
+ "ssl_ca_products": [],
57
+ "ssl_ca_root_repos": [],
58
+ "ssl_client_products": [],
59
+ "ssl_client_root_repos": [],
60
+ "ssl_key_products": [],
61
+ "ssl_key_root_repos": [],
62
+ "permissions": {
63
+ "view_content_credenials": true,
64
+ "edit_content_credenials": true,
65
+ "destroy_content_credenials": true
66
+ }
67
+ }
68
+ ]
69
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "total": 1,
3
+ "subtotal": 1,
4
+ "page": 1,
5
+ "per_page": 20,
6
+ "search": null,
7
+ "sort": {
8
+ "by": null,
9
+ "order": null
10
+ },
11
+ "results": [
12
+ {
13
+ "created_at": "2022-05-02 10:17:57 -0400",
14
+ "updated_at": "2022-05-02 10:24:21 -0400",
15
+ "hosts_count": 0,
16
+ "name": "centos7-katello-devel-stable.example.com",
17
+ "id": 1,
18
+ "url": "https://centos7-katello-devel-stable.example.com:9090",
19
+ "remote_execution_pubkey": "foreman-proxy@centos7-katello-devel-stable.example.com",
20
+ "download_policy": "on_demand",
21
+ "supported_pulp_types": [
22
+ "ansible_collection",
23
+ "deb",
24
+ "docker",
25
+ "file",
26
+ "python",
27
+ "yum"
28
+ ],
29
+ "features": [
30
+ {
31
+ "capabilities": [
32
+ "single",
33
+ "ssh"
34
+ ],
35
+ "name": "Dynflow",
36
+ "id": 17
37
+ },
38
+ {
39
+ "capabilities": [],
40
+ "name": "SSH",
41
+ "id": 18
42
+ },
43
+ {
44
+ "capabilities": [
45
+ "ansible",
46
+ "certguard",
47
+ "container",
48
+ "core",
49
+ "deb",
50
+ "file",
51
+ "python",
52
+ "rpm"
53
+ ],
54
+ "name": "Pulpcore",
55
+ "id": 3
56
+ },
57
+ {
58
+ "capabilities": [],
59
+ "name": "Logs",
60
+ "id": 13
61
+ }
62
+ ]
63
+ }
64
+ ]
65
+ }
@@ -9,19 +9,22 @@ import {
9
9
  selectAlternateContentSourcesStatus,
10
10
  } from '../ACSSelectors';
11
11
  import { useTableSort } from '../../../components/Table/TableHooks';
12
- import getAlternateContentSources, { deleteACS } from '../ACSActions';
12
+ import getAlternateContentSources, { deleteACS, refreshACS } from '../ACSActions';
13
+ import ACSCreateWizard from '../Create/ACSCreateWizard';
14
+ import LastSync from '../../ContentViews/Details/Repositories/LastSync';
13
15
 
14
16
  const ACSTable = () => {
15
17
  const response = useSelector(selectAlternateContentSources);
16
18
  const status = useSelector(selectAlternateContentSourcesStatus);
17
19
  const error = useSelector(selectAlternateContentSourcesError);
18
20
  const [searchQuery, updateSearchQuery] = useState('');
21
+ const [isCreateWizardOpen, setIsCreateWizardOpen] = useState(false);
19
22
  const dispatch = useDispatch();
20
23
  const { results, ...metadata } = response;
21
24
  const columnHeaders = [
22
25
  __('Name'),
23
26
  __('Type'),
24
- __('Id'),
27
+ __('Last Refresh'),
25
28
  ];
26
29
 
27
30
  const COLUMNS_TO_SORT_PARAMS = {
@@ -47,37 +50,35 @@ const ACSTable = () => {
47
50
  [apiSortParams],
48
51
  );
49
52
 
50
- // const createButtonOnclick = () => {
51
- // let params = {
52
- // name: `test_acs-${Math.random()}`,
53
- // label: `test_acs-${Math.random()}`,
54
- // base_url: "https://fixtures.pulpproject.org/",
55
- // subpaths: ["file/", "package/"],
56
- // smart_proxy_ids:[1],
57
- // content_type:"yum",
58
- // alternate_content_source_type:"custom"
59
- // };
60
- // dispatch(createACS(params));
61
- // };
62
-
63
53
  const onDelete = (id) => {
64
54
  dispatch(deleteACS(id, () =>
65
55
  dispatch(getAlternateContentSources())));
66
56
  };
67
57
 
58
+ const onRefresh = (id) => {
59
+ dispatch(refreshACS(id, () =>
60
+ dispatch(getAlternateContentSources())));
61
+ };
62
+
68
63
  const createButtonOnclick = () => {
69
- /* eslint-disable-next-line no-console */
70
- console.log('Dispatch create!');
64
+ setIsCreateWizardOpen(true);
71
65
  };
72
66
 
73
67
  const rowDropdownItems = ({ id }) => [
74
68
  {
75
- title: 'Delete',
69
+ title: __('Delete'),
76
70
  ouiaId: `remove-acs-${id}`,
77
71
  onClick: () => {
78
72
  onDelete(id);
79
73
  },
80
74
  },
75
+ {
76
+ title: __('Refresh'),
77
+ ouiaId: `remove-acs-${id}`,
78
+ onClick: () => {
79
+ onRefresh(id);
80
+ },
81
+ },
81
82
  ];
82
83
 
83
84
  const emptyContentTitle = __("You currently don't have any alternate content sources.");
@@ -104,9 +105,17 @@ const ACSTable = () => {
104
105
  additionalListeners={[activeSortColumn, activeSortDirection]}
105
106
  autocompleteEndpoint="/alternate_content_sources/auto_complete_search"
106
107
  actionButtons={
107
- <Button ouiaId="create-acs" onClick={createButtonOnclick} variant="primary" aria-label="create_acs">
108
- {__('Add source')}
109
- </Button>
108
+ <>
109
+ <Button ouiaId="create-acs" onClick={createButtonOnclick} variant="primary" aria-label="create_acs">
110
+ {__('Add source')}
111
+ </Button>
112
+ {isCreateWizardOpen &&
113
+ <ACSCreateWizard
114
+ show={isCreateWizardOpen}
115
+ setIsOpen={setIsCreateWizardOpen}
116
+ />
117
+ }
118
+ </>
110
119
  }
111
120
  >
112
121
  <Thead>
@@ -124,15 +133,16 @@ const ACSTable = () => {
124
133
  <Tbody>
125
134
  {results?.map((acs, index) => {
126
135
  const {
127
- id,
128
136
  name,
129
137
  alternate_content_source_type: acsType,
138
+ last_refresh: lastTask,
130
139
  } = acs;
140
+ const { last_refresh_words: lastRefreshWords, started_at: startedAt } = lastTask ?? {};
131
141
  return (
132
142
  <Tr key={index}>
133
143
  <Td>{name}</Td>
134
144
  <Td>{acsType}</Td>
135
- <Td>{id}</Td>
145
+ <Td><LastSync startedAt={startedAt} lastSync={lastTask} lastSyncWords={lastRefreshWords} emptyMessage="N/A" /></Td>
136
146
  <Td
137
147
  actions={{
138
148
  items: rowDropdownItems(acs),
@@ -1,6 +1,8 @@
1
1
  import {
2
+ selectAPIStatus,
2
3
  selectAPIResponse,
3
4
  } from 'foremanReact/redux/API/APISelectors';
5
+ import { STATUS } from 'foremanReact/constants';
4
6
 
5
7
  import { GET_CONTENT_CREDENTIALS_KEY } from './ContentCredentialConstants';
6
8
 
@@ -9,4 +11,5 @@ export const selectContentCredentials = (state) => {
9
11
  return response.results;
10
12
  };
11
13
 
12
- export default selectContentCredentials;
14
+ export const selectContentCredentialsStatus = state =>
15
+ selectAPIStatus(state, GET_CONTENT_CREDENTIALS_KEY) || STATUS.PENDING;
@@ -115,7 +115,7 @@ const CreateContentViewForm = ({ setModalOpen }) => {
115
115
  aria-label="component_tile"
116
116
  icon={<ContentViewIcon composite={false} />}
117
117
  id="component"
118
- title={__('Component content view')}
118
+ title={__('Content view')}
119
119
  onClick={() => { setComponent(true); setComposite(false); }}
120
120
  isSelected={component}
121
121
  >
@@ -133,7 +133,7 @@ const CreateContentViewForm = ({ setModalOpen }) => {
133
133
  onClick={() => { setComposite(true); setComponent(false); }}
134
134
  isSelected={composite}
135
135
  >
136
- {__('Consisting of multiple component content views')}
136
+ {__('Consisting of multiple content views')}
137
137
  </Tile>
138
138
  </GridItem>
139
139
  </Grid>
@@ -68,7 +68,7 @@ test('Displays dependent fields correctly', () => {
68
68
  expect(getByText('Name')).toBeInTheDocument();
69
69
  expect(getByText('Label')).toBeInTheDocument();
70
70
  expect(getByText('Composite content view')).toBeInTheDocument();
71
- expect(getByText('Component content view')).toBeInTheDocument();
71
+ expect(getByText('Content view')).toBeInTheDocument();
72
72
  expect(getByText('Solve dependencies')).toBeInTheDocument();
73
73
  expect(queryByText('Auto publish')).not.toBeInTheDocument();
74
74
  expect(getByText('Import only')).toBeInTheDocument();
@@ -87,7 +87,7 @@ const ComponentContentViewAddModal = ({
87
87
 
88
88
  return (
89
89
  <Modal
90
- title={componentId ? __('Update version') : __('Add component')}
90
+ title={componentId ? __('Update version') : __('Add content view')}
91
91
  variant={ModalVariant.small}
92
92
  isOpen={show}
93
93
  description={__(`Select available version of ${cvName} to use`)}
@@ -51,10 +51,10 @@ const ComponentContentViewBulkAddModal = ({ cvId, rowsToAdd, onClose }) => {
51
51
 
52
52
  return (
53
53
  <Modal
54
- title={__('Add component content views')}
54
+ title={__('Add content views')}
55
55
  variant={ModalVariant.large}
56
56
  isOpen
57
- description={__('Select available version of components to use')}
57
+ description={__('Select available version of content views to use')}
58
58
  onClose={onClose}
59
59
  appendTo={document.body}
60
60
  >
@@ -212,7 +212,7 @@ test('Can add published component views to content view with modal', async (done
212
212
  });
213
213
  fireEvent.click(getByText('Add'));
214
214
  await patientlyWaitFor(() => {
215
- expect(getByText('Add component')).toBeInTheDocument();
215
+ expect(getByText('Add content view')).toBeInTheDocument();
216
216
  });
217
217
  fireEvent.click(getByLabelText('add_component'));
218
218
  await patientlyWaitFor(() => {
@@ -317,7 +317,7 @@ test('Can bulk add component views to content view with modal', async (done) =>
317
317
  .reply(200, {});
318
318
 
319
319
  const {
320
- getByText, getByLabelText, queryByText,
320
+ getAllByText, getByLabelText, queryByText,
321
321
  } = renderWithRedux(
322
322
  <ContentViewComponents cvId={4} details={cvDetails} />,
323
323
  renderOptions,
@@ -333,14 +333,14 @@ test('Can bulk add component views to content view with modal', async (done) =>
333
333
  });
334
334
  fireEvent.click(getByLabelText('bulk_add_components'));
335
335
  await patientlyWaitFor(() => {
336
- expect(getByText('Add component content views')).toBeInTheDocument();
336
+ expect(getAllByText('Add content views')[1]).toBeInTheDocument();
337
337
  });
338
338
  fireEvent.click(getByLabelText('version-select-cv-10'));
339
339
  fireEvent.click(getByLabelText('cv-10-3.0'));
340
340
 
341
341
  fireEvent.click(getByLabelText('add_components'));
342
342
  await patientlyWaitFor(() => {
343
- expect(queryByText('Add component content views')).not.toBeInTheDocument();
343
+ expect(queryByText('Select available version of content views to use')).not.toBeInTheDocument();
344
344
  expect(getByLabelText('bulk_add_components')).toHaveAttribute('aria-disabled', 'false');
345
345
  });
346
346
 
@@ -71,7 +71,7 @@ const ContentViewInfo = ({ cvId, details }) => {
71
71
  <TextListItem component={TextListItemVariants.dd} className="foreman-spaced-list">
72
72
  <Flex>
73
73
  <FlexItem spacer={{ default: 'spacerXs' }}>
74
- <ContentViewIcon composite={composite} description={composite ? __('Composite') : __('Component')} />
74
+ <ContentViewIcon composite={composite} description={composite ? __('Composite') : __('Content view')} />
75
75
  </FlexItem>
76
76
  </Flex>
77
77
  </TextListItem>
@@ -43,14 +43,14 @@ test('Can call API for CVs and show on screen on page load', async (done) => {
43
43
  .query(true)
44
44
  .reply(200, cvIndexData);
45
45
 
46
- const { queryByText } = renderWithRedux(<ContentViewsPage />, renderOptions);
46
+ const { queryByText, queryAllByText } = renderWithRedux(<ContentViewsPage />, renderOptions);
47
47
 
48
48
  expect(queryByText(firstCV.name)).toBeNull();
49
49
 
50
50
  // Assert that the CV name is now showing on the screen, but wait for it to appear.
51
51
  await patientlyWaitFor(() => {
52
52
  expect(queryByText(firstCV.name)).toBeInTheDocument();
53
- expect(queryByText('Component content views')).toBeInTheDocument();
53
+ expect(queryAllByText('Content views')[0]).toBeInTheDocument();
54
54
  expect(queryByText('Composite content views')).toBeInTheDocument();
55
55
  });
56
56
 
@@ -354,7 +354,7 @@ test('Displays Create Content View and opens modal with Form', async () => {
354
354
  expect(queryByText('Name')).not.toBeInTheDocument();
355
355
  expect(queryByText('Label')).not.toBeInTheDocument();
356
356
  expect(queryByText('Composite content view')).not.toBeInTheDocument();
357
- expect(queryByText('Component content view')).not.toBeInTheDocument();
357
+ expect(queryByText('Content view')).not.toBeInTheDocument();
358
358
  expect(queryByText('Solve dependencies')).not.toBeInTheDocument();
359
359
  expect(queryByText('Auto publish')).not.toBeInTheDocument();
360
360
  expect(queryByText('Import only')).not.toBeInTheDocument();
@@ -365,7 +365,7 @@ test('Displays Create Content View and opens modal with Form', async () => {
365
365
  expect(getByText('Name')).toBeInTheDocument();
366
366
  expect(getByText('Label')).toBeInTheDocument();
367
367
  expect(getByText('Composite content view')).toBeInTheDocument();
368
- expect(getByText('Component content view')).toBeInTheDocument();
368
+ expect(getByText('Content view')).toBeInTheDocument();
369
369
  expect(getByText('Solve dependencies')).toBeInTheDocument();
370
370
  expect(queryByText('Auto publish')).not.toBeInTheDocument();
371
371
  expect(getByText('Import only')).toBeInTheDocument();
@@ -16,7 +16,7 @@ const ContentViewIcon = ({
16
16
  position="auto"
17
17
  enableFlip
18
18
  entryDelay={400}
19
- content={composite ? __('Composite content view') : __('Component content view')}
19
+ content={composite ? __('Composite content view') : __('Content view')}
20
20
  {...toolTipProps}
21
21
  >
22
22
  {composite ? <RegistryIcon size="md" {...props} /> : <EnterpriseIcon size="sm" {...props} />}
@@ -18,7 +18,7 @@ const ContentViewsCounter = () => {
18
18
  <b>
19
19
  <Flex>
20
20
  <FlexItem spacer={{ default: 'spacerXs' }}>
21
- <ContentViewIcon composite={false} description={__('Component content views')} count={(component || component === 0) ? component : <InProgressIcon />} />
21
+ <ContentViewIcon composite={false} description={__('Content views')} count={(component || component === 0) ? component : <InProgressIcon />} />
22
22
  </FlexItem>
23
23
  <FlexItem>
24
24
  <Tooltip
@@ -40,7 +40,7 @@ const EnvironmentPaths = ({
40
40
  <>
41
41
  <TextContent>{headerText}</TextContent>
42
42
  <div>
43
- {results.map((path, index) => {
43
+ {results?.map((path, index) => {
44
44
  const {
45
45
  environments,
46
46
  } = path || {};
@@ -14,14 +14,14 @@ const DetailsExpansion = ({
14
14
  if (cvComposite) {
15
15
  return (
16
16
  <>
17
- {__('Related component cvs: ')}
17
+ {__('Related content views: ')}
18
18
  <RelatedContentViewComponentsModal key="cvId" {...{ cvName, cvId, relatedCVCount }} />
19
19
  </>
20
20
  );
21
21
  }
22
22
  return (
23
23
  <>
24
- {__('Related composite cvs: ')}
24
+ {__('Related composite content views: ')}
25
25
  <RelatedCompositeContentViewsModal
26
26
  key={cvId}
27
27
  {...{
@@ -29,7 +29,7 @@ const RelatedContentViewsModal = ({ cvName, cvId, relatedCVCount }) => {
29
29
  <FlexItem>
30
30
  <RegistryIcon />
31
31
  <b>{` ${cvName}`}</b>
32
- {__(' content view is used in listed component content views. For more information, ')}
32
+ {__(' content view is used in listed content views. For more information, ')}
33
33
  <Link to={urlBuilder(`content_views/${cvId}#/contentviews`, '')}>
34
34
  {__('view content view tabs.')}
35
35
  </Link>
@@ -49,7 +49,7 @@ const RelatedContentViewsModal = ({ cvName, cvId, relatedCVCount }) => {
49
49
  <Grid>
50
50
  <GridItem span={12}>
51
51
  <Modal
52
- title={__('Related component content views')}
52
+ title={__('Related content views')}
53
53
  variant={ModalVariant.medium}
54
54
  isOpen={isOpen}
55
55
  description={description()}
@@ -30,7 +30,7 @@ test('Can call API and show Related Content Views Components Modal', async (done
30
30
 
31
31
  await patientlyWaitFor(() => expect(getByLabelText(`button_${cvId}`)).toBeInTheDocument());
32
32
  fireEvent.click(getByLabelText(`button_${cvId}`));
33
- await patientlyWaitFor(() => expect(getByText('Related component content views')).toBeInTheDocument());
33
+ await patientlyWaitFor(() => expect(getByText('Related content views')).toBeInTheDocument());
34
34
 
35
35
  assertNockRequest(scope, done);
36
36
  });
@@ -2,7 +2,7 @@
2
2
  import React, { Component } from 'react';
3
3
  import { DropdownButton, MenuItem } from 'patternfly-react';
4
4
  import PropTypes from 'prop-types';
5
-
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
6
  import '../index.scss';
7
7
  import Search from '../../../components/Search/index';
8
8
  import { orgId } from '../../../services/api';
@@ -14,17 +14,17 @@ class RepositorySearch extends Component {
14
14
  {
15
15
  key: 'available',
16
16
  endpoint: 'repository_sets',
17
- title: 'Available',
17
+ title: __('Available'),
18
18
  },
19
19
  {
20
20
  key: 'enabled',
21
21
  endpoint: 'enabled_repositories',
22
- title: 'Enabled',
22
+ title: __('Enabled'),
23
23
  },
24
24
  {
25
25
  key: 'both',
26
26
  endpoint: false,
27
- title: 'Both',
27
+ title: __('Both'),
28
28
  },
29
29
  ];
30
30
  this.state = { searchList: this.dropDownItems[0] };