diff --git a/frontend/src/components/Connect/Details/Topics/Topics.styled.ts b/frontend/src/components/Connect/Details/Topics/Topics.styled.ts new file mode 100644 index 0000000000..1710f5e30d --- /dev/null +++ b/frontend/src/components/Connect/Details/Topics/Topics.styled.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const ToggleButtonWrapper = styled.div` + margin-top: 8px; +`; diff --git a/frontend/src/components/Connect/Details/Topics/Topics.tsx b/frontend/src/components/Connect/Details/Topics/Topics.tsx index c7d5b8e774..8f842191c7 100644 --- a/frontend/src/components/Connect/Details/Topics/Topics.tsx +++ b/frontend/src/components/Connect/Details/Topics/Topics.tsx @@ -4,8 +4,10 @@ import { RouterParamsClusterConnectConnector } from 'lib/paths'; import { useConnector } from 'lib/hooks/api/kafkaConnect'; import Table from 'components/common/NewTable'; import { ColumnDef } from '@tanstack/react-table'; +import { Button } from 'components/common/Button/Button'; import { TopicNameCell } from './cells/TopicNameCell'; +import * as S from './Topics.styled'; const columns: ColumnDef<{ topicName: string }>[] = [ { @@ -15,17 +17,39 @@ const columns: ColumnDef<{ topicName: string }>[] = [ }, ]; +export const COLLAPSED_TOPICS_COUNT = 10; + const Topics = () => { const routerProps = useAppParams(); + const [isExpanded, setIsExpanded] = React.useState(false); const { data: connector } = useConnector(routerProps); - const tableData = (connector?.topics ?? []).map((topicName) => ({ - topicName, - })); + const topics = connector?.topics ?? []; + const visibleTopics = isExpanded + ? topics + : topics.slice(0, COLLAPSED_TOPICS_COUNT); + const tableData = visibleTopics.map((topicName) => ({ topicName })); return ( - + <> +
+ {topics.length > COLLAPSED_TOPICS_COUNT && ( + + + + )} + ); }; diff --git a/frontend/src/components/Connect/Details/Topics/__tests__/Topics.spec.tsx b/frontend/src/components/Connect/Details/Topics/__tests__/Topics.spec.tsx new file mode 100644 index 0000000000..1fb975ad40 --- /dev/null +++ b/frontend/src/components/Connect/Details/Topics/__tests__/Topics.spec.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterConnectConnectorTopicsPath } from 'lib/paths'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { connector } from 'lib/fixtures/kafkaConnect'; +import { useConnector } from 'lib/hooks/api/kafkaConnect'; +import Topics, { + COLLAPSED_TOPICS_COUNT, +} from 'components/Connect/Details/Topics/Topics'; + +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnector: jest.fn(), +})); + +const clusterName = 'local'; +const connectName = 'main-connect'; +const connectorName = 'big-connector'; +const connectorTopics = Array.from({ length: 12 }, (_, index) => { + return `topic-${index + 1}`; +}); + +describe('Connector topics', () => { + const renderComponent = (topics: string[] | undefined) => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: { + ...connector, + topics, + }, + })); + + return render( + + + , + { + initialEntries: [ + clusterConnectConnectorTopicsPath( + clusterName, + connectName, + connectorName + ), + ], + } + ); + }; + + it('renders a collapsed topic table by default and expands on demand', async () => { + renderComponent(connectorTopics); + + expect(screen.getByText('topic-10')).toBeInTheDocument(); + expect(screen.queryByText('topic-11')).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Show all 12 topics' }) + ).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole('button', { name: 'Show all 12 topics' }) + ); + + expect(screen.getByText('topic-12')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Show less' }) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Show less' })); + + expect(screen.queryByText('topic-11')).not.toBeInTheDocument(); + }); + + it('does not show a toggle button when topics do not exceed the collapsed count', () => { + const topics = Array.from( + { length: COLLAPSED_TOPICS_COUNT }, + (_, index) => { + return `topic-${index + 1}`; + } + ); + + renderComponent(topics); + + expect( + screen.getByText(`topic-${COLLAPSED_TOPICS_COUNT}`) + ).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /show/i }) + ).not.toBeInTheDocument(); + }); + + it('renders an empty state without a toggle button when topics are undefined', () => { + renderComponent(undefined); + + expect(screen.getByText('No topics found')).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /show/i }) + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.styled.ts b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.styled.ts index d1abe4f08c..a65b1f974c 100644 --- a/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.styled.ts +++ b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.styled.ts @@ -19,3 +19,7 @@ export const TagsWrapper = styled.div` } `} `; + +export const ToggleButtonWrapper = styled.div` + margin-top: 4px; +`; diff --git a/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.tsx b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.tsx index 359166e458..4544942eac 100644 --- a/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.tsx +++ b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell.tsx @@ -4,27 +4,53 @@ import { CellContext } from '@tanstack/react-table'; import { MultiLineTag } from 'components/common/Tag/Tag.styled'; import { ClusterNameRoute, clusterTopicPath } from 'lib/paths'; import useAppParams from 'lib/hooks/useAppParams'; +import { Button } from 'components/common/Button/Button'; import * as S from './TopicsCell.styled'; +export const COLLAPSED_TOPICS_COUNT = 5; + const TopicsCell: React.FC> = ({ row, }) => { const { topics } = row.original; const { clusterName } = useAppParams(); + const [isExpanded, setIsExpanded] = React.useState(false); + + const visibleTopics = isExpanded + ? topics + : topics?.slice(0, COLLAPSED_TOPICS_COUNT); + + const hiddenTopicsCount = Math.max( + 0, + (topics?.length ?? 0) - COLLAPSED_TOPICS_COUNT + ); return ( - - {topics?.map((t) => { - const href = clusterTopicPath(clusterName, t); - - return ( - - {t} - - ); - })} - + <> + + {visibleTopics?.map((t) => { + const href = clusterTopicPath(clusterName, t); + + return ( + + {t} + + ); + })} + + {hiddenTopicsCount > 0 && ( + + + + )} + ); }; diff --git a/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/__tests__/TopicsCell.spec.tsx b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/__tests__/TopicsCell.spec.tsx new file mode 100644 index 0000000000..a4bd636e85 --- /dev/null +++ b/frontend/src/components/Connect/List/ConnectorsTable/connectorsColumns/cells/__tests__/TopicsCell.spec.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterConnectorsPath } from 'lib/paths'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { connectors } from 'lib/fixtures/kafkaConnect'; +import { CellContext } from '@tanstack/react-table'; +import { FullConnectorInfo } from 'generated-sources'; +import TopicsCell, { + COLLAPSED_TOPICS_COUNT, +} from 'components/Connect/List/ConnectorsTable/connectorsColumns/cells/TopicsCell'; + +const clusterName = 'local'; +const connectorTopics = Array.from({ length: 7 }, (_, index) => { + return `topic-${index + 1}`; +}); + +const getCellProps = (topics: string[] | undefined) => + ({ + row: { + original: { + ...connectors[0], + topics, + }, + }, + }) as CellContext; + +describe('TopicsCell', () => { + const renderComponent = (topics: string[] | undefined) => + render( + + + , + { initialEntries: [clusterConnectorsPath(clusterName)] } + ); + + it('renders a collapsed topic list by default and expands on demand', async () => { + renderComponent(connectorTopics); + + expect(screen.getByText('topic-5')).toBeInTheDocument(); + expect(screen.queryByText('topic-6')).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Show 2 more' }) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Show 2 more' })); + + expect(screen.getByText('topic-7')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Show less' }) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Show less' })); + + expect(screen.queryByText('topic-6')).not.toBeInTheDocument(); + }); + + it('does not show a toggle button when topics do not exceed the collapsed count', () => { + const topics = Array.from( + { length: COLLAPSED_TOPICS_COUNT }, + (_, index) => { + return `topic-${index + 1}`; + } + ); + + renderComponent(topics); + + expect( + screen.getByText(`topic-${COLLAPSED_TOPICS_COUNT}`) + ).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /show/i }) + ).not.toBeInTheDocument(); + }); + + it.each<[string, string[] | undefined]>([ + ['empty', []], + ['undefined', undefined], + ])('does not show a toggle button when topics are %s', (_label, topics) => { + renderComponent(topics); + + expect( + screen.queryByRole('button', { name: /show/i }) + ).not.toBeInTheDocument(); + }); +});