From 8bc47be595b1b3e6b3efecb75f7a0d134740ed21 Mon Sep 17 00:00:00 2001 From: Montse Ortega Gallart Date: Mon, 30 Jun 2025 11:14:32 +0200 Subject: [PATCH] MGMT-20842: Add search to operators page (#3017) * MGMT-20842: Add search to operators page * Remove console.log * Add 'No results found' text * Expand operators section when results are founded * Change regexp for operators search * Add try-catch to highlightMatch function in operatorSpecs file --- .../components/operators/operatorSpecs.tsx | 98 ++++++++++++------- .../operators/OperatorCheckbox.tsx | 7 +- .../clusterWizard/OperatorsBundle.tsx | 22 +++-- .../clusterWizard/OperatorsSelect.tsx | 95 +++++++++++------- .../clusterWizard/OperatorsStep.tsx | 33 ++++++- 5 files changed, 172 insertions(+), 83 deletions(-) diff --git a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx index 023323252b..330d54f8af 100644 --- a/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx +++ b/libs/ui-lib/lib/common/components/operators/operatorSpecs.tsx @@ -83,12 +83,34 @@ export const isOCPVersionEqualsOrMore = ( ); }; +export const highlightMatch = (text: string, searchTerm?: string): React.ReactNode => { + if (!searchTerm) return text; + try { + const escapeSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapeSearchTerm})`, 'gi'); + + const parts = text.split(regex); + + return parts.map((part, i) => + part.toLowerCase() === searchTerm.toLowerCase() ? {part} : part, + ); + } catch (err) { + // eslint-disable-next-line no-console + console.log('failed to highlight search text', err); + return text; + } +}; + +export const HighlightedText = ({ text, searchTerm }: { text: string; searchTerm?: string }) => ( + <>{highlightMatch(text, searchTerm)} +); + export type OperatorSpec = { operatorKey: string; title: string; featureId: FeatureId; descriptionText?: string; - Description?: React.ComponentType<{ openshiftVersion?: string }>; + Description?: React.ComponentType<{ openshiftVersion?: string; searchTerm?: string }>; notStandalone?: boolean; Requirements?: React.ComponentType<{ cluster: Cluster }>; supportLevel?: SupportLevel | undefined; @@ -105,9 +127,9 @@ export const getOperatorSpecs = ( title: 'Local Storage Operator', featureId: 'LSO', descriptionText: DESCRIPTION_LSO, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_LSO}{' '} + {' '} Learn more ), @@ -119,10 +141,10 @@ export const getOperatorSpecs = ( title: useLVMS ? 'Logical Volume Manager Storage' : 'Logical Volume Manager', featureId: 'LVM', descriptionText: DESCRIPTION_LVM, - Description: ({ openshiftVersion }) => + Description: ({ openshiftVersion, searchTerm }) => useLVMS ? ( <> - {DESCRIPTION_LVM}{' '} + {' '} Learn more ) : ( @@ -140,9 +162,10 @@ export const getOperatorSpecs = ( Learn more about the requirements for OpenShift Data Foundation ), - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_ODF} Learn more + {' '} + Learn more ), supportLevel: getFeatureSupportLevel('ODF'), @@ -157,9 +180,10 @@ export const getOperatorSpecs = ( Requirements: () => ( <>Enabled CPU virtualization support in BIOS (Intel-VT / AMD-V) on all nodes ), - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_CNV} Learn more + {' '} + Learn more ), supportLevel: getFeatureSupportLevel('CNV'), @@ -169,9 +193,10 @@ export const getOperatorSpecs = ( title: 'Migration Toolkit for Virtualization', featureId: 'MTV', descriptionText: DESCRIPTION_MTV, - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_MTV} Learn more + {' '} + Learn more ), supportLevel: getFeatureSupportLevel('MTV'), @@ -203,9 +228,9 @@ export const getOperatorSpecs = ( Requirements: () => ( Learn more ), - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_OPENSHIFT_AI}{' '} + {' '} Learn more ), @@ -217,7 +242,12 @@ export const getOperatorSpecs = ( featureId: 'AMD_GPU', descriptionText: DESCRIPTION_AMD_GPU, Requirements: () => <>Requires at least one supported AMD GPU, - Description: () => <>{DESCRIPTION_AMD_GPU}, + Description: ({ searchTerm }) => ( + <> + {' '} + {' '} + + ), supportLevel: getFeatureSupportLevel('AMD_GPU'), }, { @@ -226,9 +256,9 @@ export const getOperatorSpecs = ( featureId: 'NVIDIA_GPU', descriptionText: DESCRIPTION_NVIDIA_GPU, Requirements: () => <>Requires at least one supported NVIDIA GPU, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_NVIDIA_GPU} + {' '} Learn more ), @@ -241,9 +271,9 @@ export const getOperatorSpecs = ( title: 'NMState', featureId: 'NMSTATE', descriptionText: DESCRIPTION_NMSTATE, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_NMSTATE}{' '} + {' '} Learn more ), @@ -254,9 +284,9 @@ export const getOperatorSpecs = ( title: 'Service Mesh', featureId: 'SERVICEMESH', descriptionText: DESCRIPTION_SERVICEMESH, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_SERVICEMESH}{' '} + {' '} Learn more ), @@ -270,9 +300,9 @@ export const getOperatorSpecs = ( title: 'Authorino', featureId: 'AUTHORINO', descriptionText: DESCRIPTION_AUTHORINO, - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_AUTHORINO}{' '} + {' '} Learn more ), @@ -284,9 +314,9 @@ export const getOperatorSpecs = ( title: 'Kernel Module Management', featureId: 'KMM', descriptionText: DESCRIPTION_KMM, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_KMM}{' '} + {' '} Learn more ), @@ -299,9 +329,9 @@ export const getOperatorSpecs = ( title: 'Pipelines', featureId: 'PIPELINES', descriptionText: DESCRIPTION_PIPELINES, - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_PIPELINES}{' '} + {' '} Learn more ), @@ -313,9 +343,9 @@ export const getOperatorSpecs = ( title: 'Serverless', featureId: 'SERVERLESS', descriptionText: DESCRIPTION_SERVERLESS, - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_SERVERLESS}{' '} + {' '} Learn more ), @@ -329,9 +359,9 @@ export const getOperatorSpecs = ( title: 'Multicluster engine', featureId: 'MCE', descriptionText: DESCRIPTION_MCE, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_MCE}{' '} + {' '} Learn more ), @@ -342,9 +372,9 @@ export const getOperatorSpecs = ( title: 'Node Maintenance', featureId: 'NODE_MAINTENANCE', descriptionText: DESCRIPTION_NODE_MAINTENANCE, - Description: () => ( + Description: ({ searchTerm }) => ( <> - {DESCRIPTION_NODE_MAINTENANCE}{' '} + {' '} Learn more ), @@ -373,9 +403,9 @@ export const getOperatorSpecs = ( title: 'Kube Descheduler', featureId: 'KUBE_DESCHEDULER', descriptionText: DESCRIPTION_KUBE_DESCHEDULER, - Description: ({ openshiftVersion }) => ( + Description: ({ openshiftVersion, searchTerm }) => ( <> - {DESCRIPTION_KUBE_DESCHEDULER}{' '} + {' '} Learn more ), 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..84d8f89332 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/operators/OperatorCheckbox.tsx @@ -26,6 +26,7 @@ import { getFieldId, OperatorsValues, PopoverIcon } from '../../../../common'; import { useNewFeatureSupportLevel } from '../../../../common/components/newFeatureSupportLevels'; import { getNewOperators } from './utils'; import { + highlightMatch, OperatorSpec, useOperatorSpecs, } from '../../../../common/components/operators/operatorSpecs'; @@ -118,12 +119,14 @@ const OperatorCheckbox = ({ Requirements, openshiftVersion, preflightRequirements, + searchTerm, }: { bundles: Bundle[]; cluster: Cluster; operatorId: string; openshiftVersion?: string; preflightRequirements: PreflightHardwareRequirements | undefined; + searchTerm?: string; } & OperatorSpec) => { const { getFeatureSupportLevel, getFeatureDisabledReason } = useNewFeatureSupportLevel(); const { byKey: opSpecs } = useOperatorSpecs(); @@ -166,7 +169,7 @@ const OperatorCheckbox = ({ label={ <> - + ) diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx index a41396f0d9..bfc6d5d46d 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsBundle.tsx @@ -30,17 +30,20 @@ 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 { + highlightMatch, + OperatorSpec, + useOperatorSpecs, +} from '../../../common/components/operators/operatorSpecs'; import { useClusterWizardContext } from './ClusterWizardContext'; import './OperatorsBundle.css'; -const BundleLabel = ({ bundle }: { bundle: Bundle }) => { +const BundleLabel = ({ bundle, searchTerm }: { bundle: Bundle; searchTerm?: string }) => { const { byKey: opSpecs } = useOperatorSpecs(); const bundleSpec = bundleSpecs[bundle.id || '']; - return ( <> - {bundle.title} + {highlightMatch(bundle.title || '', searchTerm)} Requirements and dependencies} @@ -100,10 +103,12 @@ const BundleCard = ({ bundle, bundles, preflightRequirements, + searchTerm, }: { bundle: Bundle; bundles: Bundle[]; preflightRequirements: PreflightHardwareRequirements | undefined; + searchTerm?: string; }) => { const { values, setFieldValue } = useFormikContext(); const isSNO = useSelector(selectIsCurrentClusterSNO); @@ -177,13 +182,13 @@ const BundleCard = ({ }} > - + -
{bundle.description}
+
{highlightMatch(bundle.description || '', searchTerm)}
@@ -206,9 +211,11 @@ const BundleCard = ({ const OperatorsBundle = ({ bundles, preflightRequirements, + searchTerm, }: { bundles: Bundle[]; preflightRequirements: PreflightHardwareRequirements | undefined; + searchTerm?: string; }) => { const isSingleClusterFeatureEnabled = useFeature('ASSISTED_INSTALLER_SINGLE_CLUSTER_FEATURE'); @@ -216,7 +223,7 @@ const OperatorsBundle = ({ - Bundles + {bundles.length > 0 ? 'Bundles' : ''} @@ -230,6 +237,7 @@ const OperatorsBundle = ({ bundle={bundle} bundles={bundles} preflightRequirements={preflightRequirements} + searchTerm={searchTerm} /> ))} diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx index 86d68955dc..5c72e31c45 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsSelect.tsx @@ -24,10 +24,12 @@ const OperatorsSelect = ({ cluster, bundles, preflightRequirements, + searchTerm, }: { cluster: Cluster; bundles: Bundle[]; preflightRequirements: PreflightHardwareRequirements | undefined; + searchTerm?: string; }) => { const [isLoading, setIsLoading] = useStateSafely(true); const { addAlert } = useAlerts(); @@ -64,50 +66,69 @@ const OperatorsSelect = ({ }); }, [isSingleClusterFeatureEnabled, supportedOperators]); - if (isLoading) { - return ; - } - const selectedOperators = values.selectedOperators.filter( (opKey) => operators.includes(opKey) && !!opSpecs[opKey], ); + if (isLoading) { + return ; + } + let foundAtLeastOneOperator = false; return ( - setIsExpanded(!isExpanded)} - isExpanded={isExpanded} - data-testid="single-operators-section" - > - - {Object.entries(byCategory).map(([categoryName, specs]) => { - const categoryOperators = specs.filter((spec) => operators.includes(spec.operatorKey)); - if (categoryOperators.length === 0) { - return null; - } + <> + setIsExpanded(!isExpanded)} + isExpanded={isExpanded} + data-testid="single-operators-section" + > + + {Object.entries(byCategory).map(([categoryName, specs]) => { + let categoryOperators = specs.filter((spec) => operators.includes(spec.operatorKey)); + // Filter by searchTerm + if (searchTerm?.trim()) { + const term = searchTerm.trim().toLowerCase(); + categoryOperators = categoryOperators.filter((spec) => { + const op = opSpecs[spec.operatorKey]; + const title = op?.title?.toLowerCase() || ''; + const description = op?.descriptionText?.toLowerCase() || ''; + return title.includes(term) || description.includes(term); + }); + } + if (categoryOperators.length === 0) { + return null; + } + foundAtLeastOneOperator = true; + if (!!searchTerm?.trim() && !isExpanded) { + //if we found some results expand operators section + setIsExpanded(true); + } - return ( - - - {categoryName} - - {categoryOperators.map((spec) => ( - - + return ( + + + {categoryName} - ))} - - ); - })} - - + {categoryOperators.map((spec) => ( + + + + ))} + + ); + })} + + + {!foundAtLeastOneOperator && !!searchTerm?.trim() && No results found} + ); }; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsStep.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsStep.tsx index 4c94f947c8..91e9ad2b2d 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsStep.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/OperatorsStep.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Stack, StackItem } from '@patternfly/react-core'; +import { Flex, FlexItem, SearchInput, Stack, StackItem } from '@patternfly/react-core'; import { Bundle } from '@openshift-assisted/types/assisted-installer-service'; import { ClusterOperatorProps, @@ -19,6 +19,7 @@ export const OperatorsStep = ({ cluster }: ClusterOperatorProps) => { const [bundlesLoading, setBundlesLoading] = React.useState(true); const [bundles, setBundles] = React.useState([]); const { preflightRequirements, isLoading } = useClusterPreflightRequirements(cluster.id); + const [searchTerm, setSearchTerm] = React.useState(''); React.useEffect(() => { const fetchBundles = async () => { try { @@ -39,6 +40,12 @@ export const OperatorsStep = ({ cluster }: ClusterOperatorProps) => { void fetchBundles(); }, [addAlert]); + const filteredBundles = bundles.filter( + (bundle) => + bundle.title?.toLowerCase().includes(searchTerm.toLowerCase()) || + bundle.description?.toLowerCase().includes(searchTerm.toLowerCase()), + ); + if (isLoading || bundlesLoading) { return ; } @@ -46,16 +53,36 @@ export const OperatorsStep = ({ cluster }: ClusterOperatorProps) => { return ( - Operators + + + Operators + + + setSearchTerm(value)} + onClear={() => setSearchTerm('')} + /> + + - +