diff --git a/frontend/packages/console-app/src/components/detect-namespace/namespace.ts b/frontend/packages/console-app/src/components/detect-namespace/namespace.ts index b5dad0137b5..7cc67e49d98 100644 --- a/frontend/packages/console-app/src/components/detect-namespace/namespace.ts +++ b/frontend/packages/console-app/src/components/detect-namespace/namespace.ts @@ -1,4 +1,4 @@ -import { createContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useState, useEffect, useCallback, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { setActiveNamespace as setActiveNamespaceForStore, @@ -37,17 +37,27 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => const useProjects: boolean = useFlag(FLAGS.OPENSHIFT); const dispatch = useConsoleDispatch(); - const updateNamespace = (ns) => { - if (ns !== activeNamespace) { - setActiveNamespace(ns); - const oldPath = window.location.pathname; - const newPath = formatNamespaceRoute(ns, oldPath, window.location); - if (newPath !== oldPath) { - navigate(newPath); + const activeNamespaceRef = useRef(activeNamespace); + activeNamespaceRef.current = activeNamespace; + const navigateRef = useRef(navigate); + navigateRef.current = navigate; + const lastNamespaceRef = useRef(lastNamespace); + lastNamespaceRef.current = lastNamespace; + + const updateNamespace = useCallback( + (ns: string) => { + if (ns !== activeNamespaceRef.current) { + setActiveNamespace(ns); + const oldPath = window.location.pathname; + const newPath = formatNamespaceRoute(ns, oldPath, window.location); + if (newPath !== oldPath) { + navigateRef.current(newPath); + } } - } - dispatch(setActiveNamespaceForStore(ns)); - }; + dispatch(setActiveNamespaceForStore(ns)); + }, + [dispatch], + ); // Set namespace when all pending namespace infos are loaded. // Automatically check if preferred and last namespace still exist. @@ -55,7 +65,12 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => !flagPending(useProjects) && preferredNamespaceLoaded && lastNamespaceLoaded; useEffect(() => { if (!urlNamespace && resourcesLoaded) { - getValueForNamespace(preferredNamespace, lastNamespace, useProjects, activeNamespace) + getValueForNamespace( + preferredNamespace, + lastNamespace, + useProjects, + activeNamespaceRef.current, + ) .then((ns: string) => { updateNamespace(ns); }) @@ -64,9 +79,14 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => console.warn('Error fetching namespace', e); }); } - // Only run this hook after all resources have loaded. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resourcesLoaded, navigate]); + }, [ + resourcesLoaded, + urlNamespace, + preferredNamespace, + lastNamespace, + useProjects, + updateNamespace, + ]); // Updates active namespace (in context and redux state) when the url changes. // This updates the redux state also after the initial rendering. @@ -74,18 +94,18 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => if (urlNamespace) { updateNamespace(urlNamespace); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlNamespace, activeNamespace, dispatch, navigate]); + }, [urlNamespace, updateNamespace]); // Change active namespace (in context and redux state) as well as last used namespace // when a component calls setNamespace, for example via useActiveNamespace() const setNamespace = useCallback( (ns: string) => { - ns !== lastNamespace && setLastNamespace(ns); + if (ns !== lastNamespaceRef.current) { + setLastNamespace(ns); + } updateNamespace(ns); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dispatch, activeNamespace, setActiveNamespace, lastNamespace, setLastNamespace, navigate], + [updateNamespace, setLastNamespace], ); return { diff --git a/frontend/packages/console-app/src/components/nav/NavHeader.tsx b/frontend/packages/console-app/src/components/nav/NavHeader.tsx index 14641adb0d3..6df73064db4 100644 --- a/frontend/packages/console-app/src/components/nav/NavHeader.tsx +++ b/frontend/packages/console-app/src/components/nav/NavHeader.tsx @@ -1,11 +1,12 @@ import type { FC, MouseEvent, Ref } from 'react'; -import { useMemo, lazy, useState, useCallback, Suspense } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import type { MenuToggleElement } from '@patternfly/react-core'; import { MenuToggle, Select, SelectList, SelectOption, Title } from '@patternfly/react-core'; import { CogsIcon } from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; import { t } from 'i18next'; import type { Perspective } from '@console/dynamic-plugin-sdk'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; +import { AsyncComponent } from '@console/internal/components/utils/async'; import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; export type NavHeaderProps = { @@ -19,8 +20,9 @@ type PerspectiveDropdownItemProps = { onClick: (perspective: string) => void; }; +const IconLoadingComponent: FC = () => <> ; + const PerspectiveDropdownItem: FC = ({ perspective, onClick }) => { - const LazyIcon = useMemo(() => lazy(perspective.properties.icon), [perspective.properties.icon]); return ( = ({ perspective onClick(perspective.properties.id); }} icon={ -  }> - - + perspective.properties.icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> } > @@ -75,8 +78,6 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { [activePerspective, perspectiveExtensions], ); - const LazyIcon = useMemo(() => icon && lazy(icon), [icon]); - return perspectiveDropdownItems.length > 1 ? ( <div className="oc-nav-header" @@ -94,7 +95,14 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { isExpanded={isPerspectiveDropdownOpen} ref={toggleRef} onClick={() => togglePerspectiveOpen()} - icon={<LazyIcon />} + icon={ + icon && ( + <AsyncComponent + loader={() => icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> + ) + } > {name && ( <Title headingLevel="h2" size="md"> diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx index db04dfb47f5..c03dbf2ce3d 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, useEffect } from 'react'; import * as _ from 'lodash'; import type { NodeKind } from '@console/internal/module/k8s'; import Dashboard from '@console/shared/src/components/dashboard/Dashboard'; @@ -77,9 +77,9 @@ export const reducer = (state: NodeDashboardState, action: NodeDashboardAction) const NodeDashboard: FC<NodeDashboardProps> = ({ obj }) => { const [state, dispatch] = useReducer(reducer, initialState(obj)); - if (obj !== state.obj) { + useEffect(() => { dispatch({ type: ActionType.OBJ, payload: obj }); - } + }, [obj]); const setCPULimit = useCallback( (payload: LimitRequested) => dispatch({ type: ActionType.CPU_LIMIT, payload }), diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx index f88b2fccb93..67e5efba05b 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react'; -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; import { Gallery, GalleryItem, Alert, Stack, StackItem } from '@patternfly/react-core'; import i18next from 'i18next'; import * as _ from 'lodash'; @@ -272,10 +272,12 @@ export const HealthChecksItem: FC<HealthChecksItemProps> = ({ disabledAlert }) = return true; }); - setHealthCheck({ - failingHealthCheck, - reboot, - }); + useEffect(() => { + setHealthCheck({ + failingHealthCheck, + reboot, + }); + }, [failingHealthCheck, reboot, setHealthCheck]); return ( <HealthItem diff --git a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx index 5e656d95a8f..f39c4635cb1 100644 --- a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx +++ b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx @@ -47,11 +47,10 @@ const UserPreferencePage: FC = () => { // fetch the default user preference group from the url params if available const { group: groupIdFromUrl } = useParams(); - const initialTabId = + const activeTabId = sortedUserPreferenceGroups.find((extension) => extension.id === groupIdFromUrl)?.id || sortedUserPreferenceGroups[0]?.id || 'general'; - const [activeTabId, setActiveTabId] = useState<string>(initialTabId); const [userPreferenceTabs, userPreferenceTabContents] = useMemo< [ReactElement<TabProps, JSXElementConstructor<TabProps>>[], ReactElement<TabContentProps>[]] @@ -102,12 +101,11 @@ const UserPreferencePage: FC = () => { const [spotlightElement, setSpotlightElement] = useState<Element | null>(null); useEffect(() => { - setActiveTabId(groupIdFromUrl ?? 'general'); if (spotlight) { const element = document.querySelector(spotlight); setSpotlightElement(element); } - }, [groupIdFromUrl, spotlight, userPreferenceItemResolved, userPreferenceTabContents]); + }, [spotlight, userPreferenceItemResolved, userPreferenceTabContents]); // utils and callbacks const handleTabClick = (event: MouseEvent<HTMLElement>, eventKey: string) => { @@ -115,7 +113,6 @@ const UserPreferencePage: FC = () => { return; } event.preventDefault(); - setActiveTabId(eventKey); navigate(`${USER_PREFERENCES_BASE_URL}/${eventKey}`, { replace: true }); }; const activeTab = sortedUserPreferenceGroups.find((group) => group.id === activeTabId)?.label; diff --git a/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx b/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx index 29b1515c7cb..7467b96149c 100644 --- a/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx @@ -247,10 +247,15 @@ export const UtilizationItem = memo<UtilizationItemProps>( } else if ([limitState, requestedState].includes(LIMIT_STATE.WARN)) { LimitIcon = YellowExclamationTriangleIcon; } - setLimitReqState && setLimitReqState({ limit: limitState, requested: requestedState }); } } + useEffect(() => { + if (setLimitReqState) { + setLimitReqState({ limit: limitState, requested: requestedState }); + } + }, [limitState, requestedState, setLimitReqState]); + const currentHumanized = !_.isNil(current) ? humanizeValue(current).string : null; return ( diff --git a/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx b/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx index 56022ddbf69..4927a832535 100644 --- a/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx +++ b/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx @@ -23,6 +23,7 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref null, ); const [usesValue] = useState<boolean>(value !== undefined); + const [mounted, setMounted] = useState(false); const shortcutPopover = useShortcutPopover(props.shortcutsPopoverProps); @@ -47,6 +48,7 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref } onSave && editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, onSave); // eslint-disable-line no-bitwise onEditorDidMount && onEditorDidMount(editor, monaco); + setMounted(true); }, [onSave, usesValue, onEditorDidMount], ); @@ -103,7 +105,11 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref }, [handleResize, minHeight, ToolbarLinks]); return ( - <div style={{ minHeight }} className="ocs-yaml-editor"> + <div + style={{ minHeight }} + className="ocs-yaml-editor" + data-test={mounted ? 'code-editor' : 'code-editor-mounting'} + > <BasicCodeEditor {...props} language={props.language ?? Language.yaml} diff --git a/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts b/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts index 9f72300abda..2da7e68a274 100644 --- a/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts @@ -53,8 +53,7 @@ describe('Alertmanager', () => { }); it('displays the Alertmanager YAML page and saves Alertmanager YAML', () => { - alertmanager.visitAlertmanagerPage(); - detailsPage.selectTab('YAML'); + alertmanager.visitYAMLPage(); yamlEditor.isLoaded(); cy.byTestID('alert-success').should('not.exist'); yamlEditor.clickSaveCreateButton(); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts index 6eaf706835d..421005da8e4 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts @@ -56,6 +56,7 @@ const createExampleRoles = () => { const createExampleRoleBindings = () => { cy.log('create RoleBindings instance'); nav.sidenav.clickNavLink(['User Management', 'RoleBindings']); + listPage.titleShouldHaveText('RoleBindings'); listPage.dvRows.shouldBeLoaded(); listPage.clickCreateYAMLbutton(); roleBindings.titleShouldHaveText('Create RoleBinding'); @@ -68,6 +69,7 @@ const createExampleRoleBindings = () => { cy.log('create ClusterRoleBindings instance'); nav.sidenav.clickNavLink(['User Management', 'RoleBindings']); + listPage.titleShouldHaveText('RoleBindings'); listPage.dvRows.shouldBeLoaded(); listPage.clickCreateYAMLbutton(); roleBindings.titleShouldHaveText('Create RoleBinding'); @@ -122,6 +124,7 @@ describe('Roles and RoleBindings', () => { nav.sidenav.clickNavLink(['User Management', 'Roles']); listPage.dvRows.shouldBeLoaded(); projectDropdown.selectProject(testName); + listPage.dvRows.shouldBeLoaded(); listPage.dvFilter.byName(roleName); listPage.dvRows.clickRowByName(roleName); detailsPage.isLoaded(); diff --git a/frontend/packages/integration-tests-cypress/views/alertmanager.ts b/frontend/packages/integration-tests-cypress/views/alertmanager.ts index cba660b292b..3a898475e53 100644 --- a/frontend/packages/integration-tests-cypress/views/alertmanager.ts +++ b/frontend/packages/integration-tests-cypress/views/alertmanager.ts @@ -5,7 +5,6 @@ import type { AlertmanagerConfig, AlertmanagerReceiver, } from '@console/internal/components/monitoring/alertmanager/alertmanager-config'; -import { detailsPage } from './details-page'; import { listPage } from './list-page'; import * as yamlEditor from './yaml-editor'; @@ -87,7 +86,7 @@ export const alertmanager = { cy.visit(`/settings/cluster/alertmanagerconfig/receivers/${receiverName}/edit`); }, visitYAMLPage: () => { - detailsPage.selectTab('YAML'); + cy.visit('/settings/cluster/alertmanageryaml'); yamlEditor.isLoaded(); }, }; diff --git a/frontend/packages/integration-tests-cypress/views/common.ts b/frontend/packages/integration-tests-cypress/views/common.ts index 2ec30169af5..ab12b8eb485 100644 --- a/frontend/packages/integration-tests-cypress/views/common.ts +++ b/frontend/packages/integration-tests-cypress/views/common.ts @@ -10,13 +10,13 @@ export const selectActionsMenuOption = (actionsMenuOption: string) => { export const projectDropdown = { shouldExist: () => cy.byLegacyTestID('namespace-bar-dropdown').should('exist'), selectProject: (projectName: string) => { - // TODO - remove and fix properly - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(3000); cy.reload(); cy.byLegacyTestID('namespace-bar-dropdown').contains('Project:').click(); cy.byTestID('showSystemSwitch').check(); cy.byTestID('dropdown-menu-item-link').contains(projectName).click(); + // TODO - remove and fix properly + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000); }, shouldContain: (name: string) => // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/frontend/packages/integration-tests-cypress/views/details-page.ts b/frontend/packages/integration-tests-cypress/views/details-page.ts index 69d700ddd3b..812ed0904d7 100644 --- a/frontend/packages/integration-tests-cypress/views/details-page.ts +++ b/frontend/packages/integration-tests-cypress/views/details-page.ts @@ -19,7 +19,7 @@ export const detailsPage = { }, breadcrumb: (breadcrumbIndex: number) => cy.byLegacyTestID(`breadcrumb-link-${breadcrumbIndex}`), selectTab: (name: string) => { - cy.get(`a[data-test-id="horizontal-link-${name}"]`).should('exist').click(); + cy.byLegacyTestID(`horizontal-link-${name}`).should('exist').click(); }, }; diff --git a/frontend/packages/integration-tests-cypress/views/labels.ts b/frontend/packages/integration-tests-cypress/views/labels.ts index 0f748715635..7e1a73d2fca 100644 --- a/frontend/packages/integration-tests-cypress/views/labels.ts +++ b/frontend/packages/integration-tests-cypress/views/labels.ts @@ -1,5 +1,5 @@ export const labels = { - inputLabel: (label: string) => cy.byTestID('tags-input').type(label), + inputLabel: (label: string) => cy.byTestID('tags-input').type(`${label}{enter}`), confirmDetailsPageLabelExists: (label: string) => cy.byTestID('label-key').contains(label), clickDetailsPageLabel: () => cy.byTestID('label-key').click(), chipExists: (label: string) => cy.get('#search-toolbar').contains(label).should('exist'), diff --git a/frontend/packages/integration-tests-cypress/views/list-page.ts b/frontend/packages/integration-tests-cypress/views/list-page.ts index 7cf97dcb4aa..17bcbe9dadc 100644 --- a/frontend/packages/integration-tests-cypress/views/list-page.ts +++ b/frontend/packages/integration-tests-cypress/views/list-page.ts @@ -62,13 +62,17 @@ export const listPage = { cy.get('.pf-v6-c-menu-toggle').first().click(), ); cy.get('.pf-v6-c-menu__list-item').contains('Name').click(); - cy.get('[aria-label="Filter by name"]').clear().type(name); + cy.get('[aria-label="Filter by name"]').should('be.enabled').clear(); + cy.get('[aria-label="Filter by name"]').type(name); }, by: (checkboxLabel: string) => { + // Wait for list data to settle before opening the filter, otherwise a + // concurrent re-render (e.g. after a project switch) closes the menu. + cy.byTestID('data-view-table').should('be.visible'); cy.get('[data-ouia-component-id="DataViewCheckboxFilter"]').click(); - cy.get( - `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`, - ).click(); + cy.get(`[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`) + .should('be.visible') + .click(); cy.url().should('include', `=${checkboxLabel}`); cy.get('[data-ouia-component-id="DataViewCheckboxFilter"]').click(); }, diff --git a/frontend/packages/integration-tests-cypress/views/secret.ts b/frontend/packages/integration-tests-cypress/views/secret.ts index e020c0ebab3..4145663a0cf 100644 --- a/frontend/packages/integration-tests-cypress/views/secret.ts +++ b/frontend/packages/integration-tests-cypress/views/secret.ts @@ -46,8 +46,7 @@ export const secrets = { clickRevealValues: () => { // Wait for page to fully stabilize cy.byTestID('loading-indicator', { timeout: 5000 }).should('not.exist'); - // Click reveal-values button with force to handle re-renders - cy.byTestID('reveal-values', { timeout: 30000 }).should('be.visible').click({ force: true }); + cy.byTestID('reveal-values', { timeout: 30000 }).should('be.visible').click(); // Wait for data to be revealed cy.byTestID('secret-data', { timeout: 10000 }).should('be.visible'); }, diff --git a/frontend/packages/integration-tests-cypress/views/yaml-editor.ts b/frontend/packages/integration-tests-cypress/views/yaml-editor.ts index 5d460be35fd..938936d4386 100644 --- a/frontend/packages/integration-tests-cypress/views/yaml-editor.ts +++ b/frontend/packages/integration-tests-cypress/views/yaml-editor.ts @@ -20,11 +20,11 @@ export const setEditorContent = (text: string) => { }); }; -// initially yamlEditor loads with all grey text, finished loading when editor is color coded -// class='mtk27' is the light green color of property such as 'apiVersion' -export const isLoaded = () => cy.get("[class='mtk27']").should('exist'); -// since yaml editor class mtk27 is a font class it doesn't work on an import page with no text -// adding a check for the 1st line number, AND providing a wait allowed the load of the full component +// CodeEditor sets data-test="code-editor" once the Monaco editor has mounted. +// Before that it is "code-editor-mounting", so this check waits for the editor to be ready. +export const isLoaded = () => cy.byTestID('code-editor').should('exist'); +// The code editor check doesn't work on an import page with no text, +// so we check for the monaco textarea and wait for the component to fully load export const isImportLoaded = () => { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(5000); diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 22e7caef504..035a80c1b16 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -11,7 +11,7 @@ import { useMemo, } from 'react'; import type { FC, Provider as ProviderComponent, ReactNode } from 'react'; -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { linkify } from 'react-linkify'; import { Provider } from 'react-redux'; @@ -253,7 +253,7 @@ const App: FC<{ }; const content = ( - <> + <Suspense fallback={<LoadingBox blame="App content suspense" />}> <ConsoleNotifier location="BannerTop" /> <QuickStartDrawer> <CloudShellDrawer> @@ -308,7 +308,7 @@ const App: FC<{ </QuickStartDrawer> <ConsoleNotifier location="BannerBottom" /> <FeatureFlagExtensionLoader /> - </> + </Suspense> ); return ( @@ -352,7 +352,8 @@ const AppWithExtensions: FC = () => { return <LoadingBox blame="AppWithExtensions" />; }; -render(<LoadingBox blame="Init" />, document.getElementById('app')); +const root = createRoot(document.getElementById('app')!); +root.render(<LoadingBox blame="Init" />); const AppRouter: FC = () => { const standaloneRouteExtensions = useExtensions(isStandaloneRoutePage); @@ -525,7 +526,7 @@ graphQLReady.onReady(() => { } } - render( + root.render( <Suspense fallback={<LoadingBox blame="Root suspense" />}> <Provider store={store}> <PluginStoreProvider store={pluginStore}> @@ -542,6 +543,5 @@ graphQLReady.onReady(() => { </PluginStoreProvider> </Provider> </Suspense>, - document.getElementById('app'), ); }); diff --git a/frontend/public/components/configmap-and-secret-data.tsx b/frontend/public/components/configmap-and-secret-data.tsx index 51f60c454a7..dd93bbd955c 100644 --- a/frontend/public/components/configmap-and-secret-data.tsx +++ b/frontend/public/components/configmap-and-secret-data.tsx @@ -139,18 +139,25 @@ const SecretDataRevealButton: FC<SecretDataRevealButtonProps> = ({ reveal, onCli export const SecretData: FC<SecretDataProps> = ({ data }) => { const [reveal, setReveal] = useState(false); - const [hasRevealableContent, setHasRevealableContent] = useState(false); const { t } = useTranslation(); + const hasRevealableContent = useMemo( + () => + data + ? Object.keys(data).some((k) => { + const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); + return !isBinary && data[k]; + }) + : false, + [data], + ); + const dataDescriptionList = useMemo(() => { return data ? Object.keys(data) .sort() .map((k) => { const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); - if (!isBinary && data[k]) { - setHasRevealableContent(hasRevealableContent || !isBinary); - } return ( <DescriptionListGroup key={k}> <DescriptionListTerm i18n-not-translated="true" data-test="secret-data-term"> @@ -167,7 +174,7 @@ export const SecretData: FC<SecretDataProps> = ({ data }) => { ); }) : []; - }, [data, reveal, hasRevealableContent]); + }, [data, reveal]); return ( <>