diff --git a/frontend/src/components/license/license-notification.tsx b/frontend/src/components/license/license-notification.tsx index 50b4d3ad9..7586f434c 100644 --- a/frontend/src/components/license/license-notification.tsx +++ b/frontend/src/components/license/license-notification.tsx @@ -1,7 +1,6 @@ import { Alert, AlertDescription, AlertIcon, Box, Button, Flex } from '@redpanda-data/ui'; import { Link, useLocation } from '@tanstack/react-router'; import { useEffect } from 'react'; -import { useStore } from 'zustand'; import { coreHasEnterpriseFeatures, @@ -13,15 +12,15 @@ import { prettyLicenseType, } from './license-utils'; import { License_Source, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb'; -import { api, useApiStore } from '../../state/backend-api'; +import { api, useApiStoreHook } from '../../state/backend-api'; import { capitalizeFirst } from '../../utils/utils'; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex business logic export const LicenseNotification = () => { - const licenses = useStore(useApiStore, (s) => s.licenses); - const licensesLoaded = useStore(useApiStore, (s) => s.licensesLoaded); - const licenseViolation = useStore(useApiStore, (s) => s.licenseViolation); - const enterpriseFeaturesUsed = useStore(useApiStore, (s) => s.enterpriseFeaturesUsed); + const licenses = useApiStoreHook((s) => s.licenses); + const licensesLoaded = useApiStoreHook((s) => s.licensesLoaded); + const licenseViolation = useApiStoreHook((s) => s.licenseViolation); + const enterpriseFeaturesUsed = useApiStoreHook((s) => s.enterpriseFeaturesUsed); const location = useLocation(); useEffect(() => { diff --git a/frontend/src/components/license/overview-license-notification.tsx b/frontend/src/components/license/overview-license-notification.tsx index eec53f646..e503c088e 100644 --- a/frontend/src/components/license/overview-license-notification.tsx +++ b/frontend/src/components/license/overview-license-notification.tsx @@ -1,7 +1,6 @@ import { Alert, AlertDescription, AlertIcon, Box, Flex, Text } from '@redpanda-data/ui'; import { Link } from 'components/redpanda-ui/components/typography'; import { type FC, type ReactElement, useEffect, useState } from 'react'; -import { useStore } from 'zustand'; import { consoleHasEnterpriseFeature, @@ -20,7 +19,7 @@ import { } from './license-utils'; import { RegisterModal } from './register-modal'; import { type License, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb'; -import { api, useApiStore } from '../../state/backend-api'; +import { api, useApiStoreHook } from '../../state/backend-api'; const getLicenseAlertContent = ( licenses: License[], @@ -255,8 +254,8 @@ const getLicenseAlertContent = ( }; export const OverviewLicenseNotification: FC = () => { - const licenses = useStore(useApiStore, (s) => s.licenses); - const clusterOverview = useStore(useApiStore, (s) => s.clusterOverview); + const licenses = useApiStoreHook((s) => s.licenses); + const clusterOverview = useApiStoreHook((s) => s.clusterOverview); const [registerModalOpen, setIsRegisterModalOpen] = useState(false); useEffect(() => { diff --git a/frontend/src/components/pages/acls/acl-list.tsx b/frontend/src/components/pages/acls/acl-list.tsx index 4cf1ee4c0..a5d17461d 100644 --- a/frontend/src/components/pages/acls/acl-list.tsx +++ b/frontend/src/components/pages/acls/acl-list.tsx @@ -517,7 +517,7 @@ const UserActions = ({ user }: { user: UsersEntry }) => { return ( <> - {Boolean(api.isAdminApiConfigured) && ( + {Boolean(api.isAdminApiConfigured) && !isServerless() && ( { - {Boolean(api.isAdminApiConfigured) && ( + {Boolean(api.isAdminApiConfigured) && !isServerless() && ( { e.stopPropagation(); diff --git a/frontend/src/components/pages/acls/serverless-password-guard.test.tsx b/frontend/src/components/pages/acls/serverless-password-guard.test.tsx new file mode 100644 index 000000000..dfa68d068 --- /dev/null +++ b/frontend/src/components/pages/acls/serverless-password-guard.test.tsx @@ -0,0 +1,93 @@ +/** + * Copyright 2022 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +// biome-ignore-all lint/style/noNamespaceImport: test file + +import { render, screen } from '@testing-library/react'; +import { UserInformationCard } from 'components/pages/roles/user-information-card'; +import { isServerless } from 'config'; + +vi.mock('config', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isServerless: vi.fn(() => false), + }; +}); + +const mockedIsServerless = vi.mocked(isServerless); + +/** + * These tests verify the serverless guard on the password change UI (UX-963). + * + * In both user-details.tsx and acl-list.tsx, the password change controls are + * gated by `api.isAdminApiConfigured && !isServerless()`. When isServerless() + * returns true, onEditPassword is undefined and the UI is hidden. + * + * We test the UserInformationCard component directly, which renders the "Edit" + * password button only when the onEditPassword callback is provided. This + * mirrors the guard logic in the parent components. + */ +describe('UX-963: password change hidden in serverless mode', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('shows the Edit password button when not in serverless mode (onEditPassword provided)', () => { + mockedIsServerless.mockReturnValue(false); + + const isAdminApiConfigured = true; + const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined; + + render(); + + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); + }); + + it('hides the Edit password button when in serverless mode (onEditPassword is undefined)', () => { + mockedIsServerless.mockReturnValue(true); + + const isAdminApiConfigured = true; + const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined; + + render(); + + expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument(); + }); + + it('hides the Edit password button when admin API is not configured', () => { + mockedIsServerless.mockReturnValue(false); + + const isAdminApiConfigured = false; + const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined; + + render(); + + expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument(); + }); + + it('evaluates the guard condition correctly for all combinations', () => { + // This directly tests the boolean logic used in user-details.tsx and acl-list.tsx: + // api.isAdminApiConfigured && !isServerless() + const cases = [ + { adminApi: true, serverless: false, expected: true }, + { adminApi: true, serverless: true, expected: false }, + { adminApi: false, serverless: false, expected: false }, + { adminApi: false, serverless: true, expected: false }, + ]; + + for (const { adminApi, serverless, expected } of cases) { + mockedIsServerless.mockReturnValue(serverless); + const result = adminApi && !isServerless(); + expect(result).toBe(expected); + } + }); +}); diff --git a/frontend/src/components/pages/acls/user-details.tsx b/frontend/src/components/pages/acls/user-details.tsx index 7520cc019..7587f3667 100644 --- a/frontend/src/components/pages/acls/user-details.tsx +++ b/frontend/src/components/pages/acls/user-details.tsx @@ -14,6 +14,7 @@ import { UserAclsCard } from 'components/pages/roles/user-acls-card'; import { UserInformationCard } from 'components/pages/roles/user-information-card'; import { UserRolesCard } from 'components/pages/roles/user-roles-card'; import { Button } from 'components/redpanda-ui/components/button'; +import { isServerless } from 'config'; import type { UpdateRoleMembershipResponse } from 'protogen/redpanda/api/console/v1alpha1/security_pb'; import { useEffect, useState } from 'react'; @@ -79,7 +80,7 @@ const UserDetailsPage = ({ userName }: UserDetailsPageProps) => {
{ setIsChangePasswordModalOpen(true); } @@ -124,7 +125,7 @@ const UserDetailsPage = ({ userName }: UserDetailsPageProps) => { )}
- {Boolean(api.isAdminApiConfigured) && ( + {Boolean(api.isAdminApiConfigured) && !isServerless() && (