diff --git a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx index cb64314c69..8fc6f735e6 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx @@ -9,7 +9,7 @@ const KsqlDb: React.FC = () => { return ( diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/KsqlDbItem.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/KsqlDbItem.tsx new file mode 100644 index 0000000000..88dcf128c6 --- /dev/null +++ b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/KsqlDbItem.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources'; +import { useTableState } from 'lib/hooks/useTableState'; +import { SmartTable } from 'components/common/SmartTable/SmartTable'; +import { TableColumn } from 'components/common/SmartTable/TableColumn'; +import { KsqlDescription } from 'redux/interfaces/ksqlDb'; +import { ksqlRowData } from 'components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData'; + +export enum KsqlDbItemType { + Tables = 'tables', + Streams = 'streams', +} + +export interface RowsType { + tables: KsqlTableDescription[]; + streams: KsqlStreamDescription[]; +} +export interface KsqlDbItemProps { + type: KsqlDbItemType; + fetching: boolean; + rows: RowsType; +} + +export type KsqlDescriptionAccessor = keyof KsqlDescription; + +export interface HeadersType { + Header: string; + accessor: KsqlDescriptionAccessor; +} + +export interface KsqlTableState { + name: string; + topic: string; + keyFormat: string; + valueFormat: string; + isWindowed: string; +} + +export const headers: HeadersType[] = [ + { Header: 'Name', accessor: 'name' }, + { Header: 'Topic', accessor: 'topic' }, + { Header: 'Key Format', accessor: 'keyFormat' }, + { Header: 'Value Format', accessor: 'valueFormat' }, + { Header: 'Is Windowed', accessor: 'isWindowed' }, +]; + +const KsqlDbItem: React.FC = ({ type, fetching, rows }) => { + const preparedRows = rows[type]?.map(ksqlRowData) || []; + const tableState = useTableState(preparedRows, { + idSelector: ({ name }) => name, + totalPages: 0, + }); + + if (fetching) { + return ; + } + return ( + + + + + + + + ); +}; + +export default KsqlDbItem; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/__test__/KsqlDbItem.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/__test__/KsqlDbItem.spec.tsx new file mode 100644 index 0000000000..e1be151503 --- /dev/null +++ b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/__test__/KsqlDbItem.spec.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterKsqlDbTablesPath } from 'lib/paths'; +import KsqlDbItem, { + KsqlDbItemProps, + KsqlDbItemType, +} from 'components/KsqlDb/List/KsqlDbItem/KsqlDbItem'; +import { screen } from '@testing-library/dom'; +import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures'; + +describe('KsqlDbItem', () => { + const tablesPathname = clusterKsqlDbTablesPath(); + + const component = (props: Partial = {}) => ( + + + + ); + + it('renders progressbar when fetching tables and streams', () => { + render(component({ fetching: true }), { + initialEntries: [clusterKsqlDbTablesPath()], + }); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + it('show no text if no data found', () => { + render(component({}), { + initialEntries: [clusterKsqlDbTablesPath()], + }); + expect(screen.getByText('No tables or streams found')).toBeInTheDocument(); + }); + it('renders with tables', () => { + render( + component({ + rows: { + tables: fetchKsqlDbTablesPayload.tables, + streams: [], + }, + }), + { + initialEntries: [clusterKsqlDbTablesPath()], + } + ); + expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10); + }); + it('renders with streams', () => { + render( + component({ + type: KsqlDbItemType.Streams, + rows: { + tables: [], + streams: fetchKsqlDbTablesPayload.streams, + }, + }), + { + initialEntries: [clusterKsqlDbTablesPath()], + } + ); + expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10); + }); +}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData.ts b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData.ts new file mode 100644 index 0000000000..a2c9e65891 --- /dev/null +++ b/kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData.ts @@ -0,0 +1,12 @@ +import { KsqlDescription } from 'redux/interfaces/ksqlDb'; +import { KsqlTableState } from 'components/KsqlDb/List/KsqlDbItem/KsqlDbItem'; + +export const ksqlRowData = (data: KsqlDescription): KsqlTableState => { + return { + name: data.name || '', + topic: data.topic || '', + keyFormat: data.keyFormat || '', + valueFormat: data.valueFormat || '', + isWindowed: 'isWindowed' in data ? String(data.isWindowed) : '-', + }; +}; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx index 543aaa43f5..7c9ab90a96 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx @@ -1,44 +1,32 @@ -import React, { FC, useEffect } from 'react'; +import React, { FC } from 'react'; import useAppParams from 'lib/hooks/useAppParams'; import * as Metrics from 'components/common/Metrics'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import ListItem from 'components/KsqlDb/List/ListItem'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; +import { useSelector, useDispatch } from 'react-redux'; import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors'; -import { clusterKsqlDbQueryRelativePath, ClusterNameRoute } from 'lib/paths'; +import { + clusterKsqlDbQueryRelativePath, + ClusterNameRoute, + clusterKsqlDbStreamsPath, + clusterKsqlDbTablesPath, + clusterKsqlDbStreamsRelativePath, + clusterKsqlDbTablesRelativePath, +} from 'lib/paths'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import { Button } from 'components/common/Button/Button'; -import { KsqlDescription } from 'redux/interfaces/ksqlDb'; +import Navbar from 'components/common/Navigation/Navbar.styled'; +import { NavLink, Route, Routes, Navigate } from 'react-router-dom'; +import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; -export type KsqlDescriptionAccessor = keyof KsqlDescription; - -interface HeadersType { - Header: string; - accessor: KsqlDescriptionAccessor; -} -const headers: HeadersType[] = [ - { Header: 'Type', accessor: 'type' }, - { Header: 'Name', accessor: 'name' }, - { Header: 'Topic', accessor: 'topic' }, - { Header: 'Key Format', accessor: 'keyFormat' }, - { Header: 'Value Format', accessor: 'valueFormat' }, - { Header: 'Is Windowed', accessor: 'isWindowed' }, -]; - -const accessors = headers.map((header) => header.accessor); +import KsqlDbItem, { KsqlDbItemType } from './KsqlDbItem/KsqlDbItem'; const List: FC = () => { - const dispatch = useDispatch(); - const { clusterName } = useAppParams(); + const dispatch = useDispatch(); const { rows, fetching, tablesCount, streamsCount } = useSelector(getKsqlDbTables); - useEffect(() => { + React.useEffect(() => { dispatch(fetchKsqlDbTables(clusterName)); }, [clusterName, dispatch]); @@ -68,31 +56,48 @@ const List: FC = () => {
- {fetching ? ( - - ) : ( - - - - {headers.map(({ Header, accessor }) => ( - - ))} - - - - {rows.map((row) => ( - - ))} - {rows.length === 0 && ( - - - - )} - -
- No tables or streams found -
- )} + + (isActive ? 'is-active' : '')} + end + > + Tables + + (isActive ? 'is-active' : '')} + end + > + Streams + + + + } + /> + + } + /> + + } + /> +
); diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx deleted file mode 100644 index e9d073bb5d..0000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { KsqlDescription } from 'redux/interfaces/ksqlDb'; -import { KsqlDescriptionAccessor } from 'components/KsqlDb/List/List'; - -interface Props { - accessors: KsqlDescriptionAccessor[]; - data: KsqlDescription; -} - -const ListItem: React.FC = ({ accessors, data }) => { - return ( - - {accessors.map((accessor) => ( - {data[accessor]?.toString()} - ))} - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx index b367a6117c..15be8055ab 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx @@ -1,32 +1,22 @@ import React from 'react'; import List from 'components/KsqlDb/List/List'; -import { clusterKsqlDbPath } from 'lib/paths'; -import { render, WithRoute } from 'lib/testHelpers'; +import { render } from 'lib/testHelpers'; import fetchMock from 'fetch-mock'; -import { screen, waitForElementToBeRemoved } from '@testing-library/dom'; - -const clusterName = 'local'; +import { screen } from '@testing-library/dom'; const renderComponent = () => { - render( - - - , - { initialEntries: [clusterKsqlDbPath(clusterName)] } - ); + render(); }; describe('KsqlDb List', () => { afterEach(() => fetchMock.reset()); - it('renders placeholder on empty data', async () => { - fetchMock.post( - { - url: `/api/clusters/${clusterName}/ksql`, - }, - { data: [] } - ); + it('renders List component with Tables and Streams tabs', async () => { renderComponent(); - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - expect(screen.getByText('No tables or streams found')).toBeInTheDocument(); + + const Tables = screen.getByTitle('Tables'); + const Streams = screen.getByTitle('Streams'); + + expect(Tables).toBeInTheDocument(); + expect(Streams).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx deleted file mode 100644 index 9014d06d4f..0000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { clusterKsqlDbPath } from 'lib/paths'; -import { render, WithRoute } from 'lib/testHelpers'; -import { screen } from '@testing-library/dom'; -import ListItem from 'components/KsqlDb/List/ListItem'; -import { KsqlDescription } from 'redux/interfaces/ksqlDb'; - -const clusterName = 'local'; - -const renderComponent = ({ - accessors, - data, -}: { - accessors: (keyof KsqlDescription)[]; - data: Record; -}) => { - render( - - - , - { initialEntries: [clusterKsqlDbPath(clusterName)] } - ); -}; - -describe('KsqlDb List Item', () => { - it('renders placeholder on one data', async () => { - renderComponent({ - accessors: ['accessors' as keyof KsqlDescription], - data: { accessors: 'accessors text' }, - }); - - expect(screen.getByText('accessors text')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/lib/paths.ts b/kafka-ui-react-app/src/lib/paths.ts index 1c4caef090..2e15a856a8 100644 --- a/kafka-ui-react-app/src/lib/paths.ts +++ b/kafka-ui-react-app/src/lib/paths.ts @@ -233,9 +233,18 @@ export type RouterParamsClusterConnectConnector = { // KsqlDb export const clusterKsqlDbRelativePath = 'ksqldb'; export const clusterKsqlDbQueryRelativePath = 'query'; +export const clusterKsqlDbTablesRelativePath = 'tables'; +export const clusterKsqlDbStreamsRelativePath = 'streams'; + export const clusterKsqlDbPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterPath(clusterName)}/${clusterKsqlDbRelativePath}`; export const clusterKsqlDbQueryPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbQueryRelativePath}`; +export const clusterKsqlDbTablesPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbTablesRelativePath}`; +export const clusterKsqlDbStreamsPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbStreamsRelativePath}`; diff --git a/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts b/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts index 93c5373368..9bad9beb26 100644 --- a/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts +++ b/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts @@ -18,7 +18,6 @@ export interface KsqlState { } export interface KsqlDescription { - type?: string; name?: string; topic?: string; keyFormat?: string; diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts index 44213996d3..5e60caa74c 100644 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts @@ -15,7 +15,10 @@ describe('TopicMessages selectors', () => { it('Returns empty state', () => { expect(selectors.getKsqlDbTables(store.getState())).toEqual({ - rows: [], + rows: { + streams: [], + tables: [], + }, fetched: false, fetching: true, tablesCount: 0, @@ -34,10 +37,10 @@ describe('TopicMessages selectors', () => { it('Returns tables and streams', () => { expect(selectors.getKsqlDbTables(store.getState())).toEqual({ - rows: [ - ...fetchKsqlDbTablesPayload.streams, - ...fetchKsqlDbTablesPayload.tables, - ], + rows: { + streams: [...fetchKsqlDbTablesPayload.streams], + tables: [...fetchKsqlDbTablesPayload.tables], + }, fetched: true, fetching: false, tablesCount: 2, diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts index 6f8bbcd61b..0e61995ea5 100644 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts @@ -15,7 +15,7 @@ const getKsqlExecutionStatus = createFetchingSelector('ksqlDb/executeKsql'); export const getKsqlDbTables = createSelector( [ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus], (state, status) => ({ - rows: [...state.streams, ...state.tables], + rows: { streams: [...state.streams], tables: [...state.tables] }, fetched: status === AsyncRequestStatus.fulfilled, fetching: status === AsyncRequestStatus.pending, tablesCount: state.tables.length,