Separating ksqlDb tables and streams by tabs (#2145)

* separating ksqlDb tables and streams by tabs

* adding tests
This commit is contained in:
Robert Azizbekyan 2022-06-16 13:57:27 +04:00 committed by GitHub
parent 41fd765d83
commit 0528c3a28f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 239 additions and 135 deletions

View file

@ -9,7 +9,7 @@ const KsqlDb: React.FC = () => {
return ( return (
<Routes> <Routes>
<Route <Route
index path="/*"
element={ element={
<BreadcrumbRoute> <BreadcrumbRoute>
<List /> <List />

View file

@ -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<KsqlDbItemProps> = ({ type, fetching, rows }) => {
const preparedRows = rows[type]?.map(ksqlRowData) || [];
const tableState = useTableState<KsqlTableState, string>(preparedRows, {
idSelector: ({ name }) => name,
totalPages: 0,
});
if (fetching) {
return <PageLoader />;
}
return (
<SmartTable
tableState={tableState}
isFullwidth
placeholder="No tables or streams found"
hoverable
>
<TableColumn title="Name" field="name" />
<TableColumn title="Topic" field="topic" />
<TableColumn title="Key Format" field="keyFormat" />
<TableColumn title="Value Format" field="valueFormat" />
<TableColumn title="Is Windowed" field="isWindowed" />
</SmartTable>
);
};
export default KsqlDbItem;

View file

@ -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<KsqlDbItemProps> = {}) => (
<WithRoute path={tablesPathname}>
<KsqlDbItem
type={KsqlDbItemType.Tables}
fetching={false}
rows={{ tables: [], streams: [] }}
{...props}
/>
</WithRoute>
);
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);
});
});

View file

@ -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) : '-',
};
};

View file

@ -1,44 +1,32 @@
import React, { FC, useEffect } from 'react'; import React, { FC } from 'react';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import * as Metrics from 'components/common/Metrics'; import * as Metrics from 'components/common/Metrics';
import PageLoader from 'components/common/PageLoader/PageLoader'; import { useSelector, useDispatch } from 'react-redux';
import ListItem from 'components/KsqlDb/List/ListItem';
import { useDispatch, useSelector } from 'react-redux';
import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors'; 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 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 { 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; import KsqlDbItem, { KsqlDbItemType } from './KsqlDbItem/KsqlDbItem';
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);
const List: FC = () => { const List: FC = () => {
const dispatch = useDispatch();
const { clusterName } = useAppParams<ClusterNameRoute>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const dispatch = useDispatch();
const { rows, fetching, tablesCount, streamsCount } = const { rows, fetching, tablesCount, streamsCount } =
useSelector(getKsqlDbTables); useSelector(getKsqlDbTables);
useEffect(() => { React.useEffect(() => {
dispatch(fetchKsqlDbTables(clusterName)); dispatch(fetchKsqlDbTables(clusterName));
}, [clusterName, dispatch]); }, [clusterName, dispatch]);
@ -68,31 +56,48 @@ const List: FC = () => {
</Metrics.Section> </Metrics.Section>
</Metrics.Wrapper> </Metrics.Wrapper>
<div> <div>
{fetching ? ( <Navbar role="navigation">
<PageLoader /> <NavLink
) : ( to={clusterKsqlDbTablesPath(clusterName)}
<Table isFullwidth> className={({ isActive }) => (isActive ? 'is-active' : '')}
<thead> end
<tr> >
{headers.map(({ Header, accessor }) => ( Tables
<TableHeaderCell title={Header} key={accessor} /> </NavLink>
))} <NavLink
</tr> to={clusterKsqlDbStreamsPath(clusterName)}
</thead> className={({ isActive }) => (isActive ? 'is-active' : '')}
<tbody> end
{rows.map((row) => ( >
<ListItem key={row.name} accessors={accessors} data={row} /> Streams
))} </NavLink>
{rows.length === 0 && ( </Navbar>
<tr> <Routes>
<td colSpan={headers.length + 1}> <Route
No tables or streams found index
</td> element={<Navigate to={clusterKsqlDbTablesRelativePath} />}
</tr> />
)} <Route
</tbody> path={clusterKsqlDbTablesRelativePath}
</Table> element={
)} <KsqlDbItem
type={KsqlDbItemType.Tables}
fetching={fetching}
rows={rows}
/>
}
/>
<Route
path={clusterKsqlDbStreamsRelativePath}
element={
<KsqlDbItem
type={KsqlDbItemType.Streams}
fetching={fetching}
rows={rows}
/>
}
/>
</Routes>
</div> </div>
</> </>
); );

View file

@ -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<Props> = ({ accessors, data }) => {
return (
<tr>
{accessors.map((accessor) => (
<td key={accessor}>{data[accessor]?.toString()}</td>
))}
</tr>
);
};
export default ListItem;

View file

@ -1,32 +1,22 @@
import React from 'react'; import React from 'react';
import List from 'components/KsqlDb/List/List'; import List from 'components/KsqlDb/List/List';
import { clusterKsqlDbPath } from 'lib/paths'; import { render } from 'lib/testHelpers';
import { render, WithRoute } from 'lib/testHelpers';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { screen, waitForElementToBeRemoved } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
const clusterName = 'local';
const renderComponent = () => { const renderComponent = () => {
render( render(<List />);
<WithRoute path={clusterKsqlDbPath()}>
<List />
</WithRoute>,
{ initialEntries: [clusterKsqlDbPath(clusterName)] }
);
}; };
describe('KsqlDb List', () => { describe('KsqlDb List', () => {
afterEach(() => fetchMock.reset()); afterEach(() => fetchMock.reset());
it('renders placeholder on empty data', async () => { it('renders List component with Tables and Streams tabs', async () => {
fetchMock.post(
{
url: `/api/clusters/${clusterName}/ksql`,
},
{ data: [] }
);
renderComponent(); 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();
}); });
}); });

View file

@ -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<string, string>;
}) => {
render(
<WithRoute path={clusterKsqlDbPath()}>
<ListItem accessors={accessors} data={data} />
</WithRoute>,
{ 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();
});
});

View file

@ -233,9 +233,18 @@ export type RouterParamsClusterConnectConnector = {
// KsqlDb // KsqlDb
export const clusterKsqlDbRelativePath = 'ksqldb'; export const clusterKsqlDbRelativePath = 'ksqldb';
export const clusterKsqlDbQueryRelativePath = 'query'; export const clusterKsqlDbQueryRelativePath = 'query';
export const clusterKsqlDbTablesRelativePath = 'tables';
export const clusterKsqlDbStreamsRelativePath = 'streams';
export const clusterKsqlDbPath = ( export const clusterKsqlDbPath = (
clusterName: ClusterName = RouteParams.clusterName clusterName: ClusterName = RouteParams.clusterName
) => `${clusterPath(clusterName)}/${clusterKsqlDbRelativePath}`; ) => `${clusterPath(clusterName)}/${clusterKsqlDbRelativePath}`;
export const clusterKsqlDbQueryPath = ( export const clusterKsqlDbQueryPath = (
clusterName: ClusterName = RouteParams.clusterName clusterName: ClusterName = RouteParams.clusterName
) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbQueryRelativePath}`; ) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbQueryRelativePath}`;
export const clusterKsqlDbTablesPath = (
clusterName: ClusterName = RouteParams.clusterName
) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbTablesRelativePath}`;
export const clusterKsqlDbStreamsPath = (
clusterName: ClusterName = RouteParams.clusterName
) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbStreamsRelativePath}`;

View file

@ -18,7 +18,6 @@ export interface KsqlState {
} }
export interface KsqlDescription { export interface KsqlDescription {
type?: string;
name?: string; name?: string;
topic?: string; topic?: string;
keyFormat?: string; keyFormat?: string;

View file

@ -15,7 +15,10 @@ describe('TopicMessages selectors', () => {
it('Returns empty state', () => { it('Returns empty state', () => {
expect(selectors.getKsqlDbTables(store.getState())).toEqual({ expect(selectors.getKsqlDbTables(store.getState())).toEqual({
rows: [], rows: {
streams: [],
tables: [],
},
fetched: false, fetched: false,
fetching: true, fetching: true,
tablesCount: 0, tablesCount: 0,
@ -34,10 +37,10 @@ describe('TopicMessages selectors', () => {
it('Returns tables and streams', () => { it('Returns tables and streams', () => {
expect(selectors.getKsqlDbTables(store.getState())).toEqual({ expect(selectors.getKsqlDbTables(store.getState())).toEqual({
rows: [ rows: {
...fetchKsqlDbTablesPayload.streams, streams: [...fetchKsqlDbTablesPayload.streams],
...fetchKsqlDbTablesPayload.tables, tables: [...fetchKsqlDbTablesPayload.tables],
], },
fetched: true, fetched: true,
fetching: false, fetching: false,
tablesCount: 2, tablesCount: 2,

View file

@ -15,7 +15,7 @@ const getKsqlExecutionStatus = createFetchingSelector('ksqlDb/executeKsql');
export const getKsqlDbTables = createSelector( export const getKsqlDbTables = createSelector(
[ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus], [ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus],
(state, status) => ({ (state, status) => ({
rows: [...state.streams, ...state.tables], rows: { streams: [...state.streams], tables: [...state.tables] },
fetched: status === AsyncRequestStatus.fulfilled, fetched: status === AsyncRequestStatus.fulfilled,
fetching: status === AsyncRequestStatus.pending, fetching: status === AsyncRequestStatus.pending,
tablesCount: state.tables.length, tablesCount: state.tables.length,