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
         
     |