Separating ksqlDb tables and streams by tabs (#2145)
* separating ksqlDb tables and streams by tabs * adding tests
This commit is contained in:
parent
41fd765d83
commit
0528c3a28f
12 changed files with 239 additions and 135 deletions
|
@ -9,7 +9,7 @@ const KsqlDb: React.FC = () => {
|
|||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
path="/*"
|
||||
element={
|
||||
<BreadcrumbRoute>
|
||||
<List />
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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) : '-',
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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}`;
|
||||
|
|
|
@ -18,7 +18,6 @@ export interface KsqlState {
|
|||
}
|
||||
|
||||
export interface KsqlDescription {
|
||||
type?: string;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
keyFormat?: string;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue