katello 4.4.0.rc1 → 4.4.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/api/v2/repositories_bulk_actions_controller.rb +10 -1
  3. data/app/controllers/katello/api/v2/repositories_controller.rb +3 -0
  4. data/app/lib/actions/katello/repository/errata_mail.rb +4 -5
  5. data/app/lib/actions/katello/repository/sync.rb +2 -2
  6. data/app/lib/actions/pulp3/abstract.rb +1 -1
  7. data/app/lib/actions/pulp3/content_view/delete_repository_references.rb +14 -4
  8. data/app/lib/actions/pulp3/content_view_version/create_import_history.rb +1 -2
  9. data/app/lib/actions/pulp3/repository/reclaim_space.rb +3 -10
  10. data/app/lib/katello/util/pulpcore_content_filters.rb +2 -1
  11. data/app/models/katello/candlepin/repository_mapper.rb +1 -0
  12. data/app/models/katello/concerns/audit_comment_extensions.rb +17 -0
  13. data/app/models/katello/concerns/host_managed_extensions.rb +11 -1
  14. data/app/models/katello/concerns/smart_proxy_extensions.rb +1 -0
  15. data/app/models/katello/content_view_version_export_history.rb +2 -1
  16. data/app/models/katello/content_view_version_import_history.rb +4 -4
  17. data/app/models/katello/host_available_module_stream.rb +10 -0
  18. data/app/models/katello/installed_package.rb +1 -0
  19. data/app/models/katello/root_repository.rb +14 -2
  20. data/app/models/setting/content.rb +9 -2
  21. data/app/services/katello/pulp3/api/yum.rb +4 -0
  22. data/app/services/katello/pulp3/repository/yum.rb +11 -4
  23. data/app/services/katello/pulp3/repository.rb +4 -2
  24. data/app/views/foreman/job_templates/remove_packages_by_search_query.erb +19 -0
  25. data/app/views/foreman/job_templates/update_packages_by_search_query.erb +19 -0
  26. data/app/views/katello/api/v2/content_views/base.json.rabl +8 -4
  27. data/app/views/katello/api/v2/host_module_streams/base.json.rabl +1 -0
  28. data/db/migrate/20210119162528_delete_puppet_and_ostree_repos.rb +2 -0
  29. data/db/migrate/20211019192121_create_cdn_configuration.katello.rb +11 -2
  30. data/db/migrate/20220209205137_expand_sync_timeout_settings.rb +23 -0
  31. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/errata/apply-errata.controller.js +10 -3
  32. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/repository-details-info.controller.js +4 -0
  33. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/new-repository.controller.js +17 -12
  34. data/lib/katello/plugin.rb +2 -0
  35. data/lib/katello/repository_types/ostree.rb +0 -6
  36. data/lib/katello/version.rb +1 -1
  37. data/webpack/components/Errata/index.js +57 -57
  38. data/webpack/components/ErratumTypeLabel.js +16 -16
  39. data/webpack/components/MultiSelect/index.js +2 -2
  40. data/webpack/components/Select/Select.js +1 -1
  41. data/webpack/components/SelectOrg/SelectOrgReducer.js +15 -15
  42. data/webpack/components/SelectOrg/SetOrganization.js +1 -1
  43. data/webpack/components/Table/TableHooks.js +1 -0
  44. data/webpack/components/Table/TableWrapper.js +4 -1
  45. data/webpack/components/TypeAhead/helpers/commonPropTypes.js +1 -1
  46. data/webpack/components/TypeAhead/helpers/helpers.js +14 -14
  47. data/webpack/components/TypeAhead/pf3Search/TypeAheadSearch.js +1 -1
  48. data/webpack/components/WithOrganization/withOrganization.js +3 -3
  49. data/webpack/components/extensions/HostDetails/HostPackages/HostPackagesActions.js +32 -1
  50. data/webpack/components/extensions/HostDetails/HostPackages/HostPackagesConstants.js +3 -2
  51. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/SecondaryTabsRoutes.js +5 -3
  52. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/constants.js +1 -0
  53. data/webpack/components/extensions/HostDetails/Tabs/ErrataTab/ErratumExpansionContents.js +3 -3
  54. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsActions.js +16 -0
  55. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsConstants.js +3 -0
  56. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsSelectors.js +19 -0
  57. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsTab.js +241 -0
  58. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/__tests__/moduleStreamsTab.test.js +108 -0
  59. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/__tests__/modules.fixtures.json +34 -0
  60. data/webpack/components/extensions/HostDetails/Tabs/PackageInstallModal.js +5 -5
  61. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab.js +255 -79
  62. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js +76 -0
  63. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionConstants.js +4 -0
  64. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packagesTab.test.js +259 -9
  65. data/webpack/components/extensions/HostDetails/Tabs/__tests__/tracesTab.test.js +22 -26
  66. data/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js +13 -0
  67. data/webpack/components/extensions/RegistrationCommands/__tests__/__snapshots__/ActivationKeys.test.js.snap +4 -0
  68. data/webpack/components/extensions/RegistrationCommands/fields/ActivationKeys.js +1 -1
  69. data/webpack/components/extensions/RegistrationCommands/fields/LifecycleEnvironment.js +1 -1
  70. data/webpack/components/extensions/about/SystemStatuses.js +1 -1
  71. data/webpack/components/extensions/about/SystemStatusesReducer.js +10 -10
  72. data/webpack/components/pf3Table/components/Table.js +2 -2
  73. data/webpack/components/pf3Table/components/TableBody.js +2 -2
  74. data/webpack/redux/OrganizationProducts/OrganizationProductsReducer.js +15 -15
  75. data/webpack/redux/reducers/RedHatRepositories/enabled.js +43 -43
  76. data/webpack/redux/reducers/RedHatRepositories/repositorySetRepositories.js +43 -43
  77. data/webpack/redux/reducers/RedHatRepositories/sets.js +31 -31
  78. data/webpack/scenes/AnsibleCollections/AnsibleCollectionsReducer.js +26 -26
  79. data/webpack/scenes/AnsibleCollections/Details/AnsibleCollectionDetailsReducer.js +19 -19
  80. data/webpack/scenes/Content/Table/ContentTable.js +1 -1
  81. data/webpack/scenes/ContentViews/Create/CreateContentViewForm.js +2 -1
  82. data/webpack/scenes/ContentViews/Delete/Steps/CVDeletionReassignHostsForm.js +2 -2
  83. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js +1 -1
  84. data/webpack/scenes/ContentViews/Details/ContentViewDetailActions.js +1 -1
  85. data/webpack/scenes/ContentViews/Details/ContentViewDetailReducer.js +8 -8
  86. data/webpack/scenes/ContentViews/Details/Filters/AffectedRepositories/AffectedRepositoryTable.js +1 -1
  87. data/webpack/scenes/ContentViews/Details/Filters/ArtifactsWithNoErrata.js +8 -8
  88. data/webpack/scenes/ContentViews/Details/Filters/CVContainerImageFilterContent.js +1 -1
  89. data/webpack/scenes/ContentViews/Details/Filters/CVErrataDateFilterContent.js +1 -1
  90. data/webpack/scenes/ContentViews/Details/Filters/CVErrataIDFilterContent.js +1 -1
  91. data/webpack/scenes/ContentViews/Details/Filters/CVFilterDetailType.js +46 -46
  92. data/webpack/scenes/ContentViews/Details/Filters/CVModuleStreamFilterContent.js +14 -14
  93. data/webpack/scenes/ContentViews/Details/Filters/CVPackageGroupFilterContent.js +14 -14
  94. data/webpack/scenes/ContentViews/Details/Filters/CVRpmFilterContent.js +1 -1
  95. data/webpack/scenes/ContentViews/Details/Filters/ContentViewFilters.js +1 -1
  96. data/webpack/scenes/ContentViews/Details/Filters/Rules/Package/AddEditPackageRuleModal.js +17 -17
  97. data/webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js +1 -1
  98. data/webpack/scenes/ContentViews/Details/Versions/ContentViewVersionContent.js +19 -18
  99. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVEnvironmentSelectionForm.js +18 -18
  100. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignActivationKeysForm.js +3 -3
  101. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignHostsForm.js +3 -3
  102. data/webpack/scenes/ContentViews/Details/Versions/Delete/affectedActivationKeys.js +1 -1
  103. data/webpack/scenes/ContentViews/Details/Versions/Delete/affectedHosts.js +1 -1
  104. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetailConfig.js +1 -1
  105. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetailsTable.js +46 -34
  106. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionRepositoryCell.js +65 -48
  107. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/__tests__/ContentViewVersionDetails.test.js +1 -1
  108. data/webpack/scenes/ContentViews/Publish/CVPublishFinish.js +2 -2
  109. data/webpack/scenes/ContentViews/Table/ContentViewsTable.js +4 -4
  110. data/webpack/scenes/ContentViews/components/EnvironmentLabels.js +18 -18
  111. data/webpack/scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths.js +10 -10
  112. data/webpack/scenes/ContentViews/expansions/DetailsExpansion.js +2 -2
  113. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +7 -7
  114. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceTemplate.js +7 -7
  115. data/webpack/scenes/Hosts/ChangeContentSource/components/FormField.js +3 -3
  116. data/webpack/scenes/ModuleStreams/Details/ModuleStreamDetailsReducer.js +18 -18
  117. data/webpack/scenes/ModuleStreams/ModuleStreamsReducer.js +26 -26
  118. data/webpack/scenes/Organizations/OrganizationReducer.js +8 -8
  119. data/webpack/scenes/RedHatRepositories/components/EnabledRepository/EnabledRepositoryContent.js +4 -4
  120. data/webpack/scenes/RedHatRepositories/components/RepositorySetRepositories.js +1 -1
  121. data/webpack/scenes/Settings/SettingsReducer.js +14 -14
  122. data/webpack/scenes/Settings/Tables/TableReducer.js +23 -23
  123. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailInfo.js +2 -2
  124. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailProductContent.js +15 -15
  125. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailProducts.js +1 -1
  126. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailReducer.js +34 -34
  127. data/webpack/scenes/Subscriptions/Details/SubscriptionDetails.js +13 -13
  128. data/webpack/scenes/Subscriptions/Manifest/CdnConfigurationTab/AirGappedTypeForm.js +3 -3
  129. data/webpack/scenes/Subscriptions/Manifest/CdnConfigurationTab/CdnTypeForm.js +4 -4
  130. data/webpack/scenes/Subscriptions/Manifest/CdnConfigurationTab/UpstreamServerTypeForm.js +3 -3
  131. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +5 -5
  132. data/webpack/scenes/Subscriptions/Manifest/ManifestHistoryReducer.js +16 -16
  133. data/webpack/scenes/Subscriptions/SubscriptionReducer.js +149 -149
  134. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsReducer.js +41 -41
  135. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/components/Dialogs/DeleteDialog.js +6 -6
  136. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/components/Table.js +12 -12
  137. data/webpack/services/index.js +36 -36
  138. data/webpack/utils/helpers.js +5 -5
  139. metadata +16 -6
@@ -2,9 +2,10 @@ import React from 'react';
2
2
  import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper';
3
3
  import { nockInstance, assertNockRequest, mockForemanAutocomplete, mockSetting } from '../../../../../test-utils/nockWrapper';
4
4
  import { foremanApi } from '../../../../../services/api';
5
- import { HOST_PACKAGES_KEY } from '../../HostPackages/HostPackagesConstants';
5
+ import { HOST_PACKAGES_KEY, PACKAGES_SEARCH_QUERY } from '../../HostPackages/HostPackagesConstants';
6
6
  import { PackagesTab } from '../PackagesTab';
7
7
  import mockPackagesData from './packages.fixtures.json';
8
+ import { REX_FEATURES } from '../RemoteExecutionConstants';
8
9
 
9
10
  const contentFacetAttributes = {
10
11
  id: 11,
@@ -13,6 +14,7 @@ const contentFacetAttributes = {
13
14
  lifecycle_environment_library: false,
14
15
  };
15
16
 
17
+ const hostname = 'test-host.example.com';
16
18
  const renderOptions = (facetAttributes = contentFacetAttributes) => ({
17
19
  apiNamespace: HOST_PACKAGES_KEY,
18
20
  initialState: {
@@ -20,7 +22,7 @@ const renderOptions = (facetAttributes = contentFacetAttributes) => ({
20
22
  HOST_DETAILS: {
21
23
  response: {
22
24
  id: 1,
23
- name: 'test-host',
25
+ name: hostname,
24
26
  content_facet_attributes: { ...facetAttributes },
25
27
  },
26
28
  status: 'RESOLVED',
@@ -30,6 +32,7 @@ const renderOptions = (facetAttributes = contentFacetAttributes) => ({
30
32
  });
31
33
 
32
34
  const hostPackages = foremanApi.getApiUrl('/hosts/1/packages');
35
+ const jobInvocations = foremanApi.getApiUrl('/job_invocations');
33
36
  const autocompleteUrl = '/hosts/1/packages/auto_complete_search';
34
37
  const defaultQueryWithoutSearch = {
35
38
  include_latest_upgradable: true,
@@ -38,13 +41,14 @@ const defaultQueryWithoutSearch = {
38
41
  };
39
42
  const defaultQuery = { ...defaultQueryWithoutSearch, search: '' };
40
43
 
41
- let firstPackages;
44
+ let firstPackage;
45
+ let secondPackage;
42
46
  let searchDelayScope;
43
47
  let autoSearchScope;
44
48
 
45
49
  beforeEach(() => {
46
50
  const { results } = mockPackagesData;
47
- [firstPackages] = results;
51
+ [firstPackage, secondPackage] = results;
48
52
  searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 0);
49
53
  autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing');
50
54
  });
@@ -65,7 +69,7 @@ test('Can call API for packages and show on screen on page load', async (done) =
65
69
  const { getAllByText } = renderWithRedux(<PackagesTab />, renderOptions());
66
70
 
67
71
  // Assert that the packages are now showing on the screen, but wait for them to appear.
68
- await patientlyWaitFor(() => expect(getAllByText(firstPackages.name)[0]).toBeInTheDocument());
72
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
69
73
  // Assert request was made and completed, see helper function
70
74
  assertNockRequest(autocompleteScope);
71
75
  assertNockRequest(scope, done); // Pass jest callback to confirm test is done
@@ -107,7 +111,7 @@ test('Can filter by package status', async (done) => {
107
111
  const scope2 = nockInstance
108
112
  .get(hostPackages)
109
113
  .query({ ...defaultQuery, status: 'upgradable' })
110
- .reply(200, { ...mockPackagesData, results: [firstPackages] });
114
+ .reply(200, { ...mockPackagesData, results: [firstPackage] });
111
115
 
112
116
  const {
113
117
  queryByText,
@@ -116,8 +120,7 @@ test('Can filter by package status', async (done) => {
116
120
  getByText,
117
121
  } = renderWithRedux(<PackagesTab />, renderOptions());
118
122
 
119
- // Assert that the errata are now showing on the screen, but wait for them to appear.
120
- await patientlyWaitFor(() => expect(getAllByText(firstPackages.name)[0]).toBeInTheDocument());
123
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
121
124
  // the Upgradable text in the table is just a text node, while the dropdown is a button
122
125
  expect(getByText('Up-to date', { ignore: ['button', 'title'] })).toBeInTheDocument();
123
126
  expect(getByText('coreutils', { ignore: ['button', 'title'] })).toBeInTheDocument();
@@ -133,10 +136,257 @@ test('Can filter by package status', async (done) => {
133
136
  expect(queryByText('acl')).not.toBeInTheDocument();
134
137
  });
135
138
 
136
-
137
139
  assertNockRequest(autocompleteScope);
138
140
  assertNockRequest(scope);
139
141
  assertNockRequest(searchDelayScope);
140
142
  assertNockRequest(autoSearchScope);
141
143
  assertNockRequest(scope2, done); // Pass jest callback to confirm test is done
142
144
  });
145
+
146
+ test('Can upgrade a package via remote execution', async (done) => {
147
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
148
+
149
+ const scope = nockInstance
150
+ .get(hostPackages)
151
+ .query(defaultQuery)
152
+ .reply(200, mockPackagesData);
153
+
154
+ const statusScope = nockInstance
155
+ .get(hostPackages)
156
+ .query({ ...defaultQuery, status: 'upgradable' })
157
+ .reply(200, { ...mockPackagesData, results: [firstPackage] });
158
+
159
+ const upgradeScope = nockInstance
160
+ .post(jobInvocations, {
161
+ job_invocation: {
162
+ inputs: {
163
+ package: firstPackage.upgradable_version,
164
+ },
165
+ search_query: `name ^ (${hostname})`,
166
+ feature: REX_FEATURES.KATELLO_PACKAGE_UPDATE,
167
+ },
168
+ })
169
+ .reply(201);
170
+
171
+ const {
172
+ getByRole,
173
+ getAllByText,
174
+ getByLabelText,
175
+ getByText,
176
+ } = renderWithRedux(<PackagesTab />, renderOptions());
177
+
178
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
179
+
180
+ const statusDropdown = getByText('Status', { ignore: 'th' });
181
+ expect(statusDropdown).toBeInTheDocument();
182
+ fireEvent.click(statusDropdown);
183
+ const upgradable = getByRole('option', { name: 'select Upgradable' });
184
+ fireEvent.click(upgradable);
185
+ await patientlyWaitFor(() => {
186
+ expect(getByText('coreutils')).toBeInTheDocument();
187
+ });
188
+
189
+ const kebabDropdown = getByLabelText('Actions');
190
+ kebabDropdown.click();
191
+
192
+ const rexAction = getByText('Upgrade via remote execution');
193
+ await patientlyWaitFor(() => expect(rexAction).toBeInTheDocument());
194
+ fireEvent.click(rexAction);
195
+
196
+ assertNockRequest(autocompleteScope);
197
+ assertNockRequest(scope);
198
+ assertNockRequest(statusScope);
199
+ assertNockRequest(upgradeScope, done);
200
+ });
201
+
202
+ test('Can upgrade a package via customized remote execution', async (done) => {
203
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
204
+
205
+ const scope = nockInstance
206
+ .get(hostPackages)
207
+ .query(defaultQuery)
208
+ .reply(200, mockPackagesData);
209
+
210
+ const statusScope = nockInstance
211
+ .get(hostPackages)
212
+ .query({ ...defaultQuery, status: 'upgradable' })
213
+ .reply(200, { ...mockPackagesData, results: [firstPackage] });
214
+
215
+ const {
216
+ getByRole,
217
+ getAllByText,
218
+ getByLabelText,
219
+ getByText,
220
+ } = renderWithRedux(<PackagesTab />, renderOptions());
221
+
222
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
223
+
224
+ const statusDropdown = getByText('Status', { ignore: 'th' });
225
+ expect(statusDropdown).toBeInTheDocument();
226
+ fireEvent.click(statusDropdown);
227
+ const upgradable = getByRole('option', { name: 'select Upgradable' });
228
+ fireEvent.click(upgradable);
229
+ await patientlyWaitFor(() => {
230
+ expect(getByText('coreutils')).toBeInTheDocument();
231
+ });
232
+
233
+ const kebabDropdown = getByLabelText('Actions');
234
+ kebabDropdown.click();
235
+
236
+ const rexAction = getByText('Upgrade via customized remote execution');
237
+ const feature = REX_FEATURES.KATELLO_PACKAGE_UPDATE;
238
+ const packageName = firstPackage.upgradable_version;
239
+
240
+ expect(rexAction).toBeInTheDocument();
241
+ expect(rexAction).toHaveAttribute(
242
+ 'href',
243
+ `/job_invocations/new?feature=${feature}&host_ids=name%20%5E%20(${hostname})&inputs%5Bpackage%5D=${packageName}`,
244
+ );
245
+
246
+ fireEvent.click(rexAction);
247
+
248
+ assertNockRequest(autocompleteScope);
249
+ assertNockRequest(scope);
250
+ assertNockRequest(statusScope, done);
251
+ });
252
+
253
+ test('Can bulk upgrade via remote execution', async (done) => {
254
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
255
+
256
+ const scope = nockInstance
257
+ .get(hostPackages)
258
+ .query(defaultQuery)
259
+ .reply(200, mockPackagesData);
260
+
261
+ const upgradeScope = nockInstance
262
+ .post(jobInvocations, {
263
+ job_invocation: {
264
+ inputs: {
265
+ [PACKAGES_SEARCH_QUERY]: `id ^ (${firstPackage.id},${secondPackage.id})`,
266
+ },
267
+ search_query: `name ^ (${hostname})`,
268
+ feature: REX_FEATURES.KATELLO_PACKAGES_UPDATE_BY_SEARCH,
269
+ },
270
+ })
271
+ .reply(201);
272
+
273
+ const {
274
+ getAllByRole,
275
+ getAllByText,
276
+ getByRole,
277
+ getByLabelText,
278
+ } = renderWithRedux(<PackagesTab />, renderOptions());
279
+
280
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
281
+
282
+ getByRole('checkbox', { name: 'Select row 0' }).click();
283
+ expect(getByLabelText('Select row 0').checked).toEqual(true);
284
+ getByRole('checkbox', { name: 'Select row 1' }).click();
285
+ expect(getByLabelText('Select row 1').checked).toEqual(true);
286
+
287
+ const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1];
288
+ fireEvent.click(upgradeDropdown);
289
+
290
+ const rexAction = getByLabelText('bulk_upgrade_rex');
291
+ expect(rexAction).toBeInTheDocument();
292
+ fireEvent.click(rexAction);
293
+
294
+ assertNockRequest(autocompleteScope);
295
+ assertNockRequest(scope);
296
+ assertNockRequest(upgradeScope, done);
297
+ });
298
+
299
+ test('Can bulk upgrade via customized remote execution', async (done) => {
300
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
301
+
302
+ const scope = nockInstance
303
+ .get(hostPackages)
304
+ .query(defaultQuery)
305
+ .reply(200, mockPackagesData);
306
+
307
+ const {
308
+ getAllByRole,
309
+ getAllByText,
310
+ getByRole,
311
+ getByLabelText,
312
+ } = renderWithRedux(<PackagesTab />, renderOptions());
313
+
314
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
315
+
316
+ const feature = REX_FEATURES.KATELLO_PACKAGES_UPDATE_BY_SEARCH;
317
+ const packages = `${firstPackage.id},${secondPackage.id}`;
318
+ const job =
319
+ `/job_invocations/new?feature=${feature}&host_ids=name%20%5E%20(${hostname})&inputs%5BPackages%20search%20query%5D=id%20%5E%20(${packages})`;
320
+
321
+ getByRole('checkbox', { name: 'Select row 0' }).click();
322
+ expect(getByLabelText('Select row 0').checked).toEqual(true);
323
+ getByRole('checkbox', { name: 'Select row 1' }).click();
324
+ expect(getByLabelText('Select row 1').checked).toEqual(true);
325
+
326
+ const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1];
327
+ fireEvent.click(upgradeDropdown);
328
+ expect(upgradeDropdown).not.toHaveAttribute('disabled');
329
+
330
+ const rexAction = getByLabelText('bulk_upgrade_customized_rex');
331
+ expect(rexAction).toBeInTheDocument();
332
+ expect(rexAction).toHaveAttribute('href', job);
333
+
334
+ assertNockRequest(autocompleteScope);
335
+ assertNockRequest(scope, done);
336
+ });
337
+
338
+ test('Upgrade is disabled when there are non-upgradable packages selected', async (done) => {
339
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
340
+
341
+ const scope = nockInstance
342
+ .get(hostPackages)
343
+ .query(defaultQuery)
344
+ .reply(200, mockPackagesData);
345
+
346
+ const {
347
+ getAllByRole,
348
+ getAllByText,
349
+ getByLabelText,
350
+ getByRole,
351
+ } = renderWithRedux(<PackagesTab />, renderOptions());
352
+
353
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
354
+
355
+ // select an upgradable package
356
+ getByRole('checkbox', { name: 'Select row 0' }).click();
357
+ // select an up-to-date package
358
+ getByRole('checkbox', { name: 'Select row 2' }).click();
359
+ expect(getByLabelText('Select row 2').checked).toEqual(true);
360
+
361
+ const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1];
362
+ expect(upgradeDropdown).toHaveAttribute('disabled');
363
+
364
+ assertNockRequest(autocompleteScope);
365
+ assertNockRequest(scope, done);
366
+ });
367
+
368
+ test('Remove is disabled when in select all mode', async (done) => {
369
+ const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl);
370
+ const scope = nockInstance
371
+ .get(hostPackages)
372
+ .query(defaultQuery)
373
+ .reply(200, mockPackagesData);
374
+
375
+ const {
376
+ getAllByText, getByRole,
377
+ } = renderWithRedux(<PackagesTab />, renderOptions());
378
+
379
+ await patientlyWaitFor(() => expect(getAllByText(firstPackage.name)[0]).toBeInTheDocument());
380
+
381
+ // find and click the select all checkbox
382
+ const selectAllCheckbox = getByRole('checkbox', { name: 'Select all' });
383
+ fireEvent.click(selectAllCheckbox);
384
+ getByRole('button', { name: 'bulk_actions' }).click();
385
+
386
+ const removeButton = getByRole('button', { name: 'bulk_remove' });
387
+ await patientlyWaitFor(() => expect(removeButton).toBeInTheDocument());
388
+ expect(removeButton).toHaveAttribute('aria-disabled', 'true');
389
+
390
+ assertNockRequest(autocompleteScope);
391
+ assertNockRequest(scope, done);
392
+ });
@@ -40,10 +40,6 @@ const renderOptions = isTracerInstalled => ({ // sets initial Redux state
40
40
  },
41
41
  });
42
42
 
43
- // Find the checkbox to the left of the trace.
44
- // (We could also have just found the checkbox by its aria-label "Select row 0",
45
- // but this is closer to how the user would do it)
46
- const checkboxToTheLeftOf = node => node.previousElementSibling.firstElementChild;
47
43
  const actionMenuToTheRightOf = node => node.nextElementSibling.firstElementChild.firstElementChild;
48
44
 
49
45
  const hostTraces = foremanApi.getApiUrl('/hosts/1/traces?per_page=20&page=1');
@@ -51,14 +47,13 @@ const autocompleteUrl = '/hosts/1/traces/auto_complete_search';
51
47
  const jobInvocations = foremanApi.getApiUrl('/job_invocations');
52
48
 
53
49
  let firstTrace;
54
- let secondTrace;
55
50
  let searchDelayScope;
56
51
  let autoSearchScope;
57
52
 
58
53
  describe('With tracer installed', () => {
59
54
  beforeEach(() => {
60
55
  const { results } = mockTraceData;
61
- [firstTrace, secondTrace] = results;
56
+ [firstTrace] = results;
62
57
  searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 0);
63
58
  autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing');
64
59
  });
@@ -119,17 +114,17 @@ describe('With tracer installed', () => {
119
114
  .reply(201, mockResolveTraceTask);
120
115
 
121
116
 
122
- const { getByText } = renderWithRedux(
117
+ const { getByText, getByLabelText } = renderWithRedux(
123
118
  <TracesTab />,
124
119
  renderOptions(true),
125
120
  );
121
+
126
122
  let traceCheckbox;
127
- // Find the trace.
123
+ // Find the trace checkbox.
128
124
  await patientlyWaitFor(() => {
129
- const traceNameNode = getByText(firstTrace.application);
130
- traceCheckbox = checkboxToTheLeftOf(traceNameNode);
125
+ traceCheckbox = getByLabelText('Select row 0');
131
126
  });
132
- traceCheckbox.click();
127
+ fireEvent.click(traceCheckbox);
133
128
  expect(traceCheckbox.checked).toEqual(true);
134
129
 
135
130
  const restartAppButton = getByText('Restart app');
@@ -158,16 +153,17 @@ describe('With tracer installed', () => {
158
153
  .reply(201, mockResolveTraceTask);
159
154
 
160
155
 
161
- const { getByText, getByLabelText, queryByText } = renderWithRedux(
156
+ const { getByLabelText, queryByText } = renderWithRedux(
162
157
  <TracesTab />,
163
158
  renderOptions(true),
164
159
  );
165
160
  let traceCheckbox;
161
+
162
+ // Find the trace.
166
163
  await patientlyWaitFor(() => {
167
- const traceNameNode = getByText(firstTrace.application);
168
- traceCheckbox = checkboxToTheLeftOf(traceNameNode);
164
+ traceCheckbox = getByLabelText('Select row 0');
169
165
  });
170
- traceCheckbox.click();
166
+ fireEvent.click(traceCheckbox);
171
167
  expect(traceCheckbox.checked).toEqual(true);
172
168
  const actionMenu = getByLabelText('bulk_actions');
173
169
  actionMenu.click();
@@ -204,14 +200,16 @@ describe('With tracer installed', () => {
204
200
  );
205
201
 
206
202
  let traceCheckbox;
207
- // Assert that the traces are now showing on the screen, but wait for them to appear.
203
+ // Find the trace.
208
204
  await patientlyWaitFor(() => {
209
- const traceNameNode = getByText(firstTrace.application);
210
- traceCheckbox = checkboxToTheLeftOf(traceNameNode);
205
+ traceCheckbox = getByLabelText('Select row 0');
211
206
  });
207
+
208
+
212
209
  const selectAllCheckbox = getByLabelText('Select all');
213
210
  fireEvent.click(selectAllCheckbox);
214
211
  expect(traceCheckbox.checked).toEqual(true);
212
+
215
213
  fireEvent.click(getByLabelText('Select row 0')); // de select
216
214
  fireEvent.click(getByLabelText('Select row 2')); // de select
217
215
 
@@ -298,16 +296,15 @@ describe('With tracer installed', () => {
298
296
  .get(hostTraces)
299
297
  .reply(200, mockTraceData);
300
298
 
301
- const { getByText, getByLabelText, queryByText } = renderWithRedux(
299
+ const { getByLabelText, queryByText } = renderWithRedux(
302
300
  <TracesTab />,
303
301
  renderOptions(true),
304
302
  );
305
303
  let traceCheckbox;
306
304
  await patientlyWaitFor(() => {
307
- const traceNameNode = getByText(firstTrace.application);
308
- traceCheckbox = checkboxToTheLeftOf(traceNameNode);
305
+ traceCheckbox = getByLabelText('Select row 0');
309
306
  });
310
- traceCheckbox.click();
307
+ fireEvent.click(traceCheckbox);
311
308
  expect(traceCheckbox.checked).toEqual(true);
312
309
 
313
310
  const actionMenu = getByLabelText('bulk_actions');
@@ -327,7 +324,7 @@ describe('With tracer installed', () => {
327
324
  describe('Remote execution URL helper logic', () => {
328
325
  beforeEach(() => {
329
326
  const { results } = mockTraceData;
330
- [firstTrace, secondTrace] = results;
327
+ [firstTrace] = results;
331
328
  });
332
329
 
333
330
  afterEach(() => {
@@ -341,14 +338,13 @@ describe('With tracer installed', () => {
341
338
  .get(hostTraces)
342
339
  .reply(200, mockTraceData);
343
340
 
344
- const { getByText } = renderWithRedux(
341
+ const { getByLabelText } = renderWithRedux(
345
342
  <TracesTab />,
346
343
  renderOptions(true),
347
344
  );
348
345
  let traceCheckbox;
349
346
  await patientlyWaitFor(() => {
350
- const traceNameNode = getByText(secondTrace.application);
351
- traceCheckbox = checkboxToTheLeftOf(traceNameNode);
347
+ traceCheckbox = getByLabelText('Select row 1');
352
348
  });
353
349
  expect(traceCheckbox.disabled).toEqual(true);
354
350
 
@@ -2,6 +2,7 @@ import { REX_FEATURES } from './RemoteExecutionConstants';
2
2
  import { TRACES_SEARCH_QUERY } from './TracesTab/HostTracesConstants';
3
3
  import { ERRATA_SEARCH_QUERY } from './ErrataTab/HostErrataConstants';
4
4
  import { PACKAGE_SEARCH_QUERY } from '../YumInstallablePackages/YumInstallablePackagesConstants';
5
+ import { PACKAGES_SEARCH_QUERY } from '../HostPackages/HostPackagesConstants';
5
6
 
6
7
  export const createJob = ({
7
8
  hostname, feature, inputs,
@@ -28,6 +29,18 @@ export const katelloPackageInstallBySearchUrl = ({ hostname, search }) => create
28
29
  inputs: { [PACKAGE_SEARCH_QUERY]: search },
29
30
  });
30
31
 
32
+ export const katelloPackageUpdateUrl = ({ hostname, packageName }) => createJob({
33
+ hostname,
34
+ feature: REX_FEATURES.KATELLO_PACKAGE_UPDATE,
35
+ inputs: { package: packageName },
36
+ });
37
+
38
+ export const packagesUpdateUrl = ({ hostname, search }) => createJob({
39
+ hostname,
40
+ feature: REX_FEATURES.KATELLO_PACKAGES_UPDATE_BY_SEARCH,
41
+ inputs: { [PACKAGES_SEARCH_QUERY]: search },
42
+ });
43
+
31
44
  export const resolveTraceUrl = ({ hostname, search }) => createJob({
32
45
  hostname,
33
46
  feature: REX_FEATURES.KATELLO_HOST_TRACER_RESOLVE,
@@ -29,12 +29,16 @@ exports[`ActivationKeys renders 1`] = `
29
29
  favorites={Array []}
30
30
  favoritesLabel="Favorites"
31
31
  hasInlineFilter={false}
32
+ hasPlaceholderStyle={false}
32
33
  id="reg_katello_ak"
33
34
  inlineFilterPlaceholderText={null}
35
+ inputAutoComplete="off"
34
36
  inputIdPrefix=""
35
37
  isCreatable={false}
38
+ isCreateSelectOptionObject={false}
36
39
  isDisabled={true}
37
40
  isGrouped={false}
41
+ isInputFilterPersisted={false}
38
42
  isInputValuePersisted={false}
39
43
  isOpen={false}
40
44
  isPlain={false}
@@ -81,7 +81,7 @@ ActivationKeys.propTypes = {
81
81
  selectedKeys: PropTypes.array,
82
82
  hostGroupActivationKeys: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
83
83
  hostGroupId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
84
- pluginValues: PropTypes.object,
84
+ pluginValues: PropTypes.objectOf(PropTypes.shape({})),
85
85
  onChange: PropTypes.func.isRequired,
86
86
  handleInvalidField: PropTypes.func.isRequired,
87
87
  isLoading: PropTypes.bool,
@@ -31,7 +31,7 @@ const LifecycleEnvironment = ({
31
31
  />
32
32
  {lifecycleEnvironments.map(lce => (
33
33
  <FormSelectOption key={lce.id} value={lce.id} label={lce.name} />
34
- ))}
34
+ ))}
35
35
  </FormSelect>
36
36
  </FormGroup>
37
37
  );
@@ -31,7 +31,7 @@ class SystemStatuses extends Component {
31
31
  <td>{value.status.toUpperCase()}</td>
32
32
  <td> {value.message}</td>
33
33
  </tr>
34
- ))}
34
+ ))}
35
35
  </tbody>
36
36
  </table>
37
37
  </LoadingState>
@@ -12,15 +12,15 @@ const initialState = Immutable({
12
12
 
13
13
  export default (state = initialState, action) => {
14
14
  switch (action.type) {
15
- case SYSTEM_STATUSES_REQUEST:
16
- return state.set('loaderStatus', 'PENDING');
17
- case SYSTEM_STATUSES_SUCCESS:
18
- return state
19
- .set('services', action.payload.services)
20
- .set('loaderStatus', 'RESOLVED');
21
- case SYSTEM_STATUSES_FAILURE:
22
- return state.set('loaderStatus', 'ERROR');
23
- default:
24
- return state;
15
+ case SYSTEM_STATUSES_REQUEST:
16
+ return state.set('loaderStatus', 'PENDING');
17
+ case SYSTEM_STATUSES_SUCCESS:
18
+ return state
19
+ .set('services', action.payload.services)
20
+ .set('loaderStatus', 'RESOLVED');
21
+ case SYSTEM_STATUSES_FAILURE:
22
+ return state.set('loaderStatus', 'ERROR');
23
+ default:
24
+ return state;
25
25
  }
26
26
  };
@@ -53,8 +53,8 @@ const Table = ({
53
53
  };
54
54
 
55
55
  Table.propTypes = {
56
- columns: PropTypes.arrayOf(PropTypes.object).isRequired,
57
- rows: PropTypes.arrayOf(PropTypes.object).isRequired,
56
+ columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
57
+ rows: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
58
58
  emptyState: PropTypes.object, // eslint-disable-line react/forbid-prop-types
59
59
  pagination: PropTypes.object, // eslint-disable-line react/forbid-prop-types
60
60
  bodyMessage: PropTypes.node,
@@ -15,8 +15,8 @@ const TableBody = ({
15
15
  };
16
16
 
17
17
  TableBody.propTypes = {
18
- columns: PropTypes.arrayOf(PropTypes.object).isRequired,
19
- rows: PropTypes.arrayOf(PropTypes.object).isRequired,
18
+ columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
19
+ rows: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
20
20
  message: PropTypes.string,
21
21
  };
22
22
 
@@ -16,23 +16,23 @@ export default (state = initialState, action) => {
16
16
  const { type, payload } = action;
17
17
 
18
18
  switch (type) {
19
- case ORGANIZATION_PRODUCTS_REQUEST:
20
- return state.set('loading', true);
19
+ case ORGANIZATION_PRODUCTS_REQUEST:
20
+ return state.set('loading', true);
21
21
 
22
- case ORGANIZATION_PRODUCTS_SUCCESS:
23
- return state.merge({
24
- ...payload,
25
- loading: false,
26
- });
22
+ case ORGANIZATION_PRODUCTS_SUCCESS:
23
+ return state.merge({
24
+ ...payload,
25
+ loading: false,
26
+ });
27
27
 
28
- case ORGANIZATION_PRODUCTS_FAILURE:
29
- return state.merge({
30
- error: payload,
31
- loading: false,
32
- results: [],
33
- });
28
+ case ORGANIZATION_PRODUCTS_FAILURE:
29
+ return state.merge({
30
+ error: payload,
31
+ loading: false,
32
+ results: [],
33
+ });
34
34
 
35
- default:
36
- return state;
35
+ default:
36
+ return state;
37
37
  }
38
38
  };