From d34fcab83c9cb51467cbc1ec35bad54880a78bac Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Tue, 25 Aug 2026 14:51:26 +0000 Subject: [PATCH 1/4] fix(lint): resolve 11 quick-win React Compiler ESLint warnings OCPBUGS-113745 Fix all warnings for set-state-in-render, static-components, purity, and use-memo React Compiler ESLint rules: - PodStatus: replace useState with useRef for updateOnEnd (set-state-in-render) - usePluginRoutes: extract lazy component cache lookup to module-level function (static-components) - WebhookSection: extract inline HelpText to module-level component (static-components) - catalog-source: extract Create HOC composition to module level (static-components) - QueryBrowser: use lazy useState initializer for Date.now() (purity) - build: wrap Date.now() fallback in useMemo (purity) - area: replace Date.now() default param with useMemo inside component (purity) - QueryBrowser: change useCallback to useMemo for _.debounce (use-memo) - MarkdownView: change useCallback to useMemo for _.debounce (use-memo) - devfileHooks: replace async useMemo with synchronous promise chain (use-memo) - Logs: change useCallback to useMemo for throttle (use-memo) Decrement MAX_WARNINGS from 341 to 330. Co-Authored-By: Claude Opus 4.6 --- frontend/package.json | 2 +- .../console-app/src/hooks/usePluginRoutes.tsx | 43 ++++---- .../src/components/markdown/MarkdownView.tsx | 15 +-- .../src/components/pod/PodStatus.tsx | 9 +- .../components/query-browser/QueryBrowser.tsx | 6 +- .../components/import/devfile/devfileHooks.ts | 26 ++--- .../pipeline/WebhookSection.tsx | 104 +++++++++--------- .../src/components/catalog-source.tsx | 90 ++++++++------- .../src/components/logs/Logs.tsx | 23 ++-- frontend/public/components/build.tsx | 10 +- frontend/public/components/graphs/area.tsx | 3 +- 11 files changed, 177 insertions(+), 154 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 74b378ecd3b..043524402d9 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=330 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/hooks/usePluginRoutes.tsx b/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx index c0a8e52bf4d..f27a30cebd6 100644 --- a/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx +++ b/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx @@ -15,27 +15,32 @@ const isRoutePageExtensionActive: IsRouteExtensionActive = (extension, activePer // Cache lazy components by extension UID to prevent recreation on re-renders const lazyComponentCache = new Map>>(); +const getOrCreateLazyComponent = ( + uid: string, + component: () => Promise>, + pluginName: string, +): React.LazyExoticComponent> => { + if (!lazyComponentCache.has(uid)) { + lazyComponentCache.set( + uid, + lazy(async () => { + const Component = await component(); + // Check falsy to determine if the component wasn't loaded + if (!Component) { + throw new Error( + `Plugin "${pluginName}" route component resolved to ${typeof Component} (extension ${uid})`, + ); + } + return { default: Component }; + }), + ); + } + return lazyComponentCache.get(uid); +}; + const LazyRoutePage: FC = ({ extension }) => { const { pluginName, uid, properties } = extension; - const { component } = properties; - const LazyComponent = useMemo(() => { - if (!lazyComponentCache.has(uid)) { - lazyComponentCache.set( - uid, - lazy(async () => { - const Component = await component(); - // Check falsy to determine if the component wasn't loaded - if (!Component) { - throw new Error( - `Plugin "${pluginName}" route component resolved to ${typeof Component} (extension ${uid})`, - ); - } - return { default: Component }; - }), - ); - } - return lazyComponentCache.get(uid); - }, [uid, component, pluginName]); + const LazyComponent = getOrCreateLazyComponent(uid, properties.component, pluginName); return ( }> diff --git a/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx b/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx index 32c82deee8e..38342ee245e 100644 --- a/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx +++ b/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx @@ -161,13 +161,14 @@ const IFrameMarkdownView: FC = ({ }); // eslint-disable-next-line react-hooks/exhaustive-deps - const updateDimensions = useCallback( - _.debounce(() => { - const el = frameRef.current?.contentWindow?.document?.body?.firstElementChild; - if (el) { - setFrameHeight(el.scrollHeight + (exactHeight ? 0 : 15)); - } - }, 100), + const updateDimensions = useMemo( + () => + _.debounce(() => { + const el = frameRef.current?.contentWindow?.document?.body?.firstElementChild; + if (el) { + setFrameHeight(el.scrollHeight + (exactHeight ? 0 : 15)); + } + }, 100), [exactHeight], ); diff --git a/frontend/packages/console-shared/src/components/pod/PodStatus.tsx b/frontend/packages/console-shared/src/components/pod/PodStatus.tsx index e0ce63930f0..098feeb7d5c 100644 --- a/frontend/packages/console-shared/src/components/pod/PodStatus.tsx +++ b/frontend/packages/console-shared/src/components/pod/PodStatus.tsx @@ -1,5 +1,5 @@ import type { ReactElement, FC } from 'react'; -import { useRef, useState, useMemo, memo } from 'react'; +import { useRef, useMemo, memo } from 'react'; import { ChartDonut } from '@patternfly/react-charts/victory'; import { Tooltip } from '@patternfly/react-core'; import * as _ from 'lodash'; @@ -56,7 +56,7 @@ const PodStatusBase: FC = ({ data, }) => { const ref = useRef(); - const [updateOnEnd, setUpdateOnEnd] = useState(false); + const updateOnEndRef = useRef(false); const forceUpdate = useForceUpdate(); const prevVData = useRef(null); @@ -76,7 +76,7 @@ const PodStatusBase: FC = ({ const prevDataPoints = _.size(_.filter(prevVData.current, (nextData) => nextData.y !== 0)); const dataPoints = _.size(_.filter(updateVData, (nextData) => nextData.y !== 0)); - setUpdateOnEnd(dataPoints === 1 && prevDataPoints > 1); + updateOnEndRef.current = dataPoints === 1 && prevDataPoints > 1; if (!_.isEqual(prevVData.current, updateVData)) { prevVData.current = updateVData; @@ -95,7 +95,7 @@ const PodStatusBase: FC = ({ ariaTitle={`${title}${subTitle && ` ${subTitle}`}`} animate={{ duration: prevVData.current ? ANIMATION_DURATION : 0, - onEnd: updateOnEnd ? forceUpdate : undefined, + onEnd: updateOnEndRef.current ? forceUpdate : undefined, }} standalone={standalone} innerRadius={innerRadius} @@ -133,7 +133,6 @@ const PodStatusBase: FC = ({ subTitleComponent, truncTitle, titleComponent, - updateOnEnd, vData, x, y, diff --git a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx index e89a95a72e9..ac2d1e68f46 100644 --- a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx +++ b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx @@ -1,5 +1,5 @@ import type { FC, Ref, ReactNode, KeyboardEvent, MouseEvent } from 'react'; -import { memo, useState, useEffect, useCallback, useLayoutEffect } from 'react'; +import { memo, useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react'; import { Chart, ChartArea, @@ -116,7 +116,7 @@ const SpanControls = memo( }, [span]); // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedOnChange = useCallback(_.debounce(onChange, 400), [onChange]); + const debouncedOnChange = useMemo(() => _.debounce(onChange, 400), [onChange]); const setSpan = (newText: string, isDebounced = false) => { const newSpan = parsePrometheusDuration(newText); @@ -366,7 +366,7 @@ const Graph = memo( const legendData: { name: string }[] = []; const { t } = useTranslation('console-shared'); - const [xDomain, setXDomain] = useState(fixedXDomain || getXDomain(Date.now(), span)); + const [xDomain, setXDomain] = useState(() => fixedXDomain || getXDomain(Date.now(), span)); // Only update X-axis if the time range (fixedXDomain or span) or graph data (allSeries) change useEffect(() => { diff --git a/frontend/packages/dev-console/src/components/import/devfile/devfileHooks.ts b/frontend/packages/dev-console/src/components/import/devfile/devfileHooks.ts index e8a688c42a2..218ac65674d 100644 --- a/frontend/packages/dev-console/src/components/import/devfile/devfileHooks.ts +++ b/frontend/packages/dev-console/src/components/import/devfile/devfileHooks.ts @@ -27,25 +27,21 @@ export const useDevfileServer = ( const { devfileContent, devfilePath } = devfile || {}; - const devfileDataPromise = useMemo(async () => { + const devfileDataPromise = useMemo(() => { if (!name || !url || !devfileContent) { - return null; + return Promise.resolve(null); } - const newDevfileContent = await convertURItoInlineYAML( - devfileContent, - url, - ref, - dir, - type, - secretResource, + return convertURItoInlineYAML(devfileContent, url, ref, dir, type, secretResource).then( + (newDevfileContent) => ({ + name, + git: { URL: url, ref, dir: prefixDotSlash(dir) }, + devfile: { + devfileContent: newDevfileContent, + devfilePath: `${smartSlashDir}${devfilePath}`, + }, + }), ); - - return { - name, - git: { URL: url, ref, dir: prefixDotSlash(dir) }, - devfile: { devfileContent: newDevfileContent, devfilePath: `${smartSlashDir}${devfilePath}` }, - }; }, [name, url, devfileContent, ref, dir, type, secretResource, smartSlashDir, devfilePath]); useEffect(() => { diff --git a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx index 2d286f99a2b..818a300c0b1 100644 --- a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx +++ b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx @@ -41,6 +41,60 @@ type WebhoookSectionProps = { formContextField?: string; }; +type WebhookHelpTextProps = { + gitProvider: GitProvider; + testId: string; +}; + +const WebhookHelpText: FC = ({ gitProvider, testId }): ReactElement => { + const { t } = useTranslation('devconsole'); + let helpText: ReactNode; + switch (gitProvider) { + case GitProvider.GITHUB: + helpText = ( + + Use your GitHub Personal token. Use this{' '} + link to create + a classic token with repo & admin:repo_hook scopes and give your + token an expiration, i.e 30d. + + ); + break; + + case GitProvider.GITLAB: + helpText = ( + + Use your Gitlab Personal access token. Use this{' '} + link to create + a token with api scope. Select the role as Maintainer/Owner. Give your + token an expiration i.e 30d. + + ); + break; + + case GitProvider.BITBUCKET: + helpText = ( + + Use your Bitbucket App password. Use this{' '} + link to + create a token with Read and Write scopes in{' '} + Account, Workspace membership, Projects, Issues, Pull requests and Webhooks. + + ); + break; + + default: + helpText = ( + + Use your Git Personal token. Create a token with repo, public_repo & admin:repo_hook + scopes and give your token an expiration, i.e 30d. + + ); + } + + return
{helpText}
; +}; + const AccessTokenDocLinks = { [GitProvider.GITHUB]: 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', @@ -116,54 +170,6 @@ const WebhookSection: FC = ({ pac, formContextField }) => } }; - const HelpText = (): ReactElement => { - let helpText: ReactNode; - switch (gitProvider) { - case GitProvider.GITHUB: - helpText = ( - - Use your GitHub Personal token. Use this{' '} - link to - create a classic token with repo & admin:repo_hook scopes and give - your token an expiration, i.e 30d. - - ); - break; - - case GitProvider.GITLAB: - helpText = ( - - Use your Gitlab Personal access token. Use this{' '} - link to - create a token with api scope. Select the role as Maintainer/Owner. Give - your token an expiration i.e 30d. - - ); - break; - - case GitProvider.BITBUCKET: - helpText = ( - - Use your Bitbucket App password. Use this{' '} - link to - create a token with Read and Write scopes in{' '} - Account, Workspace membership, Projects, Issues, Pull requests and Webhooks. - - ); - break; - - default: - helpText = ( - - Use your Git Personal token. Create a token with repo, public_repo & admin:repo_hook - scopes and give your token an expiration, i.e 30d. - - ); - } - - return
{helpText}
; - }; - return ( {gitProvider && gitProvider === GitProvider.BITBUCKET ? ( @@ -200,7 +206,7 @@ const WebhookSection: FC = ({ pac, formContextField }) => } + helpText={} required /> ), diff --git a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx index 8ac8a138d1c..7dc961f6da6 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx @@ -205,12 +205,53 @@ export const CatalogSourceDetailsPage: FC = (props) => { ); }; -export const CreateSubscriptionYAML: FC = () => { - type CreateProps = { - packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: unknown }; - operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: unknown }; - }; +type CreateSubscriptionProps = { + packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: unknown }; + operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: unknown }; +}; + +const CreateSubscriptionContent: FC = (createProps) => { + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + + if (createProps.packageManifest.loaded && createProps.packageManifest.data) { + const pkg = createProps.packageManifest.data; + const channel = pkg.status.defaultChannel + ? pkg.status.channels.find(({ name }) => name === pkg.status.defaultChannel) + : pkg.status.channels[0]; + + const template = ` + apiVersion: ${SubscriptionModel.apiGroup}/${SubscriptionModel.apiVersion} + kind: ${SubscriptionModel.kind}, + metadata: + generateName: ${pkg.metadata.name}- + namespace: default + spec: + source: ${searchParams.get('catalog')} + sourceNamespace: ${searchParams.get('catalogNamespace')} + name: ${pkg.metadata.name} + startingCSV: ${channel.currentCSV} + channel: ${channel.name} + `; + return ; + } + return ; +}; + +const CreateSubscriptionFallback: FC = () => { const { t } = useTranslation('olm'); + return ( + + {t('Cannot create a Subscription to a non-existent package.')} + + ); +}; + +const CreateSubscription = requireOperatorGroup( + withFallback(CreateSubscriptionContent, CreateSubscriptionFallback), +); + +export const CreateSubscriptionYAML: FC = () => { const params = useParams(); const location = useLocation(); const searchParams = new URLSearchParams(location.search); @@ -232,42 +273,11 @@ export const CreateSubscriptionYAML: FC = () => { }, }); - const Create = requireOperatorGroup( - withFallback( - (createProps) => { - if (createProps.packageManifest.loaded && createProps.packageManifest.data) { - const pkg = createProps.packageManifest.data; - const channel = pkg.status.defaultChannel - ? pkg.status.channels.find(({ name }) => name === pkg.status.defaultChannel) - : pkg.status.channels[0]; - - const template = ` - apiVersion: ${SubscriptionModel.apiGroup}/${SubscriptionModel.apiVersion} - kind: ${SubscriptionModel.kind}, - metadata: - generateName: ${pkg.metadata.name}- - namespace: default - spec: - source: ${searchParams.get('catalog')} - sourceNamespace: ${searchParams.get('catalogNamespace')} - name: ${pkg.metadata.name} - startingCSV: ${channel.currentCSV} - channel: ${channel.name} - `; - return ; - } - return ; - }, - () => ( - - {t('Cannot create a Subscription to a non-existent package.')} - - ), - ), - ); - return ( - + ); }; diff --git a/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx b/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx index ce8ffeb7499..a66e8ee158a 100644 --- a/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx +++ b/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useRef, useState, useCallback, useEffect } from 'react'; +import { useRef, useState, useCallback, useEffect, useMemo } from 'react'; import { Alert } from '@patternfly/react-core'; import { Base64 } from 'js-base64'; import { throttle } from 'lodash'; @@ -41,16 +41,17 @@ const Logs: FC = ({ onCompleteRef.current = onComplete; // eslint-disable-next-line react-hooks/exhaustive-deps - const addContentAndScroll = useCallback( - throttle(() => { - if (contentRef.current) { - contentRef.current.innerText += blockContentRef.current; - } - if (scrollToRef.current) { - scrollToRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); - } - blockContentRef.current = ''; - }, 1000), + const addContentAndScroll = useMemo( + () => + throttle(() => { + if (contentRef.current) { + contentRef.current.innerText += blockContentRef.current; + } + if (scrollToRef.current) { + scrollToRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + blockContentRef.current = ''; + }, 1000), [], ); diff --git a/frontend/public/components/build.tsx b/frontend/public/components/build.tsx index f295d2529bb..27a15bbb683 100644 --- a/frontend/public/components/build.tsx +++ b/frontend/public/components/build.tsx @@ -92,9 +92,13 @@ export const BuildNumberLink = ({ build }) => { const BuildMetrics = ({ obj }) => { const { t } = useTranslation('public'); const podName = obj.metadata.annotations?.['openshift.io/build.pod-name']; - const endTime = obj.status.completionTimestamp - ? new Date(obj.status.completionTimestamp).getTime() - : Date.now(); + const endTime = useMemo( + () => + obj.status.completionTimestamp + ? new Date(obj.status.completionTimestamp).getTime() + : Date.now(), + [obj.status.completionTimestamp], + ); const runTime = obj.status.startTimestamp ? endTime - new Date(obj.status.startTimestamp).getTime() : ONE_HOUR; diff --git a/frontend/public/components/graphs/area.tsx b/frontend/public/components/graphs/area.tsx index 8d11a9e158d..cc58e8f57be 100644 --- a/frontend/public/components/graphs/area.tsx +++ b/frontend/public/components/graphs/area.tsx @@ -196,7 +196,7 @@ export const AreaChart: FC = ({ }; export const Area: FC = ({ - endTime = Date.now(), + endTime: endTimeProp, namespace, query, limitQuery, @@ -206,6 +206,7 @@ export const Area: FC = ({ timespan = DEFAULT_PROMETHEUS_TIMESPAN, ...rest }) => { + const endTime = useMemo(() => endTimeProp ?? Date.now(), [endTimeProp]); const prometheusPollProps = { endpoint: PrometheusEndpoint.QUERY_RANGE, endTime, From 2a36ff854e1a85ce27d9eb9689305ddac88b45b2 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Tue, 25 Aug 2026 16:29:59 +0000 Subject: [PATCH 2/4] fix(webhook): resolve lint errors in WebhookSection OCPBUGS-113745 - Move AccessTokenDocLinks above WebhookHelpText to fix no-use-before-define - Derive controllerUrl via useMemo instead of useState+useEffect to fix set-state-in-effect - Fix prettier formatting for GitLab help text and WebhookHelpText JSX --- .../pipeline/WebhookSection.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx index 818a300c0b1..e6c30b68ff1 100644 --- a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx +++ b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx @@ -41,6 +41,13 @@ type WebhoookSectionProps = { formContextField?: string; }; +const AccessTokenDocLinks = { + [GitProvider.GITHUB]: + 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', + [GitProvider.GITLAB]: 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html', + [GitProvider.BITBUCKET]: 'https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/', +}; + type WebhookHelpTextProps = { gitProvider: GitProvider; testId: string; @@ -66,8 +73,8 @@ const WebhookHelpText: FC = ({ gitProvider, testId }): Rea Use your Gitlab Personal access token. Use this{' '} link to create - a token with api scope. Select the role as Maintainer/Owner. Give your - token an expiration i.e 30d. + a token with api scope. Select the role as Maintainer/Owner. Give your token + an expiration i.e 30d. ); break; @@ -95,13 +102,6 @@ const WebhookHelpText: FC = ({ gitProvider, testId }): Rea return
{helpText}
; }; -const AccessTokenDocLinks = { - [GitProvider.GITHUB]: - 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', - [GitProvider.GITLAB]: 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html', - [GitProvider.BITBUCKET]: 'https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/', -}; - const WebhookDocLinks = { [GitProvider.GITHUB]: 'https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks', @@ -115,17 +115,15 @@ const WebhookSection: FC = ({ pac, formContextField }) => const { values, setFieldValue } = useFormikContext(); const fieldPrefix = formContextField ? `${formContextField}.` : ''; const { gitProvider, webhook } = _.get(values, formContextField) || values; - const [controllerUrl, setControllerUrl] = useState(''); + const controllerUrl = useMemo(() => pac?.data?.['controller-url'] || '', [pac]); const [webhookSecret, setWebhookSecret] = useState(''); const { t } = useTranslation('devconsole'); useEffect(() => { - const ctlUrl = pac?.data?.['controller-url']; - if (ctlUrl) { - setControllerUrl(ctlUrl); - setFieldValue(`${fieldPrefix}webhook.url`, ctlUrl); + if (controllerUrl) { + setFieldValue(`${fieldPrefix}webhook.url`, controllerUrl); } - }, [fieldPrefix, pac, setFieldValue]); + }, [fieldPrefix, controllerUrl, setFieldValue]); const autocompleteFilter = (text: string, item: any): boolean => fuzzy(text, item?.props?.name); @@ -206,7 +204,12 @@ const WebhookSection: FC = ({ pac, formContextField }) => } + helpText={ + + } required /> ), From 7fc5786caea3b46dce6b192f3b6a153d630d2fb5 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Tue, 25 Aug 2026 20:54:13 +0000 Subject: [PATCH 3/4] fix(review): address PR review feedback OCPBUGS-113745 Remove unnecessary eslint-disable comments, use interface instead of type alias for CreateSubscriptionProps, import LazyExoticComponent directly and use ComponentType over ComponentType. Co-Authored-By: Claude Opus 4.6 --- frontend/packages/console-app/src/hooks/usePluginRoutes.tsx | 6 +++--- .../src/components/query-browser/QueryBrowser.tsx | 1 - .../src/components/catalog-source.tsx | 4 ++-- .../packages/shipwright-plugin/src/components/logs/Logs.tsx | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx b/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx index f27a30cebd6..4de69d17519 100644 --- a/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx +++ b/frontend/packages/console-app/src/hooks/usePluginRoutes.tsx @@ -1,4 +1,4 @@ -import type { FC, ReactElement, ComponentType } from 'react'; +import type { FC, ReactElement, ComponentType, LazyExoticComponent } from 'react'; import { useMemo, lazy, useEffect, Suspense } from 'react'; import type { RouteProps } from 'react-router'; import { createPath, Route, useLocation } from 'react-router'; @@ -13,13 +13,13 @@ const isRoutePageExtensionActive: IsRouteExtensionActive = (extension, activePer (extension.properties.perspective ?? activePerspective) === activePerspective; // Cache lazy components by extension UID to prevent recreation on re-renders -const lazyComponentCache = new Map>>(); +const lazyComponentCache = new Map>>(); const getOrCreateLazyComponent = ( uid: string, component: () => Promise>, pluginName: string, -): React.LazyExoticComponent> => { +): LazyExoticComponent> => { if (!lazyComponentCache.has(uid)) { lazyComponentCache.set( uid, diff --git a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx index ac2d1e68f46..e14c75d9ad0 100644 --- a/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx +++ b/frontend/packages/console-shared/src/components/query-browser/QueryBrowser.tsx @@ -115,7 +115,6 @@ const SpanControls = memo( setText(formatPrometheusDuration(span)); }, [span]); - // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedOnChange = useMemo(() => _.debounce(onChange, 400), [onChange]); const setSpan = (newText: string, isDebounced = false) => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx index 7dc961f6da6..ff0ee28f2d7 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx @@ -205,10 +205,10 @@ export const CatalogSourceDetailsPage: FC = (props) => { ); }; -type CreateSubscriptionProps = { +interface CreateSubscriptionProps { packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: unknown }; operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: unknown }; -}; +} const CreateSubscriptionContent: FC = (createProps) => { const location = useLocation(); diff --git a/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx b/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx index a66e8ee158a..737cd418c70 100644 --- a/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx +++ b/frontend/packages/shipwright-plugin/src/components/logs/Logs.tsx @@ -40,7 +40,6 @@ const Logs: FC = ({ const blockContentRef = useRef(''); onCompleteRef.current = onComplete; - // eslint-disable-next-line react-hooks/exhaustive-deps const addContentAndScroll = useMemo( () => throttle(() => { From 86ca0334c24ce4532ddc71149196d1c1a854bde4 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Wed, 26 Aug 2026 07:08:18 +0000 Subject: [PATCH 4/4] fix(review): address PR feedback and fix CI lint failure OCPBUGS-113745 - Replace useState+useEffect with useMemo for derived webhookTriggers state in webhooks.tsx (fixes react-hooks/set-state-in-effect warning that broke ci/prow/frontend) - Change type alias to interface for WebhookHelpTextProps - Replace tags with in WebhookSection help text - Remove unnecessary eslint-disable in MarkdownView.tsx - Fix PodStatus ref mutation during render: derive updateOnEnd from useMemo return value instead of mutating updateOnEndRef.current --- .../src/components/markdown/MarkdownView.tsx | 1 - .../console-shared/src/components/pod/PodStatus.tsx | 12 ++++++------ .../pipeline-section/pipeline/WebhookSection.tsx | 12 ++++++------ frontend/public/components/utils/webhooks.tsx | 12 ++++-------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx b/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx index 38342ee245e..14882a7a7e8 100644 --- a/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx +++ b/frontend/packages/console-shared/src/components/markdown/MarkdownView.tsx @@ -160,7 +160,6 @@ const IFrameMarkdownView: FC = ({ [THEME_GLASS_CLASS]: contrast === THEME_GLASS, }); - // eslint-disable-next-line react-hooks/exhaustive-deps const updateDimensions = useMemo( () => _.debounce(() => { diff --git a/frontend/packages/console-shared/src/components/pod/PodStatus.tsx b/frontend/packages/console-shared/src/components/pod/PodStatus.tsx index 098feeb7d5c..12d641ba5df 100644 --- a/frontend/packages/console-shared/src/components/pod/PodStatus.tsx +++ b/frontend/packages/console-shared/src/components/pod/PodStatus.tsx @@ -56,11 +56,10 @@ const PodStatusBase: FC = ({ data, }) => { const ref = useRef(); - const updateOnEndRef = useRef(false); const forceUpdate = useForceUpdate(); const prevVData = useRef(null); - const vData = useMemo(() => { + const { vData, updateOnEnd } = useMemo(() => { const updateVData: PodData[] = podStatus.map((pod) => ({ x: pod, y: _.sumBy(data, (d) => +(getPodStatus(d) === pod)) || 0, @@ -76,13 +75,13 @@ const PodStatusBase: FC = ({ const prevDataPoints = _.size(_.filter(prevVData.current, (nextData) => nextData.y !== 0)); const dataPoints = _.size(_.filter(updateVData, (nextData) => nextData.y !== 0)); - updateOnEndRef.current = dataPoints === 1 && prevDataPoints > 1; + const shouldUpdateOnEnd = dataPoints === 1 && prevDataPoints > 1; if (!_.isEqual(prevVData.current, updateVData)) { prevVData.current = updateVData; - return updateVData; + return { vData: updateVData, updateOnEnd: shouldUpdateOnEnd }; } - return prevVData.current; + return { vData: prevVData.current, updateOnEnd: shouldUpdateOnEnd }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); const truncTitle = title ? _.truncate(title, { length: MAX_POD_TITLE_LENGTH }) : undefined; @@ -95,7 +94,7 @@ const PodStatusBase: FC = ({ ariaTitle={`${title}${subTitle && ` ${subTitle}`}`} animate={{ duration: prevVData.current ? ANIMATION_DURATION : 0, - onEnd: updateOnEndRef.current ? forceUpdate : undefined, + onEnd: updateOnEnd ? forceUpdate : undefined, }} standalone={standalone} innerRadius={innerRadius} @@ -133,6 +132,7 @@ const PodStatusBase: FC = ({ subTitleComponent, truncTitle, titleComponent, + updateOnEnd, vData, x, y, diff --git a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx index e6c30b68ff1..2eeff675b91 100644 --- a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx +++ b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx @@ -48,10 +48,10 @@ const AccessTokenDocLinks = { [GitProvider.BITBUCKET]: 'https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/', }; -type WebhookHelpTextProps = { +interface WebhookHelpTextProps { gitProvider: GitProvider; testId: string; -}; +} const WebhookHelpText: FC = ({ gitProvider, testId }): ReactElement => { const { t } = useTranslation('devconsole'); @@ -62,7 +62,7 @@ const WebhookHelpText: FC = ({ gitProvider, testId }): Rea Use your GitHub Personal token. Use this{' '} link to create - a classic token with repo & admin:repo_hook scopes and give your + a classic token with repo & admin:repo_hook scopes and give your token an expiration, i.e 30d. ); @@ -73,7 +73,7 @@ const WebhookHelpText: FC = ({ gitProvider, testId }): Rea Use your Gitlab Personal access token. Use this{' '} link to create - a token with api scope. Select the role as Maintainer/Owner. Give your token + a token with api scope. Select the role as Maintainer/Owner. Give your token an expiration i.e 30d. ); @@ -84,8 +84,8 @@ const WebhookHelpText: FC = ({ gitProvider, testId }): Rea Use your Bitbucket App password. Use this{' '} link to - create a token with Read and Write scopes in{' '} - Account, Workspace membership, Projects, Issues, Pull requests and Webhooks. + create a token with Read and Write scopes in{' '} + Account, Workspace membership, Projects, Issues, Pull requests and Webhooks. ); break; diff --git a/frontend/public/components/utils/webhooks.tsx b/frontend/public/components/utils/webhooks.tsx index 50cd9cfc427..a0b53d16bdb 100644 --- a/frontend/public/components/utils/webhooks.tsx +++ b/frontend/public/components/utils/webhooks.tsx @@ -64,17 +64,13 @@ export const WebhookTriggers: FC = (props) => { }); const tableColumnClasses = getTableColumnClasses(canGetSecret); const [webhookSecrets, setWebhookSecrets] = useState([]); - const [webhookTriggers, setWebhookTriggers] = useState([]); + const webhookTriggers = useMemo( + () => _.filter(triggers, ({ type }) => webhookTriggerTypes.has(type)), + [triggers], + ); const [secretErrors, setSecretErrors] = useState([]); const [isLoaded, setLoaded] = useState(false); - useEffect(() => { - setWebhookTriggers((previousTriggers) => { - const newTriggers = _.filter(triggers, ({ type }) => webhookTriggerTypes.has(type)); - return _.isEqual(previousTriggers, newTriggers) ? previousTriggers : newTriggers; - }); - }, [triggers]); - const secretNames = useMemo( () => _.uniq(