Skip to content
Open
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -37,25 +37,40 @@ 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.
const resourcesLoaded: boolean =
!flagPending(useProjects) && preferredNamespaceLoaded && lastNamespaceLoaded;
useEffect(() => {
if (!urlNamespace && resourcesLoaded) {
getValueForNamespace(preferredNamespace, lastNamespace, useProjects, activeNamespace)
getValueForNamespace(
preferredNamespace,
lastNamespace,
useProjects,
activeNamespaceRef.current,
)
.then((ns: string) => {
updateNamespace(ns);
})
Expand All @@ -64,28 +79,33 @@ 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.
useEffect(() => {
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 {
Expand Down
24 changes: 16 additions & 8 deletions frontend/packages/console-app/src/components/nav/NavHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -19,8 +20,9 @@ type PerspectiveDropdownItemProps = {
onClick: (perspective: string) => void;
};

const IconLoadingComponent: FC = () => <>&emsp;</>;

const PerspectiveDropdownItem: FC<PerspectiveDropdownItemProps> = ({ perspective, onClick }) => {
const LazyIcon = useMemo(() => lazy(perspective.properties.icon), [perspective.properties.icon]);
return (
<SelectOption
key={perspective.properties.id}
Expand All @@ -29,9 +31,10 @@ const PerspectiveDropdownItem: FC<PerspectiveDropdownItemProps> = ({ perspective
onClick(perspective.properties.id);
}}
icon={
<Suspense fallback={<>&emsp;</>}>
<LazyIcon />
</Suspense>
<AsyncComponent
loader={() => perspective.properties.icon().then((m) => m.default)}
LoadingComponent={IconLoadingComponent}
/>
}
>
<Title headingLevel="h2" size="md" data-test-id="perspective-switcher-menu-option">
Expand Down Expand Up @@ -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"
Expand All @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 }),
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -272,10 +272,12 @@ export const HealthChecksItem: FC<HealthChecksItemProps> = ({ disabledAlert }) =
return true;
});

setHealthCheck({
failingHealthCheck,
reboot,
});
useEffect(() => {
setHealthCheck({
failingHealthCheck,
reboot,
});
}, [failingHealthCheck, reboot, setHealthCheck]);

return (
<HealthItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>[]]
Expand Down Expand Up @@ -102,20 +101,18 @@ 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) => {
if (isModifiedEvent(event)) {
return;
}
event.preventDefault();
setActiveTabId(eventKey);
navigate(`${USER_PREFERENCES_BASE_URL}/${eventKey}`, { replace: true });
};
const activeTab = sortedUserPreferenceGroups.find((group) => group.id === activeTabId)?.label;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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],
);
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
},
};
6 changes: 3 additions & 3 deletions frontend/packages/integration-tests-cypress/views/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down
12 changes: 8 additions & 4 deletions frontend/packages/integration-tests-cypress/views/list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand Down
Loading