foreman_scc_manager 1.8.19 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -10
  3. data/app/controllers/api/v2/scc_accounts_controller.rb +48 -5
  4. data/app/controllers/api/v2/scc_products_controller.rb +1 -1
  5. data/app/controllers/scc_accounts_controller.rb +47 -0
  6. data/app/lib/actions/scc_manager/subscribe_product.rb +61 -19
  7. data/app/models/scc_account.rb +4 -3
  8. data/app/models/scc_katello_repository.rb +4 -0
  9. data/app/models/scc_product.rb +7 -1
  10. data/app/models/scc_repository.rb +12 -4
  11. data/app/views/scc_accounts/show.html.erb +7 -51
  12. data/config/routes.rb +1 -0
  13. data/db/migrate/20220425132605_add_scc_katello_repository_relation.rb +10 -0
  14. data/db/migrate/20220429100843_remove_root_repository_id_from_scc_repository.rb +6 -0
  15. data/db/migrate/20220429102717_populate_scc_katello_repositories.rb +21 -0
  16. data/db/migrate/20220513132652_populate_upstream_authentication_token.rb +23 -0
  17. data/db/migrate/20220531120722_add_product_category_to_scc_products.rb +5 -0
  18. data/lib/foreman_scc_manager/engine.rb +1 -1
  19. data/lib/foreman_scc_manager/version.rb +1 -1
  20. data/lib/tasks/republish_repositories.rake +13 -0
  21. data/lib/tasks/setup_authentication_token.rake +27 -0
  22. data/package.json +54 -0
  23. data/test/controllers/scc_accounts_controller_test.rb +6 -0
  24. data/test/fixtures/models/katello_root_repositories.yml +44 -0
  25. data/test/fixtures/models/scc_repositories.yml +34 -0
  26. data/test/support/fixtures_support.rb +3 -1
  27. data/webpack/components/SCCProductPage/EmptySccProducts.js +46 -0
  28. data/webpack/components/SCCProductPage/SCCProductPage.js +96 -0
  29. data/webpack/components/SCCProductPage/SCCProductPageActions.js +27 -0
  30. data/webpack/components/SCCProductPage/SCCProductPageConstants.js +5 -0
  31. data/webpack/components/SCCProductPage/SCCProductPageReducer.js +34 -0
  32. data/webpack/components/SCCProductPage/SCCProductPageSelectors.js +7 -0
  33. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCGenericPicker/index.js +80 -0
  34. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/components/SCCRepoPicker/index.js +174 -0
  35. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/components/SCCRepoPicker/styles.scss +3 -0
  36. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/index.js +303 -0
  37. data/webpack/components/SCCProductPage/components/SCCProductPicker/index.js +235 -0
  38. data/webpack/components/SCCProductPage/components/SCCProductPicker/styles.scss +10 -0
  39. data/webpack/components/SCCProductPage/components/SCCProductPickerModal/index.js +81 -0
  40. data/webpack/components/SCCProductPage/components/SCCProductView/components/SCCRepoView/index.js +113 -0
  41. data/webpack/components/SCCProductPage/components/SCCProductView/components/SCCRepoView/styles.scss +14 -0
  42. data/webpack/components/SCCProductPage/components/SCCProductView/index.js +236 -0
  43. data/webpack/components/SCCProductPage/components/common/SCCGenericExpander/index.js +58 -0
  44. data/webpack/components/SCCProductPage/components/common/SCCProductTreeExpander/index.js +21 -0
  45. data/webpack/components/SCCProductPage/components/common/SCCSubscribedProductsExpander/index.js +21 -0
  46. data/webpack/components/SCCProductPage/index.js +18 -0
  47. data/webpack/components/SCCProductPage/sccProductPage.scss +8 -0
  48. data/webpack/index.js +11 -0
  49. data/webpack/reducer.js +7 -0
  50. metadata +48 -14
@@ -0,0 +1,303 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import {
6
+ TreeView,
7
+ Button,
8
+ Tooltip,
9
+ Switch,
10
+ Flex,
11
+ FlexItem,
12
+ Card,
13
+ CardBody,
14
+ } from '@patternfly/react-core';
15
+ import { cloneDeep, merge, clone } from 'lodash';
16
+ import SCCRepoPicker from './components/SCCRepoPicker';
17
+ import { subscribeProductsWithReposAction } from '../../../../SCCProductPageActions';
18
+ import SCCProductTreeExpander from '../../../common/SCCProductTreeExpander';
19
+
20
+ const addCheckBoxToTree = (tree) => {
21
+ const checkProps = {};
22
+ checkProps.checked = tree.product_id !== null;
23
+ checkProps.disabled = tree.product_id !== null;
24
+ tree.checkProps = checkProps;
25
+
26
+ return tree;
27
+ };
28
+
29
+ const addParentToTree = (tree, par) => {
30
+ tree.parent = par;
31
+ return tree;
32
+ };
33
+
34
+ const addSCCRepoPickerToTree = (
35
+ tree,
36
+ disableRepos,
37
+ activateDebugFilter,
38
+ setSelectedReposFromChild
39
+ ) => {
40
+ tree.customBadgeContent[0] = (
41
+ <Tooltip content={__('Filter repositories')}>
42
+ <SCCRepoPicker
43
+ sccRepos={tree.scc_repositories}
44
+ disableRepos={tree.product_id === null && !tree.checkProps.checked}
45
+ activateDebugFilter={activateDebugFilter}
46
+ productAlreadySynced={tree.product_id !== null}
47
+ sccProductId={tree.id}
48
+ sccProductName={tree.name}
49
+ setSelectedReposFromChild={setSelectedReposFromChild}
50
+ />{' '}
51
+ </Tooltip>
52
+ );
53
+ return tree;
54
+ };
55
+
56
+ const setupTreeViewListItem = (
57
+ tree,
58
+ isRoot,
59
+ activateDebugFilter,
60
+ setSelectedReposFromChild
61
+ ) => {
62
+ tree.customBadgeContent = [];
63
+ addCheckBoxToTree(tree);
64
+ addSCCRepoPickerToTree(
65
+ tree,
66
+ tree.product_id === null,
67
+ activateDebugFilter,
68
+ setSelectedReposFromChild
69
+ );
70
+ if ('children' in tree) {
71
+ tree.children = tree.children.map((p) =>
72
+ setupTreeViewListItem(
73
+ p,
74
+ false,
75
+ activateDebugFilter,
76
+ setSelectedReposFromChild
77
+ )
78
+ );
79
+ tree.children.map((child) => addParentToTree(child, tree));
80
+ }
81
+ return tree;
82
+ };
83
+
84
+ const checkAllParents = (
85
+ tree,
86
+ activateDebugFilter,
87
+ setSelectedReposFromChild
88
+ ) => {
89
+ if (!tree.checkProps.checked) {
90
+ tree.checkProps.checked = true;
91
+ addSCCRepoPickerToTree(
92
+ tree,
93
+ false,
94
+ activateDebugFilter,
95
+ setSelectedReposFromChild
96
+ );
97
+ }
98
+ if (tree.parent)
99
+ checkAllParents(
100
+ tree.parent,
101
+ activateDebugFilter,
102
+ setSelectedReposFromChild
103
+ );
104
+
105
+ return tree;
106
+ };
107
+
108
+ const uncheckAllChildren = (
109
+ tree,
110
+ activateDebugFilter,
111
+ setSelectedReposFromChild
112
+ ) => {
113
+ if (tree.product_id === null) {
114
+ tree.checkProps.checked = false;
115
+ addSCCRepoPickerToTree(
116
+ tree,
117
+ true,
118
+ activateDebugFilter,
119
+ setSelectedReposFromChild
120
+ );
121
+ }
122
+ if ('children' in tree)
123
+ tree.children = tree.children.map((c) =>
124
+ uncheckAllChildren(c, activateDebugFilter, setSelectedReposFromChild)
125
+ );
126
+
127
+ return tree;
128
+ };
129
+
130
+ const getRootParent = (tree) => {
131
+ if (tree.parent) return getRootParent(tree.parent);
132
+
133
+ return tree;
134
+ };
135
+
136
+ const SCCTreePicker = ({
137
+ sccProducts,
138
+ sccAccountId,
139
+ resetFormFromParent,
140
+ handleSubscribeCallback,
141
+ }) => {
142
+ const dispatch = useDispatch();
143
+ // this needs to be uninitialized such that the first call to setAllExpanded can actually
144
+ // change the value of allExpanded
145
+ const [expandAll, setExpandAll] = useState();
146
+ const [selectedRepos, setSelectedRepos] = useState({});
147
+ // the debug filter is actually a 'includeDebugRepos' setting which should not be active by default
148
+ const [activateDebugFilter, setActivateDebugFilter] = useState(false);
149
+
150
+ const setSelectedReposFromChild = (
151
+ productId,
152
+ productName,
153
+ repoIds,
154
+ repoNames
155
+ ) => {
156
+ if (repoIds.length !== 0) {
157
+ selectedRepos[productId] = {};
158
+ selectedRepos[productId].repoIds = repoIds;
159
+ selectedRepos[productId].productName = productName;
160
+ selectedRepos[productId].repoNames = repoNames;
161
+ const newSelectedRepos = clone(selectedRepos);
162
+ setSelectedRepos(newSelectedRepos);
163
+ } else if (selectedRepos !== {} && productId in selectedRepos) {
164
+ delete selectedRepos[productId];
165
+ const newSelectedRepos = clone(selectedRepos);
166
+ setSelectedRepos(newSelectedRepos);
167
+ }
168
+ };
169
+
170
+ const [sccProductTree, setSccProductTree] = useState(
171
+ cloneDeep(sccProducts).map((p) =>
172
+ setupTreeViewListItem(
173
+ p,
174
+ true,
175
+ activateDebugFilter,
176
+ setSelectedReposFromChild
177
+ )
178
+ )
179
+ );
180
+
181
+ useEffect(() => {
182
+ setSccProductTree(
183
+ cloneDeep(sccProducts).map((p) =>
184
+ setupTreeViewListItem(
185
+ p,
186
+ true,
187
+ activateDebugFilter,
188
+ setSelectedReposFromChild
189
+ )
190
+ )
191
+ );
192
+ // some thorough cleaning is required for hash maps
193
+ Object.keys(selectedRepos).forEach((k) => delete selectedRepos[k]);
194
+ }, [sccProducts]);
195
+
196
+ const setExpandAllFromChild = (expandAllFromChild) => {
197
+ setExpandAll(expandAllFromChild);
198
+ };
199
+
200
+ const debugFilterChange = (evt) => {
201
+ setActivateDebugFilter(!activateDebugFilter);
202
+ };
203
+
204
+ const onCheck = (evt, treeViewItem) => {
205
+ if (evt.target.checked) {
206
+ checkAllParents(
207
+ treeViewItem,
208
+ activateDebugFilter,
209
+ setSelectedReposFromChild
210
+ );
211
+ } else {
212
+ uncheckAllChildren(
213
+ treeViewItem,
214
+ activateDebugFilter,
215
+ setSelectedReposFromChild
216
+ );
217
+ }
218
+
219
+ setSccProductTree([...merge(sccProductTree, getRootParent(treeViewItem))]);
220
+ };
221
+
222
+ const submitForm = (evt) => {
223
+ const productsToSubscribe = [];
224
+ Object.keys(selectedRepos).forEach((k) => {
225
+ const repo = {
226
+ scc_product_id: parseInt(k, 10),
227
+ repository_list: selectedRepos[k].repoIds,
228
+ };
229
+ productsToSubscribe.push(repo);
230
+ });
231
+ dispatch(
232
+ subscribeProductsWithReposAction(
233
+ sccAccountId,
234
+ productsToSubscribe,
235
+ handleSubscribeCallback,
236
+ selectedRepos
237
+ )
238
+ );
239
+ // reset data structure and form
240
+ setSelectedRepos({});
241
+ resetFormFromParent();
242
+ };
243
+
244
+ return (
245
+ <Card>
246
+ <CardBody>
247
+ <Flex direction={{ default: 'column' }}>
248
+ <Flex>
249
+ <SCCProductTreeExpander
250
+ setExpandAllInParent={setExpandAllFromChild}
251
+ />
252
+ <FlexItem>
253
+ <Tooltip
254
+ content={__(
255
+ 'If this option is enabled, debug and source pool repositories are automatically selected if you select a product. This option is disabled by default. It applies for unselected products, only. Already selected products are not filtered.'
256
+ )}
257
+ >
258
+ <Switch
259
+ id="filter-debug-switch"
260
+ onChange={debugFilterChange}
261
+ isChecked={activateDebugFilter}
262
+ label={__('Include Debug and Source Pool repositories')}
263
+ />
264
+ </Tooltip>
265
+ </FlexItem>
266
+ </Flex>
267
+ <Flex>
268
+ <TreeView
269
+ data={sccProductTree}
270
+ allExpanded={expandAll}
271
+ onCheck={onCheck}
272
+ hasChecks
273
+ hasBadges
274
+ hasGuides
275
+ />
276
+ </Flex>
277
+ <Flex>
278
+ <Button
279
+ variant="primary"
280
+ onClick={submitForm}
281
+ isDisabled={Object.keys(selectedRepos).length === 0}
282
+ >
283
+ {__('Add product(s)')}
284
+ </Button>
285
+ </Flex>
286
+ </Flex>
287
+ </CardBody>
288
+ </Card>
289
+ );
290
+ };
291
+
292
+ SCCTreePicker.propTypes = {
293
+ sccProducts: PropTypes.array,
294
+ sccAccountId: PropTypes.number.isRequired,
295
+ resetFormFromParent: PropTypes.func.isRequired,
296
+ handleSubscribeCallback: PropTypes.func.isRequired,
297
+ };
298
+
299
+ SCCTreePicker.defaultProps = {
300
+ sccProducts: [],
301
+ };
302
+
303
+ export default SCCTreePicker;
@@ -0,0 +1,235 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TimesIcon } from '@patternfly/react-icons';
4
+ import {
5
+ Button,
6
+ Card,
7
+ CardTitle,
8
+ CardBody,
9
+ CardHeader,
10
+ CardExpandableContent,
11
+ Flex,
12
+ FlexItem,
13
+ } from '@patternfly/react-core';
14
+ import { translate as __ } from 'foremanReact/common/I18n';
15
+ import { uniq } from 'lodash';
16
+ import './styles.scss';
17
+ import SCCGenericPicker from './components/SCCGenericPicker';
18
+ import SCCTreePicker from './components/SCCTreePicker';
19
+
20
+ const resetSelectionStringProduct = __(' -- Select Product --');
21
+ const resetSelectionStringVersion = __(' -- Select Version --');
22
+ const resetSelectionStringArch = __(' -- Select Architecture --');
23
+
24
+ const genericFilter = (object, comparator) =>
25
+ // we can have architectures that are not set
26
+ comparator === '' ||
27
+ comparator === resetSelectionStringProduct ||
28
+ comparator === resetSelectionStringVersion ||
29
+ comparator === resetSelectionStringArch ||
30
+ object === comparator ||
31
+ (object === null && comparator === 'no arch');
32
+
33
+ const filterVersionByProduct = (sccProducts, product) =>
34
+ uniq(
35
+ sccProducts
36
+ .filter((p) => p.product_category === product)
37
+ .map((i) => i.version)
38
+ ).sort();
39
+
40
+ const filterArchByVersionAndProduct = (sccProducts, product, version) =>
41
+ uniq(
42
+ sccProducts
43
+ .filter((p) => p.product_category === product && p.version === version)
44
+ .map((i) => i.arch)
45
+ ).sort();
46
+
47
+ const SCCProductPicker = ({
48
+ sccProducts,
49
+ sccAccountId,
50
+ editProductId,
51
+ handleSubscribeCallback,
52
+ }) => {
53
+ const [productItems, setProductItems] = useState(
54
+ uniq(sccProducts.map((p) => p.product_category))
55
+ );
56
+ const [selectedProduct, setSelectedProduct] = useState('');
57
+ const [archItems, setArchItems] = useState([]);
58
+ const [selectedArch, setSelectedArch] = useState('');
59
+ const [versionItems, setVersionItems] = useState([]);
60
+ const [selectedVersion, setSelectedVersion] = useState('');
61
+ const [filteredSccProducts, setFilteredSccProducts] = useState([]);
62
+ const [showSearchTree, setShowSearchTree] = useState(false);
63
+ const [isExpanded, setIsExpanded] = useState(false);
64
+
65
+ useEffect(() => {
66
+ if (editProductId !== 0) {
67
+ // the id is unique, so there never should be more than 1 element in the array
68
+ const product = sccProducts.filter((p) => p.id === editProductId);
69
+ if (product.length > 0) {
70
+ setSelectedProduct(product[0].product_category);
71
+ setSelectedArch(product[0].arch);
72
+ setSelectedVersion(product[0].version);
73
+ setFilteredSccProducts(product);
74
+ setShowSearchTree(true);
75
+ setIsExpanded(true);
76
+ }
77
+ }
78
+ }, [editProductId, sccProducts]);
79
+
80
+ const onProductSelectionChange = (value) => {
81
+ if (value !== resetSelectionStringProduct) {
82
+ setVersionItems(filterVersionByProduct(sccProducts, value));
83
+ } else {
84
+ setVersionItems([]);
85
+ }
86
+ setSelectedProduct(value);
87
+ setArchItems([]);
88
+ setSelectedVersion('');
89
+ setSelectedArch('');
90
+ };
91
+
92
+ const onVersionSelectionChange = (value) => {
93
+ if (value === resetSelectionStringVersion) {
94
+ setArchItems([]);
95
+ } else {
96
+ setArchItems(
97
+ filterArchByVersionAndProduct(sccProducts, selectedProduct, value)
98
+ );
99
+ }
100
+ setSelectedVersion(value);
101
+ setSelectedArch('');
102
+ };
103
+
104
+ const onArchSelectionChange = (value) => {
105
+ setSelectedArch(value);
106
+ };
107
+
108
+ const onExpand = (evt, id) => {
109
+ setIsExpanded(!isExpanded);
110
+ };
111
+
112
+ const filterProducts = (evt) => {
113
+ setShowSearchTree(true);
114
+ setFilteredSccProducts(
115
+ sccProducts.filter(
116
+ (p) =>
117
+ genericFilter(p.product_category, selectedProduct) &&
118
+ genericFilter(p.arch, selectedArch) &&
119
+ genericFilter(p.version, selectedVersion)
120
+ )
121
+ );
122
+ };
123
+
124
+ const resetTreeForm = () => {
125
+ setShowSearchTree(false);
126
+ setSelectedProduct('');
127
+ setSelectedVersion('');
128
+ setSelectedArch('');
129
+ };
130
+
131
+ return (
132
+ <Card border="dark" id="product-selection-card" isExpanded={isExpanded}>
133
+ <CardHeader onExpand={onExpand}>
134
+ <CardTitle>{__('Select SUSE products')}</CardTitle>
135
+ </CardHeader>
136
+ <CardExpandableContent>
137
+ <CardBody>
138
+ <Flex direction={{ default: 'column' }}>
139
+ <Flex>
140
+ <FlexItem>
141
+ <SCCGenericPicker
142
+ key="prod-select"
143
+ selectionItems={
144
+ selectedProduct === ''
145
+ ? productItems
146
+ : [resetSelectionStringProduct].concat(productItems)
147
+ }
148
+ setGlobalSelected={onProductSelectionChange}
149
+ screenReaderLabel={resetSelectionStringProduct}
150
+ initialLabel={
151
+ selectedProduct === ''
152
+ ? resetSelectionStringProduct
153
+ : selectedProduct
154
+ }
155
+ />
156
+ </FlexItem>
157
+ <FlexItem>
158
+ <SCCGenericPicker
159
+ key="vers-select"
160
+ selectionItems={
161
+ selectedVersion === ''
162
+ ? versionItems
163
+ : [resetSelectionStringVersion].concat(versionItems)
164
+ }
165
+ setGlobalSelected={onVersionSelectionChange}
166
+ screenReaderLabel={resetSelectionStringVersion}
167
+ initialLabel={
168
+ selectedVersion === ''
169
+ ? resetSelectionStringVersion
170
+ : selectedVersion
171
+ }
172
+ />
173
+ </FlexItem>
174
+ <FlexItem>
175
+ <SCCGenericPicker
176
+ key="arch-select"
177
+ selectionItems={
178
+ selectedArch === ''
179
+ ? archItems
180
+ : [resetSelectionStringArch].concat(archItems)
181
+ }
182
+ setGlobalSelected={onArchSelectionChange}
183
+ screenReaderLabel={resetSelectionStringArch}
184
+ initialLabel={
185
+ selectedArch === ''
186
+ ? resetSelectionStringArch
187
+ : selectedArch
188
+ }
189
+ />
190
+ </FlexItem>
191
+ <FlexItem>
192
+ <Button variant="primary" onClick={filterProducts}>
193
+ {__('Search')}
194
+ </Button>
195
+ </FlexItem>
196
+ <FlexItem>
197
+ <Button
198
+ variant="link"
199
+ icon={<TimesIcon />}
200
+ onClick={resetTreeForm}
201
+ >
202
+ {__('Reset Selection')}
203
+ </Button>
204
+ </FlexItem>
205
+ </Flex>
206
+ <FlexItem>
207
+ {showSearchTree && (
208
+ <SCCTreePicker
209
+ sccProducts={filteredSccProducts}
210
+ sccAccountId={sccAccountId}
211
+ resetFormFromParent={resetTreeForm}
212
+ handleSubscribeCallback={handleSubscribeCallback}
213
+ />
214
+ )}
215
+ </FlexItem>
216
+ </Flex>
217
+ </CardBody>
218
+ </CardExpandableContent>
219
+ </Card>
220
+ );
221
+ };
222
+
223
+ SCCProductPicker.propTypes = {
224
+ sccProducts: PropTypes.array,
225
+ sccAccountId: PropTypes.number.isRequired,
226
+ editProductId: PropTypes.number,
227
+ handleSubscribeCallback: PropTypes.func.isRequired,
228
+ };
229
+
230
+ SCCProductPicker.defaultProps = {
231
+ sccProducts: [],
232
+ editProductId: 0,
233
+ };
234
+
235
+ export default SCCProductPicker;
@@ -0,0 +1,10 @@
1
+ @import '~@theforeman/vendor/scss/variables';
2
+
3
+ .pf-c-select__toggle {
4
+ font-size: var(--pf-global--FontSize--xs);
5
+ font-weight: var(--pf-global--FontWeight--bold);
6
+ };
7
+
8
+ .pf-c-switch__input ~ .pf-c-switch__label {
9
+ font-size: var(--pf-global--FontSize--xs);
10
+ };
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import ForemanModal from 'foremanReact/components/ForemanModal';
4
+
5
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
+ import {
7
+ Card,
8
+ CardBody,
9
+ TextContent,
10
+ Text,
11
+ TextList,
12
+ TextListItem,
13
+ TextListVariants,
14
+ TextVariants,
15
+ } from '@patternfly/react-core';
16
+
17
+ const generateRepoList = (repoNames, productName) => (
18
+ <>
19
+ <TextListItem key={productName}>{productName}</TextListItem>
20
+ <TextList key={repoNames}>
21
+ {repoNames.map((repo) => (
22
+ <TextListItem key={repo}>{repo}</TextListItem>
23
+ ))}
24
+ </TextList>
25
+ </>
26
+ );
27
+
28
+ const generateProductList = (reposToSubscribe) => (
29
+ <TextList
30
+ key={reposToSubscribe.map((p) => p.productName)}
31
+ component={TextListVariants.ol}
32
+ >
33
+ {reposToSubscribe.map((p) => generateRepoList(p.repoNames, p.productName))}
34
+ </TextList>
35
+ );
36
+
37
+ const SCCProductPickerModal = ({ id, taskId, reposToSubscribe }) => (
38
+ <>
39
+ <ForemanModal
40
+ id={id}
41
+ title={__('Summary of SCC product subscription')}
42
+ enforceFocus
43
+ >
44
+ <Card>
45
+ <CardBody>
46
+ <TextContent key={taskId}>
47
+ <Text key={'modal1'.concat(taskId)}>
48
+ {__('The subscription task with id ')}
49
+ <Text
50
+ key={'modal2'.concat(taskId)}
51
+ component={TextVariants.a}
52
+ target="_blank"
53
+ href={sprintf('/foreman_tasks/tasks/%s', taskId)}
54
+ >
55
+ {sprintf('%s', taskId)}
56
+ </Text>
57
+ {__(' has started successfully.')}
58
+ </Text>
59
+ <Text key={'modal3'.concat(taskId)}>
60
+ {__('The following products will be imported:')}
61
+ </Text>
62
+ {generateProductList(reposToSubscribe)}
63
+ </TextContent>
64
+ </CardBody>
65
+ </Card>
66
+ </ForemanModal>
67
+ </>
68
+ );
69
+ SCCProductPickerModal.propTypes = {
70
+ id: PropTypes.string,
71
+ taskId: PropTypes.string,
72
+ reposToSubscribe: PropTypes.array,
73
+ };
74
+
75
+ SCCProductPickerModal.defaultProps = {
76
+ id: '',
77
+ taskId: '',
78
+ reposToSubscribe: {},
79
+ };
80
+
81
+ export default SCCProductPickerModal;