katello 3.8.0.rc2 → 3.8.0.rc3

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 (23) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/api/v2/repositories_controller.rb +37 -24
  3. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/bulk/content-hosts-bulk-subscriptions-modal.controller.js +4 -1
  4. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/content/views/content-host-errata.html +1 -1
  5. data/lib/katello/version.rb +1 -1
  6. data/webpack/scenes/Products/ProductActions.js +24 -0
  7. data/webpack/scenes/Products/ProductConstants.js +3 -0
  8. data/webpack/scenes/Products/__tests__/ProductActions.test.js +40 -0
  9. data/webpack/scenes/Products/__tests__/products.fixtures.js +90 -0
  10. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailEnabledProducts.js +54 -0
  11. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailProduct.js +29 -0
  12. data/webpack/scenes/Subscriptions/Details/SubscriptionDetailReducer.js +29 -0
  13. data/webpack/scenes/Subscriptions/Details/SubscriptionDetails.js +67 -22
  14. data/webpack/scenes/Subscriptions/Details/SubscriptionDetails.scss +5 -0
  15. data/webpack/scenes/Subscriptions/Details/__tests__/SubscriptionDetailEnabledProducts.test.js +18 -0
  16. data/webpack/scenes/Subscriptions/Details/__tests__/SubscriptionDetailProduct.test.js +13 -0
  17. data/webpack/scenes/Subscriptions/Details/__tests__/SubscriptionDetails.test.js +4 -0
  18. data/webpack/scenes/Subscriptions/Details/__tests__/__snapshots__/SubscriptionDetailEnabledProducts.test.js.snap +45 -0
  19. data/webpack/scenes/Subscriptions/Details/__tests__/__snapshots__/SubscriptionDetailProduct.test.js.snap +67 -0
  20. data/webpack/scenes/Subscriptions/Details/__tests__/__snapshots__/SubscriptionDetails.test.js.snap +497 -411
  21. data/webpack/scenes/Subscriptions/Details/__tests__/subscriptionDetails.fixtures.js +4 -0
  22. data/webpack/scenes/Subscriptions/Details/index.js +3 -1
  23. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a29f4138e7efc30092c57d2d41cba2daa63d8f96
4
- data.tar.gz: 2d6286442b855654c1baaa52db2c96946b153c39
3
+ metadata.gz: c1e8d4def57738f812e6e9463c3a80a0ef587333
4
+ data.tar.gz: b2150d3ceeafdbd2b34ec05c8d85b69597183f10
5
5
  SHA512:
6
- metadata.gz: 98f1753327006589d1804417424b7b5252bbe60ba35cef2cac87a8b2f5088548dbfeedc5212f88d70240b97f7b80c12fad55e87c9d1985fae771120baff2bb39
7
- data.tar.gz: d7fed987e7d06d564e141b5675c08be8dc960f8b0f2e87531f3ef2268bf72250b228c518d4f6cc04ca261d4be69e166b88556ce760e5d0d6c3bf9b57fc0aef46
6
+ metadata.gz: 560eabd60a83230da3567573dfa0be73d488d92e47e5c4a1124356f1f0b54a9b82614fae8a8c3d6fa86a589779d8db6d4bc6109ef1bd5e318e7fa54ac51ca2a4
7
+ data.tar.gz: 4aa5dfd8a7d86cb8e402685d1e1da7da3a03e32baa30ae1f991eccb524f127f623a1b053357f5bb09d0a24f7e82b81f167269519cc8102cc111c5d3194eb97db
@@ -69,6 +69,7 @@ module Katello
69
69
  param :deb_id, String, :desc => N_("Id of a deb package to find repositories that contain the deb")
70
70
  param :erratum_id, String, :desc => N_("Id of an erratum to find repositories that contain the erratum")
71
71
  param :rpm_id, String, :desc => N_("Id of a rpm package to find repositories that contain the rpm")
72
+ param :file_id, String, :desc => N_("Id of a file to find repositories that contain the file")
72
73
  param :ostree_branch_id, String, :desc => N_("Id of an ostree branch to find repositories that contain that branch")
73
74
  param :library, :bool, :desc => N_("show repositories in Library and the default content view")
74
75
  param :content_type, RepositoryTypeManager.repository_types.keys, :desc => N_("limit to only repositories of this type")
@@ -107,32 +108,9 @@ module Katello
107
108
  query = index_relation_product(query)
108
109
  query = query.where(:content_type => params[:content_type]) if params[:content_type]
109
110
  query = query.where(:name => params[:name]) if params[:name]
110
-
111
- if params[:deb_id]
112
- query = query.joins(:debs).where("#{Deb.table_name}.id" => Deb.with_identifiers(params[:deb_id]))
113
- end
114
-
115
- if params[:erratum_id]
116
- query = query.joins(:errata).where("#{Erratum.table_name}.id" => Erratum.with_identifiers(params[:erratum_id]))
117
- end
118
-
119
- if params[:rpm_id]
120
- query = query.joins(:rpms).where("#{Rpm.table_name}.id" => Rpm.with_identifiers(params[:rpm_id]))
121
- end
122
-
123
- if params[:ostree_branch_id]
124
- query = query.joins(:ostree_branches).where("#{OstreeBranch.table_name}.id" => OstreeBranch.with_identifiers(params[:ostree_branch_id]))
125
- end
126
-
127
- if params[:puppet_module_id]
128
- query = query
129
- .joins(:puppet_modules)
130
- .where("#{PuppetModule.table_name}.id" => PuppetModule.with_identifiers(params[:puppet_module_id]))
131
- end
132
-
111
+ query = index_relation_content_unit(query)
133
112
  query = index_relation_content_view(query)
134
113
  query = index_relation_environment(query)
135
-
136
114
  query
137
115
  end
138
116
 
@@ -171,6 +149,41 @@ module Katello
171
149
  query
172
150
  end
173
151
 
152
+ def index_relation_content_unit(query)
153
+ if params[:deb_id]
154
+ query = query.joins(:debs)
155
+ .where("#{Deb.table_name}.id" => Deb.with_identifiers(params[:deb_id]))
156
+ end
157
+
158
+ if params[:erratum_id]
159
+ query = query.joins(:errata)
160
+ .where("#{Erratum.table_name}.id" => Erratum.with_identifiers(params[:erratum_id]))
161
+ end
162
+
163
+ if params[:rpm_id]
164
+ query = query.joins(:rpms)
165
+ .where("#{Rpm.table_name}.id" => Rpm.with_identifiers(params[:rpm_id]))
166
+ end
167
+
168
+ if params[:file_id]
169
+ query = query.joins(:files)
170
+ .where("#{FileUnit.table_name}.id" => FileUnit.with_identifiers(params[:file_id]))
171
+ end
172
+
173
+ if params[:ostree_branch_id]
174
+ query = query.joins(:ostree_branches)
175
+ .where("#{OstreeBranch.table_name}.id" => OstreeBranch.with_identifiers(params[:ostree_branch_id]))
176
+ end
177
+
178
+ if params[:puppet_module_id]
179
+ query = query
180
+ .joins(:puppet_modules)
181
+ .where("#{PuppetModule.table_name}.id" => PuppetModule.with_identifiers(params[:puppet_module_id]))
182
+ end
183
+
184
+ query
185
+ end
186
+
174
187
  api :POST, "/repositories", N_("Create a custom repository")
175
188
  param :name, String, :required => true
176
189
  param_group :repo_create
@@ -44,11 +44,14 @@ angular.module('Bastion.content-hosts').controller('ContentHostsBulkSubscription
44
44
  });
45
45
  };
46
46
 
47
- $scope.contentNutupane = new Nutupane(Subscription, params);
47
+
48
+ $scope.contentNutupane = new Nutupane(Subscription, params,
49
+ 'queryPaged', {disableAutoLoad: true});
48
50
  $scope.controllerName = 'katello_subscriptions';
49
51
  $scope.table = $scope.contentNutupane.table;
50
52
  $scope.contentNutupane.setSearchKey('subscriptionSearch');
51
53
  $scope.contentNutupane.masterOnly = true;
54
+ $scope.contentNutupane.load();
52
55
  $scope.groupedSubscriptions = {};
53
56
 
54
57
  $scope.$watch('table.rows', function (rows) {
@@ -17,7 +17,7 @@
17
17
  Content View and Lifecycle Environment. In order to apply such Errata an Incremental Update is required.
18
18
  </span>
19
19
 
20
- <a ui-sref="errata.index" translate>Click here to select Errata for an Incremental Update.</a>
20
+ <a ui-sref="errata" translate>Click here to select Errata for an Incremental Update.</a>
21
21
  </p>
22
22
  </div>
23
23
 
@@ -1,3 +1,3 @@
1
1
  module Katello
2
- VERSION = "3.8.0.rc2".freeze
2
+ VERSION = "3.8.0.rc3".freeze
3
3
  end
@@ -0,0 +1,24 @@
1
+ import api, { orgId } from '../../services/api';
2
+
3
+ import {
4
+ PRODUCTS_REQUEST,
5
+ PRODUCTS_SUCCESS,
6
+ PRODUCTS_FAILURE,
7
+ } from './ProductConstants';
8
+ import { apiError } from '../../move_to_foreman/common/helpers.js';
9
+
10
+ export const loadProducts = (params = {}) => (dispatch) => {
11
+ dispatch({ type: PRODUCTS_REQUEST });
12
+
13
+ return api
14
+ .get(`/organizations/${orgId()}/products/`, {}, params)
15
+ .then(({ data }) => {
16
+ dispatch({
17
+ type: PRODUCTS_SUCCESS,
18
+ response: data,
19
+ });
20
+ })
21
+ .catch(result => dispatch(apiError(PRODUCTS_FAILURE, result)));
22
+ };
23
+
24
+ export default loadProducts;
@@ -0,0 +1,3 @@
1
+ export const PRODUCTS_REQUEST = 'PRODUCTS_REQUEST';
2
+ export const PRODUCTS_SUCCESS = 'PRODUCTS_SUCCESS';
3
+ export const PRODUCTS_FAILURE = 'PRODUCTS_FAILURE';
@@ -0,0 +1,40 @@
1
+ import thunk from 'redux-thunk';
2
+ import Immutable from 'seamless-immutable';
3
+ import configureMockStore from 'redux-mock-store';
4
+ import { mock, mockRequest, mockErrorRequest } from '../../../mockRequest';
5
+ import {
6
+ failureActions,
7
+ successActions,
8
+ requestSuccessResponse,
9
+ } from './products.fixtures';
10
+ import { loadProducts } from '../ProductActions';
11
+
12
+ const mockStore = configureMockStore([thunk]);
13
+ const store = mockStore({ e: Immutable({}) });
14
+
15
+ beforeEach(() => {
16
+ store.clearActions();
17
+ mock.reset();
18
+ });
19
+
20
+ describe('product actions', () => {
21
+ describe('loadProducts', () => {
22
+ it('handles failed PRODUCTS_REQUEST', () => {
23
+ mockErrorRequest({
24
+ url: '/katello/api/v2/organizations/1/products/',
25
+ status: 422,
26
+ });
27
+ return store.dispatch(loadProducts())
28
+ .then(() => expect(store.getActions()).toEqual(failureActions));
29
+ });
30
+
31
+ it('handles successful PRODUCTS_REQUEST', () => {
32
+ mockRequest({
33
+ url: '/katello/api/v2/organizations/1/products/',
34
+ response: requestSuccessResponse,
35
+ });
36
+ return store.dispatch(loadProducts())
37
+ .then(() => expect(store.getActions()).toEqual(successActions));
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,90 @@
1
+ import Immutable from 'seamless-immutable';
2
+ import { toastErrorAction, failureAction } from '../../../services/api/testHelpers';
3
+
4
+ export const initialState = Immutable({
5
+ loading: true,
6
+ results: [],
7
+ pagination: {
8
+ page: 0,
9
+ perPage: 20,
10
+ },
11
+ });
12
+
13
+ export const loadingState = Immutable({
14
+ ...initialState,
15
+ });
16
+
17
+ export const emptyState = Immutable({
18
+ ...loadingState,
19
+ loading: false,
20
+ });
21
+
22
+ export const availableContent = Immutable({
23
+ enabled: true,
24
+ product_id: 114,
25
+ content: {
26
+ name: 'Red Hat Enterprise Linux 7 Server (RPMs)',
27
+ label: 'rhel-7-server-rpms',
28
+ vendor: 'Red Hat',
29
+ content_url: '/content/dist/rhel/server/7/$releasever/$basearch/os',
30
+ gpg_url: 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release',
31
+ id: '2456',
32
+ type: 'yum',
33
+ gpgUrl: 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release',
34
+ contentUrl: '/content/dist/rhel/server/7/$releasever/$basearch/os',
35
+ },
36
+ });
37
+
38
+ export const product = (content = []) => ({
39
+ id: 114,
40
+ cp_id: '69',
41
+ name: 'Red Hat Enterprise Linux Server',
42
+ label: 'Red_Hat_Enterprise_Linux_Server',
43
+ description: null,
44
+ provider_id: 2,
45
+ sync_plan_id: null,
46
+ sync_summary: {},
47
+ gpg_key_id: null,
48
+ ssl_ca_cert_id: null,
49
+ ssl_client_cert_id: null,
50
+ ssl_client_key_id: null,
51
+ sync_state: null,
52
+ last_sync: null,
53
+ last_sync_words: null,
54
+ organization_id: 1,
55
+ organization: {
56
+ name: 'Default Organization',
57
+ label: 'Default_Organization',
58
+ id: 1,
59
+ },
60
+ available_content: content,
61
+ sync_plan: null,
62
+ repository_count: 1,
63
+ });
64
+
65
+ export const requestSuccessResponse = Immutable({
66
+ results: [
67
+ product([availableContent]),
68
+ ],
69
+ });
70
+
71
+ const request = {
72
+ type: 'PRODUCTS_REQUEST',
73
+ };
74
+
75
+ export const successActions = [
76
+ request,
77
+ {
78
+ type: 'PRODUCTS_SUCCESS',
79
+ response: requestSuccessResponse,
80
+ search: undefined,
81
+ },
82
+ ];
83
+
84
+ export const failureActions = [
85
+ {
86
+ type: 'PRODUCTS_REQUEST',
87
+ },
88
+ failureAction('PRODUCTS_FAILURE'),
89
+ toastErrorAction(),
90
+ ];
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Col, ListView } from 'patternfly-react';
4
+ import SubscriptionDetailProduct from './SubscriptionDetailProduct';
5
+
6
+ const SubscriptionDetailEnabledProducts = ({ enabledProducts }) => {
7
+ const listItems = enabledProducts.results.map(product => ({
8
+ index: product.id,
9
+ title: product.name,
10
+ availableContent: (
11
+ product.available_content.map(c => (
12
+ {
13
+ enabled: c.enabled,
14
+ ...c.content,
15
+ }
16
+ ))
17
+ ),
18
+ }));
19
+
20
+ if (listItems.length > 0) {
21
+ return (
22
+ <ListView>
23
+ {listItems.map(({
24
+ index,
25
+ title,
26
+ availableContent,
27
+ }) => (
28
+ <ListView.Item
29
+ key={index}
30
+ heading={title}
31
+ hideCloseIcon
32
+ >
33
+
34
+ <Col sm={12}>
35
+ {availableContent.map(content => (
36
+ <SubscriptionDetailProduct key={content.id} content={content} />
37
+ ))}
38
+ </Col>
39
+ </ListView.Item>
40
+ ))}
41
+ </ListView>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <div>{ __('No products are enabled.') }</div>
47
+ );
48
+ };
49
+
50
+ SubscriptionDetailEnabledProducts.propTypes = {
51
+ enabledProducts: PropTypes.shape({}).isRequired,
52
+ };
53
+
54
+ export default SubscriptionDetailEnabledProducts;
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Row, Col } from 'patternfly-react';
4
+
5
+ const SubscriptionDetailProduct = ({ content }) => (
6
+ <Row key={content.id}>
7
+ <Col sm={12}>
8
+ <Row><u>{content.name}</u></Row>
9
+ </Col>
10
+ <Col sm={3}>
11
+ <Row>{ __('Content Download URL') }</Row>
12
+ <Row>{ __('GPG Key URL') }</Row>
13
+ <Row>{ __('Repo Type') }</Row>
14
+ <Row>{ __('Enabled?') }</Row>
15
+ </Col>
16
+ <Col sm={9}>
17
+ <Row>{content.content_url}</Row>
18
+ <Row>{content.gpg_url}</Row>
19
+ <Row>{content.type}</Row>
20
+ <Row>{content.enabled ? __('yes') : __('no')}</Row>
21
+ </Col>
22
+ </Row>
23
+ );
24
+
25
+ SubscriptionDetailProduct.propTypes = {
26
+ content: PropTypes.shape({}).isRequired,
27
+ };
28
+
29
+ export default SubscriptionDetailProduct;
@@ -4,9 +4,18 @@ import {
4
4
  SUBSCRIPTION_DETAILS_SUCCESS,
5
5
  SUBSCRIPTION_DETAILS_FAILURE,
6
6
  } from './SubscriptionDetailConstants';
7
+ import {
8
+ PRODUCTS_REQUEST,
9
+ PRODUCTS_SUCCESS,
10
+ PRODUCTS_FAILURE,
11
+ } from '../../Products/ProductConstants';
7
12
 
8
13
  const initialState = Immutable({
9
14
  loading: false,
15
+ enabledProducts: {
16
+ results: [],
17
+ total: 0,
18
+ },
10
19
  });
11
20
 
12
21
  export default (state = initialState, action) => {
@@ -15,6 +24,10 @@ export default (state = initialState, action) => {
15
24
  return state.set('loading', true);
16
25
  }
17
26
 
27
+ case PRODUCTS_REQUEST: {
28
+ return state.set('loading', true);
29
+ }
30
+
18
31
  case SUBSCRIPTION_DETAILS_SUCCESS: {
19
32
  const subscriptionDetails = action.response;
20
33
 
@@ -24,6 +37,15 @@ export default (state = initialState, action) => {
24
37
  });
25
38
  }
26
39
 
40
+ case PRODUCTS_SUCCESS: {
41
+ const enabledProducts = { enabledProducts: action.response };
42
+
43
+ return state.merge({
44
+ ...enabledProducts,
45
+ loading: false,
46
+ });
47
+ }
48
+
27
49
  case SUBSCRIPTION_DETAILS_FAILURE: {
28
50
  return state.merge({
29
51
  error: action.payload.message,
@@ -31,6 +53,13 @@ export default (state = initialState, action) => {
31
53
  });
32
54
  }
33
55
 
56
+ case PRODUCTS_FAILURE: {
57
+ return state.merge({
58
+ error: action.payload.message,
59
+ loading: false,
60
+ });
61
+ }
62
+
34
63
  default:
35
64
  return state;
36
65
  }
@@ -1,10 +1,11 @@
1
1
  import React, { Component } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Grid, Row, Col } from 'patternfly-react';
3
+ import { Nav, NavItem, TabPane, TabContent, TabContainer, Grid, Row, Col } from 'patternfly-react';
4
4
  import BreadcrumbsBar from 'foremanReact/components/BreadcrumbBar';
5
5
  import SubscriptionDetailInfo from './SubscriptionDetailInfo';
6
6
  import SubscriptionDetailAssociations from './SubscriptionDetailAssociations';
7
7
  import SubscriptionDetailProducts from './SubscriptionDetailProducts';
8
+ import SubscriptionDetailEnabledProducts from './SubscriptionDetailEnabledProducts';
8
9
  import { LoadingState } from '../../../move_to_pf/LoadingState';
9
10
  import { notify } from '../../../move_to_foreman/foreman_toast_notifications';
10
11
  import api from '../../../services/api';
@@ -18,12 +19,22 @@ class SubscriptionDetails extends Component {
18
19
  // eslint-disable-next-line react/prop-types
19
20
  const routerParams = this.props.match.params;
20
21
  this.props.loadSubscriptionDetails(parseInt(routerParams.id, 10));
22
+ this.props.loadProducts({
23
+ subscription_id: parseInt(routerParams.id, 10),
24
+ include_available_content: true,
25
+ enabled: true,
26
+ });
21
27
  }
22
28
 
23
29
  componentDidUpdate(prevProps) {
24
30
  const routerParams = this.props.match.params;
25
31
  if (routerParams.id !== prevProps.match.params.id) {
26
32
  this.props.loadSubscriptionDetails(parseInt(routerParams.id, 10));
33
+ this.props.loadProducts({
34
+ subscription_id: parseInt(routerParams.id, 10),
35
+ include_available_content: true,
36
+ enabled: true,
37
+ });
27
38
  }
28
39
  }
29
40
 
@@ -34,6 +45,8 @@ class SubscriptionDetails extends Component {
34
45
 
35
46
  render() {
36
47
  const { subscriptionDetails } = this.props;
48
+
49
+
37
50
  const resource = {
38
51
  nameField: 'name',
39
52
  resourceUrl: api.getApiUrl('/subscriptions'),
@@ -45,7 +58,7 @@ class SubscriptionDetails extends Component {
45
58
  }
46
59
 
47
60
  return (
48
- <Grid bsClass="container-fluid">
61
+ <div>
49
62
  {!subscriptionDetails.loading && <BreadcrumbsBar
50
63
  onSwitcherItemClick={(e, url) => this.handleBreadcrumbSwitcherItem(e, url)}
51
64
  data={{
@@ -63,32 +76,64 @@ class SubscriptionDetails extends Component {
63
76
  resource,
64
77
  }}
65
78
  />}
66
- <div>
67
- <LoadingState loading={subscriptionDetails.loading} loadingText={__('Loading')}>
68
- <Row>
69
- <Col sm={6}>
70
- <SubscriptionDetailInfo
71
- subscriptionDetails={subscriptionDetails}
72
- />
73
- </Col>
74
- <Col sm={6}>
75
- <SubscriptionDetailAssociations
76
- subscriptionDetails={subscriptionDetails}
77
- />
78
- <SubscriptionDetailProducts
79
- subscriptionDetails={subscriptionDetails}
80
- />
81
- </Col>
82
- </Row>
83
- </LoadingState>
84
- </div>
85
- </Grid>
79
+
80
+ <TabContainer id="subscription-tabs-container" defaultActiveKey={1}>
81
+ <div>
82
+ <LoadingState loading={subscriptionDetails.loading} loadingText={__('Loading')}>
83
+ <Nav bsClass="nav nav-tabs">
84
+ <NavItem eventKey={1}>
85
+ <div>{__('Details')}</div>
86
+ </NavItem>
87
+ <NavItem eventKey={2}>
88
+ <div>{__('Enabled Products')}</div>
89
+ </NavItem>
90
+ </Nav>
91
+ <Grid bsClass="container-fluid">
92
+ <TabContent animation={false}>
93
+ <TabPane eventKey={1}>
94
+ <div>
95
+ <Row>
96
+ <Col sm={6}>
97
+ <SubscriptionDetailInfo
98
+ subscriptionDetails={subscriptionDetails}
99
+ />
100
+ </Col>
101
+ <Col sm={6}>
102
+ <SubscriptionDetailAssociations
103
+ subscriptionDetails={subscriptionDetails}
104
+ />
105
+ <SubscriptionDetailProducts
106
+ subscriptionDetails={subscriptionDetails}
107
+ />
108
+ </Col>
109
+ </Row>
110
+ </div>
111
+ </TabPane>
112
+
113
+ <TabPane eventKey={2}>
114
+ <div>
115
+ <Row>
116
+ <Col sm={12}>
117
+ <SubscriptionDetailEnabledProducts
118
+ enabledProducts={subscriptionDetails.enabledProducts}
119
+ />
120
+ </Col>
121
+ </Row>
122
+ </div>
123
+ </TabPane>
124
+ </TabContent>
125
+ </Grid>
126
+ </LoadingState>
127
+ </div>
128
+ </TabContainer>
129
+ </div>
86
130
  );
87
131
  }
88
132
  }
89
133
 
90
134
  SubscriptionDetails.propTypes = {
91
135
  loadSubscriptionDetails: PropTypes.func.isRequired,
136
+ loadProducts: PropTypes.func.isRequired,
92
137
  subscriptionDetails: PropTypes.shape({}).isRequired,
93
138
  history: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
94
139
  };