From 745e3afbc09c0631b90b437c8272bac5d0db5790 Mon Sep 17 00:00:00 2001 From: Montse Ortega Date: Wed, 18 Jun 2025 07:59:35 +0200 Subject: [PATCH 1/4] Classify operators by categories --- .../clusterDetail/OperatorsProgressItem.tsx | 2 +- .../operators/operatorDescriptions.tsx | 58 ++ .../components/operators/operatorSpecs.tsx | 642 +++++++++++------- .../OcmOperatorProgressItem.tsx | 2 +- .../operators/OperatorCheckbox.tsx | 2 +- .../review/ReviewOperatorsTable.tsx | 3 +- .../clusterDetail/ProgressBarAlerts.tsx | 2 +- .../clusterWizard/OperatorsBundle.tsx | 10 +- .../clusterWizard/OperatorsSelect.tsx | 54 +- .../featureSupportLevels/featureStateUtils.ts | 12 +- 10 files changed, 490 insertions(+), 297 deletions(-) create mode 100644 libs/ui-lib/lib/common/components/operators/operatorDescriptions.tsx diff --git a/libs/ui-lib/lib/common/components/clusterDetail/OperatorsProgressItem.tsx b/libs/ui-lib/lib/common/components/clusterDetail/OperatorsProgressItem.tsx index 95555fa7d9..7db2cdc501 100644 --- a/libs/ui-lib/lib/common/components/clusterDetail/OperatorsProgressItem.tsx +++ b/libs/ui-lib/lib/common/components/clusterDetail/OperatorsProgressItem.tsx @@ -80,7 +80,7 @@ type OperatorsPopoverProps = OperatorListProps & { const OperatorsPopover = ({ operators, children }: OperatorsPopoverProps) => { const { t } = useTranslation(); - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); return ( ; notStandalone?: boolean; Requirements?: React.ComponentType<{ cluster: Cluster }>; + category: string; }; -export const getOperatorSpecs = (useLVMS?: boolean): { [key: string]: OperatorSpec } => { +export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: OperatorSpec[] } => { return { - [OPERATOR_NAME_MTV]: { - title: 'Migration Toolkit for Virtualization', - featureId: 'MTV', - Description: () => ( - <> - This Toolkit (MTV) enables you to migrate virtual machines from VMware vSphere, Red Hat - Virtualization, or OpenStack to OpenShift Virtualization running on Red Hat OpenShift.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_AMD_GPU]: { - title: 'AMD GPU', - featureId: 'AMD_GPU', - Requirements: () => <>Requires at least one supported AMD GPU, - Description: () => ( - <> - Automate the management of AMD software components needed to provision and monitor GPUs. - - ), - }, - [OPERATOR_NAME_LSO]: { - title: 'Local Storage Operator', - featureId: 'LSO', - Description: ({ openshiftVersion }) => ( - <> - Allows provisioning of persistent storage by using local volumes.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_AUTHORINO]: { - title: 'Authorino', - featureId: 'AUTHORINO', - Description: () => ( - <> - Lightweight external authorization service for tailor-made Zero Trust API security.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_CNV]: { - title: 'OpenShift Virtualization', - featureId: 'CNV', - Requirements: () => ( - <>Enabled CPU virtualization support in BIOS (Intel-VT / AMD-V) on all nodes - ), - Description: () => ( - <> - Run virtual machines alongside containers on one platform.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_FENCE_AGENTS_REMEDIATION]: { - title: 'Fence Agents Remediation', - featureId: 'FENCE_AGENTS_REMEDIATION', - Description: () => ( - <> - Externally fences failed nodes using power controllers.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_KMM]: { - title: 'Kernel Module Management', - featureId: 'KMM', - Description: ({ openshiftVersion }) => ( - <> - Management of kernel modules.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_KUBE_DESCHEDULER]: { - title: 'Kube Descheduler', - featureId: 'KUBE_DESCHEDULER', - Description: ({ openshiftVersion }) => ( - <> - Evicts pods to reschedule them onto more suitable nodes.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_MCE]: { - title: 'Multicluster engine', - featureId: 'MCE', - Description: ({ openshiftVersion }) => ( - <> - Create, import, and manage multiple clusters from this cluster.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_NMSTATE]: { - title: 'NMState', - featureId: 'NMSTATE', - Description: ({ openshiftVersion }) => ( - <> - Provides users with functionality to configure various network interface types, DNS, and - routing on cluster nodes.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_NODE_FEATURE_DISCOVERY]: { - title: 'Node Feature Discovery', - featureId: 'NODE_FEATURE_DISCOVERY', - Description: ({ openshiftVersion }) => ( - <> - Manage the detection of hardware features and configuration by labeling nodes with - hardware-specific information.{' '} - - Learn more + [categories[Category.STORAGE]]: [ + { + operatorKey: OPERATOR_NAME_LSO, + title: 'Local Storage Operator', + featureId: 'LSO', + descriptionText: DESCRIPTION_LSO, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_LSO}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.STORAGE], + }, + { + operatorKey: OPERATOR_NAME_LVM, + title: useLVMS ? 'Logical Volume Manager Storage' : 'Logical Volume Manager', + featureId: 'LVM', + descriptionText: DESCRIPTION_LVM, + Description: ({ openshiftVersion }) => + useLVMS ? ( + <> + {DESCRIPTION_LVM}{' '} + Learn more + + ) : ( + <>{DESCRIPTION_LVM} + ), + category: categories[Category.STORAGE], + }, + { + operatorKey: OPERATOR_NAME_ODF, + title: 'OpenShift Data Foundation', + featureId: 'ODF', + descriptionText: DESCRIPTION_ODF, + Requirements: () => ( + + Learn more about the requirements for OpenShift Data Foundation - - ), - }, - [OPERATOR_NAME_NODE_HEALTHCHECK]: { - title: 'Node Healthcheck', - featureId: 'NODE_HEALTHCHECK', - Description: () => ( - <> - Identify Unhealthy Nodes.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_NODE_MAINTENANCE]: { - title: 'Node Maintenance', - featureId: 'NODE_MAINTENANCE', - Description: () => ( - <> - Place nodes in maintenance mode.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_NVIDIA_GPU]: { - title: 'NVIDIA GPU', - featureId: 'NVIDIA_GPU', - Requirements: () => <>Requires at least one supported NVIDIA GPU, - Description: ({ openshiftVersion }) => ( - <> - Automate the management of NVIDIA software components needed to provision and monitor - GPUs. Learn more - - ), - }, - [OPERATOR_NAME_ODF]: { - title: 'OpenShift Data Foundation', - featureId: 'ODF', - Requirements: () => ( - - Learn more about the requirements for OpenShift Data Foundation - - ), - Description: () => ( - <> - Persistent software-defined storage for hybrid applications.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_OPENSHIFT_AI]: { - title: 'OpenShift AI', - featureId: 'OPENSHIFT_AI', - Requirements: () => ( - Learn more - ), - Description: () => ( - <> - Train, serve, monitor and manage AI/ML models and applications.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_OSC]: { - title: 'OpenShift sandboxed containers', - featureId: 'OSC', - Requirements: () => ( - - Learn more about the requirements for OpenShift sandboxed containers - - ), - Description: () => ( - <> - OpenShift sandboxed containers support for OpenShift Container Platform provides users - with built-in support for running Kata Containers as an additional optional runtime. It - provides an additional virtualization machine(VM) isolation layer for pods.{' '} - Learn more - - ), - }, - [OPERATOR_NAME_PIPELINES]: { - title: 'Pipelines', - featureId: 'PIPELINES', - Description: () => ( - <> - Cloud-native continuous integration and delivery (CI/CD) solution for building pipelines - using Tekton. Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_SELF_NODE_REMEDIATION]: { - title: 'Self Node Remediation', - featureId: 'SELF_NODE_REMEDIATION', - Description: () => ( - <> - Allows nodes to reboot themselves when they become unhealthy.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_SERVERLESS]: { - title: 'Serverless', - featureId: 'SERVERLESS', - Description: () => ( - <> - Deploy workflow applications based on the CNCF Serverless Workflow specification.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_SERVICEMESH]: { - title: 'Service Mesh', - featureId: 'SERVICEMESH', - Description: ({ openshiftVersion }) => ( - <> - Platform that provides behavioral insight and operational control over a service mesh.{' '} - Learn more - - ), - notStandalone: true, - }, - [OPERATOR_NAME_LVM]: { - title: useLVMS ? 'Logical Volume Manager Storage' : 'Logical Volume Manager', - featureId: 'LVM', - Description: ({ openshiftVersion }) => - useLVMS ? ( + ), + Description: () => ( + <> + {DESCRIPTION_ODF} Learn more + + ), + category: categories[Category.STORAGE], + }, + ], + [categories[Category.VIRT]]: [ + { + operatorKey: OPERATOR_NAME_CNV, + title: 'OpenShift Virtualization', + featureId: 'CNV', + descriptionText: DESCRIPTION_CNV, + Requirements: () => ( + <>Enabled CPU virtualization support in BIOS (Intel-VT / AMD-V) on all nodes + ), + Description: () => ( <> - Storage virtualization that offers a more flexible approach for disk space management.{' '} - Learn more + {DESCRIPTION_CNV} Learn more - ) : ( + ), + category: categories[Category.VIRT], + }, + { + operatorKey: OPERATOR_NAME_MTV, + title: 'Migration Toolkit for Virtualization', + featureId: 'MTV', + descriptionText: DESCRIPTION_MTV, + Description: () => ( + <> + {DESCRIPTION_MTV} Learn more + + ), + category: categories[Category.VIRT], + }, + ], + [categories[Category.AI]]: [ + { + operatorKey: OPERATOR_NAME_OPENSHIFT_AI, + title: 'OpenShift AI', + featureId: 'OPENSHIFT_AI', + descriptionText: DESCRIPTION_OPENSHIFT_AI, + Requirements: () => ( + Learn more + ), + Description: () => ( + <> + {DESCRIPTION_OPENSHIFT_AI}{' '} + Learn more + + ), + category: categories[Category.AI], + }, + { + operatorKey: OPERATOR_NAME_AMD_GPU, + title: 'AMD GPU', + featureId: 'AMD_GPU', + descriptionText: DESCRIPTION_AMD_GPU, + Requirements: () => <>Requires at least one supported AMD GPU, + Description: () => <>{DESCRIPTION_AMD_GPU}, + category: categories[Category.AI], + }, + { + operatorKey: OPERATOR_NAME_NVIDIA_GPU, + title: 'NVIDIA GPU', + featureId: 'NVIDIA_GPU', + descriptionText: DESCRIPTION_NVIDIA_GPU, + Requirements: () => <>Requires at least one supported NVIDIA GPU, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_NVIDIA_GPU} + Learn more + + ), + category: categories[Category.AI], + }, + ], + [categories[Category.NETWORK]]: [ + { + operatorKey: OPERATOR_NAME_NMSTATE, + title: 'NMState', + featureId: 'NMSTATE', + descriptionText: DESCRIPTION_NMSTATE, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_NMSTATE}{' '} + Learn more + + ), + category: categories[Category.NETWORK], + }, + { + operatorKey: OPERATOR_NAME_SERVICEMESH, + title: 'Service Mesh', + featureId: 'SERVICEMESH', + descriptionText: DESCRIPTION_SERVICEMESH, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_SERVICEMESH}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.NETWORK], + }, + ], + [categories[Category.REMEDIATION]]: [ + { + operatorKey: OPERATOR_NAME_FENCE_AGENTS_REMEDIATION, + title: 'Fence Agents Remediation', + featureId: 'FENCE_AGENTS_REMEDIATION', + descriptionText: DESCRIPTION_FENCE_AGENTS_REMEDIATION, + Description: () => ( + <> + {DESCRIPTION_FENCE_AGENTS_REMEDIATION}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.REMEDIATION], + }, + { + operatorKey: OPERATOR_NAME_NODE_HEALTHCHECK, + title: 'Node Healthcheck', + featureId: 'NODE_HEALTHCHECK', + descriptionText: DESCRIPTION_NODE_HEALTHCHECK, + Description: () => ( + <> + {DESCRIPTION_NODE_HEALTHCHECK}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.REMEDIATION], + }, + { + operatorKey: OPERATOR_NAME_SELF_NODE_REMEDIATION, + title: 'Self Node Remediation', + featureId: 'SELF_NODE_REMEDIATION', + descriptionText: DESCRIPTION_SELF_NODE_REMEDIATION, + Description: () => ( + <> + {DESCRIPTION_SELF_NODE_REMEDIATION}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.REMEDIATION], + }, + ], + [categories[Category.OTHER]]: [ + { + operatorKey: OPERATOR_NAME_AUTHORINO, + title: 'Authorino', + featureId: 'AUTHORINO', + descriptionText: DESCRIPTION_AUTHORINO, + Description: () => ( <> - Storage virtualization that offers a more flexible approach for disk space management. + {DESCRIPTION_AUTHORINO}{' '} + Learn more ), - }, + notStandalone: true, + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_NODE_FEATURE_DISCOVERY, + title: 'Node Feature Discovery', + featureId: 'NODE_FEATURE_DISCOVERY', + descriptionText: DESCRIPTION_NODE_FEATURE_DISCOVERY, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_NODE_FEATURE_DISCOVERY}{' '} + + Learn more + + + ), + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_PIPELINES, + title: 'Pipelines', + featureId: 'PIPELINES', + descriptionText: DESCRIPTION_PIPELINES, + Description: () => ( + <> + {DESCRIPTION_PIPELINES}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_SERVERLESS, + title: 'Serverless', + featureId: 'SERVERLESS', + descriptionText: DESCRIPTION_SERVERLESS, + Description: () => ( + <> + {DESCRIPTION_SERVERLESS}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_KMM, + title: 'Kernel Module Management', + featureId: 'KMM', + descriptionText: DESCRIPTION_KMM, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_KMM}{' '} + Learn more + + ), + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_MCE, + title: 'Multicluster engine', + featureId: 'MCE', + descriptionText: DESCRIPTION_MCE, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_MCE}{' '} + Learn more + + ), + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_OSC, + title: 'OpenShift sandboxed containers', + featureId: 'OSC', + descriptionText: DESCRIPTION_OSC, + Requirements: () => ( + + Learn more about the requirements for OpenShift sandboxed containers + + ), + Description: () => ( + <> + {DESCRIPTION_OSC} Learn more + + ), + category: categories[Category.OTHER], + }, + { + operatorKey: OPERATOR_NAME_KUBE_DESCHEDULER, + title: 'Kube Descheduler', + featureId: 'KUBE_DESCHEDULER', + descriptionText: DESCRIPTION_KUBE_DESCHEDULER, + Description: ({ openshiftVersion }) => ( + <> + {DESCRIPTION_KUBE_DESCHEDULER}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.OTHER], + }, + + { + operatorKey: OPERATOR_NAME_NODE_MAINTENANCE, + title: 'Node Maintenance', + featureId: 'NODE_MAINTENANCE', + descriptionText: DESCRIPTION_NODE_MAINTENANCE, + Description: () => ( + <> + {DESCRIPTION_NODE_MAINTENANCE}{' '} + Learn more + + ), + notStandalone: true, + category: categories[Category.OTHER], + }, + ], }; }; export const useOperatorSpecs = () => { const { getFeatureSupportLevel } = useNewFeatureSupportLevel(); const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; - return React.useMemo(() => getOperatorSpecs(useLVMS), [useLVMS]); + + // Grouped by category + const byCategory = React.useMemo(() => getOperatorSpecs(useLVMS), [useLVMS]); + + // Flat map operatorKey -> spec + const byKey: Record = React.useMemo(() => { + return Object.values(byCategory).reduce((acc, specs) => { + specs.forEach((spec) => { + acc[spec.operatorKey] = spec; + }); + return acc; + }, {} as Record); + }, [byCategory]); + + return { byCategory, byKey }; +}; + +// Utility to get flat map outside the hook +export const getOperatorSpecsByKey = (useLVMS?: boolean): Record => + Object.values(getOperatorSpecs(useLVMS)).reduce((acc, specs) => { + specs.forEach((spec) => { + acc[spec.operatorKey] = spec; + }); + return acc; + }, {} as Record); + +enum Category { + STORAGE, + VIRT, + AI, + NETWORK, + REMEDIATION, + OTHER, +} + +export const categories: { [key in Category]: string } = { + [Category.STORAGE]: 'Storage', + [Category.VIRT]: 'Virtualization', + [Category.AI]: 'AI', + [Category.NETWORK]: 'Network', + [Category.REMEDIATION]: 'Remediation', + [Category.OTHER]: 'Other', }; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmOperatorProgressItem.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmOperatorProgressItem.tsx index 6b258dd3af..3339efa9df 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmOperatorProgressItem.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmOperatorProgressItem.tsx @@ -80,7 +80,7 @@ type OperatorsPopoverProps = OperatorListProps & { const OperatorsPopover = ({ operators, children }: OperatorsPopoverProps) => { const { t } = useTranslation(); - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); return ( { const { getFeatureSupportLevel, getFeatureDisabledReason } = useNewFeatureSupportLevel(); - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); const { isViewerMode } = useSelector(selectCurrentClusterPermissionsState); const { values, setFieldValue } = useFormikContext(); diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx index 59e28118f8..fff088611d 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx @@ -6,7 +6,8 @@ import { TableSummaryExpandable } from './TableSummaryExpandable'; import { useOperatorSpecs } from '../../../../common/components/operators/operatorSpecs'; export const ReviewOperatorsTable = ({ cluster }: { cluster: Cluster }) => { - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); + const rows = React.useMemo( () => cluster.monitoredOperators diff --git a/libs/ui-lib/lib/ocm/components/clusterDetail/ProgressBarAlerts.tsx b/libs/ui-lib/lib/ocm/components/clusterDetail/ProgressBarAlerts.tsx index 207c765cd6..0878f67b86 100644 --- a/libs/ui-lib/lib/ocm/components/clusterDetail/ProgressBarAlerts.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterDetail/ProgressBarAlerts.tsx @@ -66,7 +66,7 @@ export const HostInstallationWarning = ({ message, }: InstallationProgressWarningProps) => { const { addAlert, clearAlerts } = useAlerts(); - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); return ( <> diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx index e2b33b917c..0a84d0c81b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx @@ -33,13 +33,13 @@ import { useSelector } from 'react-redux'; import { selectIsCurrentClusterSNO } from '../../store/slices/current-cluster/selectors'; import { getNewBundleOperators } from '../clusterConfiguration/operators/utils'; import { bundleSpecs } from '../clusterConfiguration/operators/bundleSpecs'; -import { useOperatorSpecs } from '../../../common/components/operators/operatorSpecs'; +import { OperatorSpec, useOperatorSpecs } from '../../../common/components/operators/operatorSpecs'; import './OperatorsBundle.css'; import { useClusterWizardContext } from './ClusterWizardContext'; const BundleLabel = ({ bundle }: { bundle: Bundle }) => { - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); const bundleSpec = bundleSpecs[bundle.id || '']; return ( @@ -81,13 +81,13 @@ const BundleLabel = ({ bundle }: { bundle: Bundle }) => { const getBundleSupportLevel = ( bundle: Bundle, - opSpecs: ReturnType, + opSpecsByKey: Record, getFeatureSupportLevel: GetFeatureSupportLevel, ): NewSupportLevelBadgeProps['supportLevel'] => { let supportLevel: NewSupportLevelBadgeProps['supportLevel'] = undefined; if (bundle.operators) { for (const op of bundle.operators) { - const operatorSpec = opSpecs[op]; + const operatorSpec = opSpecsByKey[op]; if (operatorSpec) { const opSupportLevel = getFeatureSupportLevel(operatorSpec.featureId); if (opSupportLevel === 'dev-preview') { @@ -114,7 +114,7 @@ const BundleCard = ({ const { values, setFieldValue } = useFormikContext(); const isSNO = useSelector(selectIsCurrentClusterSNO); const { isFeatureSupported, getFeatureSupportLevel } = useNewFeatureSupportLevel(); - const opSpecs = useOperatorSpecs(); + const { byKey: opSpecs } = useOperatorSpecs(); const { uiSettings } = useClusterWizardContext(); const supportLevel = getBundleSupportLevel(bundle, opSpecs, getFeatureSupportLevel); diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx index a647b7133d..86d68955dc 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx @@ -53,22 +53,16 @@ const OperatorsSelect = ({ void fetchSupportedOperators(); }, [addAlert, setSupportedOperators, setIsLoading]); - const opSpecs = useOperatorSpecs(); + const { byCategory, byKey: opSpecs } = useOperatorSpecs(); const operators = React.useMemo(() => { - return supportedOperators - .sort((a, b) => { - const aTitle = opSpecs[a]?.title || a; - const bTitle = opSpecs[b]?.title || b; - return aTitle.localeCompare(bTitle); - }) - .filter((op) => { - if (!isSingleClusterFeatureEnabled) { - return true; - } - return singleClusterOperators.includes(op); - }); - }, [isSingleClusterFeatureEnabled, supportedOperators, opSpecs]); + return supportedOperators.filter((op) => { + if (!isSingleClusterFeatureEnabled) { + return true; + } + return singleClusterOperators.includes(op); + }); + }, [isSingleClusterFeatureEnabled, supportedOperators]); if (isLoading) { return ; @@ -86,22 +80,30 @@ const OperatorsSelect = ({ data-testid="single-operators-section" > - {operators.map((operatorKey) => { - if (!opSpecs[operatorKey]) { + {Object.entries(byCategory).map(([categoryName, specs]) => { + const categoryOperators = specs.filter((spec) => operators.includes(spec.operatorKey)); + if (categoryOperators.length === 0) { return null; } return ( - - - + + + {categoryName} + + {categoryOperators.map((spec) => ( + + + + ))} + ); })} diff --git a/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts b/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts index 411265f74b..277f818cbf 100644 --- a/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts +++ b/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts @@ -17,7 +17,7 @@ import { SupportLevel, } from '@openshift-assisted/types/assisted-installer-service'; import { ExternalPlatformLabels } from '../clusterConfiguration/platformIntegration/constants'; -import { getOperatorSpecs } from '../../../common/components/operators/operatorSpecs'; +import { getOperatorSpecsByKey } from '../../../common/components/operators/operatorSpecs'; export const clusterExistsReason = 'This option is not editable after the draft cluster is created'; @@ -46,7 +46,7 @@ const getOdfDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecs(); + const opSpecs = getOperatorSpecsByKey(); const operatorTitle = opSpecs[OPERATOR_NAME_ODF]?.title || ''; @@ -75,7 +75,7 @@ const getCnvDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecs(); + const opSpecs = getOperatorSpecsByKey(); const operatorTitle = opSpecs[OPERATOR_NAME_CNV]?.title || ''; if (platformType === 'nutanix') { return `${operatorTitle} is not available when Nutanix platform type is selected.`; @@ -105,7 +105,7 @@ const getLvmDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecs(); + const opSpecs = getOperatorSpecsByKey(); const operatorTitle = opSpecs[OPERATOR_NAME_LVM]?.title; if (platformType === 'nutanix') { return `${operatorTitle} is not supported when Nutanix platform type is selected.`; @@ -125,7 +125,7 @@ const getOscDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecs(); + const opSpecs = getOperatorSpecsByKey(); const operatorTitle = opSpecs[OPERATOR_NAME_OSC]?.title || ''; if (!isSupported) { return `${operatorTitle} is not supported in this OpenShift version.`; @@ -299,7 +299,7 @@ const getOpenShiftAIDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecs(); + const opSpecs = getOperatorSpecsByKey(); const operatorTitle = opSpecs[OPERATOR_NAME_OPENSHIFT_AI]?.title || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm) { From bd0d086c4bcae82f92506d1e364cade09963c51b Mon Sep 17 00:00:00 2001 From: Montse Ortega Date: Wed, 18 Jun 2025 15:29:47 +0200 Subject: [PATCH 2/4] Improving the categories --- .../newFeatureSupportLevels/types.ts | 12 +-- .../components/operators/operatorSpecs.tsx | 90 ++++++++++++++++--- .../operators/OperatorCheckbox.tsx | 10 ++- .../clusterWizard/OperatorsBundle.tsx | 15 ++-- .../clusterWizard/OperatorsSelect.tsx | 20 ++--- .../FeatureSupportLevelProvider.tsx | 5 ++ .../featureSupportLevels/featureStateUtils.ts | 73 +++++++++++---- 7 files changed, 167 insertions(+), 58 deletions(-) diff --git a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts index 951b87c2e1..4e96b89ca8 100644 --- a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts +++ b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts @@ -14,18 +14,20 @@ export type ActiveFeatureConfiguration = { hasStaticIpNetworking: boolean; }; +export type GetFeatureSupportLevel = ( + featureId: FeatureId, + supportLevelData?: NewFeatureSupportLevelMap, +) => SupportLevel | undefined; + export type GetFeatureDisabledReason = ( featureId: FeatureId, supportLevelData?: NewFeatureSupportLevelMap, cpuArchitecture?: SupportedCpuArchitecture, platformType?: PlatformType, + getFeatureSupportLevel?: GetFeatureSupportLevel, + useLVMS?: boolean, ) => string | undefined; -export type GetFeatureSupportLevel = ( - featureId: FeatureId, - supportLevelData?: NewFeatureSupportLevelMap, -) => SupportLevel | undefined; - export type NewFeatureSupportLevelData = { getFeatureSupportLevels(): NewFeatureSupportLevelMap; getFeatureSupportLevel: GetFeatureSupportLevel; diff --git a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx index e931315609..112430e2da 100644 --- a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx +++ b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Cluster } from '@openshift-assisted/types/assisted-installer-service'; +import { Cluster, SupportLevel } from '@openshift-assisted/types/assisted-installer-service'; import { FeatureId } from '../../types'; import { OPERATOR_NAME_AMD_GPU, @@ -24,6 +24,7 @@ import { OPERATOR_NAME_FENCE_AGENTS_REMEDIATION, OPERATOR_NAME_NODE_MAINTENANCE, OPERATOR_NAME_KUBE_DESCHEDULER, + singleClusterOperators, } from '../../config/constants'; import { ExternalLink } from '../ui'; import { @@ -53,7 +54,7 @@ import { SERVERLESS_OPERATOR_LINK, } from '../../config'; import { getMajorMinorVersion } from '../../utils'; -import { useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; +import { GetFeatureSupportLevel, useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; import { DESCRIPTION_AMD_GPU, DESCRIPTION_AUTHORINO, @@ -101,9 +102,13 @@ export type OperatorSpec = { notStandalone?: boolean; Requirements?: React.ComponentType<{ cluster: Cluster }>; category: string; + supportLevel?: SupportLevel | undefined; }; -export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: OperatorSpec[] } => { +export const getOperatorSpecs = ( + getFeatureSupportLevel: GetFeatureSupportLevel, + useLVMS?: boolean, +): { [category: string]: OperatorSpec[] } => { return { [categories[Category.STORAGE]]: [ { @@ -119,6 +124,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('LSO'), }, { operatorKey: OPERATOR_NAME_LVM, @@ -135,6 +141,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera <>{DESCRIPTION_LVM} ), category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('LVM'), }, { operatorKey: OPERATOR_NAME_ODF, @@ -152,6 +159,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('ODF'), }, ], [categories[Category.VIRT]]: [ @@ -169,6 +177,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.VIRT], + supportLevel: getFeatureSupportLevel('CNV'), }, { operatorKey: OPERATOR_NAME_MTV, @@ -181,6 +190,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.VIRT], + supportLevel: getFeatureSupportLevel('MTV'), }, ], [categories[Category.AI]]: [ @@ -199,6 +209,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('OPENSHIFT_AI'), }, { operatorKey: OPERATOR_NAME_AMD_GPU, @@ -208,6 +219,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera Requirements: () => <>Requires at least one supported AMD GPU, Description: () => <>{DESCRIPTION_AMD_GPU}, category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('AMD_GPU'), }, { operatorKey: OPERATOR_NAME_NVIDIA_GPU, @@ -222,6 +234,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('NVIDIA_GPU'), }, ], [categories[Category.NETWORK]]: [ @@ -237,6 +250,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.NETWORK], + supportLevel: getFeatureSupportLevel('NMSTATE'), }, { operatorKey: OPERATOR_NAME_SERVICEMESH, @@ -251,6 +265,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.NETWORK], + supportLevel: getFeatureSupportLevel('SERVICEMESH'), }, ], [categories[Category.REMEDIATION]]: [ @@ -267,6 +282,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('FENCE_AGENTS_REMEDIATION'), }, { operatorKey: OPERATOR_NAME_NODE_HEALTHCHECK, @@ -281,6 +297,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('NODE_HEALTHCHECK'), }, { operatorKey: OPERATOR_NAME_SELF_NODE_REMEDIATION, @@ -295,6 +312,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('SELF_NODE_REMEDIATION'), }, ], [categories[Category.OTHER]]: [ @@ -311,6 +329,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('AUTHORINO'), }, { operatorKey: OPERATOR_NAME_NODE_FEATURE_DISCOVERY, @@ -326,6 +345,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('NODE_FEATURE_DISCOVERY'), }, { operatorKey: OPERATOR_NAME_PIPELINES, @@ -340,6 +360,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('PIPELINES'), }, { operatorKey: OPERATOR_NAME_SERVERLESS, @@ -354,6 +375,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('SERVERLESS'), }, { operatorKey: OPERATOR_NAME_KMM, @@ -367,6 +389,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('KMM'), }, { operatorKey: OPERATOR_NAME_MCE, @@ -380,6 +403,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('MCE'), }, { operatorKey: OPERATOR_NAME_OSC, @@ -397,6 +421,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('OSC'), }, { operatorKey: OPERATOR_NAME_KUBE_DESCHEDULER, @@ -411,6 +436,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('KUBE_DESCHEDULER'), }, { @@ -426,17 +452,38 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('NODE_MAINTENANCE'), }, ], }; }; -export const useOperatorSpecs = () => { +export const getOperators = ( + supportedOperators?: string[], + isSingleClusterFeatureEnabled?: boolean, +) => { + if (supportedOperators) { + return supportedOperators.filter((op) => { + if (!isSingleClusterFeatureEnabled) { + return true; + } + return singleClusterOperators.includes(op); + }); + } else return []; +}; + +export const useOperatorSpecs = ( + supportedOperators?: string[], + isSingleFeatureEnabled?: boolean, +) => { const { getFeatureSupportLevel } = useNewFeatureSupportLevel(); const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; // Grouped by category - const byCategory = React.useMemo(() => getOperatorSpecs(useLVMS), [useLVMS]); + const byCategory = React.useMemo( + () => getOperatorSpecs(getFeatureSupportLevel, useLVMS), + [getFeatureSupportLevel, useLVMS], + ); // Flat map operatorKey -> spec const byKey: Record = React.useMemo(() => { @@ -448,17 +495,32 @@ export const useOperatorSpecs = () => { }, {} as Record); }, [byCategory]); - return { byCategory, byKey }; + //Filter supported operators by single feature enabled + const bySingleFeatureEnabled = React.useMemo( + () => getOperators(supportedOperators, isSingleFeatureEnabled), + [isSingleFeatureEnabled, supportedOperators], + ); + + return { byCategory, byKey, bySingleFeatureEnabled }; }; -// Utility to get flat map outside the hook -export const getOperatorSpecsByKey = (useLVMS?: boolean): Record => - Object.values(getOperatorSpecs(useLVMS)).reduce((acc, specs) => { - specs.forEach((spec) => { - acc[spec.operatorKey] = spec; - }); - return acc; - }, {} as Record); +export const getOperatorSpecByKey = ( + operatorKey: string, + getFeatureSupportLevel: GetFeatureSupportLevel, + useLVMS?: boolean, +): OperatorSpec | undefined => { + const allSpecs = getOperatorSpecs(getFeatureSupportLevel, useLVMS); + + for (const specs of Object.values(allSpecs)) { + for (const spec of specs) { + if (spec.operatorKey === operatorKey) { + return spec; + } + } + } + + return undefined; +}; enum Category { STORAGE, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx index 4f0e17a346..1a5ae6f0e6 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx @@ -150,6 +150,7 @@ const OperatorCheckbox = ({ ) { parentOperatorName = opSpecs[parentOperator.operatorName]?.title || parentOperator.operatorName; } + const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; const disabledReason = isInBundle ? 'This operator is part of a bundle and cannot be deselected.' @@ -157,7 +158,14 @@ const OperatorCheckbox = ({ ? 'This operator cannot be installed as a standalone' : parentOperatorName ? `This operator is a dependency of ${parentOperatorName}` - : getFeatureDisabledReason(featureId); + : getFeatureDisabledReason( + featureId, + undefined, + undefined, + undefined, + getFeatureSupportLevel, + useLVMS, + ); return ( diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx index 0a84d0c81b..13b88f7e85 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx @@ -24,10 +24,7 @@ import NewFeatureSupportLevelBadge, { } from '../../../common/components/newFeatureSupportLevels/NewFeatureSupportLevelBadge'; import { ExternalLink, OperatorsValues, PopoverIcon, singleClusterBundles } from '../../../common'; import { useFormikContext } from 'formik'; -import { - GetFeatureSupportLevel, - useNewFeatureSupportLevel, -} from '../../../common/components/newFeatureSupportLevels'; +import { useNewFeatureSupportLevel } from '../../../common/components/newFeatureSupportLevels'; import { useFeature } from '../../hooks/use-feature'; import { useSelector } from 'react-redux'; import { selectIsCurrentClusterSNO } from '../../store/slices/current-cluster/selectors'; @@ -82,18 +79,16 @@ const BundleLabel = ({ bundle }: { bundle: Bundle }) => { const getBundleSupportLevel = ( bundle: Bundle, opSpecsByKey: Record, - getFeatureSupportLevel: GetFeatureSupportLevel, ): NewSupportLevelBadgeProps['supportLevel'] => { let supportLevel: NewSupportLevelBadgeProps['supportLevel'] = undefined; if (bundle.operators) { for (const op of bundle.operators) { const operatorSpec = opSpecsByKey[op]; if (operatorSpec) { - const opSupportLevel = getFeatureSupportLevel(operatorSpec.featureId); - if (opSupportLevel === 'dev-preview') { + if (operatorSpec.supportLevel === 'dev-preview') { supportLevel = 'dev-preview'; break; - } else if (opSupportLevel === 'tech-preview') { + } else if (operatorSpec.supportLevel === 'tech-preview') { supportLevel = 'tech-preview'; } } @@ -113,11 +108,11 @@ const BundleCard = ({ }) => { const { values, setFieldValue } = useFormikContext(); const isSNO = useSelector(selectIsCurrentClusterSNO); - const { isFeatureSupported, getFeatureSupportLevel } = useNewFeatureSupportLevel(); + const { isFeatureSupported } = useNewFeatureSupportLevel(); const { byKey: opSpecs } = useOperatorSpecs(); const { uiSettings } = useClusterWizardContext(); - const supportLevel = getBundleSupportLevel(bundle, opSpecs, getFeatureSupportLevel); + const supportLevel = getBundleSupportLevel(bundle, opSpecs); const hasUnsupportedOperators = !!bundle.operators?.some((op) => { const operatorSpec = opSpecs[op]; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx index 86d68955dc..b9b908d93b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx @@ -11,7 +11,6 @@ import { handleApiError, LoadingState, OperatorsValues, - singleClusterOperators, useAlerts, useStateSafely, } from '../../../common'; @@ -53,28 +52,23 @@ const OperatorsSelect = ({ void fetchSupportedOperators(); }, [addAlert, setSupportedOperators, setIsLoading]); - const { byCategory, byKey: opSpecs } = useOperatorSpecs(); - - const operators = React.useMemo(() => { - return supportedOperators.filter((op) => { - if (!isSingleClusterFeatureEnabled) { - return true; - } - return singleClusterOperators.includes(op); - }); - }, [isSingleClusterFeatureEnabled, supportedOperators]); + const { + byCategory, + byKey: opSpecs, + bySingleFeatureEnabled: operators, + } = useOperatorSpecs(supportedOperators, isSingleClusterFeatureEnabled); if (isLoading) { return ; } const selectedOperators = values.selectedOperators.filter( - (opKey) => operators.includes(opKey) && !!opSpecs[opKey], + (opKey) => operators?.includes(opKey) && !!opSpecs[opKey], ); return ( setIsExpanded(!isExpanded)} isExpanded={isExpanded} data-testid="single-operators-section" diff --git a/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx b/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx index 332dccc04c..7513bf7626 100644 --- a/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx +++ b/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx @@ -10,6 +10,7 @@ import { usePullSecret } from '../../hooks'; import { getNewFeatureDisabledReason, isFeatureSupportedAndAvailable } from './featureStateUtils'; import useInfraEnv from '../../hooks/useInfraEnv'; import { + GetFeatureSupportLevel, NewFeatureSupportLevelContextProvider, NewFeatureSupportLevelData, NewFeatureSupportLevelMap, @@ -119,6 +120,8 @@ export const NewFeatureSupportLevelProvider: React.FC { const isSupported = isFeatureSupportedCallback(featureId, supportLevelDataNew); return getNewFeatureDisabledReason( @@ -128,6 +131,8 @@ export const NewFeatureSupportLevelProvider: React.FC { if (!cluster) { return undefined; } - const opSpecs = getOperatorSpecsByKey(); + const opSpec = getOperatorSpecByKey(OPERATOR_NAME_ODF, getFeatureSupportLevel, useLVMS); - const operatorTitle = opSpecs[OPERATOR_NAME_ODF]?.title || ''; + const operatorTitle = opSpec?.title || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm && isSNO(cluster)) { @@ -69,14 +72,16 @@ const getOdfDisabledReason = ( const getCnvDisabledReason = ( activeFeatureConfiguration: ActiveFeatureConfiguration, isSupported: boolean, + getFeatureSupportLevel: GetFeatureSupportLevel, platformType?: PlatformType, + useLVMS?: boolean, ) => { if (!activeFeatureConfiguration) { return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_CNV]?.title || ''; + const opSpec = getOperatorSpecByKey(OPERATOR_NAME_CNV, getFeatureSupportLevel, useLVMS); + const operatorTitle = opSpec?.title || ''; if (platformType === 'nutanix') { return `${operatorTitle} is not available when Nutanix platform type is selected.`; } @@ -99,14 +104,16 @@ const getCnvDisabledReason = ( const getLvmDisabledReason = ( activeFeatureConfiguration: ActiveFeatureConfiguration, isSupported: boolean, + getFeatureSupportLevel: GetFeatureSupportLevel, platformType?: PlatformType, + useLVMS?: boolean, ) => { if (!activeFeatureConfiguration) { return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_LVM]?.title; + const opSpec = getOperatorSpecByKey(OPERATOR_NAME_LVM, getFeatureSupportLevel, useLVMS); + const operatorTitle = opSpec?.title || ''; if (platformType === 'nutanix') { return `${operatorTitle} is not supported when Nutanix platform type is selected.`; } @@ -118,15 +125,16 @@ const getLvmDisabledReason = ( const getOscDisabledReason = ( cluster: Cluster | undefined, - activeFeatureConfiguration: ActiveFeatureConfiguration | undefined, isSupported: boolean, + getFeatureSupportLevel: GetFeatureSupportLevel, + useLVMS?: boolean, ) => { if (!cluster) { return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_OSC]?.title || ''; + const opSpec = getOperatorSpecByKey(OPERATOR_NAME_OSC, getFeatureSupportLevel, useLVMS); + const operatorTitle = opSpec?.title || ''; if (!isSupported) { return `${operatorTitle} is not supported in this OpenShift version.`; } @@ -160,6 +168,8 @@ export const getNewFeatureDisabledReason = ( isSupported: boolean, cpuArchitecture?: SupportedCpuArchitecture, platformType?: PlatformType, + getFeatureSupportLevel?: GetFeatureSupportLevel, + useLVMS?: boolean, ): string | undefined => { switch (featureId) { case 'SNO': { @@ -169,27 +179,58 @@ export const getNewFeatureDisabledReason = ( return getArmDisabledReason(cluster); } case 'CNV': { + if (!getFeatureSupportLevel) { + throw new Error('getFeatureSupportLevel is required for getCnvDisabledReason'); + } return getCnvDisabledReason( activeFeatureConfiguration, isSupported, + getFeatureSupportLevel, platformType ?? cluster?.platform?.type, + useLVMS, ); } case 'LVM': { + if (!getFeatureSupportLevel) { + throw new Error('getFeatureSupportLevel is required for getLvmDisabledReason'); + } return getLvmDisabledReason( activeFeatureConfiguration, isSupported, + getFeatureSupportLevel, platformType ?? cluster?.platform?.type, + useLVMS, ); } case 'ODF': { - return getOdfDisabledReason(cluster, activeFeatureConfiguration, isSupported); + if (!getFeatureSupportLevel) { + throw new Error('getFeatureSupportLevel is required for getOdfDisabledReason'); + } + return getOdfDisabledReason( + cluster, + activeFeatureConfiguration, + isSupported, + getFeatureSupportLevel, + useLVMS, + ); } case 'OPENSHIFT_AI': { - return getOpenShiftAIDisabledReason(cluster, activeFeatureConfiguration, isSupported); + if (!getFeatureSupportLevel) { + throw new Error('getFeatureSupportLevel is required for getOpenShiftAIDisabledReason'); + } + return getOpenShiftAIDisabledReason( + cluster, + activeFeatureConfiguration, + isSupported, + getFeatureSupportLevel, + useLVMS, + ); } case 'OSC': { - return getOscDisabledReason(cluster, activeFeatureConfiguration, isSupported); + if (!getFeatureSupportLevel) { + throw new Error('getFeatureSupportLevel is required for getOscDisabledReason'); + } + return getOscDisabledReason(cluster, isSupported, getFeatureSupportLevel, useLVMS); } case 'NETWORK_TYPE_SELECTION': { return getNetworkTypeSelectionDisabledReason(cluster); @@ -294,13 +335,15 @@ const getOpenShiftAIDisabledReason = ( cluster: Cluster | undefined, activeFeatureConfiguration: ActiveFeatureConfiguration | undefined, isSupported: boolean, + getFeatureSupportLevel: GetFeatureSupportLevel, + useLVMS?: boolean, ) => { if (!cluster) { return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_OPENSHIFT_AI]?.title || ''; + const opSpec = getOperatorSpecByKey(OPERATOR_NAME_OPENSHIFT_AI, getFeatureSupportLevel, useLVMS); + const operatorTitle = opSpec?.title || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm) { return `${operatorTitle} is not available when ARM CPU architecture is selected.`; From 768b433a93b238420fcf14532d5718485fb87f8f Mon Sep 17 00:00:00 2001 From: Montse Ortega Date: Thu, 19 Jun 2025 07:35:50 +0200 Subject: [PATCH 3/4] Revert "Improving the categories" This reverts commit bd0d086c4bcae82f92506d1e364cade09963c51b. --- .../newFeatureSupportLevels/types.ts | 12 ++- .../components/operators/operatorSpecs.tsx | 90 +++---------------- .../operators/OperatorCheckbox.tsx | 10 +-- .../clusterWizard/OperatorsBundle.tsx | 15 ++-- .../clusterWizard/OperatorsSelect.tsx | 20 +++-- .../FeatureSupportLevelProvider.tsx | 5 -- .../featureSupportLevels/featureStateUtils.ts | 73 ++++----------- 7 files changed, 58 insertions(+), 167 deletions(-) diff --git a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts index 4e96b89ca8..951b87c2e1 100644 --- a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts +++ b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts @@ -14,20 +14,18 @@ export type ActiveFeatureConfiguration = { hasStaticIpNetworking: boolean; }; -export type GetFeatureSupportLevel = ( - featureId: FeatureId, - supportLevelData?: NewFeatureSupportLevelMap, -) => SupportLevel | undefined; - export type GetFeatureDisabledReason = ( featureId: FeatureId, supportLevelData?: NewFeatureSupportLevelMap, cpuArchitecture?: SupportedCpuArchitecture, platformType?: PlatformType, - getFeatureSupportLevel?: GetFeatureSupportLevel, - useLVMS?: boolean, ) => string | undefined; +export type GetFeatureSupportLevel = ( + featureId: FeatureId, + supportLevelData?: NewFeatureSupportLevelMap, +) => SupportLevel | undefined; + export type NewFeatureSupportLevelData = { getFeatureSupportLevels(): NewFeatureSupportLevelMap; getFeatureSupportLevel: GetFeatureSupportLevel; diff --git a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx index 112430e2da..e931315609 100644 --- a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx +++ b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Cluster, SupportLevel } from '@openshift-assisted/types/assisted-installer-service'; +import { Cluster } from '@openshift-assisted/types/assisted-installer-service'; import { FeatureId } from '../../types'; import { OPERATOR_NAME_AMD_GPU, @@ -24,7 +24,6 @@ import { OPERATOR_NAME_FENCE_AGENTS_REMEDIATION, OPERATOR_NAME_NODE_MAINTENANCE, OPERATOR_NAME_KUBE_DESCHEDULER, - singleClusterOperators, } from '../../config/constants'; import { ExternalLink } from '../ui'; import { @@ -54,7 +53,7 @@ import { SERVERLESS_OPERATOR_LINK, } from '../../config'; import { getMajorMinorVersion } from '../../utils'; -import { GetFeatureSupportLevel, useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; +import { useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; import { DESCRIPTION_AMD_GPU, DESCRIPTION_AUTHORINO, @@ -102,13 +101,9 @@ export type OperatorSpec = { notStandalone?: boolean; Requirements?: React.ComponentType<{ cluster: Cluster }>; category: string; - supportLevel?: SupportLevel | undefined; }; -export const getOperatorSpecs = ( - getFeatureSupportLevel: GetFeatureSupportLevel, - useLVMS?: boolean, -): { [category: string]: OperatorSpec[] } => { +export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: OperatorSpec[] } => { return { [categories[Category.STORAGE]]: [ { @@ -124,7 +119,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.STORAGE], - supportLevel: getFeatureSupportLevel('LSO'), }, { operatorKey: OPERATOR_NAME_LVM, @@ -141,7 +135,6 @@ export const getOperatorSpecs = ( <>{DESCRIPTION_LVM} ), category: categories[Category.STORAGE], - supportLevel: getFeatureSupportLevel('LVM'), }, { operatorKey: OPERATOR_NAME_ODF, @@ -159,7 +152,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.STORAGE], - supportLevel: getFeatureSupportLevel('ODF'), }, ], [categories[Category.VIRT]]: [ @@ -177,7 +169,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.VIRT], - supportLevel: getFeatureSupportLevel('CNV'), }, { operatorKey: OPERATOR_NAME_MTV, @@ -190,7 +181,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.VIRT], - supportLevel: getFeatureSupportLevel('MTV'), }, ], [categories[Category.AI]]: [ @@ -209,7 +199,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.AI], - supportLevel: getFeatureSupportLevel('OPENSHIFT_AI'), }, { operatorKey: OPERATOR_NAME_AMD_GPU, @@ -219,7 +208,6 @@ export const getOperatorSpecs = ( Requirements: () => <>Requires at least one supported AMD GPU, Description: () => <>{DESCRIPTION_AMD_GPU}, category: categories[Category.AI], - supportLevel: getFeatureSupportLevel('AMD_GPU'), }, { operatorKey: OPERATOR_NAME_NVIDIA_GPU, @@ -234,7 +222,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.AI], - supportLevel: getFeatureSupportLevel('NVIDIA_GPU'), }, ], [categories[Category.NETWORK]]: [ @@ -250,7 +237,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.NETWORK], - supportLevel: getFeatureSupportLevel('NMSTATE'), }, { operatorKey: OPERATOR_NAME_SERVICEMESH, @@ -265,7 +251,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.NETWORK], - supportLevel: getFeatureSupportLevel('SERVICEMESH'), }, ], [categories[Category.REMEDIATION]]: [ @@ -282,7 +267,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.REMEDIATION], - supportLevel: getFeatureSupportLevel('FENCE_AGENTS_REMEDIATION'), }, { operatorKey: OPERATOR_NAME_NODE_HEALTHCHECK, @@ -297,7 +281,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.REMEDIATION], - supportLevel: getFeatureSupportLevel('NODE_HEALTHCHECK'), }, { operatorKey: OPERATOR_NAME_SELF_NODE_REMEDIATION, @@ -312,7 +295,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.REMEDIATION], - supportLevel: getFeatureSupportLevel('SELF_NODE_REMEDIATION'), }, ], [categories[Category.OTHER]]: [ @@ -329,7 +311,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('AUTHORINO'), }, { operatorKey: OPERATOR_NAME_NODE_FEATURE_DISCOVERY, @@ -345,7 +326,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('NODE_FEATURE_DISCOVERY'), }, { operatorKey: OPERATOR_NAME_PIPELINES, @@ -360,7 +340,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('PIPELINES'), }, { operatorKey: OPERATOR_NAME_SERVERLESS, @@ -375,7 +354,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('SERVERLESS'), }, { operatorKey: OPERATOR_NAME_KMM, @@ -389,7 +367,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('KMM'), }, { operatorKey: OPERATOR_NAME_MCE, @@ -403,7 +380,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('MCE'), }, { operatorKey: OPERATOR_NAME_OSC, @@ -421,7 +397,6 @@ export const getOperatorSpecs = ( ), category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('OSC'), }, { operatorKey: OPERATOR_NAME_KUBE_DESCHEDULER, @@ -436,7 +411,6 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('KUBE_DESCHEDULER'), }, { @@ -452,38 +426,17 @@ export const getOperatorSpecs = ( ), notStandalone: true, category: categories[Category.OTHER], - supportLevel: getFeatureSupportLevel('NODE_MAINTENANCE'), }, ], }; }; -export const getOperators = ( - supportedOperators?: string[], - isSingleClusterFeatureEnabled?: boolean, -) => { - if (supportedOperators) { - return supportedOperators.filter((op) => { - if (!isSingleClusterFeatureEnabled) { - return true; - } - return singleClusterOperators.includes(op); - }); - } else return []; -}; - -export const useOperatorSpecs = ( - supportedOperators?: string[], - isSingleFeatureEnabled?: boolean, -) => { +export const useOperatorSpecs = () => { const { getFeatureSupportLevel } = useNewFeatureSupportLevel(); const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; // Grouped by category - const byCategory = React.useMemo( - () => getOperatorSpecs(getFeatureSupportLevel, useLVMS), - [getFeatureSupportLevel, useLVMS], - ); + const byCategory = React.useMemo(() => getOperatorSpecs(useLVMS), [useLVMS]); // Flat map operatorKey -> spec const byKey: Record = React.useMemo(() => { @@ -495,32 +448,17 @@ export const useOperatorSpecs = ( }, {} as Record); }, [byCategory]); - //Filter supported operators by single feature enabled - const bySingleFeatureEnabled = React.useMemo( - () => getOperators(supportedOperators, isSingleFeatureEnabled), - [isSingleFeatureEnabled, supportedOperators], - ); - - return { byCategory, byKey, bySingleFeatureEnabled }; + return { byCategory, byKey }; }; -export const getOperatorSpecByKey = ( - operatorKey: string, - getFeatureSupportLevel: GetFeatureSupportLevel, - useLVMS?: boolean, -): OperatorSpec | undefined => { - const allSpecs = getOperatorSpecs(getFeatureSupportLevel, useLVMS); - - for (const specs of Object.values(allSpecs)) { - for (const spec of specs) { - if (spec.operatorKey === operatorKey) { - return spec; - } - } - } - - return undefined; -}; +// Utility to get flat map outside the hook +export const getOperatorSpecsByKey = (useLVMS?: boolean): Record => + Object.values(getOperatorSpecs(useLVMS)).reduce((acc, specs) => { + specs.forEach((spec) => { + acc[spec.operatorKey] = spec; + }); + return acc; + }, {} as Record); enum Category { STORAGE, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx index 1a5ae6f0e6..4f0e17a346 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx @@ -150,7 +150,6 @@ const OperatorCheckbox = ({ ) { parentOperatorName = opSpecs[parentOperator.operatorName]?.title || parentOperator.operatorName; } - const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; const disabledReason = isInBundle ? 'This operator is part of a bundle and cannot be deselected.' @@ -158,14 +157,7 @@ const OperatorCheckbox = ({ ? 'This operator cannot be installed as a standalone' : parentOperatorName ? `This operator is a dependency of ${parentOperatorName}` - : getFeatureDisabledReason( - featureId, - undefined, - undefined, - undefined, - getFeatureSupportLevel, - useLVMS, - ); + : getFeatureDisabledReason(featureId); return ( diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx index 13b88f7e85..0a84d0c81b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx @@ -24,7 +24,10 @@ import NewFeatureSupportLevelBadge, { } from '../../../common/components/newFeatureSupportLevels/NewFeatureSupportLevelBadge'; import { ExternalLink, OperatorsValues, PopoverIcon, singleClusterBundles } from '../../../common'; import { useFormikContext } from 'formik'; -import { useNewFeatureSupportLevel } from '../../../common/components/newFeatureSupportLevels'; +import { + GetFeatureSupportLevel, + useNewFeatureSupportLevel, +} from '../../../common/components/newFeatureSupportLevels'; import { useFeature } from '../../hooks/use-feature'; import { useSelector } from 'react-redux'; import { selectIsCurrentClusterSNO } from '../../store/slices/current-cluster/selectors'; @@ -79,16 +82,18 @@ const BundleLabel = ({ bundle }: { bundle: Bundle }) => { const getBundleSupportLevel = ( bundle: Bundle, opSpecsByKey: Record, + getFeatureSupportLevel: GetFeatureSupportLevel, ): NewSupportLevelBadgeProps['supportLevel'] => { let supportLevel: NewSupportLevelBadgeProps['supportLevel'] = undefined; if (bundle.operators) { for (const op of bundle.operators) { const operatorSpec = opSpecsByKey[op]; if (operatorSpec) { - if (operatorSpec.supportLevel === 'dev-preview') { + const opSupportLevel = getFeatureSupportLevel(operatorSpec.featureId); + if (opSupportLevel === 'dev-preview') { supportLevel = 'dev-preview'; break; - } else if (operatorSpec.supportLevel === 'tech-preview') { + } else if (opSupportLevel === 'tech-preview') { supportLevel = 'tech-preview'; } } @@ -108,11 +113,11 @@ const BundleCard = ({ }) => { const { values, setFieldValue } = useFormikContext(); const isSNO = useSelector(selectIsCurrentClusterSNO); - const { isFeatureSupported } = useNewFeatureSupportLevel(); + const { isFeatureSupported, getFeatureSupportLevel } = useNewFeatureSupportLevel(); const { byKey: opSpecs } = useOperatorSpecs(); const { uiSettings } = useClusterWizardContext(); - const supportLevel = getBundleSupportLevel(bundle, opSpecs); + const supportLevel = getBundleSupportLevel(bundle, opSpecs, getFeatureSupportLevel); const hasUnsupportedOperators = !!bundle.operators?.some((op) => { const operatorSpec = opSpecs[op]; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx index b9b908d93b..86d68955dc 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx @@ -11,6 +11,7 @@ import { handleApiError, LoadingState, OperatorsValues, + singleClusterOperators, useAlerts, useStateSafely, } from '../../../common'; @@ -52,23 +53,28 @@ const OperatorsSelect = ({ void fetchSupportedOperators(); }, [addAlert, setSupportedOperators, setIsLoading]); - const { - byCategory, - byKey: opSpecs, - bySingleFeatureEnabled: operators, - } = useOperatorSpecs(supportedOperators, isSingleClusterFeatureEnabled); + const { byCategory, byKey: opSpecs } = useOperatorSpecs(); + + const operators = React.useMemo(() => { + return supportedOperators.filter((op) => { + if (!isSingleClusterFeatureEnabled) { + return true; + } + return singleClusterOperators.includes(op); + }); + }, [isSingleClusterFeatureEnabled, supportedOperators]); if (isLoading) { return ; } const selectedOperators = values.selectedOperators.filter( - (opKey) => operators?.includes(opKey) && !!opSpecs[opKey], + (opKey) => operators.includes(opKey) && !!opSpecs[opKey], ); return ( setIsExpanded(!isExpanded)} isExpanded={isExpanded} data-testid="single-operators-section" diff --git a/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx b/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx index 7513bf7626..332dccc04c 100644 --- a/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx +++ b/libs/ui-lib/lib/ocm/components/featureSupportLevels/FeatureSupportLevelProvider.tsx @@ -10,7 +10,6 @@ import { usePullSecret } from '../../hooks'; import { getNewFeatureDisabledReason, isFeatureSupportedAndAvailable } from './featureStateUtils'; import useInfraEnv from '../../hooks/useInfraEnv'; import { - GetFeatureSupportLevel, NewFeatureSupportLevelContextProvider, NewFeatureSupportLevelData, NewFeatureSupportLevelMap, @@ -120,8 +119,6 @@ export const NewFeatureSupportLevelProvider: React.FC { const isSupported = isFeatureSupportedCallback(featureId, supportLevelDataNew); return getNewFeatureDisabledReason( @@ -131,8 +128,6 @@ export const NewFeatureSupportLevelProvider: React.FC { if (!cluster) { return undefined; } - const opSpec = getOperatorSpecByKey(OPERATOR_NAME_ODF, getFeatureSupportLevel, useLVMS); + const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpec?.title || ''; + const operatorTitle = opSpecs[OPERATOR_NAME_ODF]?.title || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm && isSNO(cluster)) { @@ -72,16 +69,14 @@ const getOdfDisabledReason = ( const getCnvDisabledReason = ( activeFeatureConfiguration: ActiveFeatureConfiguration, isSupported: boolean, - getFeatureSupportLevel: GetFeatureSupportLevel, platformType?: PlatformType, - useLVMS?: boolean, ) => { if (!activeFeatureConfiguration) { return undefined; } - const opSpec = getOperatorSpecByKey(OPERATOR_NAME_CNV, getFeatureSupportLevel, useLVMS); - const operatorTitle = opSpec?.title || ''; + const opSpecs = getOperatorSpecsByKey(); + const operatorTitle = opSpecs[OPERATOR_NAME_CNV]?.title || ''; if (platformType === 'nutanix') { return `${operatorTitle} is not available when Nutanix platform type is selected.`; } @@ -104,16 +99,14 @@ const getCnvDisabledReason = ( const getLvmDisabledReason = ( activeFeatureConfiguration: ActiveFeatureConfiguration, isSupported: boolean, - getFeatureSupportLevel: GetFeatureSupportLevel, platformType?: PlatformType, - useLVMS?: boolean, ) => { if (!activeFeatureConfiguration) { return undefined; } - const opSpec = getOperatorSpecByKey(OPERATOR_NAME_LVM, getFeatureSupportLevel, useLVMS); - const operatorTitle = opSpec?.title || ''; + const opSpecs = getOperatorSpecsByKey(); + const operatorTitle = opSpecs[OPERATOR_NAME_LVM]?.title; if (platformType === 'nutanix') { return `${operatorTitle} is not supported when Nutanix platform type is selected.`; } @@ -125,16 +118,15 @@ const getLvmDisabledReason = ( const getOscDisabledReason = ( cluster: Cluster | undefined, + activeFeatureConfiguration: ActiveFeatureConfiguration | undefined, isSupported: boolean, - getFeatureSupportLevel: GetFeatureSupportLevel, - useLVMS?: boolean, ) => { if (!cluster) { return undefined; } - const opSpec = getOperatorSpecByKey(OPERATOR_NAME_OSC, getFeatureSupportLevel, useLVMS); - const operatorTitle = opSpec?.title || ''; + const opSpecs = getOperatorSpecsByKey(); + const operatorTitle = opSpecs[OPERATOR_NAME_OSC]?.title || ''; if (!isSupported) { return `${operatorTitle} is not supported in this OpenShift version.`; } @@ -168,8 +160,6 @@ export const getNewFeatureDisabledReason = ( isSupported: boolean, cpuArchitecture?: SupportedCpuArchitecture, platformType?: PlatformType, - getFeatureSupportLevel?: GetFeatureSupportLevel, - useLVMS?: boolean, ): string | undefined => { switch (featureId) { case 'SNO': { @@ -179,58 +169,27 @@ export const getNewFeatureDisabledReason = ( return getArmDisabledReason(cluster); } case 'CNV': { - if (!getFeatureSupportLevel) { - throw new Error('getFeatureSupportLevel is required for getCnvDisabledReason'); - } return getCnvDisabledReason( activeFeatureConfiguration, isSupported, - getFeatureSupportLevel, platformType ?? cluster?.platform?.type, - useLVMS, ); } case 'LVM': { - if (!getFeatureSupportLevel) { - throw new Error('getFeatureSupportLevel is required for getLvmDisabledReason'); - } return getLvmDisabledReason( activeFeatureConfiguration, isSupported, - getFeatureSupportLevel, platformType ?? cluster?.platform?.type, - useLVMS, ); } case 'ODF': { - if (!getFeatureSupportLevel) { - throw new Error('getFeatureSupportLevel is required for getOdfDisabledReason'); - } - return getOdfDisabledReason( - cluster, - activeFeatureConfiguration, - isSupported, - getFeatureSupportLevel, - useLVMS, - ); + return getOdfDisabledReason(cluster, activeFeatureConfiguration, isSupported); } case 'OPENSHIFT_AI': { - if (!getFeatureSupportLevel) { - throw new Error('getFeatureSupportLevel is required for getOpenShiftAIDisabledReason'); - } - return getOpenShiftAIDisabledReason( - cluster, - activeFeatureConfiguration, - isSupported, - getFeatureSupportLevel, - useLVMS, - ); + return getOpenShiftAIDisabledReason(cluster, activeFeatureConfiguration, isSupported); } case 'OSC': { - if (!getFeatureSupportLevel) { - throw new Error('getFeatureSupportLevel is required for getOscDisabledReason'); - } - return getOscDisabledReason(cluster, isSupported, getFeatureSupportLevel, useLVMS); + return getOscDisabledReason(cluster, activeFeatureConfiguration, isSupported); } case 'NETWORK_TYPE_SELECTION': { return getNetworkTypeSelectionDisabledReason(cluster); @@ -335,15 +294,13 @@ const getOpenShiftAIDisabledReason = ( cluster: Cluster | undefined, activeFeatureConfiguration: ActiveFeatureConfiguration | undefined, isSupported: boolean, - getFeatureSupportLevel: GetFeatureSupportLevel, - useLVMS?: boolean, ) => { if (!cluster) { return undefined; } - const opSpec = getOperatorSpecByKey(OPERATOR_NAME_OPENSHIFT_AI, getFeatureSupportLevel, useLVMS); - const operatorTitle = opSpec?.title || ''; + const opSpecs = getOperatorSpecsByKey(); + const operatorTitle = opSpecs[OPERATOR_NAME_OPENSHIFT_AI]?.title || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm) { return `${operatorTitle} is not available when ARM CPU architecture is selected.`; From b90c1426e8ee04d95c1f05e4aabbf116d9f219ed Mon Sep 17 00:00:00 2001 From: Montse Ortega Date: Thu, 19 Jun 2025 07:56:18 +0200 Subject: [PATCH 4/4] Improving categories for operators --- .../newFeatureSupportLevels/types.ts | 1 + .../components/operators/operatorSpecs.tsx | 57 +++++++++++++++---- .../clusterWizard/OperatorsBundle.tsx | 18 ++---- .../featureSupportLevels/featureStateUtils.ts | 25 +++----- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts index 951b87c2e1..a4094db7dd 100644 --- a/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts +++ b/libs/ui-lib/lib/common/components/newFeatureSupportLevels/types.ts @@ -19,6 +19,7 @@ export type GetFeatureDisabledReason = ( supportLevelData?: NewFeatureSupportLevelMap, cpuArchitecture?: SupportedCpuArchitecture, platformType?: PlatformType, + title?: string, ) => string | undefined; export type GetFeatureSupportLevel = ( diff --git a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx index e931315609..3ce7e0eea1 100644 --- a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx +++ b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Cluster } from '@openshift-assisted/types/assisted-installer-service'; +import { Cluster, SupportLevel } from '@openshift-assisted/types/assisted-installer-service'; import { FeatureId } from '../../types'; import { OPERATOR_NAME_AMD_GPU, @@ -53,7 +53,7 @@ import { SERVERLESS_OPERATOR_LINK, } from '../../config'; import { getMajorMinorVersion } from '../../utils'; -import { useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; +import { GetFeatureSupportLevel, useNewFeatureSupportLevel } from '../newFeatureSupportLevels'; import { DESCRIPTION_AMD_GPU, DESCRIPTION_AUTHORINO, @@ -101,9 +101,13 @@ export type OperatorSpec = { notStandalone?: boolean; Requirements?: React.ComponentType<{ cluster: Cluster }>; category: string; + supportLevel?: SupportLevel | undefined; }; -export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: OperatorSpec[] } => { +export const getOperatorSpecs = ( + getFeatureSupportLevel: GetFeatureSupportLevel, + useLVMS?: boolean, +): { [category: string]: OperatorSpec[] } => { return { [categories[Category.STORAGE]]: [ { @@ -119,6 +123,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('LSO'), }, { operatorKey: OPERATOR_NAME_LVM, @@ -135,6 +140,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera <>{DESCRIPTION_LVM} ), category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('LVM'), }, { operatorKey: OPERATOR_NAME_ODF, @@ -152,6 +158,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.STORAGE], + supportLevel: getFeatureSupportLevel('ODF'), }, ], [categories[Category.VIRT]]: [ @@ -169,6 +176,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.VIRT], + supportLevel: getFeatureSupportLevel('CNV'), }, { operatorKey: OPERATOR_NAME_MTV, @@ -181,6 +189,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.VIRT], + supportLevel: getFeatureSupportLevel('MTV'), }, ], [categories[Category.AI]]: [ @@ -199,6 +208,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('OPENSHIFT_AI'), }, { operatorKey: OPERATOR_NAME_AMD_GPU, @@ -208,6 +218,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera Requirements: () => <>Requires at least one supported AMD GPU, Description: () => <>{DESCRIPTION_AMD_GPU}, category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('AMD_GPU'), }, { operatorKey: OPERATOR_NAME_NVIDIA_GPU, @@ -222,6 +233,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.AI], + supportLevel: getFeatureSupportLevel('NVIDIA_GPU'), }, ], [categories[Category.NETWORK]]: [ @@ -237,6 +249,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.NETWORK], + supportLevel: getFeatureSupportLevel('NMSTATE'), }, { operatorKey: OPERATOR_NAME_SERVICEMESH, @@ -251,6 +264,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.NETWORK], + supportLevel: getFeatureSupportLevel('SERVICEMESH'), }, ], [categories[Category.REMEDIATION]]: [ @@ -267,6 +281,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('FENCE_AGENTS_REMEDIATION'), }, { operatorKey: OPERATOR_NAME_NODE_HEALTHCHECK, @@ -281,6 +296,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('NODE_HEALTHCHECK'), }, { operatorKey: OPERATOR_NAME_SELF_NODE_REMEDIATION, @@ -295,6 +311,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.REMEDIATION], + supportLevel: getFeatureSupportLevel('SELF_NODE_REMEDIATION'), }, ], [categories[Category.OTHER]]: [ @@ -311,6 +328,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('AUTHORINO'), }, { operatorKey: OPERATOR_NAME_NODE_FEATURE_DISCOVERY, @@ -326,6 +344,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('NODE_FEATURE_DISCOVERY'), }, { operatorKey: OPERATOR_NAME_PIPELINES, @@ -340,6 +359,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('PIPELINES'), }, { operatorKey: OPERATOR_NAME_SERVERLESS, @@ -354,6 +374,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('SERVERLESS'), }, { operatorKey: OPERATOR_NAME_KMM, @@ -367,6 +388,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('KMM'), }, { operatorKey: OPERATOR_NAME_MCE, @@ -380,6 +402,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('MCE'), }, { operatorKey: OPERATOR_NAME_OSC, @@ -397,6 +420,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('OSC'), }, { operatorKey: OPERATOR_NAME_KUBE_DESCHEDULER, @@ -411,6 +435,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('KUBE_DESCHEDULER'), }, { @@ -426,6 +451,7 @@ export const getOperatorSpecs = (useLVMS?: boolean): { [category: string]: Opera ), notStandalone: true, category: categories[Category.OTHER], + supportLevel: getFeatureSupportLevel('NODE_MAINTENANCE'), }, ], }; @@ -436,7 +462,10 @@ export const useOperatorSpecs = () => { const useLVMS = getFeatureSupportLevel('LVM') === 'supported'; // Grouped by category - const byCategory = React.useMemo(() => getOperatorSpecs(useLVMS), [useLVMS]); + const byCategory = React.useMemo( + () => getOperatorSpecs(getFeatureSupportLevel, useLVMS), + [getFeatureSupportLevel, useLVMS], + ); // Flat map operatorKey -> spec const byKey: Record = React.useMemo(() => { @@ -451,14 +480,18 @@ export const useOperatorSpecs = () => { return { byCategory, byKey }; }; -// Utility to get flat map outside the hook -export const getOperatorSpecsByKey = (useLVMS?: boolean): Record => - Object.values(getOperatorSpecs(useLVMS)).reduce((acc, specs) => { - specs.forEach((spec) => { - acc[spec.operatorKey] = spec; - }); - return acc; - }, {} as Record); +export const getOperatorTitleByFeatureId = (featureId: FeatureId): string | undefined => { + const allSpecs = getOperatorSpecs(() => undefined); + + for (const categorySpecs of Object.values(allSpecs)) { + const spec = categorySpecs.find((s) => s.featureId === featureId); + if (spec) { + return spec.title; + } + } + + return undefined; +}; enum Category { STORAGE, diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx index 0a84d0c81b..a41396f0d9 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx @@ -24,19 +24,15 @@ import NewFeatureSupportLevelBadge, { } from '../../../common/components/newFeatureSupportLevels/NewFeatureSupportLevelBadge'; import { ExternalLink, OperatorsValues, PopoverIcon, singleClusterBundles } from '../../../common'; import { useFormikContext } from 'formik'; -import { - GetFeatureSupportLevel, - useNewFeatureSupportLevel, -} from '../../../common/components/newFeatureSupportLevels'; +import { useNewFeatureSupportLevel } from '../../../common/components/newFeatureSupportLevels'; import { useFeature } from '../../hooks/use-feature'; import { useSelector } from 'react-redux'; import { selectIsCurrentClusterSNO } from '../../store/slices/current-cluster/selectors'; import { getNewBundleOperators } from '../clusterConfiguration/operators/utils'; import { bundleSpecs } from '../clusterConfiguration/operators/bundleSpecs'; import { OperatorSpec, useOperatorSpecs } from '../../../common/components/operators/operatorSpecs'; - -import './OperatorsBundle.css'; import { useClusterWizardContext } from './ClusterWizardContext'; +import './OperatorsBundle.css'; const BundleLabel = ({ bundle }: { bundle: Bundle }) => { const { byKey: opSpecs } = useOperatorSpecs(); @@ -82,18 +78,16 @@ const BundleLabel = ({ bundle }: { bundle: Bundle }) => { const getBundleSupportLevel = ( bundle: Bundle, opSpecsByKey: Record, - getFeatureSupportLevel: GetFeatureSupportLevel, ): NewSupportLevelBadgeProps['supportLevel'] => { let supportLevel: NewSupportLevelBadgeProps['supportLevel'] = undefined; if (bundle.operators) { for (const op of bundle.operators) { const operatorSpec = opSpecsByKey[op]; if (operatorSpec) { - const opSupportLevel = getFeatureSupportLevel(operatorSpec.featureId); - if (opSupportLevel === 'dev-preview') { + if (operatorSpec.supportLevel === 'dev-preview') { supportLevel = 'dev-preview'; break; - } else if (opSupportLevel === 'tech-preview') { + } else if (operatorSpec.supportLevel === 'tech-preview') { supportLevel = 'tech-preview'; } } @@ -113,11 +107,11 @@ const BundleCard = ({ }) => { const { values, setFieldValue } = useFormikContext(); const isSNO = useSelector(selectIsCurrentClusterSNO); - const { isFeatureSupported, getFeatureSupportLevel } = useNewFeatureSupportLevel(); + const { isFeatureSupported } = useNewFeatureSupportLevel(); const { byKey: opSpecs } = useOperatorSpecs(); const { uiSettings } = useClusterWizardContext(); - const supportLevel = getBundleSupportLevel(bundle, opSpecs, getFeatureSupportLevel); + const supportLevel = getBundleSupportLevel(bundle, opSpecs); const hasUnsupportedOperators = !!bundle.operators?.some((op) => { const operatorSpec = opSpecs[op]; diff --git a/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts b/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts index 277f818cbf..856273153d 100644 --- a/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts +++ b/libs/ui-lib/lib/ocm/components/featureSupportLevels/featureStateUtils.ts @@ -4,11 +4,6 @@ import { CpuArchitecture, FeatureId, isSNO, - OPERATOR_NAME_CNV, - OPERATOR_NAME_LVM, - OPERATOR_NAME_ODF, - OPERATOR_NAME_OPENSHIFT_AI, - OPERATOR_NAME_OSC, SupportedCpuArchitecture, } from '../../../common'; import { @@ -17,7 +12,7 @@ import { SupportLevel, } from '@openshift-assisted/types/assisted-installer-service'; import { ExternalPlatformLabels } from '../clusterConfiguration/platformIntegration/constants'; -import { getOperatorSpecsByKey } from '../../../common/components/operators/operatorSpecs'; +import { getOperatorTitleByFeatureId } from '../../../common/components/operators/operatorSpecs'; export const clusterExistsReason = 'This option is not editable after the draft cluster is created'; @@ -46,10 +41,7 @@ const getOdfDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecsByKey(); - - const operatorTitle = opSpecs[OPERATOR_NAME_ODF]?.title || ''; - + const operatorTitle = getOperatorTitleByFeatureId('ODF') || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm && isSNO(cluster)) { return `${operatorTitle} is not available when using Single Node OpenShift or ARM CPU architecture.`; @@ -75,8 +67,8 @@ const getCnvDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_CNV]?.title || ''; + const operatorTitle = getOperatorTitleByFeatureId('CNV') || ''; + if (platformType === 'nutanix') { return `${operatorTitle} is not available when Nutanix platform type is selected.`; } @@ -105,8 +97,7 @@ const getLvmDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_LVM]?.title; + const operatorTitle = getOperatorTitleByFeatureId('LVM') || ''; if (platformType === 'nutanix') { return `${operatorTitle} is not supported when Nutanix platform type is selected.`; } @@ -125,8 +116,7 @@ const getOscDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_OSC]?.title || ''; + const operatorTitle = getOperatorTitleByFeatureId('OSC') || ''; if (!isSupported) { return `${operatorTitle} is not supported in this OpenShift version.`; } @@ -299,8 +289,7 @@ const getOpenShiftAIDisabledReason = ( return undefined; } - const opSpecs = getOperatorSpecsByKey(); - const operatorTitle = opSpecs[OPERATOR_NAME_OPENSHIFT_AI]?.title || ''; + const operatorTitle = getOperatorTitleByFeatureId('OPENSHIFT_AI') || ''; const isArm = activeFeatureConfiguration?.underlyingCpuArchitecture === CpuArchitecture.ARM; if (isArm) { return `${operatorTitle} is not available when ARM CPU architecture is selected.`;