katello 4.3.0.rc3 → 4.3.0.rc4

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/concerns/api/v2/registration_controller_extensions.rb +1 -9
  3. data/app/lib/actions/katello/capsule_content/refresh_repos.rb +1 -1
  4. data/app/lib/actions/pulp3/capsule_content/generate_metadata.rb +5 -4
  5. data/app/lib/katello/resources/registry.rb +1 -1
  6. data/app/models/katello/concerns/smart_proxy_extensions.rb +15 -7
  7. data/app/services/katello/pulp3/repository.rb +0 -4
  8. data/app/services/katello/pulp3/repository_mirror.rb +1 -1
  9. data/app/views/foreman/job_templates/change_content_source.erb +42 -0
  10. data/db/migrate/20210331180353_katello_pool_organization_id_not_nullable.rb +2 -0
  11. data/lib/katello/version.rb +1 -1
  12. data/webpack/scenes/ContentViews/Copy/CopyContentViewForm.js +7 -11
  13. data/webpack/scenes/ContentViews/Copy/CopyContentViewModal.js +1 -1
  14. data/webpack/scenes/ContentViews/Copy/__tests__/copyContentView.test.js +1 -1
  15. data/webpack/scenes/ContentViews/Delete/Steps/CVDeletionFinish.js +10 -4
  16. data/webpack/scenes/ContentViews/Delete/__tests__/contentViewDelete.test.js +5 -2
  17. data/webpack/scenes/ContentViews/Details/ContentViewDetails.js +106 -41
  18. data/webpack/scenes/ContentViews/Details/Promote/ContentViewVersionPromote.js +17 -4
  19. data/webpack/scenes/ContentViews/Details/Versions/ContentViewVersions.js +25 -3
  20. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveCVVersionWizard.js +4 -1
  21. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVVersionDeleteFinish.js +14 -3
  22. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetails.js +4 -2
  23. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetailsHeader.js +133 -31
  24. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/__tests__/ContentViewVersionDetails.test.js +21 -1
  25. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/__tests__/ContentViewVersionDetailsEmpty.test.js +22 -1
  26. data/webpack/scenes/Subscriptions/__tests__/__snapshots__/SubscriptionsActions.test.js.snap +1 -0
  27. data/webpack/scenes/Tasks/TaskActions.js +4 -3
  28. data/webpack/scenes/Tasks/__tests__/__snapshots__/TaskActions.test.js.snap +1 -0
  29. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa69b2700d8537140c39e127e5e94f121aeead00124eeddd6c2e7fa94e9fed32
4
- data.tar.gz: 67ff63f7def7af9ed99bf7e73d14aa6e82cb41bd65ca73d4ec0fdd8867b2285d
3
+ metadata.gz: 83de032a01e5d20e480d164eb425db853b1eea9a4668eb1aa853bfaa5d6a703a
4
+ data.tar.gz: aad3b14f2608df9c71cb039222ced8baf98bc89537c28967fcc1d47ac3ce3c5c
5
5
  SHA512:
6
- metadata.gz: 7b8d403df79ab4e58b40d3bb675c9908f5bae448d03e410c0fb256eaa974a043c80f8bcc371ceb9e2827311cd138f8bf209f255c3a3c295f6294af6b0799b69f
7
- data.tar.gz: 469abe1ecd5ae6ce1117c9978fa3d9a1bf3dd2ad8e775de21bc6c61ba4d394e338ccec4b12e1d6226293189dfc2965f68c4377a6a5911130f1d723da14524516
6
+ metadata.gz: cb93e596697f4639b7b4ba3d512524f9880f63442bdba0b4963f73fe6aa18b61d4565f71f488a7cd99f54c0908cbe74cc59468b987450c5d02c9f8020a521761
7
+ data.tar.gz: 9ffa356f5005ab1e2119c119c24837530c23d4d004c1a9c99a3b120fc648f443bf126faae8adff3a5763714ba70d9aa412976f50b6d886e6dc3bd0fb4257dcce
@@ -28,7 +28,7 @@ module Katello
28
28
  end
29
29
 
30
30
  def context_urls
31
- super.merge(rhsm_url: rhsm_url, pulp_content_url: pulp_content_url)
31
+ super.merge(rhsm_url: smart_proxy.rhsm_url, pulp_content_url: smart_proxy.pulp_content_url)
32
32
  end
33
33
 
34
34
  private
@@ -43,14 +43,6 @@ module Katello
43
43
  proxy
44
44
  end
45
45
  end
46
-
47
- def rhsm_url
48
- URI(smart_proxy.rhsm_url)
49
- end
50
-
51
- def pulp_content_url
52
- smart_proxy.setting(SmartProxy::PULP3_FEATURE, 'content_app_url')
53
- end
54
46
  end
55
47
  end
56
48
  end
@@ -40,7 +40,7 @@ module Actions
40
40
  pulp_repo = repo.backend_service(smart_proxy)
41
41
  if !current_repos_on_capsule_ids.include?(repo.id)
42
42
  pulp_repo.create_mirror_entities
43
- elsif pulp_repo.mirror_needs_updates?
43
+ else
44
44
  tasks += pulp_repo.refresh_mirror_entities
45
45
  end
46
46
  end
@@ -17,13 +17,14 @@ module Actions
17
17
 
18
18
  def invoke_external_task
19
19
  repository = ::Katello::Repository.find(input[:repository_id])
20
+ backend = repository.backend_service(smart_proxy).with_mirror_adapter
20
21
  #yum repositories use metadata mirroring always, so we should never
21
- # regenerate metadata on proxies
22
- if repository.yum?
22
+ # regenerate metadata on proxies. but if there is no publication,
23
+ # it means the repo was likely empty and syncing didn't generate one
24
+ if repository.yum? && backend.publication_href.present?
23
25
  []
24
26
  else
25
- smart_proxy = ::SmartProxy.unscoped.find(input[:smart_proxy_id])
26
- repository.backend_service(smart_proxy).with_mirror_adapter.create_publication
27
+ backend.create_publication
27
28
  end
28
29
  end
29
30
  end
@@ -32,7 +32,7 @@ module Katello
32
32
  self.prefix = "/pulpcore_registry/"
33
33
  self.site = "#{uri.scheme}://#{uri.host}:#{uri.port}"
34
34
  self.ca_cert_file = Setting[:ssl_ca_file]
35
- pulp_primary.pulp3_ssl_configuration(self)
35
+ pulp_primary.pulp3_ssl_configuration(self, :net_http)
36
36
 
37
37
  self
38
38
  end
@@ -185,17 +185,17 @@ module Katello
185
185
  end
186
186
  end
187
187
 
188
- def pulp3_ssl_configuration(config)
188
+ def pulp3_ssl_configuration(config, connection_adapter = Faraday.default_adapter)
189
189
  legacy_pulp_cert = !self.setting(PULP3_FEATURE, 'client_authentication')&.include?('client_certificate')
190
190
 
191
- if Faraday.default_adapter == :excon
191
+ if connection_adapter == :excon
192
192
  config.ssl_client_cert = ::Cert::Certs.ssl_client_cert_filename(use_admin_as_cn_cert: legacy_pulp_cert)
193
193
  config.ssl_client_key = ::Cert::Certs.ssl_client_key_filename(use_admin_as_cn_cert: legacy_pulp_cert)
194
- elsif Faraday.default_adapter == :net_http
194
+ elsif connection_adapter == :net_http
195
195
  config.ssl_client_cert = ::Cert::Certs.ssl_client_cert(use_admin_as_cn_cert: legacy_pulp_cert)
196
196
  config.ssl_client_key = ::Cert::Certs.ssl_client_key(use_admin_as_cn_cert: legacy_pulp_cert)
197
197
  else
198
- fail "Unexpected faraday default_adapter #{Faraday.default_adapter}! Cannot continue, this is likely a bug."
198
+ fail "Unexpected connection_adapter #{Faraday.default_adapter}! Cannot continue, this is likely a bug."
199
199
  end
200
200
  end
201
201
 
@@ -446,14 +446,22 @@ module Katello
446
446
  def rhsm_url
447
447
  # Since Foreman 3.1 this setting is set
448
448
  if (rhsm_url = setting(SmartProxy::PULP3_FEATURE, 'rhsm_url'))
449
- rhsm_url
449
+ URI(rhsm_url)
450
450
  # Compatibility fall back
451
451
  elsif pulp_primary?
452
- "https://#{URI.parse(url).host}/rhsm"
452
+ URI("https://#{URI.parse(url).host}/rhsm")
453
453
  elsif pulp_mirror?
454
- "https://#{URI.parse(url).host}:8443/rhsm"
454
+ URI("https://#{URI.parse(url).host}:8443/rhsm")
455
455
  end
456
456
  end
457
+
458
+ def pulp_content_url
459
+ URI(setting(SmartProxy::PULP3_FEATURE, 'content_app_url'))
460
+ end
461
+
462
+ class ::SmartProxy::Jail < ::Safemode::Jail
463
+ allow :rhsm_url, :pulp_content_url
464
+ end
457
465
  end
458
466
  end
459
467
  end
@@ -183,10 +183,6 @@ module Katello
183
183
  RepositoryMirror.new(self).refresh_entities
184
184
  end
185
185
 
186
- def mirror_needs_updates?
187
- RepositoryMirror.new(self).needs_updates?
188
- end
189
-
190
186
  def refresh_if_needed
191
187
  tasks = []
192
188
  tasks << update_remote #always update remote
@@ -79,7 +79,7 @@ module Katello
79
79
  end
80
80
 
81
81
  def publication_href
82
- api.publications_api.list(:repository_version => version_href).results.first.pulp_href
82
+ api.publications_api.list(:repository_version => version_href).results.first&.pulp_href
83
83
  end
84
84
 
85
85
  def create_version(options = {})
@@ -0,0 +1,42 @@
1
+ <%#
2
+ kind: job_template
3
+ name: Change content source
4
+ job_category: Katello
5
+ model: JobTemplate
6
+ provider_type: SSH
7
+ description_format: Configure subscription manager to new content source
8
+ feature: katello_change_content_source
9
+ %>
10
+ #!/bin/sh
11
+ <%
12
+ content_source = @host.content_source
13
+ -%>
14
+
15
+ <% if content_source -%>
16
+ SSL_CA_CERT=$(mktemp)
17
+ cat << EOF > $SSL_CA_CERT
18
+ <%= foreman_server_ca_cert %>
19
+ EOF
20
+
21
+ KATELLO_SERVER_CA_CERT=/etc/rhsm/ca/katello-server-ca.pem
22
+ RHSM_CFG=/etc/rhsm/rhsm.conf
23
+
24
+ # Prepare SSL certificate
25
+ mkdir -p /etc/rhsm/ca
26
+ cp -f $SSL_CA_CERT $KATELLO_SERVER_CA_CERT
27
+ chmod 644 $KATELLO_SERVER_CA_CERT
28
+
29
+ # Configure subscription-manager
30
+ test -f $RHSM_CFG.bak || cp $RHSM_CFG $RHSM_CFG.bak
31
+
32
+ subscription-manager config \
33
+ --server.hostname="<%= content_source.rhsm_url.host %>" \
34
+ --server.port="<%= content_source.rhsm_url.port %>" \
35
+ --server.prefix="<%= content_source.rhsm_url.path %>" \
36
+ --rhsm.repo_ca_cert="$KATELLO_SERVER_CA_CERT" \
37
+ --rhsm.baseurl="<%= content_source.pulp_content_url %>"
38
+
39
+ <% else -%>
40
+ echo "Host [<%= @host.name %>] doesn't have assigned content source!"
41
+ exit 1
42
+ <% end -%>
@@ -1,6 +1,8 @@
1
1
  class KatelloPoolOrganizationIdNotNullable < ActiveRecord::Migration[6.0]
2
2
  def up
3
3
  ::Katello::Pool.where(organization_id: nil).destroy_all
4
+ ::Katello::Pool.where(subscription_id: nil).destroy_all
5
+
4
6
  change_column :katello_pools, :organization_id, :integer, null: false
5
7
  change_column :katello_pools, :subscription_id, :integer, null: false
6
8
 
@@ -1,3 +1,3 @@
1
1
  module Katello
2
- VERSION = "4.3.0.rc3".freeze
2
+ VERSION = "4.3.0.rc4".freeze
3
3
  end
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
3
3
  import useDeepCompareEffect from 'use-deep-compare-effect';
4
4
  import PropTypes from 'prop-types';
5
5
  import { useDispatch, useSelector } from 'react-redux';
6
- import { Redirect } from 'react-router-dom';
6
+ import { useHistory } from 'react-router-dom';
7
7
  import { translate as __ } from 'foremanReact/common/I18n';
8
8
  import { Form, FormGroup, TextInput, ActionGroup, Button } from '@patternfly/react-core';
9
9
  import {
@@ -15,21 +15,22 @@ import { copyContentView } from '../ContentViewsActions';
15
15
  const CopyContentViewForm = ({ cvId, setModalOpen }) => {
16
16
  const dispatch = useDispatch();
17
17
  const [name, setName] = useState('');
18
- const [redirect, setRedirect] = useState(false);
19
18
  const [saving, setSaving] = useState(false);
20
19
  const response = useSelector(selectCopyContentViews);
21
20
  const status = useSelector(selectCopyContentViewStatus);
22
21
  const error = useSelector(selectCopyContentViewError);
22
+ const { push } = useHistory();
23
23
 
24
24
  useDeepCompareEffect(() => {
25
25
  const { id } = response;
26
- if (id && status === STATUS.RESOLVED) {
26
+ if (saving && id && status === STATUS.RESOLVED) {
27
27
  setSaving(false);
28
- setRedirect(true);
28
+ setModalOpen(false);
29
+ push(`/content_views/${id}`);
29
30
  } else if (status === STATUS.ERROR) {
30
31
  setSaving(false);
31
32
  }
32
- }, [response, status, error]);
33
+ }, [response, status, error, saving, setSaving, setModalOpen, push]);
33
34
 
34
35
  const onSubmit = () => {
35
36
  setSaving(true);
@@ -39,11 +40,6 @@ const CopyContentViewForm = ({ cvId, setModalOpen }) => {
39
40
  }));
40
41
  };
41
42
 
42
- if (redirect) {
43
- const { id } = response;
44
- return (<Redirect to={`/content_views/${id}`} />);
45
- }
46
-
47
43
  return (
48
44
  <Form onSubmit={(e) => {
49
45
  e.preventDefault();
@@ -77,7 +73,7 @@ const CopyContentViewForm = ({ cvId, setModalOpen }) => {
77
73
  };
78
74
 
79
75
  CopyContentViewForm.propTypes = {
80
- cvId: PropTypes.string,
76
+ cvId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
81
77
  setModalOpen: PropTypes.func,
82
78
  };
83
79
 
@@ -30,7 +30,7 @@ const CopyContentViewModal = ({
30
30
  };
31
31
 
32
32
  CopyContentViewModal.propTypes = {
33
- cvId: PropTypes.string,
33
+ cvId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
34
34
  cvName: PropTypes.string,
35
35
  show: PropTypes.bool,
36
36
  setIsOpen: PropTypes.func,
@@ -32,7 +32,7 @@ test('Can copy content view from form', async (done) => {
32
32
  getByLabelText('copy_content_view').click();
33
33
  // Form closes it self on success
34
34
  await patientlyWaitFor(() => {
35
- expect(queryByText('Name')).not.toBeInTheDocument();
35
+ expect(setModalOpen).toBeCalled();
36
36
  });
37
37
 
38
38
  assertNockRequest(copyscope, done);
@@ -1,9 +1,10 @@
1
1
  import React, { useContext, useState } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
+ import { useHistory } from 'react-router-dom';
3
4
  import useDeepCompareEffect from 'use-deep-compare-effect';
4
5
  import { translate as __ } from 'foremanReact/common/I18n';
5
6
  import { STATUS } from 'foremanReact/constants';
6
- import { getContentViewVersions, removeContentViewVersion } from '../../Details/ContentViewDetailActions';
7
+ import { removeContentViewVersion } from '../../Details/ContentViewDetailActions';
7
8
  import { selectRemoveCVVersionResponse, selectRemoveCVVersionStatus } from '../../Details/ContentViewDetailSelectors';
8
9
  import getContentViews from '../../ContentViewsActions';
9
10
  import CVDeleteContext from '../CVDeleteContext';
@@ -23,14 +24,19 @@ const CVDeletionFinish = () => {
23
24
  const removeResolved = removeCVStatus === STATUS.RESOLVED;
24
25
  const dispatch = useDispatch();
25
26
  const [removeDispatched, setRemoveDispatched] = useState(false);
27
+ const { push } = useHistory();
26
28
 
27
29
  useDeepCompareEffect(() => {
28
30
  if (removeResolved && removeDispatched) {
31
+ dispatch(getContentViews());
32
+ push('/content_views');
29
33
  setIsOpen(false);
30
- dispatch(getContentViewVersions(cvId));
31
- dispatch(getContentViews);
32
34
  }
33
- }, [removeCVResponse, removeResolved, setIsOpen, dispatch, cvId, removeDispatched]);
35
+ if (removeCVStatus === STATUS.ERROR) {
36
+ setIsOpen(false);
37
+ }
38
+ }, [removeCVResponse, removeCVStatus, removeResolved,
39
+ setIsOpen, dispatch, cvId, removeDispatched, push]);
34
40
 
35
41
  useDeepCompareEffect(() => {
36
42
  if (!removeDispatched) {
@@ -119,7 +119,6 @@ test('Can open Delete wizard and delete CV with all steps', async (done) => {
119
119
 
120
120
  const cvVersionsScope = nockInstance
121
121
  .get(cvVersionsPath)
122
- .times(2)
123
122
  .query(true)
124
123
  .reply(200, cvVersionsData);
125
124
 
@@ -128,6 +127,10 @@ test('Can open Delete wizard and delete CV with all steps', async (done) => {
128
127
  .query(true)
129
128
  .reply(200, cvDetailsData);
130
129
 
130
+ const cvRedirectScope = nockInstance
131
+ .get(api.getApiUrl('/content_views?organization_id=1&nondefault=true&include_permissions=true'))
132
+ .reply(200, cvIndexData);
133
+
131
134
  const cvDeleteParams = {
132
135
  destroy_content_view: true,
133
136
  system_content_view_id: 2,
@@ -239,5 +242,5 @@ test('Can open Delete wizard and delete CV with all steps', async (done) => {
239
242
  assertNockRequest(activationKeysScope);
240
243
  assertNockRequest(cVDropDownOptionsScope);
241
244
  assertNockRequest(cvDeleteScope);
242
- assertNockRequest(cvVersionsScope, done);
245
+ assertNockRequest(cvRedirectScope, done);
243
246
  });
@@ -1,7 +1,18 @@
1
1
  import React, { useState } from 'react';
2
2
  import { useSelector, shallowEqual } from 'react-redux';
3
3
  import { useParams } from 'react-router-dom';
4
- import { Grid, GridItem, TextContent, Text, TextVariants, Button, Flex, FlexItem } from '@patternfly/react-core';
4
+ import { Grid,
5
+ GridItem,
6
+ TextContent,
7
+ Text,
8
+ TextVariants,
9
+ Button,
10
+ Flex,
11
+ FlexItem,
12
+ Dropdown,
13
+ DropdownItem,
14
+ KebabToggle,
15
+ DropdownPosition } from '@patternfly/react-core';
5
16
  import { ExternalLinkAltIcon } from '@patternfly/react-icons';
6
17
  import { translate as __ } from 'foremanReact/common/I18n';
7
18
 
@@ -18,6 +29,8 @@ import ContentViewIcon from '../components/ContentViewIcon';
18
29
  import CVBreadCrumb from '../components/CVBreadCrumb';
19
30
  import PublishContentViewWizard from '../Publish/PublishContentViewWizard';
20
31
  import { hasPermission } from '../helpers';
32
+ import CopyContentViewModal from '../Copy/CopyContentViewModal';
33
+ import ContentViewDeleteWizard from '../Delete/ContentViewDeleteWizard';
21
34
 
22
35
  export default () => {
23
36
  const { id } = useParams();
@@ -25,8 +38,31 @@ export default () => {
25
38
  const details = useSelector(state => selectCVDetails(state, cvId), shallowEqual);
26
39
  const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
27
40
  const [currentStep, setCurrentStep] = useState(1);
41
+ const [dropDownOpen, setDropdownOpen] = useState(false);
42
+ const [copying, setCopying] = useState(false);
43
+ const [deleting, setDeleting] = useState(false);
44
+ const dropDownItems = [
45
+ <DropdownItem
46
+ key="copy"
47
+ onClick={() => {
48
+ setCopying(true);
49
+ }}
50
+ >
51
+ {__('Copy')}
52
+ </DropdownItem>,
53
+ <DropdownItem
54
+ key="delete"
55
+ onClick={() => {
56
+ setDeleting(true);
57
+ }}
58
+ >
59
+ {__('Delete')}
60
+ </DropdownItem>,
61
+ ];
28
62
 
29
- const { name, composite, permissions } = details;
63
+ const {
64
+ name, composite, permissions, environments, versions,
65
+ } = details;
30
66
  const tabs = [
31
67
  {
32
68
  key: 'details',
@@ -60,27 +96,28 @@ export default () => {
60
96
  ];
61
97
 
62
98
  return (
63
- <Grid className="grid-with-margin">
64
- <DetailsContainer cvId={cvId}>
65
- <>
66
- <CVBreadCrumb />
67
- <GridItem xl={8} lg={7} sm={12} style={{ margin: '10px 0' }}>
68
- <Flex alignItems={{
99
+ <>
100
+ <Grid className="grid-with-margin">
101
+ <DetailsContainer cvId={cvId}>
102
+ <>
103
+ <CVBreadCrumb />
104
+ <GridItem xl={8} lg={7} sm={12} style={{ margin: '10px 0' }}>
105
+ <Flex alignItems={{
69
106
  default: 'alignItemsCenter',
70
107
  }}
71
- >
72
- <FlexItem>
73
- <TextContent>
74
- <Text component={TextVariants.h1}>
75
- <ContentViewIcon count={name} composite={composite} />
76
- </Text>
77
- </TextContent>
78
- </FlexItem>
79
- </Flex>
80
- </GridItem>
81
- <GridItem xl={4} lg={5} sm={12} >
82
- <Flex justifyContent={{ lg: 'justifyContentFlexEnd', sm: 'justifyContentFlexStart' }}>
83
- {hasPermission(permissions, 'publish_content_views') &&
108
+ >
109
+ <FlexItem>
110
+ <TextContent>
111
+ <Text component={TextVariants.h1}>
112
+ <ContentViewIcon count={name} composite={composite} />
113
+ </Text>
114
+ </TextContent>
115
+ </FlexItem>
116
+ </Flex>
117
+ </GridItem>
118
+ <GridItem xl={4} lg={5} sm={12} >
119
+ <Flex justifyContent={{ lg: 'justifyContentFlexEnd', sm: 'justifyContentFlexStart' }}>
120
+ {hasPermission(permissions, 'publish_content_views') &&
84
121
  <FlexItem>
85
122
  <Button onClick={() => { setIsPublishModalOpen(true); }} variant="primary" aria-label="publish_content_view">
86
123
  {__('Publish new version')}
@@ -95,25 +132,53 @@ export default () => {
95
132
  />}
96
133
  </FlexItem>
97
134
  }
98
- <FlexItem>
99
- <Button
100
- component="a"
101
- aria-label="view tasks button"
102
- href={`/foreman_tasks/tasks?search=resource_type%3D+Katello%3A%3AContentView+resource_id%3D${cvId}`}
103
- target="_blank"
104
- variant="secondary"
105
- >
106
- {'View tasks '}
107
- <ExternalLinkAltIcon />
108
- </Button>
109
- </FlexItem>
110
- </Flex>
111
- </GridItem>
112
- <GridItem span={12}>
113
- <RoutedTabs tabs={tabs} defaultTabIndex={1} />
114
- </GridItem>
115
- </ >
116
- </DetailsContainer >
117
- </Grid >
135
+ <FlexItem>
136
+ <Button
137
+ component="a"
138
+ aria-label="view tasks button"
139
+ href={`/foreman_tasks/tasks?search=resource_type%3D+Katello%3A%3AContentView+resource_id%3D${cvId}`}
140
+ target="_blank"
141
+ variant="secondary"
142
+ >
143
+ {'View tasks '}
144
+ <ExternalLinkAltIcon />
145
+ </Button>
146
+ </FlexItem>
147
+ <FlexItem>
148
+ <Dropdown
149
+ position={DropdownPosition.right}
150
+ style={{ marginLeft: 'auto' }}
151
+ toggle={<KebabToggle onToggle={setDropdownOpen} id="toggle-dropdown" />}
152
+ isOpen={dropDownOpen}
153
+ isPlain
154
+ dropdownItems={dropDownItems}
155
+ />
156
+ </FlexItem>
157
+ </Flex>
158
+ </GridItem>
159
+ <GridItem span={12}>
160
+ <RoutedTabs tabs={tabs} defaultTabIndex={1} />
161
+ </GridItem>
162
+ </ >
163
+ </DetailsContainer >
164
+ </Grid >
165
+ {copying && <CopyContentViewModal
166
+ cvId={cvId}
167
+ cvName={name}
168
+ show={copying}
169
+ setIsOpen={setCopying}
170
+ aria-label="copy_content_view_modal"
171
+ />}
172
+ {deleting && <ContentViewDeleteWizard
173
+ cvId={cvId && Number(cvId)}
174
+ cvEnvironments={environments}
175
+ cvVersions={versions}
176
+ show={deleting}
177
+ setIsOpen={setDeleting}
178
+ currentStep={currentStep}
179
+ setCurrentStep={setCurrentStep}
180
+ aria-label="delete_content_view_modal"
181
+ />}
182
+ </>
118
183
  );
119
184
  };
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useMemo, useCallback } from 'react';
2
2
  import useDeepCompareEffect from 'use-deep-compare-effect';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
+ import { Redirect } from 'react-router-dom';
4
5
  import { STATUS } from 'foremanReact/constants';
5
6
  import PropTypes from 'prop-types';
6
7
  import {
@@ -8,7 +9,6 @@ import {
8
9
  Modal, ModalVariant, Alert, TextContent, AlertActionCloseButton,
9
10
  } from '@patternfly/react-core';
10
11
  import { translate as __ } from 'foremanReact/common/I18n';
11
-
12
12
  import {
13
13
  selectEnvironmentPaths,
14
14
  selectEnvironmentPathsStatus,
@@ -23,13 +23,15 @@ import ComponentEnvironments from '../ComponentContentViews/ComponentEnvironment
23
23
  import Loading from '../../../../components/Loading';
24
24
 
25
25
  const ContentViewVersionPromote = ({
26
- cvId, versionIdToPromote, versionNameToPromote, versionEnvironments, setIsOpen,
26
+ cvId, versionIdToPromote, versionNameToPromote,
27
+ versionEnvironments, setIsOpen, detailsPage,
27
28
  }) => {
28
29
  const [description, setDescription] = useState('');
29
30
  const [userCheckedItems, setUserCheckedItems] = useState([]);
30
31
  const [alertDismissed, setAlertDismissed] = useState(false);
31
32
  const [loading, setLoading] = useState(false);
32
33
  const [forcePromote, setForcePromote] = useState([]);
34
+ const [redirect, setRedirect] = useState(false);
33
35
  const environmentPathResponse = useSelector(selectEnvironmentPaths);
34
36
  const environmentPathStatus = useSelector(selectEnvironmentPathsStatus);
35
37
  const environmentPathLoading = environmentPathStatus === STATUS.PENDING;
@@ -55,13 +57,18 @@ const ContentViewVersionPromote = ({
55
57
 
56
58
  useDeepCompareEffect(() => {
57
59
  if (promoteResolved && promoteResponse) {
58
- setIsOpen(false);
59
60
  dispatch(getContentViewVersions(cvId));
61
+ if (detailsPage) {
62
+ setRedirect(true);
63
+ } else {
64
+ setIsOpen(false);
65
+ }
60
66
  }
61
67
  if (promoteError) {
62
68
  setLoading(false);
63
69
  }
64
- }, [promoteResponse, promoteResolved, promoteError, setLoading, setIsOpen, dispatch, cvId]);
70
+ }, [promoteResponse, promoteResolved, promoteError, detailsPage,
71
+ setRedirect, setLoading, setIsOpen, dispatch, cvId]);
65
72
 
66
73
  const envPathFlat = useMemo(() => {
67
74
  if (!environmentPathLoading) {
@@ -94,6 +101,10 @@ const ContentViewVersionPromote = ({
94
101
 
95
102
  const submitDisabled = loading || userCheckedItems.length === 0;
96
103
 
104
+ if (redirect && detailsPage) {
105
+ return (<Redirect to="/versions" />);
106
+ }
107
+
97
108
  return (
98
109
  <Modal
99
110
  title={__(`Promote version ${versionNameToPromote}`)}
@@ -169,11 +180,13 @@ ContentViewVersionPromote.propTypes = {
169
180
  versionNameToPromote: PropTypes.string.isRequired,
170
181
  versionEnvironments: PropTypes.arrayOf(PropTypes.shape({})),
171
182
  setIsOpen: PropTypes.func,
183
+ detailsPage: PropTypes.bool,
172
184
  };
173
185
 
174
186
  ContentViewVersionPromote.defaultProps = {
175
187
  versionEnvironments: [],
176
188
  setIsOpen: null,
189
+ detailsPage: false,
177
190
  };
178
191
 
179
192
  export default ContentViewVersionPromote;
@@ -9,6 +9,7 @@ import { Link } from 'react-router-dom';
9
9
  import PropTypes from 'prop-types';
10
10
  import { selectIntervals } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js';
11
11
 
12
+ import { useSet } from '../../../../components/Table/TableHooks';
12
13
  import TableWrapper from '../../../../components/Table/TableWrapper';
13
14
  import InactiveText from '../../components/InactiveText';
14
15
  import ContentViewVersionEnvironments from './ContentViewVersionEnvironments';
@@ -23,7 +24,7 @@ import {
23
24
  import getEnvironmentPaths from '../../components/EnvironmentPaths/EnvironmentPathActions';
24
25
  import ContentViewVersionPromote from '../Promote/ContentViewVersionPromote';
25
26
  import TaskPresenter from '../../components/TaskPresenter/TaskPresenter';
26
- import { startPollingTask } from '../../../Tasks/TaskActions';
27
+ import { startPollingTask, stopPollingTask } from '../../../Tasks/TaskActions';
27
28
  import RemoveCVVersionWizard from './Delete/RemoveCVVersionWizard';
28
29
  import { hasPermission } from '../../helpers';
29
30
  import { pollTaskKey } from '../../../Tasks/helpers';
@@ -48,6 +49,7 @@ const ContentViewVersions = ({ cvId, details }) => {
48
49
  const [deleteVersion, setDeleteVersion] = useState(false);
49
50
  const [currentStep, setCurrentStep] = useState(1);
50
51
  const { permissions } = details;
52
+ const pendingTaskSet = useSet([]);
51
53
  const intervals = useSelector(state => selectIntervals(state));
52
54
 
53
55
  const columnHeaders = [
@@ -66,6 +68,16 @@ const ContentViewVersions = ({ cvId, details }) => {
66
68
  [dispatch],
67
69
  );
68
70
 
71
+ useEffect(() => { // eslint-disable-line arrow-body-style
72
+ return () => {
73
+ if (pendingTaskSet.size) {
74
+ pendingTaskSet.forEach(id =>
75
+ dispatch(stopPollingTask(id)));
76
+ pendingTaskSet.clear();
77
+ }
78
+ };
79
+ }, [pendingTaskSet, dispatch]);
80
+
69
81
  const buildCells = useCallback((cvVersion) => {
70
82
  const {
71
83
  version,
@@ -98,8 +110,18 @@ const ContentViewVersions = ({ cvId, details }) => {
98
110
  } = cvVersion;
99
111
  const { task } = activeHistory[0];
100
112
  const { result } = task || {};
113
+
101
114
  if (result !== 'error' && !pollIntervals[pollTaskKey(task.id)]) {
102
- dispatch(startPollingTask(task.id, task));
115
+ pendingTaskSet.add(task.id);
116
+ dispatch(startPollingTask(
117
+ task.id, task,
118
+ ({ data: { pending } = {} }) => {
119
+ if (!pending) {
120
+ dispatch(stopPollingTask(task.id));
121
+ pendingTaskSet.delete(task.id);
122
+ }
123
+ },
124
+ ));
103
125
  }
104
126
 
105
127
  return [
@@ -115,7 +137,7 @@ const ContentViewVersions = ({ cvId, details }) => {
115
137
  { title: '' },
116
138
  { title: description ? <TableText wrapModifier="truncate">{description}</TableText> : <InactiveText text={__('No description')} /> },
117
139
  ];
118
- }, [dispatch]);
140
+ }, [dispatch, pendingTaskSet]);
119
141
 
120
142
  useDeepCompareEffect(() => {
121
143
  const buildRows = () => {
@@ -17,7 +17,7 @@ import { useSet } from '../../../../../components/Table/TableHooks';
17
17
  const RemoveCVVersionWizard = ({
18
18
  cvId, versionIdToRemove, versionNameToRemove,
19
19
  versionEnvironments, show, setIsOpen,
20
- currentStep, setCurrentStep, deleteWizard,
20
+ currentStep, setCurrentStep, deleteWizard, detailsPage,
21
21
  }) => {
22
22
  const [selectedEnvForAK, setSelectedEnvForAK] = useState([]);
23
23
  const [selectedEnvForHost, setSelectedEnvForHost] = useState([]);
@@ -133,6 +133,7 @@ const RemoveCVVersionWizard = ({
133
133
  selectedEnvForHost,
134
134
  setSelectedEnvForHost,
135
135
  selectedEnvSet,
136
+ detailsPage,
136
137
  }}
137
138
  >
138
139
  <Wizard
@@ -165,12 +166,14 @@ RemoveCVVersionWizard.propTypes = {
165
166
  currentStep: PropTypes.number.isRequired,
166
167
  setCurrentStep: PropTypes.func.isRequired,
167
168
  deleteWizard: PropTypes.bool.isRequired,
169
+ detailsPage: PropTypes.bool,
168
170
  };
169
171
 
170
172
  RemoveCVVersionWizard.defaultProps = {
171
173
  versionEnvironments: [],
172
174
  show: false,
173
175
  setIsOpen: null,
176
+ detailsPage: false,
174
177
  };
175
178
 
176
179
  export default RemoveCVVersionWizard;
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useContext } from 'react';
2
2
  import { useDispatch, useSelector } from 'react-redux';
3
+ import { Redirect } from 'react-router-dom';
3
4
  import useDeepCompareEffect from 'use-deep-compare-effect';
4
5
  import { STATUS } from 'foremanReact/constants';
5
6
  import { translate as __ } from 'foremanReact/common/I18n';
@@ -13,7 +14,8 @@ const CVVersionDeleteFinish = () => {
13
14
  cvId, versionIdToRemove, versionEnvironments,
14
15
  setIsOpen, selectedEnvSet,
15
16
  selectedCVForAK, selectedEnvForAK, selectedCVForHosts,
16
- selectedEnvForHost, affectedActivationKeys, affectedHosts, deleteFlow, removeDeletionFlow,
17
+ selectedEnvForHost, affectedActivationKeys, affectedHosts,
18
+ deleteFlow, removeDeletionFlow, detailsPage,
17
19
  } = useContext(DeleteContext);
18
20
  const removeCVVersionResponse = useSelector(state =>
19
21
  selectRemoveCVVersionResponse(state, versionIdToRemove, versionEnvironments));
@@ -22,14 +24,20 @@ const CVVersionDeleteFinish = () => {
22
24
  const removeResolved = removeCVVersionStatus === STATUS.RESOLVED;
23
25
  const dispatch = useDispatch();
24
26
  const [removeDispatched, setRemoveDispatched] = useState(false);
27
+ const [redirect, setRedirect] = useState(false);
25
28
  const selectedEnv = versionEnvironments.filter(env => selectedEnvSet.has(env.id));
26
29
 
27
30
  useDeepCompareEffect(() => {
28
31
  if (removeResolved && removeCVVersionResponse && removeDispatched) {
29
- setIsOpen(false);
30
32
  dispatch(getContentViewVersions(cvId));
33
+ if (detailsPage) {
34
+ setRedirect(true);
35
+ } else {
36
+ setIsOpen(false);
37
+ }
31
38
  }
32
- }, [removeCVVersionResponse, removeResolved, setIsOpen, dispatch, cvId, removeDispatched]);
39
+ }, [removeCVVersionResponse, removeResolved, setIsOpen,
40
+ dispatch, cvId, removeDispatched, detailsPage, setRedirect]);
33
41
 
34
42
  /*
35
43
  The remove version from environment API takes the following params :
@@ -88,6 +96,9 @@ const CVVersionDeleteFinish = () => {
88
96
  selectedEnvForAK, selectedEnvForHost, selectedEnv,
89
97
  removeCVVersionResponse, removeCVVersionStatus, removeDispatched]);
90
98
 
99
+ if (redirect) {
100
+ return (<Redirect to="/versions" />);
101
+ }
91
102
  return <Loading loadingText={__('Please wait while the task starts..')} />;
92
103
  };
93
104
 
@@ -21,6 +21,7 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
21
21
  const { push } = useHistory();
22
22
  const dispatch = useDispatch();
23
23
  const [versionDetails, setVersionDetails] = useState({});
24
+ const [mounted, setMounted] = useState(true);
24
25
  // Example urls expected:/versions/:id or /versions/:id/repositories.
25
26
  const tab = pathname.split('/')[3];
26
27
  const response = useSelector(state =>
@@ -31,10 +32,11 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
31
32
  const tableConfigs = getCVVersionTableConfigs({ cvId, versionId });
32
33
 
33
34
  useEffect(() => {
34
- if (isEmpty(response) && status === STATUS.PENDING) {
35
+ if (mounted || (isEmpty(response) && status === STATUS.PENDING)) {
35
36
  dispatch(getContentViewVersionDetails(versionId, cvId));
36
37
  }
37
- }, [dispatch, versionId, cvId, response, status]);
38
+ return () => { setMounted(false); };
39
+ }, [dispatch, mounted, setMounted, versionId, cvId, response, status]);
38
40
 
39
41
  useDeepCompareEffect(() => {
40
42
  if (loaded) {
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useDispatch } from 'react-redux';
2
3
  import PropTypes from 'prop-types';
3
4
  import {
4
5
  GridItem,
@@ -10,52 +11,153 @@ import {
10
11
  Label,
11
12
  Flex,
12
13
  FlexItem,
14
+ Dropdown,
15
+ DropdownItem,
16
+ DropdownToggle,
17
+ DropdownPosition,
13
18
  } from '@patternfly/react-core';
14
19
  import { translate as __ } from 'foremanReact/common/I18n';
15
20
  import EditableTextInput from '../../../../../components/EditableTextInput';
16
21
  import { hasPermission } from '../../../helpers';
22
+ import ContentViewVersionPromote from '../../Promote/ContentViewVersionPromote';
23
+ import getEnvironmentPaths from '../../../components/EnvironmentPaths/EnvironmentPathActions';
24
+ import RemoveCVVersionWizard from '../Delete/RemoveCVVersionWizard';
17
25
 
18
26
  const ContentViewVersionDetailsHeader = ({
19
27
  versionDetails: {
20
- version, description, environments,
28
+ version, description, environments, content_view_id: cvId, id,
21
29
  },
22
30
  onEdit,
23
31
  details: { permissions },
24
- }) => (
25
- <>
26
- <GridItem span={12}>
27
- <TextContent>
28
- <Text component={TextVariants.h2}>{__('Version ')}{version}</Text>
29
- </TextContent>
30
- </GridItem>
31
- <GridItem className="content-view-header-content" span={12}>
32
- <TextContent>
33
- <TextList component={TextListVariants.dl}>
34
- <EditableTextInput
35
- key={description} // This fixes a render issue with the initial value
36
- textArea
37
- label={__('Description')}
38
- attribute="description"
39
- placeholder={__('No description')}
40
- onEdit={onEdit}
41
- disabled={!hasPermission(permissions, 'edit_content_views')}
42
- value={description}
43
- />
44
- </TextList>
45
- </TextContent>
46
- <Flex>
47
- {environments?.map(({ name, id }) =>
48
- <FlexItem key={name}><Label isTruncated color="purple" href={`/lifecycle_environments/${id}`}>{name}</Label></FlexItem>)}
49
- </Flex>
50
- </GridItem>
51
- </>
52
- );
32
+ }) => {
33
+ const dispatch = useDispatch();
34
+ useEffect(
35
+ () => {
36
+ dispatch(getEnvironmentPaths());
37
+ },
38
+ [dispatch],
39
+ );
40
+ const [dropdownOpen, setDropdownOpen] = useState(false);
41
+ const [promoting, setPromoting] = useState(false);
42
+ const [removingFromEnv, setRemovingFromEnv] = useState(false);
43
+ const [currentStep, setCurrentStep] = useState(1);
44
+ const [deleteVersion, setDeleteVersion] = useState(false);
45
+ const dropDownItems = [
46
+ <DropdownItem
47
+ key="promote"
48
+ onClick={() => {
49
+ setPromoting(true);
50
+ }}
51
+ >
52
+ {__('Promote')}
53
+ </DropdownItem>,
54
+ <DropdownItem
55
+ key="remove"
56
+ onClick={() => {
57
+ setRemovingFromEnv(true);
58
+ }}
59
+ >
60
+ {__('Remove from environment')}
61
+ </DropdownItem>,
62
+ <DropdownItem
63
+ key="delete"
64
+ onClick={() => {
65
+ setCurrentStep(1);
66
+ setDeleteVersion(true);
67
+ setRemovingFromEnv(true);
68
+ }}
69
+ >
70
+ {__('Delete')}
71
+ </DropdownItem>,
72
+ ];
73
+
74
+ return (
75
+ <>
76
+ <GridItem span={10}>
77
+ <TextContent>
78
+ <Text component={TextVariants.h2}>{__('Version ')}{version}</Text>
79
+ </TextContent>
80
+ </GridItem>
81
+ <GridItem span={2} style={{ display: 'flex' }}>
82
+ <Dropdown
83
+ aria-label="version-action-dropdown"
84
+ position={DropdownPosition.right}
85
+ style={{ marginLeft: 'auto' }}
86
+ toggle={
87
+ <DropdownToggle
88
+ onToggle={setDropdownOpen}
89
+ id="toggle-id"
90
+ >
91
+ {__('Actions')}
92
+ </DropdownToggle>
93
+ }
94
+ isOpen={dropdownOpen}
95
+ dropdownItems={dropDownItems}
96
+ />
97
+ </GridItem>
98
+ <GridItem className="content-view-header-content" span={12}>
99
+ <TextContent>
100
+ <TextList component={TextListVariants.dl}>
101
+ <EditableTextInput
102
+ key={description} // This fixes a render issue with the initial value
103
+ textArea
104
+ label={__('Description')}
105
+ attribute="description"
106
+ placeholder={__('No description')}
107
+ onEdit={onEdit}
108
+ disabled={!hasPermission(permissions, 'edit_content_views')}
109
+ value={description}
110
+ />
111
+ </TextList>
112
+ </TextContent>
113
+ <Flex>
114
+ {environments?.map(({ name, id: envId }) =>
115
+ <FlexItem key={name}><Label isTruncated color="purple" href={`/lifecycle_environments/${envId}`}>{name}</Label></FlexItem>)}
116
+ </Flex>
117
+ </GridItem>
118
+ {promoting &&
119
+ <ContentViewVersionPromote
120
+ cvId={cvId}
121
+ versionIdToPromote={id}
122
+ versionNameToPromote={version}
123
+ versionEnvironments={environments}
124
+ setIsOpen={setPromoting}
125
+ detailsPage
126
+ aria-label="promote_content_view_modal"
127
+ />
128
+ }
129
+ {removingFromEnv &&
130
+ <RemoveCVVersionWizard
131
+ cvId={cvId}
132
+ versionIdToRemove={id}
133
+ versionNameToRemove={version}
134
+ versionEnvironments={environments}
135
+ show={removingFromEnv}
136
+ setIsOpen={setRemovingFromEnv}
137
+ currentStep={currentStep}
138
+ setCurrentStep={setCurrentStep}
139
+ deleteWizard={deleteVersion}
140
+ detailsPage
141
+ aria-label="remove_content_view_version_modal"
142
+ />
143
+ }
144
+ </>
145
+ );
146
+ };
53
147
 
54
148
  ContentViewVersionDetailsHeader.propTypes = {
55
149
  versionDetails: PropTypes.shape({
56
150
  version: PropTypes.string,
57
151
  environments: PropTypes.arrayOf(PropTypes.shape({})),
58
152
  description: PropTypes.string,
153
+ content_view_id: PropTypes.oneOfType([
154
+ PropTypes.string,
155
+ PropTypes.number,
156
+ ]),
157
+ id: PropTypes.oneOfType([
158
+ PropTypes.string,
159
+ PropTypes.number,
160
+ ]),
59
161
  }).isRequired,
60
162
  onEdit: PropTypes.func.isRequired,
61
163
  details: PropTypes.shape({
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { renderWithRedux, patientlyWaitFor } from 'react-testing-lib-wrapper';
3
3
  import { Route } from 'react-router-dom';
4
4
  import { head, last } from 'lodash';
5
- import { nockInstance, assertNockRequest, mockSetting, mockAutocomplete } from '../../../../../../test-utils/nockWrapper';
5
+ import nock, { nockInstance, assertNockRequest, mockSetting, mockAutocomplete } from '../../../../../../test-utils/nockWrapper';
6
6
  import api from '../../../../../../services/api';
7
7
  import { cvVersionDetailsKey } from '../../../../ContentViewsConstants';
8
8
  import ContentViewVersionDetails from '../ContentViewVersionDetails';
@@ -20,12 +20,15 @@ import ContentViewVersionModuleStreamsData from './ContentViewVersionModuleStrea
20
20
  import ContentViewVersionDebPackagesData from './ContentViewVersionDebPackages.fixtures.json';
21
21
  import ContentViewVersionAnsibleCollectionsData from './ContentViewVersionAnsibleCollections.fixtures.json';
22
22
  import ContentViewVersionDockerTagsData from './ContentViewVersionDockerTags.fixtures.json';
23
+ import environmentPathsData from '../../Delete/__tests__/versionRemoveEnvPaths.fixtures';
23
24
 
24
25
  // This changes the api count value so that only the specified tab will show.
25
26
  const getTabSpecificData = key => ({
26
27
  ...ContentViewVersionDetailsData,
27
28
  [key]: ContentViewVersionDetailsCounts[key],
28
29
  });
30
+ let envScope;
31
+ const environmentPathsPath = api.getApiUrl('/organizations/1/environments/paths');
29
32
 
30
33
  const withCVRoute = component => <Route path="/versions/:versionId([0-9]+)">{component}</Route>;
31
34
 
@@ -39,11 +42,23 @@ const renderOptions = {
39
42
  },
40
43
  };
41
44
 
45
+ beforeEach(() => {
46
+ envScope = nockInstance
47
+ .get(environmentPathsPath)
48
+ .query(true)
49
+ .reply(200, environmentPathsData);
50
+ });
51
+
52
+ afterEach(() => {
53
+ assertNockRequest(envScope);
54
+ nock.cleanAll();
55
+ });
42
56
  // This is written separately, as the autocomplete/search scopes are not needed.
43
57
  test('Can show versions details - Components Tab', async (done) => {
44
58
  const { version } = ContentViewVersionDetailsData;
45
59
  const scope = nockInstance
46
60
  .get(cvVersions)
61
+ .times(2)
47
62
  .query(true)
48
63
  .reply(200, getTabSpecificData('component_view_count'));
49
64
 
@@ -72,6 +87,7 @@ test('Can show versions details - Components Tab', async (done) => {
72
87
  .content_view.name)).toBeTruthy();
73
88
  });
74
89
 
90
+ assertNockRequest(scope);
75
91
  assertNockRequest(scope);
76
92
  assertNockRequest(componentScope, done);
77
93
  });
@@ -188,6 +204,7 @@ testConfig.forEach(({
188
204
 
189
205
  const scope = nockInstance
190
206
  .get(cvVersions)
207
+ .times(2)
191
208
  .query(true)
192
209
  .reply(200, getTabSpecificData(countKey));
193
210
 
@@ -219,6 +236,7 @@ testConfig.forEach(({
219
236
  assertNockRequest(autocompleteScope);
220
237
  assertNockRequest(searchDelayScope);
221
238
  assertNockRequest(autoSearchScope);
239
+ assertNockRequest(scope);
222
240
  assertNockRequest(tabScope);
223
241
  assertNockRequest(scope, done);
224
242
  }));
@@ -239,6 +257,7 @@ test('Can change repository selector', async (done) => {
239
257
  const scope = nockInstance
240
258
  .get(cvVersions)
241
259
  .query(true)
260
+ .times(2)
242
261
  .reply(200, {
243
262
  ...getTabSpecificData(countKey),
244
263
  repositories: ContentViewVersionDetailsCounts.repositories,
@@ -284,6 +303,7 @@ test('Can change repository selector', async (done) => {
284
303
  assertNockRequest(autocompleteScope);
285
304
  assertNockRequest(searchDelayScope);
286
305
  assertNockRequest(autoSearchScope);
306
+ assertNockRequest(scope);
287
307
  assertNockRequest(tabScope);
288
308
  assertNockRequest(scope, done);
289
309
  });
@@ -2,16 +2,20 @@ import React from 'react';
2
2
  import { renderWithRedux, patientlyWaitFor } from 'react-testing-lib-wrapper';
3
3
  import { Route } from 'react-router-dom';
4
4
 
5
- import { nockInstance, assertNockRequest } from '../../../../../../test-utils/nockWrapper';
5
+ import nock, { nockInstance, assertNockRequest } from '../../../../../../test-utils/nockWrapper';
6
6
  import api from '../../../../../../services/api';
7
7
  import { cvVersionDetailsKey } from '../../../../ContentViewsConstants';
8
8
  import ContentViewVersionDetails from '../ContentViewVersionDetails';
9
9
  import ContentViewVersionDetailsEmptyData from './ContentViewVersionDetails.fixtures.json';
10
10
  import cvDetailData from '../../../../__tests__/mockDetails.fixtures.json';
11
+ import environmentPathsData from '../../Delete/__tests__/versionRemoveEnvPaths.fixtures';
11
12
 
12
13
  const withCVRoute = component =>
13
14
  <Route path="/versions/:versionId([0-9]+)">{component}</Route>;
14
15
  const cvVersions = api.getApiUrl('/content_view_versions/73');
16
+ let envScope;
17
+ let versionScope;
18
+ const environmentPathsPath = api.getApiUrl('/organizations/1/environments/paths');
15
19
 
16
20
  const renderOptions = {
17
21
  apiNamespace: cvVersionDetailsKey(3, 73),
@@ -21,6 +25,23 @@ const renderOptions = {
21
25
  },
22
26
  };
23
27
 
28
+ beforeEach(() => {
29
+ envScope = nockInstance
30
+ .get(environmentPathsPath)
31
+ .query(true)
32
+ .reply(200, environmentPathsData);
33
+ versionScope = nockInstance
34
+ .get(cvVersions)
35
+ .query(true)
36
+ .reply(200, ContentViewVersionDetailsEmptyData);
37
+ });
38
+
39
+ afterEach(() => {
40
+ assertNockRequest(envScope);
41
+ assertNockRequest(versionScope);
42
+ nock.cleanAll();
43
+ });
44
+
24
45
  test('Can show versions detail header', async (done) => {
25
46
  const { version } = ContentViewVersionDetailsEmptyData;
26
47
  const scope = nockInstance
@@ -84,6 +84,7 @@ Array [
84
84
  Object {
85
85
  "interval": 5000,
86
86
  "payload": Object {
87
+ "handleSuccess": undefined,
87
88
  "key": "SUBSCRIPTIONS_POLL_TASK",
88
89
  "url": "/foreman_tasks/api/tasks/eb1b6271-8a69-4d98-84fc-bea06ddcc166",
89
90
  },
@@ -31,12 +31,13 @@ export const startPollingTasks = (key, taskSearchParams = {}) =>
31
31
 
32
32
  export const stopPollingTasks = key => stopInterval(bulkSearchKey(key));
33
33
 
34
- const getTask = (key, task) => get({
34
+ const getTask = (key, task, handleSuccess) => get({
35
35
  key,
36
36
  url: `${foremanTasksApi.baseApiPath}/tasks/${task.id}`,
37
+ handleSuccess,
37
38
  });
38
39
 
39
- export const startPollingTask = (key, task) =>
40
- withInterval(getTask(pollTaskKey(key), task));
40
+ export const startPollingTask = (key, task, handleSuccess) =>
41
+ withInterval(getTask(pollTaskKey(key), task, handleSuccess));
41
42
 
42
43
  export const stopPollingTask = key => stopInterval(pollTaskKey(key));
@@ -4,6 +4,7 @@ exports[`task actions can poll a task 1`] = `
4
4
  Object {
5
5
  "interval": 5000,
6
6
  "payload": Object {
7
+ "handleSuccess": undefined,
7
8
  "key": "TEST_POLL_TASK",
8
9
  "url": "/foreman_tasks/api/tasks/12345",
9
10
  },
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katello
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0.rc3
4
+ version: 4.3.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - N/A
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-07 00:00:00.000000000 Z
11
+ date: 2021-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -1572,6 +1572,7 @@ files:
1572
1572
  - app/views/dashboard/_subscription_status_widget.html.erb
1573
1573
  - app/views/dashboard/_subscription_widget.html.erb
1574
1574
  - app/views/dashboard/_sync_widget.html.erb
1575
+ - app/views/foreman/job_templates/change_content_source.erb
1575
1576
  - app/views/foreman/job_templates/install_errata.erb
1576
1577
  - app/views/foreman/job_templates/install_errata_-_katello_ansible_default.erb
1577
1578
  - app/views/foreman/job_templates/install_group.erb