From 9c00d265deb9ea6bab1641234772b077943c5f41 Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Tue, 15 Sep 2026 01:18:59 +0200 Subject: [PATCH 01/12] feat: drop the subscription licence and the file that gated it get_licenser chose between two answers by whether /var/lib/ascender/.tower_version existed: present meant the subscription product, an entitlement manifest to parse and a subscription to keep in date. Nothing in this ecosystem has ever written that file, so the only way to reach that branch was to carry it over from the Tower an install was migrated from, and Ascender has no subscription to check. It returns the open licence, always. What goes with it: - The Licenser class, its entitlement certificate parsing, its signature verification and its expiry arithmetic. licensing.py is 76 lines where it was 288, and the cryptography, zipfile and date parsing imports go with it. - validate_entitlement_manifest, which only that class used. - POST to /api/v2/config/, which took a manifest. The method stays and answers 400 saying there is no licence to install, rather than disappearing and leaving a client with a bare 405. - The EULA on the config endpoint, which was rendered only when the licence was not open, so never. - Two branches that asked which product this is and now cannot vary: server_product_name, and the inventory source that chose redhat.satellite over theforeman.foreman. Every install already took the second one. The API shape does not move. get_licenser().validate() is still what the config view, the access checks, the bulk serializer and the host metrics read, and it still returns license_type open with a valid key. test_tasks patched Licenser.validate to keep the project update off the network; it patches OpenLicense.validate now, which is the class that method is on. --- ascender/api/views/root.py | 62 ++---- ascender/main/models/inventory.py | 5 - ascender/main/tests/unit/test_tasks.py | 6 +- ascender/main/utils/common.py | 4 +- ascender/main/utils/licensing.py | 273 ++----------------------- 5 files changed, 36 insertions(+), 314 deletions(-) diff --git a/ascender/api/views/root.py b/ascender/api/views/root.py index 35048f7f5..0190934dc 100644 --- a/ascender/api/views/root.py +++ b/ascender/api/views/root.py @@ -1,17 +1,13 @@ # Copyright (c) 2018 Ansible, Inc. # All Rights Reserved. -import base64 -import json import logging import operator from collections import OrderedDict from ascender.settings.typed import settings -from django.utils.encoding import smart_str from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie -from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from rest_framework.permissions import AllowAny, IsAuthenticated @@ -21,11 +17,9 @@ from ascender import MODE from ascender.api.generics import APIView -from ascender.conf.registry import settings_registry from ascender.main.analytics import all_collectors from ascender.main.ha import is_ha_environment from ascender.main.utils import get_ascender_version -from ascender.main.utils.licensing import validate_entitlement_manifest from ascender.api.versioning import reverse, drf_reverse from ascender.main.constants import PRIVILEGE_ESCALATION_METHODS from ascender.main.models import Project, Organization, Instance, InstanceGroup @@ -207,7 +201,7 @@ def get(self, request, format=None): time_zone=settings.TIME_ZONE, license_info=license_data, version=get_ascender_version(), - eula=render_to_string("eula.md") if license_data.get('license_type', 'UNLICENSED') != 'open' else '', + eula='', analytics_status=pendo_state, analytics_collectors=all_collectors(), become_methods=PRIVILEGE_ESCALATION_METHODS, @@ -239,48 +233,18 @@ def get(self, request, format=None): return Response(data) def post(self, request): - if not isinstance(request.data, dict): - return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST) - try: - data_actual = json.dumps(request.data) - except Exception: - logger.info(smart_str("Invalid JSON submitted for license."), extra=dict(actor=request.user.username)) - return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST) - - license_data = json.loads(data_actual) - if 'license_key' in license_data: - return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST) - if 'manifest' in license_data: - try: - json_actual = json.loads(base64.b64decode(license_data['manifest'])) - if 'license_key' in json_actual: - return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST) - except Exception: - pass - try: - license_data = validate_entitlement_manifest(license_data['manifest']) - except ValueError as e: - return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - except Exception: - logger.exception('Invalid manifest submitted. {}') - return Response({"error": _('Invalid manifest submitted.')}, status=status.HTTP_400_BAD_REQUEST) - - try: - license_data_validated = get_licenser().license_from_manifest(license_data) - except Exception: - logger.warning(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username)) - return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST) - else: - license_data_validated = get_licenser().validate() - - # If the license is valid, write it to the database. - if license_data_validated['valid_key']: - if not settings_registry.is_setting_read_only('ASCENDER_URL_BASE'): - settings.ASCENDER_URL_BASE = "{}://{}".format(request.scheme, request.get_host()) - return Response(license_data_validated) - - logger.warning(smart_str("Invalid subscription submitted."), extra=dict(actor=request.user.username)) - return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST) + """There is no subscription to submit. + + This used to take an entitlement manifest, validate its signature and + write the resulting licence to the database. Ascender has no + subscription, so there is nothing a manifest could say. The method stays + rather than disappearing so a client that posts one is told why instead + of getting a bare 405. + """ + return Response( + {"error": _("Ascender does not use subscriptions. There is no licence to install.")}, + status=status.HTTP_400_BAD_REQUEST, + ) def delete(self, request): try: diff --git a/ascender/main/models/inventory.py b/ascender/main/models/inventory.py index 47a7b3d3a..d418a6dc3 100644 --- a/ascender/main/models/inventory.py +++ b/ascender/main/models/inventory.py @@ -56,7 +56,6 @@ from ascender.main.utils import _inventory_updates from ascender.main.utils.safe_yaml import sanitize_jinja from ascender.main.utils.execution_environments import to_container_path, get_control_plane_execution_environment -from ascender.main.utils.licensing import OPEN_PRODUCT_NAME, server_product_name __all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'SmartInventoryMembership', 'HostMetric', 'HostMetricSummaryMonthly'] @@ -1514,8 +1513,6 @@ def inventory_as_dict(self, inventory_update, private_data_dir): if self.plugin_name is not None: if self.get_alternate_plugin(source_vars) is not None: pass # user selected an alternate supported plugin, keep it - elif hasattr(self, 'downstream_namespace') and server_product_name() != OPEN_PRODUCT_NAME: - source_vars['plugin'] = f'{self.downstream_namespace}.{self.downstream_collection}.{self.plugin_name}' elif self.use_fqcn: source_vars['plugin'] = f'{self.namespace}.{self.collection}.{self.plugin_name}' else: @@ -1666,8 +1663,6 @@ class satellite6(PluginFileInjector): plugin_name = 'foreman' namespace = 'theforeman' collection = 'foreman' - downstream_namespace = 'redhat' - downstream_collection = 'satellite' use_fqcn = True def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): diff --git a/ascender/main/tests/unit/test_tasks.py b/ascender/main/tests/unit/test_tasks.py index cf3cbdc61..879aabba0 100644 --- a/ascender/main/tests/unit/test_tasks.py +++ b/ascender/main/tests/unit/test_tasks.py @@ -38,7 +38,7 @@ from ascender.main.utils.safe_yaml import SafeLoader from ascender.main.utils.execution_environments import CONTAINER_ROOT -from ascender.main.utils.licensing import Licenser +from ascender.main.utils.licensing import OpenLicense from ascender.main.constants import JOB_VARIABLE_PREFIXES from receptorctl.socket_interface import ReceptorControl @@ -1468,7 +1468,7 @@ def _write_extra_vars_file(self, private_data_dir, extra_vars, *kw): task.instance = project_update env = task.build_env(project_update, private_data_dir) - with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): + with mock.patch.object(OpenLicense, 'validate', lambda *args, **kw: {}): task.build_extra_vars_file(project_update, private_data_dir) assert task.__vars__['roles_enabled'] is False @@ -1496,7 +1496,7 @@ def _write_extra_vars_file(self, private_data_dir, extra_vars, *kw): task.instance = project_update env = task.build_env(project_update, private_data_dir) - with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): + with mock.patch.object(OpenLicense, 'validate', lambda *args, **kw: {}): task.build_extra_vars_file(project_update, private_data_dir) assert task.__vars__['roles_enabled'] is True diff --git a/ascender/main/utils/common.py b/ascender/main/utils/common.py index e67920962..4162bf445 100644 --- a/ascender/main/utils/common.py +++ b/ascender/main/utils/common.py @@ -42,7 +42,7 @@ # Ascender from ascender.conf.license import get_license -from ascender.main.utils.licensing import OPEN_PRODUCT_NAME, SUBSCRIPTION_PRODUCT_NAME +from ascender.main.utils.licensing import OPEN_PRODUCT_NAME logger = logging.getLogger('awx.main.utils') @@ -235,7 +235,7 @@ def get_ascender_http_client_headers(): license = get_license().get('license_type', 'UNLICENSED') headers = { 'Content-Type': 'application/json', - 'User-Agent': '{} {} ({})'.format(OPEN_PRODUCT_NAME if license == 'open' else SUBSCRIPTION_PRODUCT_NAME, get_ascender_version(), license), + 'User-Agent': '{} {} ({})'.format(OPEN_PRODUCT_NAME, get_ascender_version(), license), } return headers diff --git a/ascender/main/utils/licensing.py b/ascender/main/utils/licensing.py index 588e344c9..80e031594 100644 --- a/ascender/main/utils/licensing.py +++ b/ascender/main/utils/licensing.py @@ -1,38 +1,16 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -''' -This is intended to be a lightweight license class for verifying subscriptions, and parsing subscription data -from entitlement certificates. +"""What licence this product has, which is one answer. -The Licenser class can do the following: - - Parse an Entitlement cert to generate license -''' +There used to be a Licenser here that parsed an entitlement certificate and +decided whether a subscription was valid and in date. Ascender does not have a +subscription, so the only licence is the open one and this says so. +""" -import base64 -from datetime import datetime, timezone -import copy -import io -import os -import json import logging -import re -import time -import zipfile -from dateutil.parser import parse as parse_date - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography import x509 - -# Django -from ascender.settings.typed import settings -from django.utils.translation import gettext_lazy as _ - -from ascender.main.constants import SUBSCRIPTION_USAGE_MODEL_UNIQUE_HOSTS +from django.utils.translation import gettext_lazy as _ # noqa: F401 MAX_INSTANCES = 9999999 @@ -41,46 +19,10 @@ # matching. The diagnostic names move in their own change. logger = logging.getLogger('awx.main.utils.licensing') -#: What the product calls itself, unsubscribed and subscribed. These are read -#: back by callers deciding which product they are, so they are constants rather -#: than literals repeated at each site: a comparison against a spelling is the -#: kind of thing a rename breaks silently, by flipping a branch rather than -#: failing. +#: What the product calls itself. A constant rather than a literal repeated at +#: each site, because callers compare against it as well as print it, and a +#: comparison against a spelling is the kind of thing a rename breaks silently. OPEN_PRODUCT_NAME = 'Ascender' -SUBSCRIPTION_PRODUCT_NAME = 'Red Hat Ansible Automation Platform' - - -def validate_entitlement_manifest(data): - buff = io.BytesIO() - buff.write(base64.b64decode(data)) - try: - z = zipfile.ZipFile(buff) - except zipfile.BadZipFile as e: - raise ValueError(_("Invalid manifest: a subscription manifest zip file is required.")) from e - buff = io.BytesIO() - - files = z.namelist() - if 'consumer_export.zip' not in files or 'signature' not in files: - raise ValueError(_("Invalid manifest: missing required files.")) - export = z.open('consumer_export.zip').read() - sig = z.open('signature').read() - with open('/etc/ascender/candlepin-redhat-ca.crt', 'rb') as f: - cert = x509.load_pem_x509_certificate(f.read(), backend=default_backend()) - key = cert.public_key() - try: - key.verify(sig, export, padding=padding.PKCS1v15(), algorithm=hashes.SHA256()) - except InvalidSignature as e: - raise ValueError(_("Invalid manifest: signature verification failed.")) from e - - buff.write(export) - z = zipfile.ZipFile(buff) - subs = [] - for f in z.filelist: - if f.filename.startswith('export/entitlements') and f.filename.endswith('.json'): - subs.append(json.loads(z.open(f).read())) - if subs: - return subs - raise ValueError(_("Invalid manifest: manifest contains no subscriptions.")) class OpenLicense(object): @@ -93,196 +35,17 @@ def validate(self): ) -class Licenser(object): - # warn when there is a month (30 days) left on the subscription - SUBSCRIPTION_TIMEOUT = 60 * 60 * 24 * 30 - - UNLICENSED_DATA = dict( - subscription_name=None, - sku=None, - support_level=None, - instance_count=0, - license_date=0, - license_type="UNLICENSED", - product_name=SUBSCRIPTION_PRODUCT_NAME, - valid_key=False, - ) - - def __init__(self, **kwargs): - self._attrs = dict( - instance_count=0, - license_date=0, - license_type='UNLICENSED', - ) - if not kwargs: - license_setting = getattr(settings, 'LICENSE', None) - if license_setting is not None: - kwargs = license_setting - - if 'company_name' in kwargs: - kwargs.pop('company_name') - self._attrs.update(kwargs) - if 'valid_key' in self._attrs: - if not self._attrs['valid_key']: - self._unset_attrs() - else: - self._unset_attrs() - - def _unset_attrs(self): - self._attrs = self.UNLICENSED_DATA.copy() - - def license_from_manifest(self, manifest): - def is_appropriate_manifest_sub(sub): - if sub['pool']['activeSubscription'] is False: - return False - now = datetime.now(timezone.utc) - if parse_date(sub['startDate']) > now: - return False - if parse_date(sub['endDate']) < now: - return False - products = sub['pool']['providedProducts'] - if any(product.get('productId') == '480' for product in products): - return True - return False - - def _can_aggregate(sub, license): - # We aggregate multiple subs into a larger meta-sub, if they match - # - # No current sub in aggregate - if not license: - return True - # Same SKU type (SER vs MCT vs others)? - if license['sku'][0:3] != sub['pool']['productId'][0:3]: - return False - return True - - # Parse output for subscription metadata to build config - license = dict() - for sub in manifest: - if not is_appropriate_manifest_sub(sub): - logger.warning("Subscription %s (%s) in manifest is not active or for another product" % (sub['pool']['productName'], sub['pool']['productId'])) - continue - if not _can_aggregate(sub, license): - logger.warning( - "Subscription %s (%s) in manifest does not match other manifest subscriptions" % (sub['pool']['productName'], sub['pool']['productId']) - ) - continue - - license.setdefault('sku', sub['pool']['productId']) - license.setdefault('subscription_name', sub['pool']['productName']) - license.setdefault('subscription_id', sub['pool']['subscriptionId']) - license.setdefault('account_number', sub['pool']['accountNumber']) - license.setdefault('pool_id', sub['pool']['id']) - license.setdefault('product_name', sub['pool']['productName']) - license.setdefault('valid_key', True) - if sub['pool']['productId'].startswith('S'): - license.setdefault('trial', True) - license.setdefault('license_type', 'trial') - else: - license.setdefault('trial', False) - license.setdefault('license_type', 'enterprise') - license.setdefault('satellite', False) - # Use the nearest end date - endDate = parse_date(sub['endDate']) - currentEndDateStr = license.get('license_date', '4102462800') # 2100-01-01 - currentEndDate = datetime.fromtimestamp(int(currentEndDateStr), timezone.utc) - if endDate < currentEndDate: - license['license_date'] = endDate.strftime('%s') - instances = sub['quantity'] - license['instance_count'] = license.get('instance_count', 0) + instances - license['subscription_name'] = re.sub(r'[\d]* Managed Nodes', '%d Managed Nodes' % license['instance_count'], license['subscription_name']) - - license['support_level'] = '' - license['usage'] = '' - for attr in sub['pool'].get('productAttributes', []): - if attr.get('name') == 'support_level': - license['support_level'] = attr.get('value') - elif attr.get('name') == 'usage': - license['usage'] = attr.get('value') - elif attr.get('name') == 'ph_product_name' and attr.get('value') == 'RHEL Developer': - license['license_type'] = 'developer' - - if not license: - logger.error("No valid subscriptions found in manifest") - self._attrs.update(license) - settings.LICENSE = self._attrs - return self._attrs - - def update(self, **kwargs): - # Update attributes of the current license. - if 'instance_count' in kwargs: - kwargs['instance_count'] = int(kwargs['instance_count']) - if 'license_date' in kwargs: - kwargs['license_date'] = int(kwargs['license_date']) - self._attrs.update(kwargs) - - def validate(self): - # Return license attributes with additional validation info. - attrs = copy.deepcopy(self._attrs) - type = attrs.get('license_type', 'none') - - if type == 'UNLICENSED' or False: - attrs.update(dict(valid_key=False, compliant=False)) - return attrs - attrs['valid_key'] = True - - from ascender.main.models import Host, HostMetric, Instance - - current_instances = Host.objects.active_count() - license_date = int(attrs.get('license_date', 0) or 0) - - subscription_model = getattr(settings, 'SUBSCRIPTION_USAGE_MODEL', '') - if subscription_model == SUBSCRIPTION_USAGE_MODEL_UNIQUE_HOSTS: - automated_instances = HostMetric.active_objects.count() - first_host = HostMetric.active_objects.only('first_automation').order_by('first_automation').first() - attrs['deleted_instances'] = HostMetric.objects.filter(deleted=True).count() - attrs['reactivated_instances'] = HostMetric.active_objects.filter(deleted_counter__gte=1).count() - else: - automated_instances = 0 - first_host = HostMetric.objects.only('first_automation').order_by('first_automation').first() - attrs['deleted_instances'] = 0 - attrs['reactivated_instances'] = 0 - - if first_host: - automated_since = int(first_host.first_automation.timestamp()) - else: - automated_since = int(Instance.objects.order_by('id').first().created.timestamp()) - instance_count = int(attrs.get('instance_count', 0)) - attrs['current_instances'] = current_instances - attrs['automated_instances'] = automated_instances - attrs['automated_since'] = automated_since - free_instances = instance_count - automated_instances - attrs['free_instances'] = max(0, free_instances) - - current_date = int(time.time()) - time_remaining = license_date - current_date - attrs['time_remaining'] = time_remaining - if attrs.setdefault('trial', False): - attrs['grace_period_remaining'] = time_remaining - else: - attrs['grace_period_remaining'] = (license_date + 2592000) - current_date - attrs['compliant'] = bool(time_remaining > 0 and free_instances >= 0) - attrs['date_warning'] = bool(time_remaining < self.SUBSCRIPTION_TIMEOUT) - attrs['date_expired'] = bool(time_remaining <= 0) - return attrs - - def get_licenser(*args, **kwargs): - from ascender.main.utils.licensing import Licenser, OpenLicense + """The licence this product has, which is the open one. - try: - # .tower_version keeps its name deliberately. Its presence is what says - # this is the subscription product rather than the open one, and nothing - # in this ecosystem writes it: an install that has it got it from the - # Tower it was migrated from. Renaming the check would silently move - # such an install onto the open licence. - if os.path.exists('/var/lib/ascender/.tower_version'): - return Licenser(*args, **kwargs) - else: - return OpenLicense() - except Exception as e: - raise ValueError(_('Error importing License: %s') % e) + There used to be a second answer here, chosen by whether + /var/lib/ascender/.tower_version existed: present meant the subscription + product and an entitlement manifest to validate. Nothing in this ecosystem + ever wrote that file, so the branch was only ever reachable on an install + carried over from Tower, and Ascender does not have a subscription to check. + """ + return OpenLicense() def server_product_name(): - return OPEN_PRODUCT_NAME if isinstance(get_licenser(), OpenLicense) else SUBSCRIPTION_PRODUCT_NAME + return OPEN_PRODUCT_NAME From 22c8dc2121409df5da108156f1f5eacdada9fc9c Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Tue, 15 Sep 2026 01:43:09 +0200 Subject: [PATCH 02/12] feat: remove the subscription pages, which no longer have an API behind them The previous commit took away the manifest endpoint and made the licence always open. That left the UI for it standing: a Subscription settings page whose wizard posted a manifest to an endpoint that now answers 400, and a Subscription Usage graph reading a usage model nothing sets. Both were already unreachable, which is the point cigamit made on the forum thread. The settings page redirects to /settings when the licence type is open, which it now always is. The usage route and the host metrics route are both deleted from the route config unless SUBSCRIPTION_USAGE_MODEL is unique_managed_hosts, and the default is empty. So this removes pages nobody could open, rather than taking anything away. Gone: the Subscription settings screen and its detail and edit wizard, the Subscription Usage screen and its chart, the SubscriptionUsage API model, the settings list entry, the settings route and its breadcrumbs. The app level gate goes with them. useAuthorizedPath answered false when the licence had no valid key, which sent every route to /subscription_management and the wizard. The open licence is always valid and there is no wizard to send anyone to, so it returns true, and the branch that rendered the wizard is gone. The hook stays rather than being deleted because AppContainer reads it for sidebar visibility. Host metrics is deliberately untouched. cigamit said those are useful and might be opened up for everyone, which is a decision rather than a removal. Checked: type check, eslint and prettier clean, and the UI suite passes whole, 549 files and 3,050 tests. --- ascender/ui/src/App.tsx | 100 ++--- ascender/ui/src/api/index.ts | 3 - .../ui/src/api/models/SubscriptionUsage.ts | 18 - ascender/ui/src/contexts/Config.tsx | 15 +- ascender/ui/src/routeConfig.test.ts | 2 - ascender/ui/src/routeConfig.tsx | 10 - .../ui/src/screens/Setting/SettingList.tsx | 69 ++-- ascender/ui/src/screens/Setting/Settings.tsx | 16 +- .../Subscription/Subscription.test.tsx | 56 --- .../Setting/Subscription/Subscription.tsx | 37 -- .../SubscriptionDetail/SubscriptionDetail.css | 5 - .../SubscriptionDetail.test.tsx | 92 ----- .../SubscriptionDetail/SubscriptionDetail.tsx | 238 ------------ .../Subscription/SubscriptionDetail/index.ts | 1 - .../SubscriptionEdit/AnalyticsStep.test.tsx | 30 -- .../SubscriptionEdit/AnalyticsStep.tsx | 129 ------ .../SubscriptionEdit/EulaStep.test.tsx | 27 -- .../SubscriptionEdit/EulaStep.tsx | 26 -- .../SubscriptionEdit.test.tsx | 266 ------------- .../SubscriptionEdit/SubscriptionEdit.tsx | 289 -------------- .../SubscriptionEdit/SubscriptionStep.css | 6 - .../SubscriptionStep.test.tsx | 121 ------ .../SubscriptionEdit/SubscriptionStep.tsx | 165 -------- .../Subscription/SubscriptionEdit/index.ts | 1 - .../src/screens/Setting/Subscription/index.ts | 1 - .../ChartComponents/UsageChart.tsx | 367 ------------------ .../ChartComponents/UsageChartTooltip.ts | 247 ------------ .../SubscriptionUsage/SubscriptionUsage.css | 10 - .../SubscriptionUsage/SubscriptionUsage.tsx | 31 -- .../SubscriptionUsageChart.css | 23 -- .../SubscriptionUsageChart.tsx | 162 -------- 31 files changed, 64 insertions(+), 2499 deletions(-) delete mode 100644 ascender/ui/src/api/models/SubscriptionUsage.ts delete mode 100644 ascender/ui/src/screens/Setting/Subscription/Subscription.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/Subscription.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.css delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/index.ts delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionEdit.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionStep.css delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionStep.test.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionStep.tsx delete mode 100644 ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/index.ts delete mode 100644 ascender/ui/src/screens/Setting/Subscription/index.ts delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChart.tsx delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/ChartComponents/UsageChartTooltip.ts delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/SubscriptionUsage.css delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/SubscriptionUsage.tsx delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.css delete mode 100644 ascender/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.tsx diff --git a/ascender/ui/src/App.tsx b/ascender/ui/src/App.tsx index 1777f7358..15cfb2c04 100644 --- a/ascender/ui/src/App.tsx +++ b/ascender/ui/src/App.tsx @@ -13,11 +13,7 @@ import { I18nProvider } from '@lingui/react'; import { i18n } from '@lingui/core'; import { QueryClientProvider } from '@tanstack/react-query'; import { Card, PageSection } from '@patternfly/react-core'; -import { - ConfigProvider, - useAuthorizedPath, - useUserProfile, -} from 'contexts/Config'; +import { ConfigProvider, useUserProfile } from 'contexts/Config'; import { SessionProvider, useSession } from 'contexts/Session'; import AppContainer from 'components/AppContainer'; import ContentError from 'components/ContentError'; @@ -34,9 +30,6 @@ import { SESSION_REDIRECT_URL } from './constants'; import queryClient from './queryClient'; const Metrics = React.lazy(() => import('screens/Metrics')); -const SubscriptionEdit = React.lazy( - () => import('screens/Setting/Subscription/SubscriptionEdit') -); export interface ErrorFallbackProps { error: unknown; @@ -68,75 +61,46 @@ export interface AuthorizedRoutesProps { routeConfig: AppRouteGroup[]; } -const AuthorizedRoutes = ({ routeConfig }: AuthorizedRoutesProps) => { - const isAuthorized = useAuthorizedPath(); - - if (!isAuthorized) { - return ( - }> - +const AuthorizedRoutes = ({ routeConfig }: AuthorizedRoutesProps) => ( + }> + + {routeConfig + .flatMap(({ routes }) => routes) + .map(({ path, screen: Screen }) => ( + // /* so each screen's own nested can match the rest - - - - - + } /> + )) + .concat( + + + } + />, + } + element={ + + + + } /> - - - ); - } - - return ( - }> - - {routeConfig - .flatMap(({ routes }) => routes) - .map(({ path, screen: Screen }) => ( - // /* so each screen's own nested can match the rest - - - - } - /> - )) - .concat( - - - - } - />, - - - - } - /> - )} - - - ); -}; + )} + + +); export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { diff --git a/ascender/ui/src/api/index.ts b/ascender/ui/src/api/index.ts index 591447aa9..01169e15f 100644 --- a/ascender/ui/src/api/index.ts +++ b/ascender/ui/src/api/index.ts @@ -35,7 +35,6 @@ import Roles from './models/Roles'; import Root from './models/Root'; import Schedules from './models/Schedules'; import Settings from './models/Settings'; -import SubscriptionUsage from './models/SubscriptionUsage'; import SystemJobs from './models/SystemJobs'; import SystemJobTemplates from './models/SystemJobTemplates'; import Teams from './models/Teams'; @@ -87,7 +86,6 @@ const RolesAPI = new Roles(); const RootAPI = new Root(); const SchedulesAPI = new Schedules(); const SettingsAPI = new Settings(); -const SubscriptionUsageAPI = new SubscriptionUsage(); const SystemJobsAPI = new SystemJobs(); const SystemJobTemplatesAPI = new SystemJobTemplates(); const TeamsAPI = new Teams(); @@ -140,7 +138,6 @@ export { RootAPI, SchedulesAPI, SettingsAPI, - SubscriptionUsageAPI, SystemJobsAPI, SystemJobTemplatesAPI, TeamsAPI, diff --git a/ascender/ui/src/api/models/SubscriptionUsage.ts b/ascender/ui/src/api/models/SubscriptionUsage.ts deleted file mode 100644 index 57882acab..000000000 --- a/ascender/ui/src/api/models/SubscriptionUsage.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { HostMetricSummaryMonthly, Paginated } from '../../types/api'; -import Base from '../Base'; -import type { Http } from '../Base'; - -class SubscriptionUsage extends Base { - constructor(http?: Http) { - super(http); - this.baseUrl = 'api/v2/host_metric_summary_monthly/'; - } - - readSubscriptionUsageChart(dateRange: unknown) { - return this.http.get>( - `${this.baseUrl}?date__gte=${dateRange}&order_by=date&page_size=100` - ); - } -} - -export default SubscriptionUsage; diff --git a/ascender/ui/src/contexts/Config.tsx b/ascender/ui/src/contexts/Config.tsx index 8d78fa2b9..d5697b02d 100644 --- a/ascender/ui/src/contexts/Config.tsx +++ b/ascender/ui/src/contexts/Config.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; -import { useMatch } from 'react-router'; import { useLingui } from '@lingui/react/macro'; import { @@ -271,11 +270,9 @@ export const useUserProfile = (): UserProfile => { }; }; -export const useAuthorizedPath = () => { - const config = useConfig(); - const subscriptionMgmtRoute = useMatch({ - path: '/subscription_management', - end: false, - }); - return !!config.license_info?.valid_key && !subscriptionMgmtRoute; -}; +export const useAuthorizedPath = () => + // Kept as a hook rather than removed so its callers read the same. It used to + // answer false when the licence had no valid key, which sent the whole app to + // the subscription wizard. There is no subscription and no wizard, and the + // open licence is always valid, so there is nothing left for it to refuse. + true; diff --git a/ascender/ui/src/routeConfig.test.ts b/ascender/ui/src/routeConfig.test.ts index 40299ca6d..e34ad30f4 100644 --- a/ascender/ui/src/routeConfig.test.ts +++ b/ascender/ui/src/routeConfig.test.ts @@ -33,7 +33,6 @@ describe('getRouteConfig', () => { '/activity_stream', '/workflow_approvals', '/host_metrics', - '/subscription_usage', '/labels', '/templates', '/credentials', @@ -65,7 +64,6 @@ describe('getRouteConfig', () => { '/activity_stream', '/workflow_approvals', '/host_metrics', - '/subscription_usage', '/labels', '/templates', '/credentials', diff --git a/ascender/ui/src/routeConfig.tsx b/ascender/ui/src/routeConfig.tsx index 2bd95e3b9..a64ce4526 100644 --- a/ascender/ui/src/routeConfig.tsx +++ b/ascender/ui/src/routeConfig.tsx @@ -25,9 +25,6 @@ const Organizations = React.lazy(() => import('screens/Organization')); const Projects = React.lazy(() => import('screens/Project')); const Schedules = React.lazy(() => import('screens/Schedule')); const Settings = React.lazy(() => import('screens/Setting')); -const SubscriptionUsage = React.lazy( - () => import('screens/SubscriptionUsage/SubscriptionUsage') -); const Teams = React.lazy(() => import('screens/Team')); const Templates = React.lazy(() => import('screens/Template')); const TopologyView = React.lazy(() => import('screens/TopologyView')); @@ -89,11 +86,6 @@ function getRouteConfig(userProfile: Partial = {}) { path: '/host_metrics', screen: HostMetrics, }, - { - title: Subscription Usage, - path: '/subscription_usage', - screen: SubscriptionUsage, - }, ], }, { @@ -227,7 +219,6 @@ function getRouteConfig(userProfile: Partial = {}) { 'unique_managed_hosts' ) { deleteRoute('host_metrics'); - deleteRoute('subscription_usage'); } if (userProfile?.isSuperUser || userProfile?.isSystemAuditor) return routeConfig; @@ -236,7 +227,6 @@ function getRouteConfig(userProfile: Partial = {}) { deleteRoute('management_jobs'); deleteRoute('topology_view'); deleteRoute('instances'); - deleteRoute('subscription_usage'); if (userProfile?.isOrgAdmin) return routeConfig; if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates'); diff --git a/ascender/ui/src/screens/Setting/SettingList.tsx b/ascender/ui/src/screens/Setting/SettingList.tsx index b3a96b5a9..5a6ec693c 100644 --- a/ascender/ui/src/screens/Setting/SettingList.tsx +++ b/ascender/ui/src/screens/Setting/SettingList.tsx @@ -103,17 +103,6 @@ function SettingList() { }, ], }, - { - header: t`Subscription`, - description: t`View and edit your subscription information`, - id: 'subscription', - routes: [ - { - title: t`Subscription settings`, - path: '/settings/subscription', - }, - ], - }, { header: t`Troubleshooting`, description: t`View and edit debug options`, @@ -139,39 +128,31 @@ function SettingList() { return ( - {settingRoutes.map(({ description, header, id, routes }) => { - if ( - id === 'subscription' && - config?.license_info?.license_type === 'open' - ) { - return null; - } - return ( - - - {header} -
- {description} -
-
- - {routes.map(({ title, path }) => ( - - - - {title} - , - ]} - /> - - - ))} - -
- ); - })} + {settingRoutes.map(({ description, header, id, routes }) => ( + + + {header} +
+ {description} +
+
+ + {routes.map(({ title, path }) => ( + + + + {title} + , + ]} + /> + + + ))} + +
+ ))}
); } diff --git a/ascender/ui/src/screens/Setting/Settings.tsx b/ascender/ui/src/screens/Setting/Settings.tsx index ef8c0a3a2..36b9b18dc 100644 --- a/ascender/ui/src/screens/Setting/Settings.tsx +++ b/ascender/ui/src/screens/Setting/Settings.tsx @@ -15,7 +15,6 @@ import GoogleOAuth2 from './GoogleOAuth2'; import OIDC from './OIDC'; import Jobs from './Jobs'; import LDAP from './LDAP'; -import Subscription from './Subscription'; import Logging from './Logging'; import MiscAuthentication from './MiscAuthentication'; import MiscSystem from './MiscSystem'; @@ -28,7 +27,7 @@ import Troubleshooting from './Troubleshooting'; function Settings() { const { t } = useLingui(); - const { license_info = {}, me } = useConfig(); + const { me } = useConfig(); const { request, result, isLoading, error } = useRequest( useCallback(async () => { @@ -115,9 +114,6 @@ function Settings() { '/settings/saml': t`SAML`, '/settings/saml/details': t`Details`, '/settings/saml/edit': t`Edit Details`, - '/settings/subscription': t`Subscription`, - '/settings/subscription/details': t`Details`, - '/settings/subscription/edit': t`Edit Details`, '/settings/tacacs': t`TACACS+`, '/settings/tacacs/details': t`Details`, '/settings/tacacs/edit': t`Edit Details`, @@ -164,16 +160,6 @@ function Settings() { } /> } /> } /> - - ) : ( - - ) - } - /> } /> ); -vi.mocked(RootAPI.readAssetVariables).mockResolvedValue({ - data: { - BRAND_NAME: 'Ascender Automation', - PENDO_API_KEY: '', - }, -} as unknown as ResponseOf); - -describe('', () => { - afterEach(() => { - vi.clearAllMocks(); - }); - - test('should redirect to subscription details', async () => { - const history = createMemoryHistory({ - initialEntries: ['/settings/subscription'], - }); - renderWithContexts( - - } /> - , - { - context: { - router: { history }, - config: { - license_info: { - license_type: 'enterprise', - automated_instances: '1', - automated_since: '1614714228', - }, - }, - }, - } - ); - await waitFor(() => - expect(history.location.pathname).toEqual( - '/settings/subscription/details' - ) - ); - // SubscriptionDetail renders the read-only subscription type detail - expect(await screen.findByText('Subscription type')).toBeInTheDocument(); - }); -}); diff --git a/ascender/ui/src/screens/Setting/Subscription/Subscription.tsx b/ascender/ui/src/screens/Setting/Subscription/Subscription.tsx deleted file mode 100644 index c76fbd48c..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/Subscription.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Link, Routes, Route, Navigate } from 'react-router'; -import { useLingui } from '@lingui/react/macro'; -import { PageSection, Card } from '@patternfly/react-core'; -import ContentError from 'components/ContentError'; -import SubscriptionDetail from './SubscriptionDetail'; -import SubscriptionEdit from './SubscriptionEdit'; - -function Subscription() { - const { t } = useLingui(); - const baseURL = '/settings/subscription'; - - return ( - - - - } - /> - } /> - } /> - - {t`View Settings`} - - } - /> - - - - ); -} - -export default Subscription; diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.css b/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.css deleted file mode 100644 index 55061c8b5..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.css +++ /dev/null @@ -1,5 +0,0 @@ -/* SubscriptionDetail: the styling that used to be generated by styled-components. */ - -.ascender-subscription-detail__helper-text { - margin-top: 10px; -} diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.tsx deleted file mode 100644 index 128b7aa31..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import { - renderWithContexts, - assertDetail, -} from '../../../../../testUtils/rtlContexts'; -import SubscriptionDetail from './SubscriptionDetail'; - -const config = { - me: { - is_superuser: false, - }, - version: '1.2.3', - license_info: { - compliant: true, - current_instances: 1, - date_expired: false, - date_warning: true, - free_instances: 1000, - automated_instances: '12', - automated_since: '1614714228', - grace_period_remaining: 2904229, - instance_count: 1001, - license_date: '1614401999', - license_type: 'enterprise', - pool_id: '123', - product_name: 'Red Hat Ansible Automation, Standard (5000 Managed Nodes)', - satellite: false, - sku: 'ABC', - subscription_name: - 'Red Hat Ansible Automation, Standard (1001 Managed Nodes)', - support_level: null, - time_remaining: 312229, - trial: false, - valid_key: true, - }, - systemConfig: { - SUBSCRIPTION_USAGE_MODEL: 'unique_managed_hosts', - }, -}; - -describe('', () => { - test('initially renders without crashing', () => { - renderWithContexts(, { context: { config } }); - expect(screen.getByText('Status')).toBeInTheDocument(); - }); - - test('should render expected details', () => { - renderWithContexts(, { context: { config } }); - - const status = screen.getByText('Status'); - expect(status.nextElementSibling).toHaveTextContent('Compliant'); - expect(status.nextElementSibling).toHaveTextContent( - 'The number of hosts you have automated against is below your subscription count.' - ); - assertDetail('Automation controller version', '1.2.3'); - assertDetail('Subscription type', 'enterprise'); - assertDetail( - 'Subscription', - 'Red Hat Ansible Automation, Standard (1001 Managed Nodes)' - ); - assertDetail('Trial', 'False'); - assertDetail('Expires on', '2/27/2021, 4:59:59 AM'); - assertDetail('Days remaining', '3'); - assertDetail('Hosts imported', '1'); - assertDetail('Hosts remaining', '1000'); - assertDetail('Hosts automated', '12 since 3/2/2021, 7:43:48 PM'); - - expect( - screen.queryByRole('link', { name: 'edit' }) - ).not.toBeInTheDocument(); - }); - - test('should render edit button for system admin', () => { - renderWithContexts(, { - context: { config: { ...config, me: { is_superuser: true } } }, - }); - expect(screen.getByRole('link', { name: 'edit' })).toBeInTheDocument(); - }); - - test('should not render Hosts Automated Detail if license_info.automated_instances is undefined', () => { - renderWithContexts(, { - context: { - config: { - ...config, - license_info: { ...config.license_info, automated_instances: null }, - }, - }, - }); - expect(screen.queryByText('Hosts automated')).not.toBeInTheDocument(); - }); -}); diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.tsx deleted file mode 100644 index b8a1fa10a..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router'; -import { Trans, useLingui } from '@lingui/react/macro'; -import { - Button, - HelperText as PFHelperText, - HelperTextItem, - Label, -} from '@patternfly/react-core'; -import { - CaretLeftIcon, - CheckIcon, - ExclamationCircleIcon, -} from '@patternfly/react-icons'; -import RoutedTabs from 'components/RoutedTabs'; -import { CardBody, CardActionsRow } from 'components/Card'; -import { DetailList, Detail } from 'components/DetailList'; -import { useConfig } from 'contexts/Config'; -import { formatDateString, secondsToDays } from 'util/dates'; -import './SubscriptionDetail.css'; - -function SubscriptionDetail() { - const { t } = useLingui(); - const { me = {}, license_info, version, systemConfig } = useConfig(); - const baseURL = '/settings/subscription'; - const tabsArray = [ - { - name: ( - <> - - {t`Back to Settings`} - - ), - link: '/settings', - id: 99, - }, - { - name: t`Subscription Details`, - link: `${baseURL}/details`, - id: 0, - }, - ]; - - const { automated_instances: automatedInstancesCount, automated_since } = - license_info ?? {}; - - const automatedInstancesSinceDateTime = automated_since - ? formatDateString(new Date(automated_since * 1000).toISOString()) - : null; - - return ( - <> - - - - {systemConfig?.SUBSCRIPTION_USAGE_MODEL === - 'unique_managed_hosts' && ( - - - - - {t`The number of hosts you have automated against is below your subscription count.`} - - - - ) : ( - <> - - - - {t`You have automated against more hosts than your subscription allows.`} - - - - ) - } - /> - )} - {typeof automatedInstancesCount !== 'undefined' && - automatedInstancesCount !== null && - systemConfig?.SUBSCRIPTION_USAGE_MODEL !== '' && ( - - {automatedInstancesCount} since{' '} - {automatedInstancesSinceDateTime} - - ) : ( - automatedInstancesCount - ) - } - /> - )} - {systemConfig?.SUBSCRIPTION_USAGE_MODEL !== '' && ( - - )} - {systemConfig?.SUBSCRIPTION_USAGE_MODEL === - 'unique_managed_hosts' && ( - - )} - {systemConfig?.SUBSCRIPTION_USAGE_MODEL === - 'unique_managed_hosts' && ( - - )} - {systemConfig?.SUBSCRIPTION_USAGE_MODEL === - 'unique_managed_hosts' && ( - - )} - - {systemConfig?.SUBSCRIPTION_USAGE_MODEL !== '' && - (license_info?.instance_count ?? 0) < 9999999 && ( - - )} - {systemConfig?.SUBSCRIPTION_USAGE_MODEL !== '' && - (license_info?.instance_count ?? 0) >= 9999999 && ( - - )} - - - - - - - - -
- - If you are ready to upgrade or renew, please{' '} - - - {me.is_superuser && ( - - - - )} -
- - ); -} - -export default SubscriptionDetail; diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/index.ts b/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/index.ts deleted file mode 100644 index 9f45dc3c2..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './SubscriptionDetail'; diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.test.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.test.tsx deleted file mode 100644 index 2c9bcf7f4..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { FormRoot } from 'components/Form'; -import { screen } from '@testing-library/react'; -import { renderWithContexts } from '../../../../../testUtils/rtlContexts'; -import AnalyticsStep from './AnalyticsStep'; - -describe('', () => { - test('initially renders the expected content', async () => { - renderWithContexts( - {}} - initialValues={{ - insights: false, - manifest_file: null, - manifest_filename: '', - pendo: false, - subscription: null, - password: '', - username: '', - }} - > - - - ); - // AnalyticsStep resets username/password on mount via a useEffect; await an - // async query so that Formik state update settles inside act() - expect(await screen.findByText('User analytics')).toBeInTheDocument(); - expect(screen.getByText('Automation Analytics')).toBeInTheDocument(); - }); -}); diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.tsx deleted file mode 100644 index 93b8fa4a8..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/AnalyticsStep.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useEffect } from 'react'; - -import { useField } from 'components/Form'; -import { Button, Flex, FormGroup } from '@patternfly/react-core'; -import getDocsBaseUrl from 'util/getDocsBaseUrl'; -import { required } from 'util/validators'; -import FormField, { CheckboxField, PasswordField } from 'components/FormField'; -import { useConfig } from 'contexts/Config'; -import { Trans, useLingui } from '@lingui/react/macro'; - -const ANALYTICSLINK = 'https://www.ansible.com/products/automation-analytics'; - -function AnalyticsStep() { - const { t } = useLingui(); - const config = useConfig(); - const [manifest] = useField('manifest_file'); - const [insights] = useField('insights'); - const [, , usernameHelpers] = useField('username'); - const [, , passwordHelpers] = useField('password'); - const requireCredentialFields = manifest.value && insights.value; - - useEffect(() => { - if (!requireCredentialFields) { - usernameHelpers.setValue(''); - passwordHelpers.setValue(''); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [requireCredentialFields]); - - return ( - - User and Automation Analytics -

- - By default, we collect and transmit analytics data on the service - usage to Red Hat. There are two categories of data collected by the - service. For more information, see{' '} - - . Uncheck the following boxes to disable this feature. - -

- - - - - - - {requireCredentialFields && ( - <> -
-

- - Provide your Red Hat or Red Hat Satellite credentials to enable - Automation Analytics. - -

- - - - )} - - {t`Automation - - -
- ); -} -export default AnalyticsStep; diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.test.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.test.tsx deleted file mode 100644 index 5b3d66d07..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { FormRoot } from 'components/Form'; -import { screen } from '@testing-library/react'; -import { renderWithContexts } from '../../../../../testUtils/rtlContexts'; -import EulaStep from './EulaStep'; - -describe('', () => { - test('initially renders the expected content', () => { - renderWithContexts( - {}} - initialValues={{ - insights: false, - manifest_file: null, - manifest_filename: '', - pendo: false, - subscription: null, - password: '', - username: '', - }} - > - - - ); - expect(screen.getByText('End User License Agreement')).toBeInTheDocument(); - }); -}); diff --git a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.tsx b/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.tsx deleted file mode 100644 index afeb07b43..000000000 --- a/ascender/ui/src/screens/Setting/Subscription/SubscriptionEdit/EulaStep.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { useLingui } from '@lingui/react/macro'; -import { Flex, FormGroup, TextArea } from '@patternfly/react-core'; -import { useConfig } from 'contexts/Config'; - -function EulaStep() { - const { t } = useLingui(); - const { eula } = useConfig(); - return ( - - -