From 7dd609189492fff66d13227708ca2830d97b2646 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Tue, 25 Aug 2026 15:43:15 +0000 Subject: [PATCH 1/6] OCPBUGS-113746: fix React Compiler immutability and preserve-manual-memoization warnings Remove module-level render caches (camel-case-wrap, resource-icon), refactor mutable variables to index-based approach (ProgressiveListFooter), create local model copies instead of mutating props (Topology, PipelineVisualizationSurface), remove unnecessary manual useMemo/useCallback where React Compiler handles memoization automatically, and add eslint-disable comments for intentional context/Formik mutations. Decrements MAX_WARNINGS from 341 to 312. Co-Authored-By: Claude Opus 4.6 --- frontend/package.json | 2 +- .../console-operator/ConsolePluginsTable.tsx | 5 +- .../node-storage/PersistentVolumes.tsx | 4 +- .../app/components/utils/camel-case-wrap.tsx | 10 +- .../formik-fields/NumberSpinnerField.tsx | 22 ++-- .../ProgressiveListFooter.tsx | 17 +-- .../advanced-options/LifecycleHookField.tsx | 4 + .../PipelineTaskNode.tsx | 14 +-- .../PipelineVisualizationSurface.tsx | 18 ++- .../resource-quota/ResourceQuotaAlert.tsx | 6 +- .../topology/components/nodes/EventSink.tsx | 20 +--- .../src/components/clusterserviceversion.tsx | 19 ++-- .../src/components/k8s-resource.tsx | 12 +- .../operator-hub/operator-hub-subscribe.tsx | 22 ++-- .../src/components/graph-view/Topology.tsx | 16 ++- .../providers/SideBarTabHookResolver.tsx | 13 ++- .../data-transforms/TopologyDataRetriever.tsx | 4 + frontend/public/components/edit-yaml.tsx | 5 +- .../modals/cluster-update-modal.tsx | 107 ++++++++---------- frontend/public/components/pod-connect.tsx | 8 +- .../public/components/utils/resource-icon.tsx | 13 +-- 21 files changed, 138 insertions(+), 203 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 74b378ecd3b..4a526412dd4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "check-cycles": "CHECK_CYCLES=true yarn dev-once", "coverage": "jest --coverage .", "eslint": "node ./node_modules/.bin/eslint --max-warnings ${MAX_WARNINGS:-0} --color", - "lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=341 yarn eslint --format ./scripts/eslint-exact-warnings.js .", + "lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=312 yarn eslint --format ./scripts/eslint-exact-warnings.js .", "gherkin-lint": "./node_modules/.bin/gherkin-lint -c ./packages/dev-console/integration-tests/.gherkin-lintrc ./packages/*/integration-tests/features", "test": "LANG=en_US.UTF-8 jest", "debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand", diff --git a/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx b/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx index add8deeaa69..62396b9b4bb 100644 --- a/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx +++ b/frontend/packages/console-app/src/components/console-operator/ConsolePluginsTable.tsx @@ -474,10 +474,7 @@ const useConsolePluginRows = (enabledPlugins: string[]) => { }; const PluginsPage: FC = (props) => { - const enabledPlugins = useMemo( - () => props?.obj?.spec?.plugins ?? [], - [props?.obj?.spec?.plugins], - ); + const enabledPlugins = props?.obj?.spec?.plugins ?? []; const { rows, loaded } = useConsolePluginRows(enabledPlugins); return ( diff --git a/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx b/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx index 88532f3ddcc..0b1e6abad55 100644 --- a/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx +++ b/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx @@ -41,7 +41,7 @@ type PersistentVolumeRowProps = { const PersistentVolumeRow: FC = ({ persistentVolumeData, pods }) => { const { t } = useTranslation('console-app'); - const pod = useMemo(() => { + const pod = (() => { if (persistentVolumeData.vmi) { return getVMIPod(persistentVolumeData.vmi, pods); } @@ -55,7 +55,7 @@ const PersistentVolumeRow: FC = ({ persistentVolumeDat ), ); return podsForPVC ? getCurrentPod(podsForPVC) : undefined; - }, [persistentVolumeData.vmi, persistentVolumeData.persistentVolumeClaim?.metadata.name, pods]); + })(); return ( diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/components/utils/camel-case-wrap.tsx b/frontend/packages/console-dynamic-plugin-sdk/src/app/components/utils/camel-case-wrap.tsx index c445ae9a53d..2bcd474e4e2 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/components/utils/camel-case-wrap.tsx +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/components/utils/camel-case-wrap.tsx @@ -1,20 +1,14 @@ import type { FC } from 'react'; import { Fragment } from 'react'; -const MEMO = {}; - const CamelCaseWrap: FC = ({ value, dataTest }) => { if (!value) { return '-'; } - if (MEMO[value]) { - return MEMO[value]; - } - // Add word break points before capital letters (but keep consecutive capital letters together). const words = value.match(/[A-Z]+[^A-Z]*|[^A-Z]+/g); - const rendered = ( + return ( {words.map((word, i) => ( // eslint-disable-next-line react/no-array-index-key @@ -25,8 +19,6 @@ const CamelCaseWrap: FC = ({ value, dataTest }) => { ))} ); - MEMO[value] = rendered; - return rendered; }; type CamelCaseWrapProps = { diff --git a/frontend/packages/console-shared/src/components/formik-fields/NumberSpinnerField.tsx b/frontend/packages/console-shared/src/components/formik-fields/NumberSpinnerField.tsx index 512f9d97741..96d7b2bd0da 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/NumberSpinnerField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/NumberSpinnerField.tsx @@ -1,5 +1,4 @@ import type { FC, ReactEventHandler } from 'react'; -import { useCallback } from 'react'; import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; import type { FormikValues } from 'formik'; import { useField, useFormikContext } from 'formik'; @@ -31,18 +30,15 @@ export const NumberSpinnerField: FC = ({ useFormikValidationFix(field.value); - const handleChange: ReactEventHandler = useCallback( - (event) => { - field.onChange(event); - setFieldValue( - props.name, - props?.setOutputAsIntegerFlag - ? _.toInteger(event.currentTarget.value) - : event.currentTarget.value, - ); - }, - [field, props.name, setFieldValue, props?.setOutputAsIntegerFlag], - ); + const handleChange: ReactEventHandler = (event) => { + field.onChange(event); + setFieldValue( + props.name, + props?.setOutputAsIntegerFlag + ? _.toInteger(event.currentTarget.value) + : event.currentTarget.value, + ); + }; return ( diff --git a/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx b/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx index 51aa71a841c..2e495205849 100644 --- a/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx +++ b/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx @@ -23,27 +23,22 @@ export const ProgressiveListFooter: FC = ({ type: 'conjunction', }).format(items); - let lastIdx = 0; - let lastLen = 0; + const positions = items.map((item) => formattedString.indexOf(item)); return (
<> - {items.map((item) => { - const currentIdx = formattedString.indexOf(item); - const element = ( + {items.map((item, i) => { + const currentIdx = positions[i]; + const prevEnd = i > 0 ? positions[i - 1] + items[i - 1].length : 0; + return ( - {formattedString.slice(lastIdx + lastLen, currentIdx)} + {formattedString.slice(prevEnd, currentIdx)} ); - - lastIdx = currentIdx; - lastLen = item.length; - - return element; })}
diff --git a/frontend/packages/dev-console/src/components/deployments/deployment-strategy/advanced-options/LifecycleHookField.tsx b/frontend/packages/dev-console/src/components/deployments/deployment-strategy/advanced-options/LifecycleHookField.tsx index 8ada000313f..fa8c22bbd37 100644 --- a/frontend/packages/dev-console/src/components/deployments/deployment-strategy/advanced-options/LifecycleHookField.tsx +++ b/frontend/packages/dev-console/src/components/deployments/deployment-strategy/advanced-options/LifecycleHookField.tsx @@ -53,8 +53,10 @@ const LifecycleHookField: FC = ({ `formData.deploymentStrategy.${dataAttribute}.${lifecycleHookName}.isAddingLch`, false, ); + // eslint-disable-next-line react-hooks/immutability -- intentional Formik initialValues sync initialValues.formData.deploymentStrategy[dataAttribute][lifecycleHookName] = deploymentStrategy[dataAttribute][lifecycleHookName]; + // eslint-disable-next-line react-hooks/immutability -- intentional Formik initialValues sync initialValues.formData.deploymentStrategy.imageStreamData[lifecycleHookName] = deploymentStrategy.imageStreamData[lifecycleHookName]; }, [ @@ -75,8 +77,10 @@ const LifecycleHookField: FC = ({ resNamespace, resourceType, ); + // eslint-disable-next-line react-hooks/immutability -- intentional Formik initialValues sync initialValues.formData.deploymentStrategy[dataAttribute][lifecycleHookName] = data[dataAttribute][lifecycleHookName]; + // eslint-disable-next-line react-hooks/immutability -- intentional Formik initialValues sync initialValues.formData.deploymentStrategy.imageStreamData[lifecycleHookName] = data.imageStreamData[lifecycleHookName]; setFieldValue( diff --git a/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineTaskNode.tsx b/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineTaskNode.tsx index c7e0d2b98f0..a1b1221f8b7 100644 --- a/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineTaskNode.tsx +++ b/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineTaskNode.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useRef, useMemo, memo } from 'react'; +import { useRef, memo } from 'react'; import { Tooltip } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import type { Node, WithContextMenuProps, WithSelectionProps } from '@patternfly/react-topology'; @@ -123,15 +123,9 @@ const PipelineTaskNode: FC = ({ ? `${succeededStepsCount}/${stepStatusList.length}` : null; - const passedData = useMemo(() => { - const newData = { ...data }; - Object.keys(newData).forEach((key) => { - if (newData[key] === undefined) { - delete newData[key]; - } - }); - return newData; - }, [data]); + const passedData = Object.fromEntries( + Object.entries(data).filter(([, value]) => value !== undefined), + ); const hasTaskIcon = !!(data.taskIconClass || data.taskIcon); const tooltipContent = getTooltipContent(data.task?.status?.reason); diff --git a/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineVisualizationSurface.tsx b/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineVisualizationSurface.tsx index c51953987aa..9185c4c06e6 100644 --- a/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineVisualizationSurface.tsx +++ b/frontend/packages/dev-console/src/components/pipelines-visualization/PipelineVisualizationSurface.tsx @@ -96,22 +96,20 @@ const PipelineVisualizationSurface: FC = ({ }); setVis(visualization); } else { - const graph = storedGraphModel.current; - if (graph) { - model.graph = graph; - } - vis.fromModel(model); + const localModel = storedGraphModel.current + ? { ...model, graph: storedGraphModel.current } + : model; + vis.fromModel(localModel); vis.getGraph().layout(); } }, [vis, model, onLayoutUpdate, componentFactory]); useEffect(() => { if (model && vis) { - const graph = storedGraphModel.current; - if (graph) { - model.graph = graph; - } - vis.fromModel(model); + const localModel = storedGraphModel.current + ? { ...model, graph: storedGraphModel.current } + : model; + vis.fromModel(localModel); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [model, vis]); diff --git a/frontend/packages/dev-console/src/components/resource-quota/ResourceQuotaAlert.tsx b/frontend/packages/dev-console/src/components/resource-quota/ResourceQuotaAlert.tsx index a49fae4ad5e..7e14b35a83a 100644 --- a/frontend/packages/dev-console/src/components/resource-quota/ResourceQuotaAlert.tsx +++ b/frontend/packages/dev-console/src/components/resource-quota/ResourceQuotaAlert.tsx @@ -68,11 +68,9 @@ export const ResourceQuotaAlert: FC = ({ namespace }) = [appliedclusterresourcequotas], ); - let totalResourcesAtQuota = useMemo( - () => [...totalRQatQuota, ...totalACRQatQuota], - [totalRQatQuota, totalACRQatQuota], + const totalResourcesAtQuota = [...totalRQatQuota, ...totalACRQatQuota].filter( + (resourceAtQuota) => resourceAtQuota !== 0, ); - totalResourcesAtQuota = totalResourcesAtQuota.filter((resourceAtQuota) => resourceAtQuota !== 0); useEffect(() => { if (totalResourcesAtQuota.length === 1) { diff --git a/frontend/packages/knative-plugin/src/topology/components/nodes/EventSink.tsx b/frontend/packages/knative-plugin/src/topology/components/nodes/EventSink.tsx index 78645bbc5e0..c4b4df5b24f 100644 --- a/frontend/packages/knative-plugin/src/topology/components/nodes/EventSink.tsx +++ b/frontend/packages/knative-plugin/src/topology/components/nodes/EventSink.tsx @@ -1,5 +1,5 @@ import type { ReactNode, FC } from 'react'; -import { useRef, useMemo } from 'react'; +import { useRef } from 'react'; import { Tooltip } from '@patternfly/react-core'; import type { Node, @@ -75,10 +75,7 @@ const EventSink: FC = ({ element.getSourceEdges()?.filter((edge: Edge) => edge.getType() === TYPE_KAFKA_CONNECTION_LINK) .length > 0; const { revisions, associatedDeployment } = resources; - const revisionIds = useMemo( - () => revisions?.map((revision) => revision.metadata.uid), - [revisions], - ); + const revisionIds = revisions?.map((revision) => revision.metadata.uid); const { loaded, loadError, pods } = usePodsForRevisions(revisionIds, resource.metadata.namespace); const controller = useVisualizationController(); @@ -97,7 +94,7 @@ const EventSink: FC = ({ const isKafkaSink = referenceFor(resource) === referenceForModel(KafkaSinkModel); - const donutStatus = useMemo(() => { + const donutStatus = (() => { if (!revisionIds && loadedDeployment && !loadErrorDeployment) { return podsDeployment; } @@ -113,16 +110,7 @@ const EventSink: FC = ({ }; } return null; - }, [ - revisionIds, - loadedDeployment, - loadErrorDeployment, - loaded, - loadError, - podsDeployment, - pods, - resource, - ]); + })(); return ( = (pro const providedAPIs = providedAPIsForCSV(props.obj); const marketplaceSupportWorkflow = metadata?.annotations?.[OLMAnnotation.SupportWorkflow] || ''; const initializationLink = getInitializationLink(metadata?.annotations); - const initializationResource = useMemo( - () => - !initializationLink && - getInitializationResource(metadata?.annotations, { - onError: (error) => { - // eslint-disable-next-line no-console - console.error('Error while parsing CSV initialization resource JSON,', error.message); - }, - }), - [metadata?.annotations, initializationLink], - ); + const initializationResource = + !initializationLink && + getInitializationResource(metadata?.annotations, { + onError: (error) => { + // eslint-disable-next-line no-console + console.error('Error while parsing CSV initialization resource JSON,', error.message); + }, + }); const supportWorkflowUrl = useMemo(() => { if (marketplaceSupportWorkflow) { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx b/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx index 6dfc7822ccb..986f9df88fc 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx @@ -1,5 +1,4 @@ import type { FC } from 'react'; -import { useMemo } from 'react'; import { sortable } from '@patternfly/react-table'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; @@ -166,13 +165,10 @@ export const Resources: FC = (props) => { }, ); - const customData = useMemo( - () => ({ - linkFor: linkForCsvResource, - providedAPI, - }), - [providedAPI], - ); + const customData = { + linkFor: linkForCsvResource, + providedAPI, + }; return ( = (props) => { }); const csvPlugins = getClusterServiceVersionPlugins(currentCSVDesc?.annotations); - const initializationResourceReference = useMemo( - () => (initializationResource ? referenceFor(initializationResource) : null), - [initializationResource], - ); + const initializationResourceReference = initializationResource + ? referenceFor(initializationResource) + : null; const globalNS = (props.operatorGroup?.data || ([] as OperatorGroupKind[])).find( @@ -332,14 +331,11 @@ const OperatorHubSubscribeForm: FC = (props) => { ); const supportsGlobal = globalInstallMode && globalInstallMode.supported; - const navigateToInstallPage = useCallback( - (csvName: string) => { - navigate( - `/operatorhub/install/${catalogNamespace}/${catalog}/${pkg}/${csvName}/to/${selectedTargetNamespace}`, - ); - }, - [catalog, catalogNamespace, navigate, pkg, selectedTargetNamespace], - ); + const navigateToInstallPage = (csvName: string) => { + navigate( + `/operatorhub/install/${catalogNamespace}/${catalog}/${pkg}/${csvName}/to/${selectedTargetNamespace}`, + ); + }; if (!supportsSingle && !supportsGlobal) { return ( diff --git a/frontend/packages/topology/src/components/graph-view/Topology.tsx b/frontend/packages/topology/src/components/graph-view/Topology.tsx index c45b88e06fe..45679a362bb 100644 --- a/frontend/packages/topology/src/components/graph-view/Topology.tsx +++ b/frontend/packages/topology/src/components/graph-view/Topology.tsx @@ -213,10 +213,16 @@ const TopologyComponent: FC< useEffect(() => { if (model && visualizationReady) { + const localModel = { + ...model, + graph: { ...model.graph }, + nodes: model.nodes.map((n) => ({ ...n })), + edges: model.edges.map((e) => ({ ...e })), + }; if (!storedLayoutApplied.current) { const storedGraphModel = getStoredGraphModel(namespace); if (storedGraphModel) { - model.graph = { + localModel.graph = { ...graphModel.graph, x: storedGraphModel.x, y: storedGraphModel.y, @@ -227,7 +233,7 @@ const TopologyComponent: FC< } const storedLayout = topologyLayoutDataJson?.[namespace]; if (storedLayout) { - model.nodes.forEach((n) => { + localModel.nodes.forEach((n) => { const storedNode = storedLayout.nodes.find((sn) => sn.id === n.id); if (storedNode) { STORED_NODE_LAYOUT_FIELDS.forEach((key) => { @@ -238,20 +244,20 @@ const TopologyComponent: FC< } } - model.nodes.forEach((n) => { + localModel.nodes.forEach((n) => { const oldNode = visualization.getNodeById(n.id); if (oldNode && _.isEqual(oldNode.getData(), n.data)) { n.data = oldNode.getData(); } }); - model.edges.forEach((e) => { + localModel.edges.forEach((e) => { const oldEdge = visualization.getEdgeById(e.id); if (oldEdge && _.isEqual(oldEdge.getData(), e.data)) { e.data = oldEdge.getData(); } }); - visualization.fromModel(model); + visualization.fromModel(localModel); // Make sure something is visible in the case where stored locations are off the screen if (!storedLayoutApplied.current) { diff --git a/frontend/packages/topology/src/components/side-bar/providers/SideBarTabHookResolver.tsx b/frontend/packages/topology/src/components/side-bar/providers/SideBarTabHookResolver.tsx index f47851fab69..169bdef31a9 100644 --- a/frontend/packages/topology/src/components/side-bar/providers/SideBarTabHookResolver.tsx +++ b/frontend/packages/topology/src/components/side-bar/providers/SideBarTabHookResolver.tsx @@ -51,6 +51,7 @@ const TabBarTabHookResolver: FC = ({ [contentElement] = hookResult; } else if (section) { if (!blamedDeprecatedPlugins[rest.id]) { + // eslint-disable-next-line react-hooks/immutability -- intentional warn-once cache blamedDeprecatedPlugins[rest.id] = true; // eslint-disable-next-line no-console console.warn( @@ -99,16 +100,20 @@ const TabBarTabHookResolver: FC = ({ // show default side bar if (tabsLoaded && tabs.length === 0) { const resource = getResource(graphElement); - resource && - tabs.push({ + const defaultTabs: Tab[] = []; + if (resource) { + defaultTabs.push({ name: t('Details'), component: () => , }); - isEdge(graphElement) && - tabs.push({ + } + if (isEdge(graphElement)) { + defaultTabs.push({ name: t('Resources'), component: () => , }); + } + return children(defaultTabs, tabsLoaded); } return children(tabs, tabsLoaded); diff --git a/frontend/packages/topology/src/data-transforms/TopologyDataRetriever.tsx b/frontend/packages/topology/src/data-transforms/TopologyDataRetriever.tsx index 76a84303114..6e221a799e4 100644 --- a/frontend/packages/topology/src/data-transforms/TopologyDataRetriever.tsx +++ b/frontend/packages/topology/src/data-transforms/TopologyDataRetriever.tsx @@ -34,6 +34,7 @@ const TopologyDataRetriever: FC = ({ trafficData }) // Wipe the current model on a namespace change useEffect(() => { + // eslint-disable-next-line react-hooks/immutability -- intentional context model reset on namespace change dataModelContext.model = null; // eslint-disable-next-line react-hooks/exhaustive-deps }, [namespace]); @@ -42,9 +43,12 @@ const TopologyDataRetriever: FC = ({ trafficData }) if (!_.isEmpty(resources)) { updateTopologyDataModel(dataModelContext, resources, trafficData, monitoringAlerts) .then((res) => { + // eslint-disable-next-line react-hooks/immutability -- intentional context state update dataModelContext.loadError = res.loadError; if (res.loaded) { + // eslint-disable-next-line react-hooks/immutability -- intentional context state update dataModelContext.loaded = true; + // eslint-disable-next-line react-hooks/immutability -- intentional context state update dataModelContext.model = res.model; } }) diff --git a/frontend/public/components/edit-yaml.tsx b/frontend/public/components/edit-yaml.tsx index 904ccc0a866..0da5c073eb6 100644 --- a/frontend/public/components/edit-yaml.tsx +++ b/frontend/public/components/edit-yaml.tsx @@ -190,10 +190,7 @@ const EditYAMLInner: FC = (props) => { const launchModal = useOverlay(); const [templateExtensions, resolvedTemplates] = useResolvedExtensions( - useCallback( - (e): e is YAMLTemplate => isYAMLTemplate(e) && e.properties.model.kind === props?.obj?.kind, - [props?.obj?.kind], - ), + (e): e is YAMLTemplate => isYAMLTemplate(e) && e.properties.model.kind === props?.obj?.kind, ); const { theme, fontSize, showTooltips, stickyScrollEnabled } = useEditYamlSettings(); diff --git a/frontend/public/components/modals/cluster-update-modal.tsx b/frontend/public/components/modals/cluster-update-modal.tsx index 430a8841bf2..289bd42756f 100644 --- a/frontend/public/components/modals/cluster-update-modal.tsx +++ b/frontend/public/components/modals/cluster-update-modal.tsx @@ -1,5 +1,5 @@ import type { FormEvent, FormEventHandler } from 'react'; -import { useState, useEffect, useCallback, Fragment } from 'react'; +import { useState, useEffect, Fragment } from 'react'; import { Alert, Button, @@ -114,67 +114,52 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { const desiredNotRecommendedUpdateConditions = getNotRecommendedUpdateCondition( desiredNotRecommendedUpdate?.conditions, ); - const submit: FormEventHandler = useCallback( - (e) => { - e.preventDefault(); - if (!desiredRecommendedUpdate && !desiredNotRecommendedUpdate) { - setError( - t( - 'Version {{desiredVersion}} not found among the supported updates. Select another version.', - { desiredVersion }, - ), - ); - return; - } + const submit: FormEventHandler = (e) => { + e.preventDefault(); + if (!desiredRecommendedUpdate && !desiredNotRecommendedUpdate) { + setError( + t( + 'Version {{desiredVersion}} not found among the supported updates. Select another version.', + { desiredVersion }, + ), + ); + return; + } - // Clear any previous error message. - setError(''); - let MCPsToPausePromises; - let MCPsToResumePromises; - if (upgradeType === UpgradeTypes.Full) { - MCPsToPausePromises = []; - MCPsToResumePromises = getMCPsToPausePromises(pausedMCPs, false); - } else { - const MCPsToPause = pauseableMCPs.filter((mcp) => - machineConfigPoolsToPause.find((m) => m === mcp.metadata.name), - ); - const MCPsToResume = pauseableMCPs.filter((mcp) => !MCPsToPause.includes(mcp)); - MCPsToPausePromises = getMCPsToPausePromises(MCPsToPause, true); - MCPsToResumePromises = getMCPsToPausePromises(MCPsToResume, false); - } - const patch = [ - { - op: 'add', - path: '/spec/desiredUpdate', - value: desiredNotRecommendedUpdate - ? desiredNotRecommendedUpdate.release - : desiredRecommendedUpdate, - }, - ]; - handlePromise( - Promise.all([ - k8sPatch(ClusterVersionModel, cv, patch), - ...MCPsToResumePromises, - ...MCPsToPausePromises, - ]), - ) - .then(() => close()) - .catch(() => {}); - }, - [ - desiredRecommendedUpdate, - desiredNotRecommendedUpdate, - t, - desiredVersion, - upgradeType, - pausedMCPs, - pauseableMCPs, - machineConfigPoolsToPause, - handlePromise, - cv, - close, - ], - ); + // Clear any previous error message. + setError(''); + let MCPsToPausePromises; + let MCPsToResumePromises; + if (upgradeType === UpgradeTypes.Full) { + MCPsToPausePromises = []; + MCPsToResumePromises = getMCPsToPausePromises(pausedMCPs, false); + } else { + const MCPsToPause = pauseableMCPs.filter((mcp) => + machineConfigPoolsToPause.find((m) => m === mcp.metadata.name), + ); + const MCPsToResume = pauseableMCPs.filter((mcp) => !MCPsToPause.includes(mcp)); + MCPsToPausePromises = getMCPsToPausePromises(MCPsToPause, true); + MCPsToResumePromises = getMCPsToPausePromises(MCPsToResume, false); + } + const patch = [ + { + op: 'add', + path: '/spec/desiredUpdate', + value: desiredNotRecommendedUpdate + ? desiredNotRecommendedUpdate.release + : desiredRecommendedUpdate, + }, + ]; + handlePromise( + Promise.all([ + k8sPatch(ClusterVersionModel, cv, patch), + ...MCPsToResumePromises, + ...MCPsToPausePromises, + ]), + ) + .then(() => close()) + .catch(() => {}); + }; const dropdownItem = (version) => { const isDisabled = clusterUpgradeableFalse && isMinorVersionNewer(currentVersion, version); return { diff --git a/frontend/public/components/pod-connect.tsx b/frontend/public/components/pod-connect.tsx index 1d84bed9029..43f8ac9184c 100644 --- a/frontend/public/components/pod-connect.tsx +++ b/frontend/public/components/pod-connect.tsx @@ -90,11 +90,9 @@ export const PodConnect: FC = ({ const containers = useMemo(() => _.keyBy(_.get(obj, 'spec.containers', []), 'name'), [obj]); - const podName = useMemo(() => obj.metadata?.name || '', [obj?.metadata?.name]); - const namespace = useMemo(() => obj.metadata?.namespace || 'default', [obj?.metadata?.namespace]); - // We are being more specific with the dependency array here to avoid additional rerenders when other fields in obj changes - // eslint-disable-next-line react-hooks/exhaustive-deps - const isWindows = useMemo(() => isWindowsPod(obj), [obj?.spec?.tolerations]); + const podName = obj.metadata?.name || ''; + const namespace = obj.metadata?.namespace || 'default'; + const isWindows = isWindowsPod(obj); const connect = useCallback(() => { const usedClient = isOpenShift ? 'oc' : 'kubectl'; diff --git a/frontend/public/components/utils/resource-icon.tsx b/frontend/public/components/utils/resource-icon.tsx index c91f2503e49..b2ee5caea25 100644 --- a/frontend/public/components/utils/resource-icon.tsx +++ b/frontend/public/components/utils/resource-icon.tsx @@ -7,25 +7,19 @@ import type { K8sResourceKindReference } from '../../module/k8s'; import { kindToAbbr } from '../../module/k8s/get-resources'; import { modelFor } from '../../module/k8s/k8s-models'; -const MEMO = {}; - export const ResourceIcon: FC = ({ className, groupVersionKind, kind }) => { // if no kind or groupVersionKind, return null so an empty icon isn't rendered if (!kind && !groupVersionKind) { return null; } const kindReference = kind || getReference(groupVersionKind); - const memoKey = className ? `${kindReference}/${className}` : kindReference; - if (MEMO[memoKey]) { - return MEMO[memoKey]; - } const kindObj = modelFor(kindReference); const kindStr = kindObj?.kind ?? kindReference; const backgroundColor = _.get(kindObj, 'color', undefined); const klass = css(`co-m-resource-icon co-m-resource-${kindStr.toLowerCase()}`, className); const iconLabel = (kindObj && kindObj.abbr) || kindToAbbr(kindStr); - const rendered = ( + return ( <> {kindStr} @@ -33,11 +27,6 @@ export const ResourceIcon: FC = ({ className, groupVersionKin ); - if (kindObj) { - MEMO[memoKey] = rendered; - } - - return rendered; }; export type ResourceNameProps = { From 4d99af2f77b1d2f1afa71452b8152170d10c709d Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Tue, 25 Aug 2026 16:04:55 +0000 Subject: [PATCH 2/6] OCPBUGS-113746: fix duplicate item positions and restore pod memoization Fix ProgressiveListFooter indexOf always matching first occurrence for repeated items by searching after preceding match position. Restore useMemo for pod lookup in PersistentVolumeRow with Compiler-compatible deps [persistentVolumeData, pods]. Add regression test for duplicate item labels. Co-Authored-By: Claude Opus 4.6 --- .../node-storage/PersistentVolumes.tsx | 4 ++-- .../progressive-list/ProgressiveListFooter.tsx | 8 +++++++- .../__tests__/ProgressiveListFooter.spec.tsx | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx b/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx index 0b1e6abad55..85fc8ca4e03 100644 --- a/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx +++ b/frontend/packages/console-app/src/components/nodes/configuration/node-storage/PersistentVolumes.tsx @@ -41,7 +41,7 @@ type PersistentVolumeRowProps = { const PersistentVolumeRow: FC = ({ persistentVolumeData, pods }) => { const { t } = useTranslation('console-app'); - const pod = (() => { + const pod = useMemo(() => { if (persistentVolumeData.vmi) { return getVMIPod(persistentVolumeData.vmi, pods); } @@ -55,7 +55,7 @@ const PersistentVolumeRow: FC = ({ persistentVolumeDat ), ); return podsForPVC ? getCurrentPod(podsForPVC) : undefined; - })(); + }, [persistentVolumeData, pods]); return ( diff --git a/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx b/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx index 2e495205849..6e8c9690399 100644 --- a/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx +++ b/frontend/packages/console-shared/src/components/progressive-list/ProgressiveListFooter.tsx @@ -23,7 +23,13 @@ export const ProgressiveListFooter: FC = ({ type: 'conjunction', }).format(items); - const positions = items.map((item) => formattedString.indexOf(item)); + const positions = items.reduce( + (result, item, index) => [ + ...result, + formattedString.indexOf(item, index === 0 ? 0 : result[index - 1] + items[index - 1].length), + ], + [], + ); return (