foreman_scc_manager 5.1.0 → 5.2.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.
- checksums.yaml +4 -4
- data/app/controllers/scc_accounts_controller.rb +0 -3
- data/app/views/scc_accounts/index.html.erb +14 -33
- data/lib/foreman_scc_manager/version.rb +1 -1
- data/webpack/components/SCCAccountIndex/SCCAccountIndex.scss +26 -0
- data/webpack/components/SCCAccountIndex/SCCAccountIndex.test.js +291 -0
- data/webpack/components/SCCAccountIndex/SCCAccountIndexActions.js +205 -0
- data/webpack/components/SCCAccountIndex/SCCAccountIndexConstants.js +9 -0
- data/webpack/components/SCCAccountIndex/index.js +262 -0
- data/webpack/components/SCCProductPage/EmptySccProducts.js +10 -7
- data/webpack/components/SCCProductPage/components/SCCProductPicker/components/SCCGenericPicker/index.js +25 -11
- data/webpack/components/SCCProductPage/components/SCCProductPicker/styles.scss +8 -3
- data/webpack/components/SCCProductPage/sccProductPage.scss +5 -0
- data/webpack/index.js +6 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4712a817369b23dadf22e9d9e0590e89b671d0f2fd80e4dd815f520dd116ab99
|
|
4
|
+
data.tar.gz: 86222d85800bd462624453f0c3740c1f420465806d7a4d6504f6bf9a41dd0585
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33c055ee8e13998d7e9c7742c25f48942002e329d22dae4a382c7cd69501401407e400c0dcf5bb57898a2f9d0b6b8705a7c39f4b2eccbc790be3c9a32152cf9e
|
|
7
|
+
data.tar.gz: 6e9b07ca8d97f35653d9d21fa68ba28ab64baff7c3f13731b8f9fde03c90b88c981f9e51c6ebbe66ea37fccefedc9337e174f94501396895da0868fa7ec91366
|
|
@@ -4,13 +4,10 @@ class SccAccountsController < ApplicationController
|
|
|
4
4
|
before_action :find_organization
|
|
5
5
|
before_action :find_resource, only: %i[show edit update destroy sync]
|
|
6
6
|
before_action :find_available_gpg_keys, only: %i[new edit update create]
|
|
7
|
-
include Foreman::Controller::AutoCompleteSearch
|
|
8
7
|
|
|
9
8
|
# GET /scc_accounts
|
|
10
9
|
def index
|
|
11
10
|
@scc_accounts = resource_base.where(organization: @organization)
|
|
12
|
-
.search_for(params[:search], order: params[:order])
|
|
13
|
-
.paginate(:page => params[:page], :per_page => params[:per_page])
|
|
14
11
|
# overwrite the product list with filtered products that do not include products with empty repositories
|
|
15
12
|
@scc_accounts.each do |scc_account|
|
|
16
13
|
scc_account.scc_products_with_repos_count = scc_account.scc_products.only_products_with_repos.count
|
|
@@ -1,37 +1,18 @@
|
|
|
1
|
-
<% javascript 'foreman_scc_manager/scc_accounts' %>
|
|
2
1
|
<% title _("SUSE subscriptions") %>
|
|
3
|
-
<% title_actions new_link(_("Add SCC account")) %>
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<tr>
|
|
8
|
-
<th class="col-md-4"><%= sort :name %></th>
|
|
9
|
-
<th class="col-md-3"><%= _("Products") %></th>
|
|
10
|
-
<th class="col-md-3"><%= _("Last synced") %></th>
|
|
11
|
-
<th class="col-md-2"><%= _("Actions") %></th>
|
|
12
|
-
</tr>
|
|
13
|
-
</thead>
|
|
14
|
-
<tbody>
|
|
15
|
-
<% @scc_accounts.each do |scc_account| %>
|
|
16
|
-
<tr>
|
|
17
|
-
<td class="display-two-pane ellipsis">
|
|
18
|
-
<%= link_to_if_authorized(scc_account.name, hash_for_edit_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer)) %>
|
|
19
|
-
</td>
|
|
20
|
-
<td><%= scc_account.scc_products_with_repos_count.to_s %></td>
|
|
21
|
-
<td><%= link_to_if(scc_account.sync_task, scc_account.sync_status, scc_account.sync_task) %></td>
|
|
22
|
-
<td>
|
|
23
|
-
<%= action_buttons(
|
|
24
|
-
display_link_if_authorized(_("Select products"), hash_for_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer, :permission => 'view_scc_accounts')),
|
|
25
|
-
display_link_if_authorized(_("Sync"), hash_for_sync_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer),
|
|
26
|
-
:method => :put),
|
|
27
|
-
display_delete_if_authorized(hash_for_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer, :permission => 'delete_scc_accounts'),
|
|
28
|
-
:data => { :confirm => _("WARNING: If you want to switch SCC accounts and retain the synchronized content, DO NOT delete your old SCC account, even if it is expired. Please change the login and password of your SCC account, instead.\n\nIf you delete your old SCC account, you CANNOT reuse existing repositories, products, content views, and composite content views.\n\nReally delete SCC account %s?") % scc_account.to_s })
|
|
3
|
+
<%= webpacked_plugins_js_for :foreman_scc_manager %>
|
|
4
|
+
<%= webpacked_plugins_css_for :foreman_scc_manager %>
|
|
29
5
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
6
|
+
<% initial_accounts = @scc_accounts.map do |a|
|
|
7
|
+
{
|
|
8
|
+
id: a.id,
|
|
9
|
+
name: a.name,
|
|
10
|
+
scc_products_with_repos_count: a.scc_products_with_repos_count,
|
|
11
|
+
sync_status: a.sync_status,
|
|
12
|
+
sync_task: a.sync_task
|
|
13
|
+
}
|
|
14
|
+
end %>
|
|
15
|
+
<%= react_component('SCCAccountIndex', {
|
|
16
|
+
initialAccounts: initial_accounts,
|
|
17
|
+
}) %>
|
|
36
18
|
|
|
37
|
-
<%= will_paginate_with_info @scc_accounts %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.scc-account-add-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: flex-end;
|
|
4
|
+
margin-top: -8px;
|
|
5
|
+
margin-bottom: 8px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.scc-account-delete-warning {
|
|
9
|
+
background-color: #f9f9f9;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
padding: 12px 16px;
|
|
12
|
+
white-space: pre-wrap;
|
|
13
|
+
overflow-x: auto;
|
|
14
|
+
font-size: 0.9rem;
|
|
15
|
+
max-height: 300px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.scc-account-actions {
|
|
19
|
+
display: flex;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.pf-v5-c-page__main-section.pf-m-light {
|
|
25
|
+
margin-top: -1rem;
|
|
26
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types, global-require */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
within,
|
|
8
|
+
fireEvent,
|
|
9
|
+
waitFor,
|
|
10
|
+
} from '@testing-library/react';
|
|
11
|
+
|
|
12
|
+
import SccAccountsIndex from './index';
|
|
13
|
+
import { deleteSccAccountAction } from './SCCAccountIndexActions';
|
|
14
|
+
|
|
15
|
+
jest.mock('foremanReact/common/I18n', () => ({ translate: (s) => s }), {
|
|
16
|
+
virtual: true,
|
|
17
|
+
});
|
|
18
|
+
jest.mock('foremanReact/common/helpers', () => ({ foremanUrl: (p) => p }), {
|
|
19
|
+
virtual: true,
|
|
20
|
+
});
|
|
21
|
+
jest.mock('react-redux', () => ({ useDispatch: () => jest.fn() }), {
|
|
22
|
+
virtual: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock(
|
|
26
|
+
'./SCCAccountIndexActions',
|
|
27
|
+
() => ({
|
|
28
|
+
deleteSccAccountAction: jest.fn(),
|
|
29
|
+
}),
|
|
30
|
+
{ virtual: true }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
jest.mock(
|
|
34
|
+
'@patternfly/react-core',
|
|
35
|
+
() => {
|
|
36
|
+
const { forwardRef } = require('react');
|
|
37
|
+
const PageSection = ({ children }) => <section>{children}</section>;
|
|
38
|
+
|
|
39
|
+
const Button = ({
|
|
40
|
+
children,
|
|
41
|
+
onClick,
|
|
42
|
+
component,
|
|
43
|
+
href,
|
|
44
|
+
'aria-label': ariaLabel,
|
|
45
|
+
}) => {
|
|
46
|
+
if (component === 'a') {
|
|
47
|
+
return (
|
|
48
|
+
<a href={href} onClick={onClick} aria-label={ariaLabel}>
|
|
49
|
+
{children}
|
|
50
|
+
</a>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<button type="button" onClick={onClick} aria-label={ariaLabel}>
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Dropdown = ({ isOpen, onSelect, onOpenChange, toggle, children }) => (
|
|
61
|
+
<div>
|
|
62
|
+
{typeof toggle === 'function' ? toggle({ current: null }) : null}
|
|
63
|
+
{isOpen ? (
|
|
64
|
+
<div data-testid="menu">
|
|
65
|
+
{children}
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
aria-label="Close menu"
|
|
69
|
+
onClick={() => {
|
|
70
|
+
if (onSelect) onSelect();
|
|
71
|
+
if (onOpenChange) onOpenChange(false);
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const DropdownList = ({ children }) => <div role="menu">{children}</div>;
|
|
80
|
+
|
|
81
|
+
const DropdownItem = ({ children, onClick, isDisabled }) => (
|
|
82
|
+
<div
|
|
83
|
+
role="menuitem"
|
|
84
|
+
aria-disabled={!!isDisabled}
|
|
85
|
+
onClick={(e) => {
|
|
86
|
+
if (!isDisabled && onClick) onClick(e);
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const MenuToggle = forwardRef(
|
|
94
|
+
({ children, onClick, 'aria-label': ariaLabel }, ref) => (
|
|
95
|
+
<button
|
|
96
|
+
ref={ref}
|
|
97
|
+
type="button"
|
|
98
|
+
aria-label={ariaLabel}
|
|
99
|
+
onClick={onClick}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</button>
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const Modal = ({ title, isOpen, children, actions }) =>
|
|
107
|
+
isOpen ? (
|
|
108
|
+
<div role="dialog" aria-label={title}>
|
|
109
|
+
<h2>{title}</h2>
|
|
110
|
+
{children}
|
|
111
|
+
{Array.isArray(actions) &&
|
|
112
|
+
actions.map((node, i) => <div key={i}>{node}</div>)}
|
|
113
|
+
</div>
|
|
114
|
+
) : null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
__esModule: true,
|
|
118
|
+
PageSection,
|
|
119
|
+
Button,
|
|
120
|
+
Dropdown,
|
|
121
|
+
DropdownList,
|
|
122
|
+
DropdownItem,
|
|
123
|
+
MenuToggle,
|
|
124
|
+
Modal,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
{ virtual: true }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
jest.mock(
|
|
131
|
+
'@patternfly/react-table',
|
|
132
|
+
() => ({
|
|
133
|
+
__esModule: true,
|
|
134
|
+
Table: ({ children, ..._rest }) => <table>{children}</table>,
|
|
135
|
+
Thead: ({ children }) => <thead>{children}</thead>,
|
|
136
|
+
Tbody: ({ children }) => <tbody>{children}</tbody>,
|
|
137
|
+
Tr: ({ children }) => <tr>{children}</tr>,
|
|
138
|
+
Th: ({ children }) => <th>{children}</th>,
|
|
139
|
+
Td: ({ children }) => <td>{children}</td>,
|
|
140
|
+
}),
|
|
141
|
+
{ virtual: true }
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Helpers
|
|
145
|
+
const renderComponent = (initialAccounts = []) =>
|
|
146
|
+
render(<SccAccountsIndex initialAccounts={initialAccounts} />);
|
|
147
|
+
|
|
148
|
+
const openActionsMenu = () => {
|
|
149
|
+
fireEvent.click(screen.getByLabelText('Actions menu'));
|
|
150
|
+
return screen.getByTestId('menu');
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getRowByAccountName = (name) =>
|
|
154
|
+
screen.getByRole('row', { name: new RegExp(name, 'i') });
|
|
155
|
+
|
|
156
|
+
// Tests
|
|
157
|
+
describe('SccAccountsIndex', () => {
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
jest.clearAllMocks();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('renders without crashing and shows the Add button', () => {
|
|
163
|
+
renderComponent();
|
|
164
|
+
expect(screen.getByText('Add SCC account')).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shows expected table headers', () => {
|
|
168
|
+
renderComponent();
|
|
169
|
+
['Name', 'Products', 'Last synced', 'Actions'].forEach((h) => {
|
|
170
|
+
expect(screen.getByText(h)).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('renders rows from props (names + product counts) with edit links', () => {
|
|
175
|
+
const data = [
|
|
176
|
+
{ id: 1, name: 'Acc A', scc_products_with_repos_count: 2 },
|
|
177
|
+
{ id: 2, name: 'Acc B', scc_products_with_repos_count: 5 },
|
|
178
|
+
];
|
|
179
|
+
renderComponent(data);
|
|
180
|
+
|
|
181
|
+
expect(screen.getByRole('link', { name: 'Acc A' })).toHaveAttribute(
|
|
182
|
+
'href',
|
|
183
|
+
'/scc_accounts/1/edit'
|
|
184
|
+
);
|
|
185
|
+
expect(screen.getByRole('link', { name: 'Acc B' })).toHaveAttribute(
|
|
186
|
+
'href',
|
|
187
|
+
'/scc_accounts/2/edit'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const rowA = getRowByAccountName('Acc A');
|
|
191
|
+
const rowB = getRowByAccountName('Acc B');
|
|
192
|
+
expect(within(rowA).getByText('2')).toBeInTheDocument();
|
|
193
|
+
expect(within(rowB).getByText('5')).toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('shows correct "Last synced" content: link when task exists, "never synced" otherwise', () => {
|
|
197
|
+
const data = [
|
|
198
|
+
{
|
|
199
|
+
id: 10,
|
|
200
|
+
name: 'Successful Sync',
|
|
201
|
+
scc_products_with_repos_count: 1,
|
|
202
|
+
sync_status: 'success',
|
|
203
|
+
sync_task: { id: 'task-123' },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 11,
|
|
207
|
+
name: 'Failed Sync',
|
|
208
|
+
scc_products_with_repos_count: 0,
|
|
209
|
+
sync_status: 'error',
|
|
210
|
+
sync_task: { id: 'task-456' },
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 12,
|
|
214
|
+
name: 'No Sync',
|
|
215
|
+
scc_products_with_repos_count: 0,
|
|
216
|
+
sync_status: null,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
renderComponent(data);
|
|
220
|
+
|
|
221
|
+
const successRow = getRowByAccountName('Successful Sync');
|
|
222
|
+
expect(
|
|
223
|
+
within(successRow).getByRole('link', { name: 'success' })
|
|
224
|
+
).toHaveAttribute('href', '/foreman_tasks/tasks/task-123');
|
|
225
|
+
|
|
226
|
+
const errorRow = getRowByAccountName('Failed Sync');
|
|
227
|
+
expect(
|
|
228
|
+
within(errorRow).getByRole('link', { name: 'error' })
|
|
229
|
+
).toHaveAttribute('href', '/foreman_tasks/tasks/task-456');
|
|
230
|
+
|
|
231
|
+
const noSyncRow = getRowByAccountName('No Sync');
|
|
232
|
+
expect(within(noSyncRow).getByText('never synced')).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('delete flow: open modal, confirm calls action, cancel closes modal', async () => {
|
|
236
|
+
const data = [
|
|
237
|
+
{
|
|
238
|
+
id: 5,
|
|
239
|
+
name: 'ToDelete',
|
|
240
|
+
scc_products_with_repos_count: 0,
|
|
241
|
+
sync_status: 'never synced',
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
renderComponent(data);
|
|
245
|
+
|
|
246
|
+
const menu = openActionsMenu();
|
|
247
|
+
fireEvent.click(within(menu).getByRole('menuitem', { name: 'Delete' }));
|
|
248
|
+
|
|
249
|
+
const dialog = screen.getByRole('dialog');
|
|
250
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Delete' }));
|
|
251
|
+
|
|
252
|
+
expect(deleteSccAccountAction).toHaveBeenCalledWith(
|
|
253
|
+
expect.any(Function), // dispatch
|
|
254
|
+
5, // accountId
|
|
255
|
+
expect.any(Function), // setAccounts
|
|
256
|
+
expect.any(Function), // setDeleteOpen
|
|
257
|
+
expect.any(Object) // deletingIdRef
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
fireEvent.click(
|
|
261
|
+
within(screen.getByRole('dialog')).getByRole('button', { name: 'Cancel' })
|
|
262
|
+
);
|
|
263
|
+
await waitFor(() =>
|
|
264
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const menu2 = openActionsMenu();
|
|
268
|
+
fireEvent.click(within(menu2).getByRole('menuitem', { name: 'Delete' }));
|
|
269
|
+
fireEvent.click(
|
|
270
|
+
within(screen.getByRole('dialog')).getByRole('button', { name: 'Cancel' })
|
|
271
|
+
);
|
|
272
|
+
await waitFor(() =>
|
|
273
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('empty state: renders headers and no data rows', () => {
|
|
278
|
+
renderComponent([]);
|
|
279
|
+
|
|
280
|
+
['Name', 'Products', 'Last synced', 'Actions'].forEach((h) => {
|
|
281
|
+
expect(screen.getByText(h)).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(screen.getAllByRole('row')).toHaveLength(1);
|
|
285
|
+
|
|
286
|
+
expect(screen.queryByLabelText('Actions menu')).not.toBeInTheDocument();
|
|
287
|
+
expect(
|
|
288
|
+
screen.queryByRole('button', { name: 'Select Products' })
|
|
289
|
+
).not.toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
2
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
|
|
4
|
+
import { INITIAL_DELAY, MAX_DELAY, BACKOFF } from './SCCAccountIndexConstants';
|
|
5
|
+
|
|
6
|
+
const isDone = (state, result) =>
|
|
7
|
+
state === 'stopped' || result === 'success' || result === 'error';
|
|
8
|
+
|
|
9
|
+
const nextDelay = (prevDelay, changed) => {
|
|
10
|
+
const base = changed ? INITIAL_DELAY : prevDelay;
|
|
11
|
+
const next = Math.min(Math.ceil(base * BACKOFF), MAX_DELAY);
|
|
12
|
+
return next;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const schedule = (taskTimeoutRef, taskId, ms, fn) => {
|
|
16
|
+
if (taskTimeoutRef.current[taskId])
|
|
17
|
+
clearTimeout(taskTimeoutRef.current[taskId]);
|
|
18
|
+
taskTimeoutRef.current[taskId] = setTimeout(fn, ms);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const deriveSyncStatus = (done, result, endedAt, state, fallback) => {
|
|
22
|
+
if (!done) return state || fallback;
|
|
23
|
+
return result === 'success' ? endedAt || __('finished') : __('error');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const checkUntilChanged = (
|
|
27
|
+
dispatch,
|
|
28
|
+
taskId,
|
|
29
|
+
accountId,
|
|
30
|
+
setAccounts,
|
|
31
|
+
taskTimeoutRef,
|
|
32
|
+
lastStateRef,
|
|
33
|
+
delay = INITIAL_DELAY
|
|
34
|
+
) => {
|
|
35
|
+
if (!taskId) return;
|
|
36
|
+
|
|
37
|
+
dispatch(
|
|
38
|
+
APIActions.get({
|
|
39
|
+
key: `task_${taskId}`,
|
|
40
|
+
url: `/foreman_tasks/api/tasks/${taskId}`,
|
|
41
|
+
handleSuccess: (payload) => {
|
|
42
|
+
const task = payload?.data ?? payload;
|
|
43
|
+
const { state, result, ended_at: endedAt } = task || {};
|
|
44
|
+
const prev = lastStateRef.current[accountId];
|
|
45
|
+
const done = isDone(state, result);
|
|
46
|
+
|
|
47
|
+
setAccounts((prevState) =>
|
|
48
|
+
prevState.map((acc) => {
|
|
49
|
+
if (acc.id !== accountId) {
|
|
50
|
+
return acc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const syncStatus = deriveSyncStatus(
|
|
54
|
+
done,
|
|
55
|
+
result,
|
|
56
|
+
endedAt,
|
|
57
|
+
state,
|
|
58
|
+
acc.sync_status
|
|
59
|
+
);
|
|
60
|
+
return {
|
|
61
|
+
...acc,
|
|
62
|
+
sync_status: syncStatus,
|
|
63
|
+
sync_task: {
|
|
64
|
+
...(acc.sync_task || {}),
|
|
65
|
+
id: taskId,
|
|
66
|
+
ended_at: endedAt,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
lastStateRef.current[accountId] = state;
|
|
73
|
+
|
|
74
|
+
if (done) {
|
|
75
|
+
if (taskTimeoutRef.current[taskId])
|
|
76
|
+
clearTimeout(taskTimeoutRef.current[taskId]);
|
|
77
|
+
delete taskTimeoutRef.current[taskId];
|
|
78
|
+
delete lastStateRef.current[accountId];
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const changed = state !== prev;
|
|
83
|
+
const newDelay = nextDelay(delay, changed);
|
|
84
|
+
|
|
85
|
+
schedule(taskTimeoutRef, taskId, newDelay, () =>
|
|
86
|
+
checkUntilChanged(
|
|
87
|
+
dispatch,
|
|
88
|
+
taskId,
|
|
89
|
+
accountId,
|
|
90
|
+
setAccounts,
|
|
91
|
+
taskTimeoutRef,
|
|
92
|
+
lastStateRef,
|
|
93
|
+
newDelay
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
handleError: () => {
|
|
98
|
+
const newDelay = nextDelay(delay, false);
|
|
99
|
+
schedule(taskTimeoutRef, taskId, newDelay, () =>
|
|
100
|
+
checkUntilChanged(
|
|
101
|
+
dispatch,
|
|
102
|
+
taskId,
|
|
103
|
+
accountId,
|
|
104
|
+
setAccounts,
|
|
105
|
+
taskTimeoutRef,
|
|
106
|
+
lastStateRef,
|
|
107
|
+
newDelay
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
errorToast: () => null,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const syncSccAccountAction = (
|
|
117
|
+
dispatch,
|
|
118
|
+
accountId,
|
|
119
|
+
setAccounts,
|
|
120
|
+
taskTimeoutRef,
|
|
121
|
+
lastStateRef
|
|
122
|
+
) => {
|
|
123
|
+
if (!accountId) return;
|
|
124
|
+
|
|
125
|
+
dispatch(
|
|
126
|
+
APIActions.put({
|
|
127
|
+
key: `syncSccAccount_${accountId}`,
|
|
128
|
+
url: `/api/v2/scc_accounts/${accountId}/sync`,
|
|
129
|
+
successToast: () => __('Sync task started.'),
|
|
130
|
+
errorToast: () => __('Failed to start sync task.'),
|
|
131
|
+
handleSuccess: (resp) => {
|
|
132
|
+
const taskId = resp?.data?.id;
|
|
133
|
+
const initialState = resp?.data?.state || 'planned';
|
|
134
|
+
|
|
135
|
+
lastStateRef.current[accountId] = initialState;
|
|
136
|
+
|
|
137
|
+
setAccounts((prev) =>
|
|
138
|
+
prev.map((acc) =>
|
|
139
|
+
acc.id === accountId
|
|
140
|
+
? {
|
|
141
|
+
...acc,
|
|
142
|
+
sync_status: initialState || __('running'),
|
|
143
|
+
}
|
|
144
|
+
: acc
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (taskTimeoutRef.current[taskId]) {
|
|
149
|
+
clearTimeout(taskTimeoutRef.current[taskId]);
|
|
150
|
+
}
|
|
151
|
+
taskTimeoutRef.current[taskId] = setTimeout(() => {
|
|
152
|
+
checkUntilChanged(
|
|
153
|
+
dispatch,
|
|
154
|
+
taskId,
|
|
155
|
+
accountId,
|
|
156
|
+
setAccounts,
|
|
157
|
+
taskTimeoutRef,
|
|
158
|
+
lastStateRef
|
|
159
|
+
);
|
|
160
|
+
}, 15000);
|
|
161
|
+
},
|
|
162
|
+
handleError: () => {
|
|
163
|
+
setAccounts((prev) =>
|
|
164
|
+
prev.map((acc) =>
|
|
165
|
+
acc.id === accountId
|
|
166
|
+
? {
|
|
167
|
+
...acc,
|
|
168
|
+
sync_status: __('error'),
|
|
169
|
+
taskId: null,
|
|
170
|
+
}
|
|
171
|
+
: acc
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const deleteSccAccountAction = (
|
|
180
|
+
dispatch,
|
|
181
|
+
id,
|
|
182
|
+
setAccounts,
|
|
183
|
+
setDeleteOpen,
|
|
184
|
+
deletingIdRef
|
|
185
|
+
) => {
|
|
186
|
+
if (!id) return;
|
|
187
|
+
|
|
188
|
+
dispatch(
|
|
189
|
+
APIActions.delete({
|
|
190
|
+
key: `deleteSccAccount_${id}`,
|
|
191
|
+
url: `/api/v2/scc_accounts/${id}`,
|
|
192
|
+
successToast: () => __('SCC account deleted successfully.'),
|
|
193
|
+
errorToast: () => __('Failed to delete SCC account.'),
|
|
194
|
+
handleSuccess: () => {
|
|
195
|
+
setAccounts((prev) => prev.filter((acc) => acc.id !== id));
|
|
196
|
+
setDeleteOpen(false);
|
|
197
|
+
deletingIdRef.current = null;
|
|
198
|
+
},
|
|
199
|
+
handleError: () => {
|
|
200
|
+
setDeleteOpen(false);
|
|
201
|
+
deletingIdRef.current = null;
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
|
+
|
|
3
|
+
export const WARN_DELETE = __(
|
|
4
|
+
'WARNING: If you want to switch SCC accounts and retain the synchronized content, DO NOT delete your old SCC account, even if it is expired. Please change the login and password of your SCC account, instead.\n\nIf you delete your old SCC account, you CANNOT reuse existing repositories, products, content views, and composite content views.\n\n Do you Really want to delete this SCC account %acc_name?'
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const INITIAL_DELAY = 5000;
|
|
8
|
+
export const MAX_DELAY = 30000;
|
|
9
|
+
export const BACKOFF = 1.5;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
PageSection,
|
|
5
|
+
Modal,
|
|
6
|
+
Button,
|
|
7
|
+
Dropdown,
|
|
8
|
+
DropdownList,
|
|
9
|
+
DropdownItem,
|
|
10
|
+
MenuToggle,
|
|
11
|
+
} from '@patternfly/react-core';
|
|
12
|
+
import { Table, Thead, Tr, Th, Td, Tbody } from '@patternfly/react-table';
|
|
13
|
+
import { EllipsisVIcon } from '@patternfly/react-icons';
|
|
14
|
+
import { useDispatch } from 'react-redux';
|
|
15
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
16
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
17
|
+
import { WARN_DELETE } from './SCCAccountIndexConstants';
|
|
18
|
+
import {
|
|
19
|
+
syncSccAccountAction,
|
|
20
|
+
deleteSccAccountAction,
|
|
21
|
+
} from './SCCAccountIndexActions';
|
|
22
|
+
import './SCCAccountIndex.scss';
|
|
23
|
+
|
|
24
|
+
function SccAccountsIndex({ initialAccounts }) {
|
|
25
|
+
const dispatch = useDispatch();
|
|
26
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
27
|
+
const [openMenuRow, setOpenMenuRow] = useState(null);
|
|
28
|
+
const [accounts, setAccounts] = useState(initialAccounts);
|
|
29
|
+
const [selectedAccountName, setSelectedAccountName] = useState(null);
|
|
30
|
+
const deletingIdRef = useRef(null);
|
|
31
|
+
const taskTimeoutRef = useRef({});
|
|
32
|
+
const lastStateRef = useRef({});
|
|
33
|
+
|
|
34
|
+
const handleSync = (accountId) => {
|
|
35
|
+
syncSccAccountAction(
|
|
36
|
+
dispatch,
|
|
37
|
+
accountId,
|
|
38
|
+
setAccounts,
|
|
39
|
+
taskTimeoutRef,
|
|
40
|
+
lastStateRef
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleDelete = (id) => {
|
|
45
|
+
deleteSccAccountAction(
|
|
46
|
+
dispatch,
|
|
47
|
+
id,
|
|
48
|
+
setAccounts,
|
|
49
|
+
setDeleteOpen,
|
|
50
|
+
deletingIdRef
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(
|
|
55
|
+
() => () => {
|
|
56
|
+
Object.values(taskTimeoutRef.current).forEach(clearTimeout);
|
|
57
|
+
taskTimeoutRef.current = {};
|
|
58
|
+
},
|
|
59
|
+
[]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<PageSection ouiaId="scc-accounts-index-page-section">
|
|
64
|
+
<div className="scc-account-add-container">
|
|
65
|
+
<Button
|
|
66
|
+
component="a"
|
|
67
|
+
href={foremanUrl('/scc_accounts/new')}
|
|
68
|
+
variant="primary"
|
|
69
|
+
ouiaId="scc-account-add-button"
|
|
70
|
+
>
|
|
71
|
+
{__('Add SCC account')}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<Table
|
|
76
|
+
aria-label={__('SUSE subscriptions')}
|
|
77
|
+
ouiaId="scc-accounts-table"
|
|
78
|
+
variant="compact"
|
|
79
|
+
>
|
|
80
|
+
<Thead ouiaId="scc-accounts-table-head">
|
|
81
|
+
<Tr ouiaId="scc-accounts-table-header-row">
|
|
82
|
+
<Th ouiaId="scc-accounts-name-header">{__('Name')}</Th>
|
|
83
|
+
<Th ouiaId="scc-accounts-products-header">{__('Products')}</Th>
|
|
84
|
+
<Th ouiaId="scc-accounts-last-synced-header">
|
|
85
|
+
{__('Last synced')}
|
|
86
|
+
</Th>
|
|
87
|
+
<Th width={10} ouiaId="scc-accounts-actions-header">
|
|
88
|
+
{__('Actions')}
|
|
89
|
+
</Th>
|
|
90
|
+
</Tr>
|
|
91
|
+
</Thead>
|
|
92
|
+
<Tbody ouiaId="scc-accounts-table-body">
|
|
93
|
+
{accounts &&
|
|
94
|
+
accounts.map((acc) => {
|
|
95
|
+
const lastSynced = acc.sync_task ? (
|
|
96
|
+
<a
|
|
97
|
+
target="_blank"
|
|
98
|
+
href={foremanUrl(`/foreman_tasks/tasks/${acc.sync_task.id}`)}
|
|
99
|
+
rel="noreferrer"
|
|
100
|
+
>
|
|
101
|
+
{acc.sync_status}
|
|
102
|
+
</a>
|
|
103
|
+
) : (
|
|
104
|
+
acc.sync_status || __('never synced')
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Tr key={acc.id} ouiaId={`scc-account-row-${acc.id}`}>
|
|
109
|
+
<Td
|
|
110
|
+
dataLabel={__('Name')}
|
|
111
|
+
ouiaId={`scc-account-name-${acc.id}`}
|
|
112
|
+
>
|
|
113
|
+
<a href={`/scc_accounts/${acc.id}/edit`}>{acc.name}</a>
|
|
114
|
+
</Td>
|
|
115
|
+
<Td
|
|
116
|
+
dataLabel={__('Products')}
|
|
117
|
+
ouiaId={`scc-account-products-${acc.id}`}
|
|
118
|
+
>
|
|
119
|
+
{acc.scc_products_with_repos_count}
|
|
120
|
+
</Td>
|
|
121
|
+
|
|
122
|
+
<Td
|
|
123
|
+
dataLabel={__('Last synced')}
|
|
124
|
+
ouiaId={`scc-account-last-synced-${acc.id}`}
|
|
125
|
+
>
|
|
126
|
+
{lastSynced}
|
|
127
|
+
</Td>
|
|
128
|
+
<Td
|
|
129
|
+
dataLabel={__('Actions')}
|
|
130
|
+
ouiaId={`scc-account-actions-${acc.id}`}
|
|
131
|
+
>
|
|
132
|
+
<div className="scc-account-actions">
|
|
133
|
+
<Button
|
|
134
|
+
variant="primary"
|
|
135
|
+
size="sm"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
window.location.href = `/scc_accounts/${acc.id}`;
|
|
138
|
+
}}
|
|
139
|
+
ouiaId={`scc-account-select-products-button-${acc.id}`}
|
|
140
|
+
>
|
|
141
|
+
{__('Select Products')}
|
|
142
|
+
</Button>
|
|
143
|
+
|
|
144
|
+
<Dropdown
|
|
145
|
+
isOpen={openMenuRow === acc.id}
|
|
146
|
+
onSelect={() => setOpenMenuRow(null)}
|
|
147
|
+
onOpenChange={(isOpen) =>
|
|
148
|
+
setOpenMenuRow(isOpen ? acc.id : null)
|
|
149
|
+
}
|
|
150
|
+
ouiaId={`scc-account-actions-dropdown-${acc.id}`}
|
|
151
|
+
toggle={(toggleRef) => (
|
|
152
|
+
<MenuToggle
|
|
153
|
+
ref={toggleRef}
|
|
154
|
+
aria-label={__('Actions menu')}
|
|
155
|
+
variant="plain"
|
|
156
|
+
isExpanded={openMenuRow === acc.id}
|
|
157
|
+
onClick={() =>
|
|
158
|
+
setOpenMenuRow(
|
|
159
|
+
openMenuRow === acc.id ? null : acc.id
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
ouiaId={`scc-account-actions-menu-toggle-${acc.id}`}
|
|
163
|
+
>
|
|
164
|
+
<EllipsisVIcon />
|
|
165
|
+
</MenuToggle>
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
<DropdownList
|
|
169
|
+
ouiaId={`scc-account-actions-dropdown-list-${acc.id}`}
|
|
170
|
+
>
|
|
171
|
+
<DropdownItem
|
|
172
|
+
key="sync"
|
|
173
|
+
isDisabled={
|
|
174
|
+
acc.sync_status === 'running' ||
|
|
175
|
+
acc.sync_status === 'planned'
|
|
176
|
+
}
|
|
177
|
+
onClick={() => {
|
|
178
|
+
setOpenMenuRow(null);
|
|
179
|
+
handleSync(acc.id);
|
|
180
|
+
}}
|
|
181
|
+
ouiaId={`scc-account-sync-item-${acc.id}`}
|
|
182
|
+
>
|
|
183
|
+
{acc.sync_status === 'running' ||
|
|
184
|
+
acc.sync_status === 'planned'
|
|
185
|
+
? __('Syncing...')
|
|
186
|
+
: __('Sync')}
|
|
187
|
+
</DropdownItem>
|
|
188
|
+
|
|
189
|
+
<DropdownItem
|
|
190
|
+
key="delete"
|
|
191
|
+
onClick={() => {
|
|
192
|
+
setOpenMenuRow(null);
|
|
193
|
+
deletingIdRef.current = acc.id;
|
|
194
|
+
setDeleteOpen(true);
|
|
195
|
+
setSelectedAccountName(acc.name);
|
|
196
|
+
}}
|
|
197
|
+
ouiaId={`scc-account-delete-item-${acc.id}`}
|
|
198
|
+
>
|
|
199
|
+
{__('Delete')}
|
|
200
|
+
</DropdownItem>
|
|
201
|
+
</DropdownList>
|
|
202
|
+
</Dropdown>
|
|
203
|
+
</div>
|
|
204
|
+
</Td>
|
|
205
|
+
</Tr>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</Tbody>
|
|
209
|
+
</Table>
|
|
210
|
+
|
|
211
|
+
<Modal
|
|
212
|
+
title={__('Delete SCC Account')}
|
|
213
|
+
isOpen={deleteOpen}
|
|
214
|
+
onClose={() => setDeleteOpen(false)}
|
|
215
|
+
variant="small"
|
|
216
|
+
ouiaId="scc-account-delete-modal"
|
|
217
|
+
actions={[
|
|
218
|
+
<Button
|
|
219
|
+
key="confirm"
|
|
220
|
+
variant="danger"
|
|
221
|
+
onClick={() => handleDelete(deletingIdRef.current)}
|
|
222
|
+
ouiaId="scc-account-delete-confirm-button"
|
|
223
|
+
>
|
|
224
|
+
{__('Delete')}
|
|
225
|
+
</Button>,
|
|
226
|
+
<Button
|
|
227
|
+
key="cancel"
|
|
228
|
+
variant="link"
|
|
229
|
+
onClick={() => setDeleteOpen(false)}
|
|
230
|
+
ouiaId="scc-account-delete-cancel-button"
|
|
231
|
+
>
|
|
232
|
+
{__('Cancel')}
|
|
233
|
+
</Button>,
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
236
|
+
<div className="scc-account-delete-warning">
|
|
237
|
+
{WARN_DELETE.replace('%acc_name', selectedAccountName || '')}
|
|
238
|
+
</div>
|
|
239
|
+
</Modal>
|
|
240
|
+
</PageSection>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
SccAccountsIndex.propTypes = {
|
|
245
|
+
initialAccounts: PropTypes.arrayOf(
|
|
246
|
+
PropTypes.shape({
|
|
247
|
+
id: PropTypes.number.isRequired,
|
|
248
|
+
name: PropTypes.string.isRequired,
|
|
249
|
+
sync_status: PropTypes.string,
|
|
250
|
+
sync_task: PropTypes.shape({
|
|
251
|
+
id: PropTypes.string,
|
|
252
|
+
}),
|
|
253
|
+
scc_products_with_repos_count: PropTypes.number,
|
|
254
|
+
})
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
SccAccountsIndex.defaultProps = {
|
|
259
|
+
initialAccounts: [],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export default SccAccountsIndex;
|
|
@@ -5,6 +5,7 @@ import { Button } from '@patternfly/react-core';
|
|
|
5
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
6
|
import EmptyState from 'foremanReact/components/common/EmptyState';
|
|
7
7
|
import { syncSccAccountAction } from './SCCProductPageActions';
|
|
8
|
+
import './sccProductPage.scss';
|
|
8
9
|
|
|
9
10
|
export const EmptySccProducts = ({ canCreate, sccAccountId }) => {
|
|
10
11
|
const dispatch = useDispatch();
|
|
@@ -27,13 +28,15 @@ export const EmptySccProducts = ({ canCreate, sccAccountId }) => {
|
|
|
27
28
|
'https://docs.orcharhino.com/or/docs/sources/usage_guides/managing_sles_systems_guide.html#mssg_adding_scc_accounts',
|
|
28
29
|
}}
|
|
29
30
|
/>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
<div className="scc-sync-button-container">
|
|
32
|
+
<Button
|
|
33
|
+
onClick={onSyncStart}
|
|
34
|
+
ouiaId="scc-manager-welcome-sync-products"
|
|
35
|
+
className="btn btn-primary"
|
|
36
|
+
>
|
|
37
|
+
{__('Synchronize SUSE Account')}
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
37
40
|
</>
|
|
38
41
|
);
|
|
39
42
|
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '@patternfly/react-core';
|
|
15
15
|
import { TimesIcon } from '@patternfly/react-icons';
|
|
16
16
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
17
|
+
import '../../../SCCProductPicker/styles.scss';
|
|
17
18
|
|
|
18
19
|
const GenericSelector = ({
|
|
19
20
|
initialSelectOptions,
|
|
@@ -234,18 +235,31 @@ const GenericSelector = ({
|
|
|
234
235
|
}}
|
|
235
236
|
toggle={toggle}
|
|
236
237
|
shouldFocusFirstItemOnOpen={false}
|
|
238
|
+
popperProps={{
|
|
239
|
+
direction: 'down',
|
|
240
|
+
appendTo: () => document.body,
|
|
241
|
+
}}
|
|
237
242
|
>
|
|
238
|
-
<SelectList
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
<SelectList
|
|
244
|
+
id={initialLabel.concat('select-typeahead-listbox')}
|
|
245
|
+
isAriaMultiselectable={false}
|
|
246
|
+
className="product-picker__select-list"
|
|
247
|
+
>
|
|
248
|
+
{selectOptions.length > 0 ? (
|
|
249
|
+
selectOptions.map((option, index) => (
|
|
250
|
+
<SelectOption
|
|
251
|
+
key={option.value || option.children}
|
|
252
|
+
isFocused={focusedItemIndex === index}
|
|
253
|
+
id={createItemId(option.value)}
|
|
254
|
+
className={option.className}
|
|
255
|
+
{...option}
|
|
256
|
+
/>
|
|
257
|
+
))
|
|
258
|
+
) : (
|
|
259
|
+
<SelectOption isDisabled key="no-options">
|
|
260
|
+
{__('No options available')}
|
|
261
|
+
</SelectOption>
|
|
262
|
+
)}
|
|
249
263
|
</SelectList>
|
|
250
264
|
</Select>
|
|
251
265
|
);
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
.pf-v5-c-select__toggle {
|
|
4
4
|
font-size: var(--pf-v5-global--FontSize--xs);
|
|
5
5
|
font-weight: var(--pf-v5-global--FontWeight--bold);
|
|
6
|
-
}
|
|
6
|
+
}
|
|
7
7
|
|
|
8
8
|
.pf-v5-c-switch__input ~ .pf-v5-c-switch__label {
|
|
9
|
-
|
|
10
|
-
}
|
|
9
|
+
font-size: var(--pf-v5-global--FontSize--xs);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.product-picker__select-list {
|
|
13
|
+
max-height: 200px;
|
|
14
|
+
overflow-y: auto;
|
|
15
|
+
}
|
data/webpack/index.js
CHANGED
|
@@ -3,12 +3,18 @@ import injectReducer from 'foremanReact/redux/reducers/registerReducer';
|
|
|
3
3
|
import SCCProductPage from './components/SCCProductPage';
|
|
4
4
|
import reducer from './reducer';
|
|
5
5
|
import SCCAccountForm from './components/SCCAccountForm';
|
|
6
|
+
import SCCAccountIndex from './components/SCCAccountIndex';
|
|
6
7
|
|
|
7
8
|
componentRegistry.register({
|
|
8
9
|
name: 'SCCAccountForm',
|
|
9
10
|
type: SCCAccountForm,
|
|
10
11
|
});
|
|
11
12
|
|
|
13
|
+
componentRegistry.register({
|
|
14
|
+
name: 'SCCAccountIndex',
|
|
15
|
+
type: SCCAccountIndex,
|
|
16
|
+
});
|
|
17
|
+
|
|
12
18
|
componentRegistry.register({
|
|
13
19
|
name: 'SCCProductPage',
|
|
14
20
|
type: SCCProductPage,
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_scc_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ATIX AG
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-11-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rdoc
|
|
@@ -203,6 +203,11 @@ files:
|
|
|
203
203
|
- webpack/components/SCCAccountForm/components/SCCSyncSettingsCard.js
|
|
204
204
|
- webpack/components/SCCAccountForm/components/SCCTokenRefreshCard.js
|
|
205
205
|
- webpack/components/SCCAccountForm/index.js
|
|
206
|
+
- webpack/components/SCCAccountIndex/SCCAccountIndex.scss
|
|
207
|
+
- webpack/components/SCCAccountIndex/SCCAccountIndex.test.js
|
|
208
|
+
- webpack/components/SCCAccountIndex/SCCAccountIndexActions.js
|
|
209
|
+
- webpack/components/SCCAccountIndex/SCCAccountIndexConstants.js
|
|
210
|
+
- webpack/components/SCCAccountIndex/index.js
|
|
206
211
|
- webpack/components/SCCProductPage/EmptySccProducts.js
|
|
207
212
|
- webpack/components/SCCProductPage/SCCProductPage.js
|
|
208
213
|
- webpack/components/SCCProductPage/SCCProductPageActions.js
|