Browse Source

Separating ksqlDb tables and streams by tabs (#2145)

* separating ksqlDb tables and streams by tabs

* adding tests
Robert Azizbekyan 3 years ago
parent
commit
0528c3a28f

+ 1 - 1
kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx

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

+ 74 - 0
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<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;

+ 66 - 0
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<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);
+  });
+});

+ 12 - 0
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) : '-',
+  };
+};

+ 58 - 53
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';
-
-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' },
-];
+import Navbar from 'components/common/Navigation/Navbar.styled';
+import { NavLink, Route, Routes, Navigate } from 'react-router-dom';
+import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 
-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>
     </>
   );

+ 0 - 20
kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx

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

+ 10 - 20
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(
-    <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();
   });
 });

+ 0 - 34
kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx

@@ -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();
-  });
-});

+ 9 - 0
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}`;

+ 0 - 1
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;

+ 8 - 5
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,

+ 1 - 1
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,