Skip to content

Commit eead313

Browse files
committed
Merge branch 'master' into pr2/log-explorer-molecule
2 parents af622f2 + cc77615 commit eead313

34 files changed

+1728
-239
lines changed

frontend/src/app.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { CustomFeatureFlagProvider } from 'custom-feature-flag-provider';
4444
import useDeveloperView from 'hooks/use-developer-view';
4545
import { protobufRegistry } from 'protobuf-registry';
4646
import queryClient from 'query-client';
47+
import { useEffect } from 'react';
4748
import { getBasePath } from 'utils/env';
4849

4950
import { NotFoundPage } from './components/misc/not-found-page';
@@ -90,9 +91,14 @@ declare module '@tanstack/react-router' {
9091
}
9192
}
9293

94+
const EMPTY_SETUP_ARGS = {};
95+
9396
const App = () => {
9497
const developerView = useDeveloperView();
95-
setup({});
98+
99+
useEffect(() => {
100+
setup(EMPTY_SETUP_ARGS);
101+
}, []);
96102

97103
// Need to use CustomFeatureFlagProvider for completeness with EmbeddedApp
98104
return (
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { createGroupedSidebarItems } from 'utils/route-utils';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
5+
import type { EndpointCompatibility } from '../../state/rest-interfaces';
6+
import { Feature, useSupportedFeaturesStore } from '../../state/supported-features';
7+
8+
// Mock config to enable embedded + ADP mode (required for Transcripts route visibility)
9+
vi.mock('../../config', async (importOriginal) => {
10+
const actual = await importOriginal<typeof import('../../config')>();
11+
return {
12+
...actual,
13+
isEmbedded: () => true,
14+
isAdpEnabled: () => true,
15+
};
16+
});
17+
18+
describe('SidebarNavigation re-renders on endpointCompatibility change (UX-972)', () => {
19+
afterEach(() => {
20+
// Reset store to initial state between tests
21+
useSupportedFeaturesStore.setState({
22+
endpointCompatibility: null,
23+
tracingService: false,
24+
});
25+
});
26+
27+
it('TracingService defaults to unsupported when endpointCompatibility is null', () => {
28+
const state = useSupportedFeaturesStore.getState();
29+
expect(state.endpointCompatibility).toBeNull();
30+
expect(state.tracingService).toBe(false);
31+
});
32+
33+
it('Transcripts item is hidden when TracingService is not supported', () => {
34+
const groups = createGroupedSidebarItems();
35+
const allItems = groups.flatMap((g) => g.items);
36+
const transcripts = allItems.find((item) => item.to === '/transcripts');
37+
expect(transcripts).toBeUndefined();
38+
});
39+
40+
it('Transcripts item appears after endpointCompatibility loads with TracingService supported', () => {
41+
const compatibility: EndpointCompatibility = {
42+
kafkaVersion: '3.6.0',
43+
endpoints: [
44+
{
45+
endpoint: Feature.TracingService.endpoint,
46+
method: Feature.TracingService.method,
47+
isSupported: true,
48+
},
49+
],
50+
};
51+
52+
act(() => {
53+
useSupportedFeaturesStore.getState().setEndpointCompatibility(compatibility);
54+
});
55+
56+
const groups = createGroupedSidebarItems();
57+
const allItems = groups.flatMap((g) => g.items);
58+
const transcripts = allItems.find((item) => item.to === '/transcripts');
59+
expect(transcripts).toBeDefined();
60+
expect(transcripts?.title).toBe('Transcripts');
61+
});
62+
63+
it('store selector triggers re-render when endpointCompatibility changes', () => {
64+
const selector = (s: { endpointCompatibility: EndpointCompatibility | null }) => s.endpointCompatibility;
65+
const { result } = renderHook(() => useSupportedFeaturesStore(selector));
66+
67+
expect(result.current).toBeNull();
68+
69+
const compatibility: EndpointCompatibility = {
70+
kafkaVersion: '3.6.0',
71+
endpoints: [
72+
{
73+
endpoint: Feature.TracingService.endpoint,
74+
method: Feature.TracingService.method,
75+
isSupported: true,
76+
},
77+
],
78+
};
79+
80+
act(() => {
81+
useSupportedFeaturesStore.getState().setEndpointCompatibility(compatibility);
82+
});
83+
84+
expect(result.current).toBe(compatibility);
85+
});
86+
});

frontend/src/components/layout/sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import RedpandaIcon from '../../assets/redpanda/redpanda-icon-next.svg';
4444
import RedpandaLogoWhite from '../../assets/redpanda/redpanda-logo-next-white.svg';
4545
import { AuthenticationMethod } from '../../protogen/redpanda/api/console/v1alpha1/authentication_pb';
4646
import { api, useApiStoreHook } from '../../state/backend-api';
47+
import { useSupportedFeaturesStore } from '../../state/supported-features';
4748
import { AppFeatures } from '../../utils/env';
4849
import { getUserInitials } from '../../utils/string';
4950
import { UserPreferencesDialog } from '../misc/user-preferences';
@@ -225,6 +226,7 @@ function SidebarNavItem({ item, isActive, onNavClick }: NavItemProps) {
225226
const SidebarNavigation = () => {
226227
const location = useLocation();
227228
const { isMobile, setOpenMobile } = useSidebar();
229+
useSupportedFeaturesStore((s) => s.endpointCompatibility); // re-render when endpoint compatibility loads
228230
const groupedItems = createGroupedSidebarItems();
229231

230232
const handleNavClick = () => {

frontend/src/components/license/license-notification.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Alert, AlertDescription, AlertIcon, Box, Button, Flex } from '@redpanda-data/ui';
22
import { Link, useLocation } from '@tanstack/react-router';
33
import { useEffect } from 'react';
4-
import { useStore } from 'zustand';
54

65
import {
76
coreHasEnterpriseFeatures,
@@ -13,15 +12,15 @@ import {
1312
prettyLicenseType,
1413
} from './license-utils';
1514
import { License_Source, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb';
16-
import { api, useApiStore } from '../../state/backend-api';
15+
import { api, useApiStoreHook } from '../../state/backend-api';
1716
import { capitalizeFirst } from '../../utils/utils';
1817

1918
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex business logic
2019
export const LicenseNotification = () => {
21-
const licenses = useStore(useApiStore, (s) => s.licenses);
22-
const licensesLoaded = useStore(useApiStore, (s) => s.licensesLoaded);
23-
const licenseViolation = useStore(useApiStore, (s) => s.licenseViolation);
24-
const enterpriseFeaturesUsed = useStore(useApiStore, (s) => s.enterpriseFeaturesUsed);
20+
const licenses = useApiStoreHook((s) => s.licenses);
21+
const licensesLoaded = useApiStoreHook((s) => s.licensesLoaded);
22+
const licenseViolation = useApiStoreHook((s) => s.licenseViolation);
23+
const enterpriseFeaturesUsed = useApiStoreHook((s) => s.enterpriseFeaturesUsed);
2524
const location = useLocation();
2625

2726
useEffect(() => {

frontend/src/components/license/overview-license-notification.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Alert, AlertDescription, AlertIcon, Box, Flex, Text } from '@redpanda-data/ui';
22
import { Link } from 'components/redpanda-ui/components/typography';
33
import { type FC, type ReactElement, useEffect, useState } from 'react';
4-
import { useStore } from 'zustand';
54

65
import {
76
consoleHasEnterpriseFeature,
@@ -20,7 +19,7 @@ import {
2019
} from './license-utils';
2120
import { RegisterModal } from './register-modal';
2221
import { type License, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb';
23-
import { api, useApiStore } from '../../state/backend-api';
22+
import { api, useApiStoreHook } from '../../state/backend-api';
2423

2524
const getLicenseAlertContent = (
2625
licenses: License[],
@@ -255,8 +254,8 @@ const getLicenseAlertContent = (
255254
};
256255

257256
export const OverviewLicenseNotification: FC = () => {
258-
const licenses = useStore(useApiStore, (s) => s.licenses);
259-
const clusterOverview = useStore(useApiStore, (s) => s.clusterOverview);
257+
const licenses = useApiStoreHook((s) => s.licenses);
258+
const clusterOverview = useApiStoreHook((s) => s.clusterOverview);
260259
const [registerModalOpen, setIsRegisterModalOpen] = useState(false);
261260

262261
useEffect(() => {

frontend/src/components/pages/acls/acl-list.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ const UserActions = ({ user }: { user: UsersEntry }) => {
517517

518518
return (
519519
<>
520-
{Boolean(api.isAdminApiConfigured) && (
520+
{Boolean(api.isAdminApiConfigured) && !isServerless() && (
521521
<ChangePasswordModal
522522
isOpen={isChangePasswordModalOpen}
523523
setIsOpen={setIsChangePasswordModalOpen}
@@ -533,7 +533,7 @@ const UserActions = ({ user }: { user: UsersEntry }) => {
533533
<Icon as={MoreHorizontalIcon} />
534534
</MenuButton>
535535
<MenuList>
536-
{Boolean(api.isAdminApiConfigured) && (
536+
{Boolean(api.isAdminApiConfigured) && !isServerless() && (
537537
<MenuItem
538538
onClick={(e) => {
539539
e.stopPropagation();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright 2022 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
// biome-ignore-all lint/style/noNamespaceImport: test file
13+
14+
import { render, screen } from '@testing-library/react';
15+
import { UserInformationCard } from 'components/pages/roles/user-information-card';
16+
import { isServerless } from 'config';
17+
18+
vi.mock('config', async (importOriginal) => {
19+
const actual = await importOriginal<typeof import('config')>();
20+
return {
21+
...actual,
22+
isServerless: vi.fn(() => false),
23+
};
24+
});
25+
26+
const mockedIsServerless = vi.mocked(isServerless);
27+
28+
/**
29+
* These tests verify the serverless guard on the password change UI (UX-963).
30+
*
31+
* In both user-details.tsx and acl-list.tsx, the password change controls are
32+
* gated by `api.isAdminApiConfigured && !isServerless()`. When isServerless()
33+
* returns true, onEditPassword is undefined and the UI is hidden.
34+
*
35+
* We test the UserInformationCard component directly, which renders the "Edit"
36+
* password button only when the onEditPassword callback is provided. This
37+
* mirrors the guard logic in the parent components.
38+
*/
39+
describe('UX-963: password change hidden in serverless mode', () => {
40+
afterEach(() => {
41+
vi.restoreAllMocks();
42+
});
43+
44+
it('shows the Edit password button when not in serverless mode (onEditPassword provided)', () => {
45+
mockedIsServerless.mockReturnValue(false);
46+
47+
const isAdminApiConfigured = true;
48+
const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined;
49+
50+
render(<UserInformationCard onEditPassword={onEditPassword} username="test-user" />);
51+
52+
expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument();
53+
});
54+
55+
it('hides the Edit password button when in serverless mode (onEditPassword is undefined)', () => {
56+
mockedIsServerless.mockReturnValue(true);
57+
58+
const isAdminApiConfigured = true;
59+
const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined;
60+
61+
render(<UserInformationCard onEditPassword={onEditPassword} username="test-user" />);
62+
63+
expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
64+
});
65+
66+
it('hides the Edit password button when admin API is not configured', () => {
67+
mockedIsServerless.mockReturnValue(false);
68+
69+
const isAdminApiConfigured = false;
70+
const onEditPassword = isAdminApiConfigured && !isServerless() ? vi.fn() : undefined;
71+
72+
render(<UserInformationCard onEditPassword={onEditPassword} username="test-user" />);
73+
74+
expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
75+
});
76+
77+
it('evaluates the guard condition correctly for all combinations', () => {
78+
// This directly tests the boolean logic used in user-details.tsx and acl-list.tsx:
79+
// api.isAdminApiConfigured && !isServerless()
80+
const cases = [
81+
{ adminApi: true, serverless: false, expected: true },
82+
{ adminApi: true, serverless: true, expected: false },
83+
{ adminApi: false, serverless: false, expected: false },
84+
{ adminApi: false, serverless: true, expected: false },
85+
];
86+
87+
for (const { adminApi, serverless, expected } of cases) {
88+
mockedIsServerless.mockReturnValue(serverless);
89+
const result = adminApi && !isServerless();
90+
expect(result).toBe(expected);
91+
}
92+
});
93+
});

frontend/src/components/pages/acls/user-details.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { UserAclsCard } from 'components/pages/roles/user-acls-card';
1414
import { UserInformationCard } from 'components/pages/roles/user-information-card';
1515
import { UserRolesCard } from 'components/pages/roles/user-roles-card';
1616
import { Button } from 'components/redpanda-ui/components/button';
17+
import { isServerless } from 'config';
1718
import type { UpdateRoleMembershipResponse } from 'protogen/redpanda/api/console/v1alpha1/security_pb';
1819
import { useEffect, useState } from 'react';
1920

@@ -79,7 +80,7 @@ const UserDetailsPage = ({ userName }: UserDetailsPageProps) => {
7980
<div className="flex flex-col gap-4">
8081
<UserInformationCard
8182
onEditPassword={
82-
api.isAdminApiConfigured
83+
api.isAdminApiConfigured && !isServerless()
8384
? () => {
8485
setIsChangePasswordModalOpen(true);
8586
}
@@ -124,7 +125,7 @@ const UserDetailsPage = ({ userName }: UserDetailsPageProps) => {
124125
)}
125126
</div>
126127

127-
{Boolean(api.isAdminApiConfigured) && (
128+
{Boolean(api.isAdminApiConfigured) && !isServerless() && (
128129
<ChangePasswordModal
129130
isOpen={isChangePasswordModalOpen}
130131
setIsOpen={setIsChangePasswordModalOpen}

0 commit comments

Comments
 (0)