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 (
<Routes>
<Route
index
path="/*"
element={
<BreadcrumbRoute>
<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 * 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<ClusterNameRoute>();
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 = () => {
</Metrics.Section>
</Metrics.Wrapper>
<div>
{fetching ? (
<PageLoader />
) : (
<Table isFullwidth>
<thead>
<tr>
{headers.map(({ Header, accessor }) => (
<TableHeaderCell title={Header} key={accessor} />
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<ListItem key={row.name} accessors={accessors} data={row} />
))}
{rows.length === 0 && (
<tr>
<td colSpan={headers.length + 1}>
No tables or streams found
</td>
</tr>
)}
</tbody>
</Table>
)}
<Navbar role="navigation">
<NavLink
to={clusterKsqlDbTablesPath(clusterName)}
className={({ isActive }) => (isActive ? 'is-active' : '')}
end
>
Tables
</NavLink>
<NavLink
to={clusterKsqlDbStreamsPath(clusterName)}
className={({ isActive }) => (isActive ? 'is-active' : '')}
end
>
Streams
</NavLink>
</Navbar>
<Routes>
<Route
index
element={<Navigate to={clusterKsqlDbTablesRelativePath} />}
/>
<Route
path={clusterKsqlDbTablesRelativePath}
element={
<KsqlDbItem
type={KsqlDbItemType.Tables}
fetching={fetching}
rows={rows}
/>
}
/>
<Route
path={clusterKsqlDbStreamsRelativePath}
element={
<KsqlDbItem
type={KsqlDbItemType.Streams}
fetching={fetching}
rows={rows}
/>
}
/>
</Routes>
</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 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(
<WithRoute path={clusterKsqlDbPath()}>
<List />
</WithRoute>,
{ initialEntries: [clusterKsqlDbPath(clusterName)] }
);
render(<List />);
};
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();
});
});

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
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}`;

View file

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

View file

@ -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,

View file

@ -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,