diff --git a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx index 0cbe4dd78..2e19eaa63 100644 --- a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx +++ b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx @@ -49,7 +49,7 @@ describe('Connectors List Page', () => { 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/Nav/ClusterMenu/ClusterMenu.tsx b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx index f6062402f..ec8aa641a 100644 --- a/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx +++ b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx @@ -1,23 +1,16 @@ -import React, { type FC, useState } from 'react'; +import React, { FC } from 'react'; import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; import * as S from 'components/Nav/Nav.styled'; import MenuTab from 'components/Nav/Menu/MenuTab'; import MenuItem from 'components/Nav/Menu/MenuItem'; import { clusterACLPath, - clusterAclRelativePath, - clusterBrokerRelativePath, - clusterBrokersPath, clusterConnectorsPath, - clusterConnectorsRelativePath, clusterConsumerGroupsPath, - clusterConsumerGroupsRelativePath, clusterKsqlDbPath, - clusterKsqlDbRelativePath, clusterSchemasPath, - clusterSchemasRelativePath, clusterTopicsPath, - clusterTopicsRelativePath, + clusterBrokersPath, } from 'lib/paths'; import { useLocation } from 'react-router-dom'; import { useLocalStorage } from 'lib/hooks/useLocalStorage'; @@ -27,18 +20,17 @@ interface ClusterMenuProps { name: Cluster['name']; status: Cluster['status']; features: Cluster['features']; - singleMode?: boolean; + openTab: string | undefined; + onTabClick: (tabName: string) => 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 [colorKey, setColorKey] = useLocalStorage( `clusterColor-${name}`, @@ -48,49 +40,52 @@ const ClusterMenu: FC = ({ const getIsMenuItemActive = (path: string) => location.pathname.includes(path); + const hasFeatureConfigured = (key: ClusterFeaturesEnum) => + features?.includes(key); + return ( setIsOpen((prev) => !prev)} setColorKey={setColorKey} + isOpen={openTab === name} + onClick={() => onTabClick(name)} /> - {isOpen && ( + {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && ( )} {hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_CONNECT) && ( )} {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && ( @@ -98,13 +93,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..c1aaecf30 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) => ( ); 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 6ee78ae18..7620bcbfe 100644 --- a/frontend/src/components/Nav/Menu/MenuTab.tsx +++ b/frontend/src/components/Nav/Menu/MenuTab.tsx @@ -9,18 +9,18 @@ export interface MenuTabProps { title: string; status: ServerStatus; isOpen: boolean; - toggleClusterMenu: () => void; + onClick: () => void; setColorKey: Dispatch>; } const MenuTab: FC = ({ title, - toggleClusterMenu, + onClick, status, isOpen, setColorKey, }) => ( - + diff --git a/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx b/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx index d8d08a9a8..028798ced 100644 --- a/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx +++ b/frontend/src/components/Nav/Menu/__tests__/MenuTab.spec.tsx @@ -15,7 +15,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 7d25483e7..8bdb52c98 100644 --- a/frontend/src/components/Nav/Nav.styled.ts +++ b/frontend/src/components/Nav/Nav.styled.ts @@ -20,3 +20,12 @@ export const ClusterList = styled.ul.attrs<{ $colorKey: ClusterColorKey }>({ background-color: ${({ theme, $colorKey }) => theme.clusterMenu.backgroundColor[$colorKey]}; `; + +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..3c83edd17 100644 --- a/frontend/src/components/Nav/Nav.tsx +++ b/frontend/src/components/Nav/Nav.tsx @@ -1,26 +1,28 @@ +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(); + const { isSuccess, data: clusters } = useClusters(); 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/Topics/New/__test__/New.spec.tsx b/frontend/src/components/Topics/New/__test__/New.spec.tsx index 867291e0b..2793840fc 100644 --- a/frontend/src/components/Topics/New/__test__/New.spec.tsx +++ b/frontend/src/components/Topics/New/__test__/New.spec.tsx @@ -42,33 +42,35 @@ describe('New', () => { })); }); 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/common/PageHeading/PageHeading.styled.ts b/frontend/src/components/common/PageHeading/PageHeading.styled.ts index 7010429ec..77513688b 100644 --- a/frontend/src/components/common/PageHeading/PageHeading.styled.ts +++ b/frontend/src/components/common/PageHeading/PageHeading.styled.ts @@ -6,6 +6,11 @@ export const Breadcrumbs = styled.div` align-items: baseline; `; +export const ClusterTitle = styled.text` + color: ${({ theme }) => 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..8762e38a3 100644 --- a/frontend/src/components/common/PageHeading/PageHeading.tsx +++ b/frontend/src/components/common/PageHeading/PageHeading.tsx @@ -1,5 +1,7 @@ import React, { PropsWithChildren } from 'react'; import Heading from 'components/common/heading/Heading.styled'; +import { ClusterGroupParam } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; import * as S from './PageHeading.styled'; @@ -16,12 +18,26 @@ const PageHeading: React.FC> = ({ children, }) => { const isBackButtonVisible = backTo && backText; + const { clusterName } = useAppParams(); 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 d0f6c4067..80882193f 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -435,6 +435,7 @@ export const theme = { dividerColor: Colors.neutral[30], backLink: { color: { + disabled: Colors.neutral[50], normal: Colors.brand[70], hover: Colors.brand[60], }, @@ -964,6 +965,7 @@ export const darkTheme: ThemeType = { dividerColor: Colors.neutral[50], backLink: { color: { + disabled: Colors.neutral[50], normal: Colors.brand[30], hover: Colors.brand[15], },