From 0d84540710715ed9b0524021a2fafa391b4f14b9 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:17:20 -0400 Subject: [PATCH 01/17] fix: Update CoreState type to include userResource for k8s API integration --- .../packages/console-dynamic-plugin-sdk/src/app/redux-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts index 5f2f5a1f13..64b1de8af0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts @@ -16,6 +16,7 @@ export type ImpersonateKind = { export type CoreState = { user?: UserInfo; + userResource?: any; // UserKind from k8s API impersonate?: ImpersonateKind; admissionWebhookWarnings?: ImmutableMap; }; From 2c5976688bde2d39892b2408009538b81795940b Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:17:57 -0400 Subject: [PATCH 02/17] feat: Add userResource management to core actions and reducer --- .../src/app/core/actions/core.ts | 4 ++++ .../src/app/core/reducers/core.ts | 7 +++++++ .../src/app/core/reducers/coreSelectors.ts | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts index 6b7577064a..f202410656 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts @@ -4,6 +4,7 @@ import { AdmissionWebhookWarning } from '../../redux-types'; export enum ActionType { SetUser = 'setUser', + SetUserResource = 'setUserResource', BeginImpersonate = 'beginImpersonate', EndImpersonate = 'endImpersonate', SetActiveCluster = 'setActiveCluster', @@ -12,6 +13,8 @@ export enum ActionType { } export const setUser = (userInfo: UserInfo) => action(ActionType.SetUser, { userInfo }); +export const setUserResource = (userResource: any) => + action(ActionType.SetUserResource, { userResource }); export const beginImpersonate = (kind: string, name: string, subprotocols: string[]) => action(ActionType.BeginImpersonate, { kind, name, subprotocols }); export const endImpersonate = () => action(ActionType.EndImpersonate); @@ -21,6 +24,7 @@ export const removeAdmissionWebhookWarning = (id) => action(ActionType.RemoveAdmissionWebhookWarning, { id }); const coreActions = { setUser, + setUserResource, beginImpersonate, endImpersonate, setAdmissionWebhookWarning, diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts index 3598222a68..6706a12536 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/core.ts @@ -14,6 +14,7 @@ import { ActionType, CoreAction } from '../actions/core'; export const coreReducer = ( state: CoreState = { user: {}, + userResource: null, admissionWebhookWarnings: ImmutableMap(), }, action: CoreAction, @@ -47,6 +48,12 @@ export const coreReducer = ( user: action.payload.userInfo, }; + case ActionType.SetUserResource: + return { + ...state, + userResource: action.payload.userResource, + }; + case ActionType.SetAdmissionWebhookWarning: return { ...state, diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts index 99a287dfd8..53e121a0a7 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts @@ -4,6 +4,7 @@ import { ImpersonateKind, SDKStoreState, AdmissionWebhookWarning } from '../../r type GetImpersonate = (state: SDKStoreState) => ImpersonateKind; type GetUser = (state: SDKStoreState) => UserInfo; +type GetUserResource = (state: SDKStoreState) => any; type GetAdmissionWebhookWarnings = ( state: SDKStoreState, ) => ImmutableMap; @@ -31,6 +32,13 @@ export const impersonateStateToProps = (state: SDKStoreState) => { */ export const getUser: GetUser = (state) => state.sdkCore.user; +/** + * It provides user resource details from the redux store. + * @param state the root state + * @returns The the user resource state. + */ +export const getUserResource: GetUserResource = (state) => state.sdkCore.userResource; + /** * It provides admission webhook warning data from the redux store. * @param state the root state From e81d4fc9ab5dc5d54b5346e89aed01fb7c8d7741 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:18:10 -0400 Subject: [PATCH 03/17] feat: Introduce useUser hook for centralized user data management --- .../console-shared/src/hooks/index.ts | 1 + .../console-shared/src/hooks/useUser.ts | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 frontend/packages/console-shared/src/hooks/useUser.ts diff --git a/frontend/packages/console-shared/src/hooks/index.ts b/frontend/packages/console-shared/src/hooks/index.ts index c833450d80..8ce5e626c4 100644 --- a/frontend/packages/console-shared/src/hooks/index.ts +++ b/frontend/packages/console-shared/src/hooks/index.ts @@ -37,3 +37,4 @@ export * from './usePrometheusGate'; export * from './useCopyCodeModal'; export * from './useCopyLoginCommands'; export * from './useQuickStartContext'; +export * from './useUser'; diff --git a/frontend/packages/console-shared/src/hooks/useUser.ts b/frontend/packages/console-shared/src/hooks/useUser.ts new file mode 100644 index 0000000000..f00f6486d7 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useUser.ts @@ -0,0 +1,47 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getUser, getUserResource, setUserResource } from '@console/dynamic-plugin-sdk'; +import { useK8sGet } from '@console/internal/components/utils/k8s-get-hook'; +import { UserModel } from '@console/internal/models'; +import type { UserKind } from '@console/internal/module/k8s/types'; + +/** + * Custom hook that provides centralized user data fetching and management. + * This hook fetches both the UserInfo (from authentication) and UserKind (from k8s API) + * and stores them in Redux for use throughout the application. + * + * @returns Object containing user info, user resource, and loading states + */ +export const useUser = () => { + const dispatch = useDispatch(); + + // Get current user info from Redux (username, groups, etc.) + const user = useSelector(getUser); + + // Get current user resource from Redux (fullName, identities, etc.) + const userResource = useSelector(getUserResource); + + // Fetch user resource from k8s API + const [userResourceData, userResourceLoaded, userResourceError] = useK8sGet( + UserModel, + '~', + ); + + // Update Redux when user resource is loaded + useEffect(() => { + if (userResourceLoaded && userResourceData && !userResourceError) { + dispatch(setUserResource(userResourceData)); + } + }, [dispatch, userResourceData, userResourceLoaded, userResourceError]); + + return { + user, + userResource: userResource || userResourceData, + userResourceLoaded, + userResourceError, + // Computed properties for convenience + username: user?.username, + fullName: (userResource || userResourceData)?.fullName, + displayName: (userResource || userResourceData)?.fullName || user?.username || '', + }; +}; From 308f1d7316c67158df8b54f90bf502ba2a288e52 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:29:44 -0400 Subject: [PATCH 04/17] refactor: Replace direct user data fetching with centralized useUser hook in telemetry and masthead components --- .../console-shared/src/hooks/useTelemetry.ts | 6 +++--- .../public/components/masthead/masthead-toolbar.tsx | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/useTelemetry.ts b/frontend/packages/console-shared/src/hooks/useTelemetry.ts index edb26fa712..e318cfe15e 100644 --- a/frontend/packages/console-shared/src/hooks/useTelemetry.ts +++ b/frontend/packages/console-shared/src/hooks/useTelemetry.ts @@ -6,14 +6,13 @@ import { TelemetryEventListener, UserInfo, } from '@console/dynamic-plugin-sdk'; -import { useK8sGet } from '@console/internal/components/utils/k8s-get-hook'; -import { UserModel } from '@console/internal/models'; import type { UserKind } from '@console/internal/module/k8s/types'; import { CLUSTER_TELEMETRY_ANALYTICS, PREFERRED_TELEMETRY_USER_SETTING_KEY, USER_TELEMETRY_ANALYTICS, } from '../constants'; +import { useUser } from './useUser'; import { useUserSettings } from './useUserSettings'; export interface ClusterProperties { @@ -81,7 +80,8 @@ export const useTelemetry = () => { true, ); - const [userResource, userResourceIsLoaded] = useK8sGet(UserModel, '~'); + // Use centralized user data instead of fetching directly + const { userResource, userResourceLoaded: userResourceIsLoaded } = useUser(); const [extensions] = useResolvedExtensions(isTelemetryListener); diff --git a/frontend/public/components/masthead/masthead-toolbar.tsx b/frontend/public/components/masthead/masthead-toolbar.tsx index 1f52977c27..ff65086b94 100644 --- a/frontend/public/components/masthead/masthead-toolbar.tsx +++ b/frontend/public/components/masthead/masthead-toolbar.tsx @@ -27,6 +27,7 @@ import { useCopyLoginCommands, useFlag, useTelemetry, + useUser, YellowExclamationTriangleIcon, } from '@console/shared'; import { formatNamespacedRouteForResource } from '@console/shared/src/utils'; @@ -34,7 +35,7 @@ import { ExternalLinkButton } from '@console/shared/src/components/links/Externa import { LinkTo } from '@console/shared/src/components/links/LinkTo'; import { CloudShellMastheadButton } from '@console/webterminal-plugin/src/components/cloud-shell/CloudShellMastheadButton'; import { CloudShellMastheadAction } from '@console/webterminal-plugin/src/components/cloud-shell/CloudShellMastheadAction'; -import { getUser, useActivePerspective } from '@console/dynamic-plugin-sdk'; +import { useActivePerspective } from '@console/dynamic-plugin-sdk'; import * as UIActions from '../../actions/ui'; import { flagPending, featureReducerName } from '../../reducers/features'; import { authSvc } from '../../module/auth'; @@ -162,12 +163,15 @@ const MastheadToolbarContents: React.FCC = ({ t('public~Login with this command'), externalLoginCommand, ); - const { clusterID, user, alertCount, canAccessNS } = useSelector((state: RootState) => ({ + const { clusterID, alertCount, canAccessNS } = useSelector((state: RootState) => ({ clusterID: state.UI.get('clusterID'), - user: getUser(state), alertCount: state.observe.getIn(['alertCount']), canAccessNS: !!state[featureReducerName].get(FLAGS.CAN_GET_NS), })); + + // Use centralized user hook for user data + const { displayName, username } = useUser(); + const [isAppLauncherDropdownOpen, setIsAppLauncherDropdownOpen] = useState(false); const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false); const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); @@ -181,7 +185,6 @@ const MastheadToolbarContents: React.FCC = ({ const kebabMenuRef = useRef(null); const reportBugLink = cv ? getReportBugLink(cv) : null; const userInactivityTimeout = useRef(null); - const username = user?.username ?? ''; const isKubeAdmin = username === 'kube:admin'; const drawerToggle = useCallback(() => dispatch(UIActions.notificationDrawerToggleExpanded()), [ @@ -614,7 +617,7 @@ const MastheadToolbarContents: React.FCC = ({ const userToggle = ( - {authEnabledFlag ? username : t('public~Auth disabled')} + {authEnabledFlag ? displayName : t('public~Auth disabled')} ); From 824bb983b3d011808b902de1f44e8f754c3184f0 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:36:58 -0400 Subject: [PATCH 05/17] test: auto generated the unit tests for useUser hook to validate user data retrieval and dispatch behavior --- .../src/hooks/__tests__/useUser.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts new file mode 100644 index 0000000000..a9077c7461 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts @@ -0,0 +1,82 @@ +import { useSelector, useDispatch } from 'react-redux'; +import { useK8sGet } from '@console/internal/components/utils/k8s-get-hook'; +import { testHook } from '@console/shared/src/test-utils/hooks-utils'; +import { useUser } from '../useUser'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + +jest.mock('@console/internal/components/utils/k8s-get-hook', () => ({ + useK8sGet: jest.fn(), +})); + +jest.mock('@console/dynamic-plugin-sdk', () => ({ + ...jest.requireActual('@console/dynamic-plugin-sdk'), + getUser: jest.fn(), + getUserResource: jest.fn(), + setUserResource: jest.fn(), +})); + +const mockDispatch = jest.fn(); +const mockUseSelector = useSelector as jest.Mock; +const mockUseK8sGet = useK8sGet as jest.Mock; +const mockUseDispatch = useDispatch as jest.Mock; + +describe('useUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseDispatch.mockReturnValue(mockDispatch); + }); + + it('should return user data with displayName from fullName when available', () => { + const mockUser = { username: 'testuser@example.com', uid: '123' }; + const mockUserResource = { fullName: 'Test User', identities: ['testuser'] }; + + mockUseSelector + .mockReturnValueOnce(mockUser) // for getUser + .mockReturnValueOnce(mockUserResource); // for getUserResource + + mockUseK8sGet.mockReturnValue([mockUserResource, true, null]); + + const { result } = testHook(() => useUser()); + + expect(result.current.user).toEqual(mockUser); + expect(result.current.userResource).toEqual(mockUserResource); + expect(result.current.username).toBe('testuser@example.com'); + expect(result.current.fullName).toBe('Test User'); + expect(result.current.displayName).toBe('Test User'); // Should prefer fullName + }); + + it('should fallback to username when fullName is not available', () => { + const mockUser = { username: 'testuser@example.com', uid: '123' }; + const mockUserResource = { identities: ['testuser'] }; // No fullName + + mockUseSelector.mockReturnValueOnce(mockUser).mockReturnValueOnce(mockUserResource); + + mockUseK8sGet.mockReturnValue([mockUserResource, true, null]); + + const { result } = testHook(() => useUser()); + + expect(result.current.displayName).toBe('testuser@example.com'); // Should fallback to username + expect(result.current.fullName).toBeUndefined(); + }); + + it('should dispatch setUserResource when user resource is loaded', () => { + const mockUser = { username: 'testuser@example.com' }; + const mockUserResource = { fullName: 'Test User' }; + + mockUseSelector.mockReturnValueOnce(mockUser).mockReturnValueOnce(null); // No userResource in Redux yet + + mockUseK8sGet.mockReturnValue([mockUserResource, true, null]); + + testHook(() => useUser()); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'setUserResource', + payload: { userResource: mockUserResource }, + }); + }); +}); From 2cbbed833ff6c8e11833ace5fc6a6424c58cbfb2 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:59:16 -0400 Subject: [PATCH 06/17] feat: Enhance useUser hook to provide robust display name handling with fallbacks and add corresponding unit tests --- .../src/hooks/__tests__/useUser.spec.ts | 26 +++++++++++++++++++ .../console-shared/src/hooks/useUser.ts | 26 ++++++++++++++++--- .../components/masthead/masthead-toolbar.tsx | 2 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts index a9077c7461..799c798adc 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts @@ -79,4 +79,30 @@ describe('useUser', () => { payload: { userResource: mockUserResource }, }); }); + + it('should handle edge cases with empty strings and fallback to "Unknown User"', () => { + const mockUser = { username: '' }; // Empty username + const mockUserResource = { fullName: ' ' }; // Whitespace-only fullName + + mockUseSelector.mockReturnValueOnce(mockUser).mockReturnValueOnce(mockUserResource); + + mockUseK8sGet.mockReturnValue([mockUserResource, true, null]); + + const { result } = testHook(() => useUser()); + + expect(result.current.displayName).toBe('Unknown User'); // Should fallback to "Unknown User" + }); + + it('should trim whitespace from fullName and username', () => { + const mockUser = { username: ' testuser@example.com ' }; + const mockUserResource = { fullName: ' Test User ' }; + + mockUseSelector.mockReturnValueOnce(mockUser).mockReturnValueOnce(mockUserResource); + + mockUseK8sGet.mockReturnValue([mockUserResource, true, null]); + + const { result } = testHook(() => useUser()); + + expect(result.current.displayName).toBe('Test User'); // Should be trimmed + }); }); diff --git a/frontend/packages/console-shared/src/hooks/useUser.ts b/frontend/packages/console-shared/src/hooks/useUser.ts index f00f6486d7..948c732670 100644 --- a/frontend/packages/console-shared/src/hooks/useUser.ts +++ b/frontend/packages/console-shared/src/hooks/useUser.ts @@ -34,14 +34,32 @@ export const useUser = () => { } }, [dispatch, userResourceData, userResourceLoaded, userResourceError]); + const currentUserResource = userResource || userResourceData; + const currentUsername = user?.username; + const currentFullName = currentUserResource?.fullName; + + // Create a robust display name that always has a fallback + const getDisplayName = () => { + // Prefer fullName if it exists and is not empty + if (currentFullName && currentFullName.trim()) { + return currentFullName.trim(); + } + // Fallback to username if it exists and is not empty + if (currentUsername && currentUsername.trim()) { + return currentUsername.trim(); + } + // Final fallback for edge cases + return 'Unknown User'; + }; + return { user, - userResource: userResource || userResourceData, + userResource: currentUserResource, userResourceLoaded, userResourceError, // Computed properties for convenience - username: user?.username, - fullName: (userResource || userResourceData)?.fullName, - displayName: (userResource || userResourceData)?.fullName || user?.username || '', + username: currentUsername, + fullName: currentFullName, + displayName: getDisplayName(), }; }; diff --git a/frontend/public/components/masthead/masthead-toolbar.tsx b/frontend/public/components/masthead/masthead-toolbar.tsx index ff65086b94..94a9a3486c 100644 --- a/frontend/public/components/masthead/masthead-toolbar.tsx +++ b/frontend/public/components/masthead/masthead-toolbar.tsx @@ -617,7 +617,7 @@ const MastheadToolbarContents: React.FCC = ({ const userToggle = ( - {authEnabledFlag ? displayName : t('public~Auth disabled')} + {authEnabledFlag ? displayName || username || 'User' : t('public~Auth disabled')} ); From ccb1777f7f56a9d6831971a8279746c992cf6f9a Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:34:49 -0400 Subject: [PATCH 07/17] fix: apply the review feedback --- .../console-shared/src/hooks/__tests__/useUser.spec.ts | 8 +++++++- frontend/packages/console-shared/src/hooks/useUser.ts | 4 +++- frontend/public/components/masthead/masthead-toolbar.tsx | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts index 799c798adc..8268d4222a 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts @@ -3,6 +3,12 @@ import { useK8sGet } from '@console/internal/components/utils/k8s-get-hook'; import { testHook } from '@console/shared/src/test-utils/hooks-utils'; import { useUser } from '../useUser'; +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key.replace('public~', ''), + }), +})); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), @@ -90,7 +96,7 @@ describe('useUser', () => { const { result } = testHook(() => useUser()); - expect(result.current.displayName).toBe('Unknown User'); // Should fallback to "Unknown User" + expect(result.current.displayName).toBe('Unknown User'); // Should fallback to translated "Unknown User" }); it('should trim whitespace from fullName and username', () => { diff --git a/frontend/packages/console-shared/src/hooks/useUser.ts b/frontend/packages/console-shared/src/hooks/useUser.ts index 948c732670..8c2f9643e1 100644 --- a/frontend/packages/console-shared/src/hooks/useUser.ts +++ b/frontend/packages/console-shared/src/hooks/useUser.ts @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { getUser, getUserResource, setUserResource } from '@console/dynamic-plugin-sdk'; import { useK8sGet } from '@console/internal/components/utils/k8s-get-hook'; @@ -13,6 +14,7 @@ import type { UserKind } from '@console/internal/module/k8s/types'; * @returns Object containing user info, user resource, and loading states */ export const useUser = () => { + const { t } = useTranslation(); const dispatch = useDispatch(); // Get current user info from Redux (username, groups, etc.) @@ -49,7 +51,7 @@ export const useUser = () => { return currentUsername.trim(); } // Final fallback for edge cases - return 'Unknown User'; + return t('public~Unknown User'); }; return { diff --git a/frontend/public/components/masthead/masthead-toolbar.tsx b/frontend/public/components/masthead/masthead-toolbar.tsx index 94a9a3486c..ff65086b94 100644 --- a/frontend/public/components/masthead/masthead-toolbar.tsx +++ b/frontend/public/components/masthead/masthead-toolbar.tsx @@ -617,7 +617,7 @@ const MastheadToolbarContents: React.FCC = ({ const userToggle = ( - {authEnabledFlag ? displayName || username || 'User' : t('public~Auth disabled')} + {authEnabledFlag ? displayName : t('public~Auth disabled')} ); From aedb5b8e04fbcc360a5f12b17c49574d6c9bc1b8 Mon Sep 17 00:00:00 2001 From: Leo Li <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:35:48 -0400 Subject: [PATCH 08/17] Apply suggestions from code review Co-authored-by: logonoff --- .../src/app/core/reducers/coreSelectors.ts | 2 +- .../packages/console-dynamic-plugin-sdk/src/app/redux-types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts index 53e121a0a7..851f6bf2c0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts @@ -35,7 +35,7 @@ export const getUser: GetUser = (state) => state.sdkCore.user; /** * It provides user resource details from the redux store. * @param state the root state - * @returns The the user resource state. + * @returns The user resource state. */ export const getUserResource: GetUserResource = (state) => state.sdkCore.userResource; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts index 64b1de8af0..e80eacf27e 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts @@ -16,7 +16,7 @@ export type ImpersonateKind = { export type CoreState = { user?: UserInfo; - userResource?: any; // UserKind from k8s API + userResource?: UserKind; impersonate?: ImpersonateKind; admissionWebhookWarnings?: ImmutableMap; }; From 535a460176a4b0f6725dbfe9c521423603111548 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:37:10 -0400 Subject: [PATCH 09/17] feat: run i18n --- frontend/public/locales/en/public.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index 1ec26b6566..b47b790502 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -1748,6 +1748,7 @@ "The resulting dataset is too large to graph.": "The resulting dataset is too large to graph.", "Stacked": "Stacked", "unknown host": "unknown host", + "Unknown User": "Unknown User", "Just now": "Just now", "Disable": "Disable", "Disabled": "Disabled", From 009e7dee1bfb4805efcbb8d4aae7a025a66f0b6f Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:44:28 -0400 Subject: [PATCH 10/17] feat: Add UserKind type import --- .../packages/console-dynamic-plugin-sdk/src/app/redux-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts index e80eacf27e..503b7a954f 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts @@ -1,4 +1,5 @@ import { Map as ImmutableMap } from 'immutable'; +import type { UserKind } from '@console/internal/module/k8s/types'; import { UserInfo } from '../extensions/console-types'; export type K8sState = ImmutableMap; From acf30a3c1fd5c87537d91483076de2a8963a97db Mon Sep 17 00:00:00 2001 From: Leo Li <36619969+Leo6Leo@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:40:52 -0400 Subject: [PATCH 11/17] Apply suggestions from code review Co-authored-by: logonoff --- .../console-shared/src/hooks/__tests__/useUser.spec.ts | 6 +++--- frontend/packages/console-shared/src/hooks/useUser.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts index 8268d4222a..323fef09f1 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts @@ -5,7 +5,7 @@ import { useUser } from '../useUser'; jest.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key.replace('public~', ''), + t: (key: string) => key, }), })); @@ -86,7 +86,7 @@ describe('useUser', () => { }); }); - it('should handle edge cases with empty strings and fallback to "Unknown User"', () => { + it('should handle edge cases with empty strings and fallback to "Unknown user"', () => { const mockUser = { username: '' }; // Empty username const mockUserResource = { fullName: ' ' }; // Whitespace-only fullName @@ -96,7 +96,7 @@ describe('useUser', () => { const { result } = testHook(() => useUser()); - expect(result.current.displayName).toBe('Unknown User'); // Should fallback to translated "Unknown User" + expect(result.current.displayName).toBe('Unknown User'); // Should fallback to translated "Unknown user" }); it('should trim whitespace from fullName and username', () => { diff --git a/frontend/packages/console-shared/src/hooks/useUser.ts b/frontend/packages/console-shared/src/hooks/useUser.ts index 8c2f9643e1..822ec9ce2b 100644 --- a/frontend/packages/console-shared/src/hooks/useUser.ts +++ b/frontend/packages/console-shared/src/hooks/useUser.ts @@ -14,7 +14,7 @@ import type { UserKind } from '@console/internal/module/k8s/types'; * @returns Object containing user info, user resource, and loading states */ export const useUser = () => { - const { t } = useTranslation(); + const { t } = useTranslation('public'); const dispatch = useDispatch(); // Get current user info from Redux (username, groups, etc.) @@ -51,7 +51,7 @@ export const useUser = () => { return currentUsername.trim(); } // Final fallback for edge cases - return t('public~Unknown User'); + return t('Unknown user'); }; return { From ad7479f2081d75688af744e8db3bfa17ee45ff85 Mon Sep 17 00:00:00 2001 From: Leo Li <36619969+Leo6Leo@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:12:07 -0400 Subject: [PATCH 12/17] Update frontend/public/locales/en/public.json Co-authored-by: logonoff --- frontend/public/locales/en/public.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index b47b790502..c66f78de64 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -1748,7 +1748,7 @@ "The resulting dataset is too large to graph.": "The resulting dataset is too large to graph.", "Stacked": "Stacked", "unknown host": "unknown host", - "Unknown User": "Unknown User", + "Unknown user": "Unknown user", "Just now": "Just now", "Disable": "Disable", "Disabled": "Disabled", From b549dccb9253ac1d6d52fe6d687ae1f18d976ebc Mon Sep 17 00:00:00 2001 From: Leo Li <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:57:49 -0400 Subject: [PATCH 13/17] Update frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts Co-authored-by: logonoff --- .../console-dynamic-plugin-sdk/src/app/core/actions/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts index f202410656..d8aab203ac 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts @@ -13,7 +13,7 @@ export enum ActionType { } export const setUser = (userInfo: UserInfo) => action(ActionType.SetUser, { userInfo }); -export const setUserResource = (userResource: any) => +export const setUserResource = (userResource: UserKind) => action(ActionType.SetUserResource, { userResource }); export const beginImpersonate = (kind: string, name: string, subprotocols: string[]) => action(ActionType.BeginImpersonate, { kind, name, subprotocols }); From b0045590a3f351839fcc9ebdfad4afd4409adf5a Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:10:39 -0400 Subject: [PATCH 14/17] feat: Import UserKind type in core actions --- .../console-dynamic-plugin-sdk/src/app/core/actions/core.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts index d8aab203ac..c60f2345bb 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/actions/core.ts @@ -1,4 +1,5 @@ import { action, ActionType as Action } from 'typesafe-actions'; +import type { UserKind } from '@console/internal/module/k8s/types'; import { UserInfo } from '../../../extensions'; import { AdmissionWebhookWarning } from '../../redux-types'; From a8e3d23cbed43ce03a687cea00721a69e31ba452 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:51:47 -0400 Subject: [PATCH 15/17] refactor: Update GetUserResource type to use UserKind for improved type safety --- .../src/app/core/reducers/coreSelectors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts index 851f6bf2c0..2db2fc3bca 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/core/reducers/coreSelectors.ts @@ -1,10 +1,11 @@ import { Map as ImmutableMap } from 'immutable'; +import type { UserKind } from '@console/internal/module/k8s/types'; import { UserInfo } from '../../../extensions'; import { ImpersonateKind, SDKStoreState, AdmissionWebhookWarning } from '../../redux-types'; type GetImpersonate = (state: SDKStoreState) => ImpersonateKind; type GetUser = (state: SDKStoreState) => UserInfo; -type GetUserResource = (state: SDKStoreState) => any; +type GetUserResource = (state: SDKStoreState) => UserKind; type GetAdmissionWebhookWarnings = ( state: SDKStoreState, ) => ImmutableMap; From 6d670707f426a3a6da6974fed73144356515c7cc Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:33:59 -0400 Subject: [PATCH 16/17] test: Update useUser.spec.ts to mock setUserResource and correct displayName expectation --- .../console-shared/src/hooks/__tests__/useUser.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts index 323fef09f1..a9942ace26 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useUser.spec.ts @@ -19,11 +19,16 @@ jest.mock('@console/internal/components/utils/k8s-get-hook', () => ({ useK8sGet: jest.fn(), })); +const mockSetUserResource = jest.fn((userResource) => ({ + type: 'setUserResource', + payload: { userResource }, +})); + jest.mock('@console/dynamic-plugin-sdk', () => ({ ...jest.requireActual('@console/dynamic-plugin-sdk'), getUser: jest.fn(), getUserResource: jest.fn(), - setUserResource: jest.fn(), + setUserResource: (...args) => mockSetUserResource(...args), })); const mockDispatch = jest.fn(); @@ -96,7 +101,7 @@ describe('useUser', () => { const { result } = testHook(() => useUser()); - expect(result.current.displayName).toBe('Unknown User'); // Should fallback to translated "Unknown user" + expect(result.current.displayName).toBe('Unknown user'); // Should fallback to translated "Unknown user" }); it('should trim whitespace from fullName and username', () => { From f3a30295643f17adcdc07a777a23174ee32ae5b1 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:56:50 -0400 Subject: [PATCH 17/17] test: fixing the failing CI issue --- .../src/hooks/__tests__/useTelemetry.spec.ts | 12 ++++++++++++ .../__tests__/ExportApplicationModal.spec.tsx | 12 +++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts index f8df18d22b..646f87e9ca 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts @@ -24,6 +24,18 @@ jest.mock('@console/shared/src/hooks/useUserSettings', () => ({ useUserSettings: jest.fn(), })); +jest.mock('@console/shared/src/hooks/useUser', () => ({ + useUser: jest.fn(() => ({ + user: {}, + userResource: {}, + userResourceLoaded: true, + userResourceError: null, + username: 'testuser', + fullName: 'Test User', + displayName: 'Test User', + })), +})); + const mockUserResource = {}; const exampleReturnValue = { diff --git a/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx b/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx index afefbafa52..6bbff647af 100644 --- a/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx +++ b/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx @@ -1,4 +1,4 @@ -import { screen, render, fireEvent } from '@testing-library/react'; +import { screen, fireEvent } from '@testing-library/react'; import * as _ from 'lodash'; import { act } from 'react-dom/test-utils'; import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; @@ -32,7 +32,7 @@ describe('ExportApplicationModal', () => { }); it('should show cancel and ok buttons when export app resource is not found', async () => { - render(); + renderWithProviders(); expect(screen.getByTestId('cancel-btn')).toBeInTheDocument(); expect(screen.getByTestId('close-btn')).toBeInTheDocument(); }); @@ -58,7 +58,7 @@ describe('ExportApplicationModal', () => { }); it('should show cancel and ok buttons when export app resource is created', async () => { - render( + renderWithProviders( { it('should call k8sCreate with correct data on click of Ok button when the export resource is not created', async () => { const spyk8sCreate = jest.spyOn(k8sResourceModule, 'k8sCreate'); - render(); + renderWithProviders( + , + ); await act(async () => { fireEvent.click(screen.getByTestId('close-btn')); }); @@ -84,7 +86,7 @@ describe('ExportApplicationModal', () => { const spyk8sKill = jest.spyOn(k8sResourceModule, 'k8sKill'); const spyk8sCreate = jest.spyOn(k8sResourceModule, 'k8sCreate'); - render( + renderWithProviders(