foreman_scc_manager 1.8.20 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -11
  3. data/app/controllers/api/v2/scc_accounts_controller.rb +55 -4
  4. data/app/controllers/api/v2/scc_products_controller.rb +1 -1
  5. data/app/controllers/scc_accounts_controller.rb +49 -0
  6. data/app/lib/actions/scc_manager/subscribe_product.rb +56 -22
  7. data/app/models/scc_account.rb +22 -1
  8. data/app/models/scc_product.rb +2 -5
  9. data/app/views/api/v2/scc_accounts/main.json.rabl +1 -1
  10. data/app/views/scc_accounts/_form.html.erb +8 -4
  11. data/app/views/scc_accounts/show.html.erb +7 -51
  12. data/config/routes.rb +1 -0
  13. data/db/migrate/20220429102717_populate_scc_katello_repositories.rb +5 -0
  14. data/db/migrate/20220531120722_add_product_category_to_scc_products.rb +5 -0
  15. data/db/migrate/20221013214310_add_sync_options_to_scc_account.rb +11 -0
  16. data/lib/foreman_scc_manager/engine.rb +1 -1
  17. data/lib/foreman_scc_manager/version.rb +1 -1
  18. data/lib/tasks/republish_repositories.rake +13 -0
  19. data/package.json +54 -0
  20. data/test/controllers/scc_accounts_controller_test.rb +7 -1
  21. data/test/fixtures/models/katello_root_repositories.yml +0 -12
  22. data/test/test_plugin_helper.rb +0 -6
  23. data/webpack/components/SCCProductPage/EmptySccProducts.js +46 -0
  24. data/webpack/components/SCCProductPage/SCCProductPage.js +96 -0
  25. data/webpack/components/SCCProductPage/SCCProductPageActions.js +27 -0
  26. data/webpack/components/SCCProductPage/SCCProductPageConstants.js +5 -0
  27. data/webpack/components/SCCProductPage/SCCProductPageReducer.js +34 -0
  28. data/webpack/components/SCCProductPage/SCCProductPageSelectors.js +7 -0
  29. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCGenericPicker/index.js +80 -0
  30. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/components/SCCRepoPicker/index.js +174 -0
  31. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/components/SCCRepoPicker/styles.scss +3 -0
  32. data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCTreePicker/index.js +303 -0
  33. data/webpack/components/SCCProductPage/components/SCCProductPicker/index.js +235 -0
  34. data/webpack/components/SCCProductPage/components/SCCProductPicker/styles.scss +10 -0
  35. data/webpack/components/SCCProductPage/components/SCCProductPickerModal/index.js +81 -0
  36. data/webpack/components/SCCProductPage/components/SCCProductView/components/SCCRepoView/index.js +113 -0
  37. data/webpack/components/SCCProductPage/components/SCCProductView/components/SCCRepoView/styles.scss +14 -0
  38. data/webpack/components/SCCProductPage/components/SCCProductView/index.js +236 -0
  39. data/webpack/components/SCCProductPage/components/common/SCCGenericExpander/index.js +58 -0
  40. data/webpack/components/SCCProductPage/components/common/SCCProductTreeExpander/index.js +21 -0
  41. data/webpack/components/SCCProductPage/components/common/SCCSubscribedProductsExpander/index.js +21 -0
  42. data/webpack/components/SCCProductPage/index.js +18 -0
  43. data/webpack/components/SCCProductPage/sccProductPage.scss +8 -0
  44. data/webpack/index.js +11 -0
  45. data/webpack/reducer.js +7 -0
  46. metadata +43 -15
@@ -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;