diff --git a/frontend/src/components/ACLPage/List/List.tsx b/frontend/src/components/ACLPage/List/List.tsx index 26155172b..93a8c1e77 100644 --- a/frontend/src/components/ACLPage/List/List.tsx +++ b/frontend/src/components/ACLPage/List/List.tsx @@ -149,7 +149,7 @@ const ACList: React.FC = () => { return ( - + { return ( <> { return ( <> - + { return (
{ return ( <> - + {!isReadOnly && ( { it('renders header without create button for readonly cluster', async () => { await renderComponent({ ...initialValue, isReadOnly: true }); expect( - screen.getByRole('heading', { name: 'Connectors' }) + screen.getByRole('heading', { name: /local \/ Connectors/ }) ).toBeInTheDocument(); expect( screen.queryByRole('link', { name: 'Create Connector' }) @@ -59,7 +59,7 @@ describe('Connectors List Page', () => { it('renders header with create button for read/write cluster', async () => { await renderComponent(); expect( - screen.getByRole('heading', { name: 'Connectors' }) + screen.getByRole('heading', { name: /local \/ Connectors/ }) ).toBeInTheDocument(); expect( screen.getByRole('link', { name: 'Create Connector' }) diff --git a/frontend/src/components/Connect/New/New.tsx b/frontend/src/components/Connect/New/New.tsx index db1d82e3c..26a45c07e 100644 --- a/frontend/src/components/Connect/New/New.tsx +++ b/frontend/src/components/Connect/New/New.tsx @@ -95,6 +95,7 @@ const New: React.FC = () => { return ( {
{ return ( <> { return ( <> - + diff --git a/frontend/src/components/KsqlDb/KsqlDb.tsx b/frontend/src/components/KsqlDb/KsqlDb.tsx index d105720aa..6876a3685 100644 --- a/frontend/src/components/KsqlDb/KsqlDb.tsx +++ b/frontend/src/components/KsqlDb/KsqlDb.tsx @@ -29,7 +29,7 @@ const KsqlDb: React.FC = () => { return ( <> - + void; } const ClusterMenu: FC = ({ name, status, features, - singleMode, + openTab, + onTabClick, }) => { - const hasFeatureConfigured = (key: ClusterFeaturesEnum) => - features?.includes(key); - const [isOpen, setIsOpen] = useState(!!singleMode); const location = useLocation(); const getIsMenuItemActive = (path: string) => location.pathname.includes(path); + const hasFeatureConfigured = (key: ClusterFeaturesEnum) => + features?.includes(key); + return (
    setIsOpen((prev) => !prev)} + isOpen={openTab === name} + onClick={() => onTabClick(name)} /> - {isOpen && ( + {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && ( )} {hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_CONNECT) && ( )} {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && ( @@ -91,13 +86,13 @@ const ClusterMenu: FC = ({ {(hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_VIEW) || hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_EDIT)) && ( )} - )} +
); }; diff --git a/frontend/src/components/Nav/ClusterMenu/__tests__/ClusterMenu.spec.tsx b/frontend/src/components/Nav/ClusterMenu/__tests__/ClusterMenu.spec.tsx index ce0bb9731..2da2202c8 100644 --- a/frontend/src/components/Nav/ClusterMenu/__tests__/ClusterMenu.spec.tsx +++ b/frontend/src/components/Nav/ClusterMenu/__tests__/ClusterMenu.spec.tsx @@ -2,22 +2,23 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; import ClusterMenu from 'components/Nav/ClusterMenu/ClusterMenu'; -import userEvent from '@testing-library/user-event'; import { clusterConnectorsPath } from 'lib/paths'; import { render } from 'lib/testHelpers'; import { onlineClusterPayload } from 'lib/fixtures/clusters'; describe('ClusterMenu', () => { - const setupComponent = (cluster: Cluster, singleMode?: boolean) => ( + const handleTabClick = jest.fn(); + + const setupComponent = (cluster: Cluster, openTab?: string | false) => ( ); const getMenuItems = () => screen.getAllByRole('menuitem'); - const getMenuItem = () => screen.getByRole('menuitem'); const getBrokers = () => screen.getByTitle('Brokers'); const getTopics = () => screen.getByTitle('Brokers'); const getConsumers = () => screen.getByTitle('Brokers'); @@ -28,8 +29,6 @@ describe('ClusterMenu', () => { render(setupComponent(onlineClusterPayload)); expect(getCluster()).toBeInTheDocument(); - expect(getMenuItems().length).toEqual(1); - await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(4); expect(getBrokers()).toBeInTheDocument(); @@ -47,8 +46,6 @@ describe('ClusterMenu', () => { ], }) ); - expect(getMenuItems().length).toEqual(1); - await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(7); expect(getBrokers()).toBeInTheDocument(); @@ -59,7 +56,7 @@ describe('ClusterMenu', () => { expect(screen.getByTitle('KSQL DB')).toBeInTheDocument(); }); it('renders open cluster menu', () => { - render(setupComponent(onlineClusterPayload, true), { + render(setupComponent(onlineClusterPayload), { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)], }); @@ -77,8 +74,6 @@ describe('ClusterMenu', () => { }), { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)] } ); - expect(getMenuItems().length).toEqual(1); - await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(5); const kafkaConnect = getKafkaConnect(); diff --git a/frontend/src/components/Nav/Menu/MenuTab.tsx b/frontend/src/components/Nav/Menu/MenuTab.tsx index 9214c3b00..36752bedd 100644 --- a/frontend/src/components/Nav/Menu/MenuTab.tsx +++ b/frontend/src/components/Nav/Menu/MenuTab.tsx @@ -1,4 +1,4 @@ -import React, { type FC } from 'react'; +import React, { FC } from 'react'; import { ServerStatus } from 'generated-sources'; import * as S from './styled'; @@ -7,16 +7,11 @@ export interface MenuTabProps { title: string; status: ServerStatus; isOpen: boolean; - toggleClusterMenu: () => void; + onClick: () => void; } -const MenuTab: FC = ({ - title, - toggleClusterMenu, - status, - isOpen, -}) => ( - +const MenuTab: FC = ({ title, status, isOpen, onClick }) => ( + diff --git a/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx b/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx index 50b4ba88e..d94abe03d 100644 --- a/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx +++ b/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx @@ -14,7 +14,7 @@ describe('MenuTab component', () => { status={ServerStatus.ONLINE} isOpen title={testClusterName} - toggleClusterMenu={toggleClusterMenuMock} + onClick={toggleClusterMenuMock} {...props} /> ); diff --git a/frontend/src/components/Nav/Nav.styled.ts b/frontend/src/components/Nav/Nav.styled.ts index 4f6bde408..1aef992b7 100644 --- a/frontend/src/components/Nav/Nav.styled.ts +++ b/frontend/src/components/Nav/Nav.styled.ts @@ -11,3 +11,12 @@ export const List = styled.ul.attrs({ role: 'menu' })` margin-bottom: 2px; } `; + +export const AccordionContent = styled.div<{ isOpen: boolean }>` + overflow: hidden; + max-height: ${({ isOpen }) => (isOpen ? '500px' : '0')}; + opacity: ${({ isOpen }) => (isOpen ? '1' : '0')}; + transition: + max-height 0.4s ease-out, + opacity 0.3s ease-out; +`; diff --git a/frontend/src/components/Nav/Nav.tsx b/frontend/src/components/Nav/Nav.tsx index 2741b4351..a9bbf45b1 100644 --- a/frontend/src/components/Nav/Nav.tsx +++ b/frontend/src/components/Nav/Nav.tsx @@ -1,26 +1,32 @@ +import React, { FC, useState } from 'react'; import { useClusters } from 'lib/hooks/api/clusters'; -import React, { type FC } from 'react'; import * as S from './Nav.styled'; import MenuItem from './Menu/MenuItem'; import ClusterMenu from './ClusterMenu/ClusterMenu'; const Nav: FC = () => { - const clusters = useClusters(); + const [openTab, setOpenTab] = useState(false); + const { isSuccess, data: clusters } = useClusters(); + + const handleTabChange = (tabName: string) => { + setOpenTab((prev) => (prev === tabName ? false : tabName)); + }; return ( diff --git a/frontend/src/components/Nav/__tests__/Nav.spec.tsx b/frontend/src/components/Nav/__tests__/Nav.spec.tsx index 582c34141..5ce170093 100644 --- a/frontend/src/components/Nav/__tests__/Nav.spec.tsx +++ b/frontend/src/components/Nav/__tests__/Nav.spec.tsx @@ -34,8 +34,8 @@ describe('Nav', () => { it('renders ClusterMenu', () => { renderComponent([onlineClusterPayload, offlineClusterPayload]); - expect(screen.getAllByRole('menu').length).toEqual(3); - expect(getMenuItemsCount()).toEqual(3); + expect(screen.getAllByRole('menu').length).toEqual(5); + expect(getMenuItemsCount()).toEqual(9); expect(getDashboard()).toBeInTheDocument(); expect(screen.getByText(onlineClusterPayload.name)).toBeInTheDocument(); expect(screen.getByText(offlineClusterPayload.name)).toBeInTheDocument(); diff --git a/frontend/src/components/Schemas/Details/Details.tsx b/frontend/src/components/Schemas/Details/Details.tsx index 2d0c345d7..e275226c2 100644 --- a/frontend/src/components/Schemas/Details/Details.tsx +++ b/frontend/src/components/Schemas/Details/Details.tsx @@ -72,6 +72,7 @@ const Details: React.FC = () => { return ( <> { return ( <> = ({ schema }) => { return ( { return ( <> - + {!isReadOnly && ( <> diff --git a/frontend/src/components/Schemas/New/New.tsx b/frontend/src/components/Schemas/New/New.tsx index 4bfcc4605..3f45b8364 100644 --- a/frontend/src/components/Schemas/New/New.tsx +++ b/frontend/src/components/Schemas/New/New.tsx @@ -72,6 +72,7 @@ const New: React.FC = () => { return ( { const { isReadOnly } = React.useContext(ClusterContext); + const { clusterName } = useAppParams(); const [searchParams, setSearchParams] = useSearchParams(); // Set the search params to the url based on the localStorage value @@ -46,7 +48,7 @@ const ListPage: React.FC = () => { return ( <> - + {!isReadOnly && ( { return ( <> { })); }); it('checks header for create new', async () => { - await act(() => { + await act(async () => { renderComponent(clusterTopicNewPath(clusterName)); }); - expect(screen.getByRole('heading', { name: 'Create' })).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: /local \/ Topics \/ Create/ }) + ).toBeInTheDocument(); }); it('checks header for copy', async () => { - await act(() => { + await act(async () => { renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`); }); - expect(screen.getByRole('heading', { name: 'Copy' })).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: /local \/ Topics \/ Copy/ }) + ).toBeInTheDocument(); }); it('validates form', async () => { renderComponent(clusterTopicNewPath(clusterName)); await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName); await userEvent.clear(screen.getByPlaceholderText('Topic Name')); await userEvent.tab(); - await expect( - screen.getByText('Topic Name is required') - ).toBeInTheDocument(); + expect(screen.getByText('Topic Name is required')).toBeInTheDocument(); await userEvent.type( screen.getByLabelText('Number of Partitions *'), minValue ); await userEvent.clear(screen.getByLabelText('Number of Partitions *')); await userEvent.tab(); - await expect( + expect( screen.getByText('Number of Partitions is required and must be a number') ).toBeInTheDocument(); diff --git a/frontend/src/components/Topics/Topic/Topic.tsx b/frontend/src/components/Topics/Topic/Topic.tsx index a40bcfc12..b24e1cf82 100644 --- a/frontend/src/components/Topics/Topic/Topic.tsx +++ b/frontend/src/components/Topics/Topic/Topic.tsx @@ -68,6 +68,7 @@ const Topic: React.FC = () => { return ( <> theme.pageHeading.backLink.color.disabled}; + position: relative; +`; + export const BackLink = styled(NavLink)` color: ${({ theme }) => theme.pageHeading.backLink.color.normal}; position: relative; @@ -13,16 +18,12 @@ export const BackLink = styled(NavLink)` &:hover { ${({ theme }) => theme.pageHeading.backLink.color.hover}; } +`; - &::after { - content: ''; - position: absolute; - right: -11px; - bottom: 2px; - border-left: 1px solid ${({ theme }) => theme.pageHeading.dividerColor}; - height: 20px; - transform: rotate(14deg); - } +export const Slash = styled.text` + color: ${({ theme }) => theme.pageHeading.backLink.color.disabled}; + position: relative; + margin: 0 8px; `; export const Wrapper = styled.div` diff --git a/frontend/src/components/common/PageHeading/PageHeading.tsx b/frontend/src/components/common/PageHeading/PageHeading.tsx index ffbdc1a27..56624254c 100644 --- a/frontend/src/components/common/PageHeading/PageHeading.tsx +++ b/frontend/src/components/common/PageHeading/PageHeading.tsx @@ -5,12 +5,14 @@ import * as S from './PageHeading.styled'; interface PageHeadingProps { text: string; + clusterName?: string; backTo?: string; backText?: string; } const PageHeading: React.FC> = ({ text, + clusterName, backTo, backText, children, @@ -20,8 +22,21 @@ const PageHeading: React.FC> = ({ return ( - {isBackButtonVisible && {backText}} - {text} + + {clusterName && ( + <> + {clusterName} + / + + )} + {isBackButtonVisible && ( + <> + {backText} + / + + )} + {text} +
{children}
diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index f6cd2bacc..6c1d9cee5 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -371,6 +371,7 @@ export const theme = { dividerColor: Colors.neutral[30], backLink: { color: { + disabled: Colors.neutral[50], normal: Colors.brand[70], hover: Colors.brand[60], }, @@ -855,6 +856,7 @@ export const darkTheme: ThemeType = { dividerColor: Colors.neutral[50], backLink: { color: { + disabled: Colors.neutral[50], normal: Colors.brand[30], hover: Colors.brand[15], },