Przeglądaj źródła

Migrate KSQL to RQ. Get rid of redux reducer (#3478)

* Migrate KSQL to RQ. Get rid of redux reducer

* refactor react-ace using

* get rid of deadcode

* ace

* Fix add/remove stream props

* ace

* Fix Clear result handler

* Fix error handler

* rollback removal of yup async validation helper

* reduce re-renders

* move ace to separate chunk

* upd KsqlQueryForm

* feedback

---------

Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>
Oleg Shur 2 lat temu
rodzic
commit
84d3b329ba
39 zmienionych plików z 543 dodań i 1374 usunięć
  1. 1 2
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java
  2. 3 0
      kafka-ui-react-app/src/components/App.tsx
  3. 101 7
      kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
  4. 0 58
      kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/KsqlDbItem.tsx
  5. 0 59
      kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/__test__/KsqlDbItem.spec.tsx
  6. 0 12
      kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData.ts
  7. 0 111
      kafka-ui-react-app/src/components/KsqlDb/List/List.tsx
  8. 0 22
      kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx
  9. 0 9
      kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts
  10. 36 205
      kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
  11. 12 51
      kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts
  12. 139 153
      kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx
  13. 0 189
      kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/__test__/QueryForm.spec.tsx
  14. 0 116
      kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx
  15. 6 12
      kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx
  16. 0 71
      kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/__test__/TableRenderer.spec.tsx
  17. 39 0
      kafka-ui-react-app/src/components/KsqlDb/TableView.tsx
  18. 0 42
      kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx
  19. 0 6
      kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts
  20. 1 1
      kafka-ui-react-app/src/components/common/ActionComponent/__tests__/fixtures.ts
  21. 1 0
      kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx
  22. 1 3
      kafka-ui-react-app/src/components/common/Editor/Editor.tsx
  23. 0 11
      kafka-ui-react-app/src/components/common/NewTable/TimestampCell copy.tsx
  24. 2 2
      kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx
  25. 2 2
      kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx
  26. 1 1
      kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx
  27. 1 1
      kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts
  28. 1 1
      kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts
  29. 184 0
      kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx
  30. 1 1
      kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts
  31. 2 2
      kafka-ui-react-app/src/lib/paths.ts
  32. 2 1
      kafka-ui-react-app/src/lib/yupExtended.ts
  33. 0 19
      kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts
  34. 0 2
      kafka-ui-react-app/src/redux/reducers/index.ts
  35. 0 43
      kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/fixtures.ts
  36. 0 51
      kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts
  37. 0 75
      kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts
  38. 0 33
      kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts
  39. 7 0
      kafka-ui-react-app/vite.config.ts

+ 1 - 2
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java

@@ -16,7 +16,6 @@ import static com.codeborne.selenide.Selenide.$$x;
 import static com.codeborne.selenide.Selenide.$x;
 
 public class KsqlQueryForm extends BasePage {
-    protected SelenideElement pageTitle = $x("//h1[text()='Query']");
     protected SelenideElement clearBtn = $x("//div/button[text()='Clear']");
     protected SelenideElement executeBtn = $x("//div/button[text()='Execute']");
     protected SelenideElement stopQueryBtn = $x("//div/button[text()='Stop query']");
@@ -31,7 +30,7 @@ public class KsqlQueryForm extends BasePage {
     @Step
     public KsqlQueryForm waitUntilScreenReady() {
         waitUntilSpinnerDisappear();
-        pageTitle.shouldBe(Condition.visible);
+        executeBtn.shouldBe(Condition.visible);
         return this;
     }
 

+ 3 - 0
kafka-ui-react-app/src/components/App.tsx

@@ -30,6 +30,9 @@ const queryClient = new QueryClient({
   defaultOptions: {
     queries: {
       suspense: true,
+      onError(error) {
+        showServerError(error as Response);
+      },
     },
     mutations: {
       onError(error) {

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

@@ -1,15 +1,109 @@
 import React from 'react';
-import { Route, Routes } from 'react-router-dom';
-import { clusterKsqlDbQueryRelativePath } from 'lib/paths';
-import List from 'components/KsqlDb/List/List';
 import Query from 'components/KsqlDb/Query/Query';
+import useAppParams from 'lib/hooks/useAppParams';
+import * as Metrics from 'components/common/Metrics';
+import {
+  clusterKsqlDbQueryRelativePath,
+  clusterKsqlDbStreamsPath,
+  clusterKsqlDbStreamsRelativePath,
+  clusterKsqlDbTablesPath,
+  clusterKsqlDbTablesRelativePath,
+  ClusterNameRoute,
+} from 'lib/paths';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import { ActionButton } from 'components/common/ActionComponent';
+import Navbar from 'components/common/Navigation/Navbar.styled';
+import { Navigate, NavLink, Route, Routes } from 'react-router-dom';
+import { Action, ResourceType } from 'generated-sources';
+import { useKsqlkDb } from 'lib/hooks/api/ksqlDb';
+import 'ace-builds/src-noconflict/ace';
+
+import TableView from './TableView';
 
 const KsqlDb: React.FC = () => {
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+
+  const [tables, streams] = useKsqlkDb(clusterName);
+
+  const isFetching = tables.isFetching || streams.isFetching;
+
   return (
-    <Routes>
-      <Route path="/*" element={<List />} />
-      <Route path={clusterKsqlDbQueryRelativePath} element={<Query />} />
-    </Routes>
+    <>
+      <PageHeading text="KSQL DB">
+        <ActionButton
+          to={clusterKsqlDbQueryRelativePath}
+          buttonType="primary"
+          buttonSize="M"
+          permission={{
+            resource: ResourceType.KSQL,
+            action: Action.EXECUTE,
+          }}
+        >
+          Execute KSQL Request
+        </ActionButton>
+      </PageHeading>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator
+            label="Tables"
+            title="Tables"
+            fetching={isFetching}
+          >
+            {tables.isSuccess ? tables.data.length : '-'}
+          </Metrics.Indicator>
+          <Metrics.Indicator
+            label="Streams"
+            title="Streams"
+            fetching={isFetching}
+          >
+            {streams.isSuccess ? streams.data.length : '-'}
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <div>
+        <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={
+              <TableView
+                fetching={tables.isFetching}
+                rows={tables.data || []}
+              />
+            }
+          />
+          <Route
+            path={clusterKsqlDbStreamsRelativePath}
+            element={
+              <TableView
+                fetching={streams.isFetching}
+                rows={streams.data || []}
+              />
+            }
+          />
+          <Route path={clusterKsqlDbQueryRelativePath} element={<Query />} />
+        </Routes>
+      </div>
+    </>
   );
 };
 

+ 0 - 58
kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/KsqlDbItem.tsx

@@ -1,58 +0,0 @@
-import React from 'react';
-import PageLoader from 'components/common/PageLoader/PageLoader';
-import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources';
-import { ksqlRowData } from 'components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData';
-import Table from 'components/common/NewTable';
-import { ColumnDef } from '@tanstack/react-table';
-
-export enum KsqlDbItemType {
-  Tables = 'tables',
-  Streams = 'streams',
-}
-
-interface RowsType {
-  tables: KsqlTableDescription[];
-  streams: KsqlStreamDescription[];
-}
-export interface KsqlDbItemProps {
-  type: KsqlDbItemType;
-  fetching: boolean;
-  rows: RowsType;
-}
-
-export interface KsqlTableState {
-  name: string;
-  topic: string;
-  keyFormat: string;
-  valueFormat: string;
-  isWindowed: string;
-}
-
-const KsqlDbItem: React.FC<KsqlDbItemProps> = ({ type, fetching, rows }) => {
-  const preparedRows = rows[type]?.map(ksqlRowData) || [];
-
-  const columns = React.useMemo<ColumnDef<KsqlTableState>[]>(
-    () => [
-      { header: 'Name', accessorKey: 'name' },
-      { header: 'Topic', accessorKey: 'topic' },
-      { header: 'Key Format', accessorKey: 'keyFormat' },
-      { header: 'Value Format', accessorKey: 'valueFormat' },
-      { header: 'Is Windowed', accessorKey: 'isWindowed' },
-    ],
-    []
-  );
-
-  if (fetching) {
-    return <PageLoader />;
-  }
-  return (
-    <Table
-      data={preparedRows}
-      columns={columns}
-      emptyMessage="No tables or streams found"
-      enableSorting={false}
-    />
-  );
-};
-
-export default KsqlDbItem;

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

@@ -1,59 +0,0 @@
-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 renderComponent = (props: Partial<KsqlDbItemProps> = {}) => {
-    render(
-      <WithRoute path={tablesPathname}>
-        <KsqlDbItem
-          type={KsqlDbItemType.Tables}
-          fetching={false}
-          rows={{ tables: [], streams: [] }}
-          {...props}
-        />
-      </WithRoute>,
-      {
-        initialEntries: [clusterKsqlDbTablesPath()],
-      }
-    );
-  };
-
-  it('renders progressbar when fetching tables and streams', () => {
-    renderComponent({ fetching: true });
-    expect(screen.getByRole('progressbar')).toBeInTheDocument();
-  });
-
-  it('show no text if no data found', () => {
-    renderComponent({});
-    expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
-  });
-
-  it('renders with tables', () => {
-    renderComponent({
-      rows: {
-        tables: fetchKsqlDbTablesPayload.tables,
-        streams: [],
-      },
-    });
-
-    expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
-  });
-  it('renders with streams', () => {
-    renderComponent({
-      type: KsqlDbItemType.Streams,
-      rows: {
-        tables: [],
-        streams: fetchKsqlDbTablesPayload.streams,
-      },
-    });
-    expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
-  });
-});

+ 0 - 12
kafka-ui-react-app/src/components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData.ts

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

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

@@ -1,111 +0,0 @@
-import React, { FC } from 'react';
-import useAppParams from 'lib/hooks/useAppParams';
-import * as Metrics from 'components/common/Metrics';
-import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
-import {
-  clusterKsqlDbQueryRelativePath,
-  clusterKsqlDbStreamsPath,
-  clusterKsqlDbStreamsRelativePath,
-  clusterKsqlDbTablesPath,
-  clusterKsqlDbTablesRelativePath,
-  ClusterNameRoute,
-} from 'lib/paths';
-import PageHeading from 'components/common/PageHeading/PageHeading';
-import { ActionButton } from 'components/common/ActionComponent';
-import Navbar from 'components/common/Navigation/Navbar.styled';
-import { Navigate, NavLink, Route, Routes } from 'react-router-dom';
-import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
-import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
-import { Action, ResourceType } from 'generated-sources';
-
-import KsqlDbItem, { KsqlDbItemType } from './KsqlDbItem/KsqlDbItem';
-
-const List: FC = () => {
-  const { clusterName } = useAppParams<ClusterNameRoute>();
-  const dispatch = useAppDispatch();
-
-  const { rows, fetching, tablesCount, streamsCount } =
-    useAppSelector(getKsqlDbTables);
-
-  React.useEffect(() => {
-    dispatch(fetchKsqlDbTables(clusterName));
-  }, [clusterName, dispatch]);
-
-  return (
-    <>
-      <PageHeading text="KSQL DB">
-        <ActionButton
-          to={clusterKsqlDbQueryRelativePath}
-          buttonType="primary"
-          buttonSize="M"
-          permission={{
-            resource: ResourceType.KSQL,
-            action: Action.EXECUTE,
-          }}
-        >
-          Execute KSQL Request
-        </ActionButton>
-      </PageHeading>
-      <Metrics.Wrapper>
-        <Metrics.Section>
-          <Metrics.Indicator label="Tables" title="Tables" fetching={fetching}>
-            {tablesCount}
-          </Metrics.Indicator>
-          <Metrics.Indicator
-            label="Streams"
-            title="Streams"
-            fetching={fetching}
-          >
-            {streamsCount}
-          </Metrics.Indicator>
-        </Metrics.Section>
-      </Metrics.Wrapper>
-      <div>
-        <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>
-    </>
-  );
-};
-
-export default List;

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

@@ -1,22 +0,0 @@
-import React from 'react';
-import List from 'components/KsqlDb/List/List';
-import { render } from 'lib/testHelpers';
-import fetchMock from 'fetch-mock';
-import { screen } from '@testing-library/dom';
-import { act } from '@testing-library/react';
-
-describe('KsqlDb List', () => {
-  const renderComponent = async () => {
-    await act(() => {
-      render(<List />);
-    });
-  };
-  afterEach(() => fetchMock.reset());
-  it('renders List component with Tables and Streams tabs', async () => {
-    await renderComponent();
-    const Tables = screen.getByTitle('Tables');
-    const Streams = screen.getByTitle('Streams');
-    expect(Tables).toBeInTheDocument();
-    expect(Streams).toBeInTheDocument();
-  });
-});

+ 0 - 9
kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts

@@ -1,9 +0,0 @@
-import PageLoader from 'components/common/PageLoader/PageLoader';
-import styled from 'styled-components';
-
-export const ContinuousLoader = styled(PageLoader)`
-  & > div {
-    transform: scale(0.5);
-    padding-top: 0;
-  }
-`;

+ 36 - 205
kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx

@@ -1,223 +1,54 @@
-import React, { useCallback, useEffect, FC, useState } from 'react';
+import React from 'react';
 import useAppParams from 'lib/hooks/useAppParams';
 import TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer';
+import { ClusterNameRoute } from 'lib/paths';
 import {
-  executeKsql,
-  resetExecutionResult,
-} from 'redux/reducers/ksqlDb/ksqlDbSlice';
-import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors';
-import { BASE_PARAMS } from 'lib/constants';
-import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
-import { clusterKsqlDbPath, ClusterNameRoute } from 'lib/paths';
-import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
-import { showAlert, showSuccessAlert } from 'lib/errorHandling';
-import PageHeading from 'components/common/PageHeading/PageHeading';
+  useExecuteKsqlkDbQueryMutation,
+  useKsqlkDbSSE,
+} from 'lib/hooks/api/ksqlDb';
 
 import type { FormValues } from './QueryForm/QueryForm';
-import * as S from './Query.styled';
 import QueryForm from './QueryForm/QueryForm';
 
-export const getFormattedErrorFromTableData = (
-  responseValues: KsqlTableResponse['values']
-): { title: string; message: string } => {
-  // We expect someting like that
-  // [[
-  //   "@type",
-  //   "error_code",
-  //   "message",
-  //   "statementText"?,
-  //   "entities"?
-  // ]],
-  // or
-  // [["message"]]
-
-  if (!responseValues || !responseValues.length) {
-    return {
-      title: 'Unknown error',
-      message: 'Recieved empty response',
-    };
-  }
-
-  let title = '';
-  let message = '';
-  if (responseValues[0].length < 2) {
-    const [messageText] = responseValues[0];
-    title = messageText;
-  } else {
-    const [type, errorCode, messageText, statementText, entities] =
-      responseValues[0];
-    title = `[Error #${errorCode}] ${type}`;
-    message =
-      (entities?.length ? `[${entities.join(', ')}] ` : '') +
-      (statementText ? `"${statementText}" ` : '') +
-      messageText;
-  }
-
-  return {
-    title,
-    message,
-  };
-};
-
-const Query: FC = () => {
+const Query = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
-
-  const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({
-    sse: null,
-    isOpen: false,
-  });
-  const [fetching, setFetching] = useState(false);
-  const dispatch = useAppDispatch();
-
-  const { executionResult } = useAppSelector(getKsqlExecution);
-  const [KSQLTable, setKSQLTable] = useState<KsqlTableResponse | null>(null);
-
-  const reset = useCallback(() => {
-    dispatch(resetExecutionResult());
-  }, [dispatch]);
-
-  useEffect(() => {
-    return reset;
-  }, [reset]);
-
-  const destroySSE = () => {
-    if (sseRef.current?.sse) {
-      sseRef.current.sse.close();
-      setFetching(false);
-      sseRef.current.sse = null;
-      sseRef.current.isOpen = false;
-    }
+  const executeQuery = useExecuteKsqlkDbQueryMutation();
+  const [pipeId, setPipeId] = React.useState<string | false>(false);
+
+  const sse = useKsqlkDbSSE({ clusterName, pipeId });
+
+  const isFetching = executeQuery.isLoading || sse.isFetching;
+
+  const submitHandler = async (values: FormValues) => {
+    const filtered = values.streamsProperties.filter(({ key }) => key != null);
+    const streamsProperties = filtered.reduce<Record<string, string>>(
+      (acc, current) => ({ ...acc, [current.key]: current.value }),
+      {}
+    );
+    await executeQuery.mutateAsync(
+      {
+        clusterName,
+        ksqlCommandV2: {
+          ...values,
+          streamsProperties:
+            values.streamsProperties[0].key !== ''
+              ? JSON.parse(JSON.stringify(streamsProperties))
+              : undefined,
+        },
+      },
+      { onSuccess: (data) => setPipeId(data.pipeId) }
+    );
   };
 
-  const handleSSECancel = useCallback(() => {
-    reset();
-    destroySSE();
-  }, [reset]);
-
-  const createSSE = useCallback(
-    (pipeId: string) => {
-      const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/ksql/response?pipeId=${pipeId}`;
-      const sse = new EventSource(url);
-      sseRef.current.sse = sse;
-      setFetching(true);
-
-      sse.onopen = () => {
-        sseRef.current.isOpen = true;
-      };
-
-      sse.onmessage = ({ data }) => {
-        const { table }: KsqlResponse = JSON.parse(data);
-        if (table) {
-          switch (table?.header) {
-            case 'Execution error': {
-              const { title, message } = getFormattedErrorFromTableData(
-                table.values
-              );
-              const id = `${url}-executionError`;
-              showAlert('error', { id, title, message });
-              break;
-            }
-            case 'Schema': {
-              setKSQLTable(table);
-              break;
-            }
-            case 'Row': {
-              setKSQLTable((PrevKSQLTable) => {
-                return {
-                  header: PrevKSQLTable?.header,
-                  columnNames: PrevKSQLTable?.columnNames,
-                  values: [
-                    ...(PrevKSQLTable?.values || []),
-                    ...(table?.values || []),
-                  ],
-                };
-              });
-              break;
-            }
-            case 'Query Result': {
-              const id = `${url}-querySuccess`;
-              showSuccessAlert({ id, title: 'Query succeed', message: '' });
-              break;
-            }
-            case 'Source Description':
-            case 'properties':
-            default: {
-              setKSQLTable(table);
-              break;
-            }
-          }
-        }
-        return sse;
-      };
-
-      sse.onerror = () => {
-        // if it's open - we know that server responded without opening SSE
-        if (!sseRef.current.isOpen) {
-          showAlert('error', {
-            id: `${url}-connectionClosedError`,
-            title: '',
-            message: 'SSE connection closed',
-          });
-        }
-        destroySSE();
-      };
-    },
-    [clusterName, dispatch]
-  );
-
-  const submitHandler = useCallback(
-    (values: FormValues) => {
-      const filteredProperties = values.streamsProperties.filter(
-        (property) => property.key != null
-      );
-      const streamsProperties = filteredProperties.reduce(
-        (acc, current) => ({
-          ...acc,
-          [current.key as keyof string]: current.value,
-        }),
-        {} as { [key: string]: string }
-      );
-      setFetching(true);
-      dispatch(
-        executeKsql({
-          clusterName,
-          ksqlCommandV2: {
-            ...values,
-            streamsProperties:
-              values.streamsProperties[0].key !== ''
-                ? JSON.parse(JSON.stringify(streamsProperties))
-                : undefined,
-          },
-        })
-      );
-    },
-    [dispatch, clusterName]
-  );
-  useEffect(() => {
-    if (executionResult?.pipeId) {
-      createSSE(executionResult.pipeId);
-    }
-    return () => {
-      destroySSE();
-    };
-  }, [createSSE, executionResult]);
-
   return (
     <>
-      <PageHeading
-        text="Query"
-        backText="KSQL DB"
-        backTo={clusterKsqlDbPath(clusterName)}
-      />
       <QueryForm
-        fetching={fetching}
-        hasResults={!!KSQLTable}
-        handleClearResults={() => setKSQLTable(null)}
-        handleSSECancel={handleSSECancel}
+        fetching={isFetching}
+        hasResults={!!sse.data && !!pipeId}
+        resetResults={() => setPipeId(false)}
         submitHandler={submitHandler}
       />
-      {KSQLTable && <TableRenderer table={KSQLTable} />}
-      {fetching && <S.ContinuousLoader />}
+      {pipeId && !!sse.data && <TableRenderer table={sse.data} />}
     </>
   );
 };

+ 12 - 51
kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts

@@ -6,13 +6,12 @@ export const QueryWrapper = styled.div`
 `;
 
 export const KSQLInputsWrapper = styled.div`
-  width: 100%;
   display: flex;
   gap: 24px;
-
   padding-bottom: 16px;
-  & > div {
-    flex-grow: 1;
+
+  @media screen and (max-width: 769px) {
+    flex-direction: column;
   }
 `;
 
@@ -22,61 +21,23 @@ export const KSQLInputHeader = styled.div`
   color: ${({ theme }) => theme.default.color.normal};
 `;
 
-export const KSQLButtons = styled.div`
-  display: flex;
-  gap: 16px;
-`;
-
-export const StreamPropertiesContainer = styled.label`
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-  width: 50%;
-  color: ${({ theme }) => theme.default.color.normal};
-`;
-
 export const InputsContainer = styled.div`
-  overflow: hidden;
-  width: 100%;
-  display: flex;
-  justify-content: center;
+  display: grid;
+  grid-template-columns: 1fr 1fr 30px;
+  align-items: center;
   gap: 10px;
 `;
 
-export const StreamPropertiesInputWrapper = styled.div`
-  & {
-    width: 100%;
-  }
-  & > input {
-    width: 100%;
-    height: 40px;
-    border: 1px solid grey;
-    &:focus {
-      outline: none;
-      border-color: ${({ theme }) => theme.input.borderColor.focus};
-      &::placeholder {
-        color: transparent;
-      }
-    }
-    border-radius: 4px;
-    font-size: 16px;
-    padding-left: 15px;
-    background-color: ${({ theme }) => theme.input.backgroundColor.normal};
-    color: ${({ theme }) => theme.input.color.normal};
-  }
-`;
-
-export const DeleteButtonWrapper = styled.div`
-  min-height: 32px;
+export const Fieldset = styled.fieldset`
   display: flex;
+  flex: 1;
   flex-direction: column;
-  align-items: center;
-  justify-self: flex-start;
-  margin-top: 10px;
+  gap: 8px;
 `;
 
-export const Fieldset = styled.fieldset`
-  width: 50%;
+export const ButtonsContainer = styled.div`
+  display: flex;
+  gap: 8px;
 `;
 
 export const SQLEditor = styled(BaseSQLEditor)(

+ 139 - 153
kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx

@@ -1,22 +1,27 @@
-import React, { useCallback, useRef } from 'react';
+import React from 'react';
 import { FormError } from 'components/common/Input/Input.styled';
 import { ErrorMessage } from '@hookform/error-message';
-import { useForm, Controller, useFieldArray } from 'react-hook-form';
+import {
+  useForm,
+  Controller,
+  useFieldArray,
+  FormProvider,
+} from 'react-hook-form';
 import { Button } from 'components/common/Button/Button';
 import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import CloseIcon from 'components/common/Icons/CloseIcon';
 import { yupResolver } from '@hookform/resolvers/yup';
 import yup from 'lib/yupExtended';
 import PlusIcon from 'components/common/Icons/PlusIcon';
-import ReactAce from 'react-ace/lib/ace';
+import ReactAce from 'react-ace';
+import Input from 'components/common/Input/Input';
 
 import * as S from './QueryForm.styled';
 
-export interface Props {
+interface QueryFormProps {
   fetching: boolean;
   hasResults: boolean;
-  handleClearResults: () => void;
-  handleSSECancel: () => void;
+  resetResults: () => void;
   submitHandler: (values: FormValues) => void;
 }
 type StreamsPropertiesType = {
@@ -37,20 +42,13 @@ const validationSchema = yup.object({
   streamsProperties: yup.array().of(streamsPropertiesSchema),
 });
 
-const QueryForm: React.FC<Props> = ({
+const QueryForm: React.FC<QueryFormProps> = ({
   fetching,
   hasResults,
-  handleClearResults,
-  handleSSECancel,
   submitHandler,
+  resetResults,
 }) => {
-  const {
-    handleSubmit,
-    setValue,
-    getValues,
-    control,
-    formState: { errors },
-  } = useForm<FormValues>({
+  const methods = useForm<FormValues>({
     mode: 'onTouched',
     resolver: yupResolver(validationSchema),
     defaultValues: {
@@ -58,7 +56,16 @@ const QueryForm: React.FC<Props> = ({
       streamsProperties: [{ key: '', value: '' }],
     },
   });
-  const { fields, append, remove } = useFieldArray<
+
+  const {
+    handleSubmit,
+    setValue,
+    control,
+    watch,
+    formState: { errors, isDirty },
+  } = methods;
+
+  const { fields, append, remove, update } = useFieldArray<
     FormValues,
     'streamsProperties'
   >({
@@ -66,17 +73,24 @@ const QueryForm: React.FC<Props> = ({
     name: 'streamsProperties',
   });
 
-  const handleAddNewProperty = useCallback(() => {
-    if (
-      getValues().streamsProperties.every((prop) => {
-        return prop.key;
-      })
-    ) {
-      append({ key: '', value: '' });
+  const watchStreamProps = watch('streamsProperties');
+
+  const appendProperty = () => {
+    append({ key: '', value: '' });
+  };
+  const removeProperty = (index: number) => () => {
+    if (fields.length === 1) {
+      update(index, { key: '', value: '' });
+      return;
     }
-  }, []);
 
-  const inputRef = useRef<ReactAce>(null);
+    remove(index);
+  };
+
+  const isAppendDisabled =
+    fetching || !!watchStreamProps.find((field) => !field.key);
+
+  const inputRef = React.useRef<ReactAce>(null);
 
   const handleFocus = () => {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -87,145 +101,117 @@ const QueryForm: React.FC<Props> = ({
     }
   };
 
+  const handleClear = () => {
+    handleFocus();
+    resetResults();
+  };
+
   return (
-    <S.QueryWrapper>
-      <form onSubmit={handleSubmit(submitHandler)}>
-        <S.KSQLInputsWrapper>
-          <S.Fieldset aria-labelledby="ksqlLabel">
-            <S.KSQLInputHeader>
-              <label id="ksqlLabel">KSQL</label>
-              <Button
-                onClick={() => setValue('ksql', '')}
-                buttonType="primary"
-                buttonSize="S"
-                isInverted
-              >
-                Clear
-              </Button>
-            </S.KSQLInputHeader>
-            <Controller
-              control={control}
-              name="ksql"
-              render={({ field }) => (
-                <S.SQLEditor
-                  {...field}
-                  commands={[
-                    {
-                      // commands is array of key bindings.
-                      // name for the key binding.
-                      name: 'commandName',
-                      // key combination used for the command.
-                      bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
-                      // function to execute when keys are pressed.
-                      exec: () => {
-                        handleSubmit(submitHandler)();
+    <FormProvider {...methods}>
+      <S.QueryWrapper>
+        <form onSubmit={handleSubmit(submitHandler)}>
+          <S.KSQLInputsWrapper>
+            <S.Fieldset>
+              <S.KSQLInputHeader>
+                <label id="ksqlLabel">KSQL</label>
+                <Button
+                  onClick={() => setValue('ksql', '')}
+                  buttonType="primary"
+                  buttonSize="S"
+                  isInverted
+                >
+                  Clear
+                </Button>
+              </S.KSQLInputHeader>
+              <Controller
+                control={control}
+                name="ksql"
+                render={({ field }) => (
+                  <S.SQLEditor
+                    {...field}
+                    commands={[
+                      {
+                        // commands is array of key bindings.
+                        // name for the key binding.
+                        name: 'commandName',
+                        // key combination used for the command.
+                        bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
+                        // function to execute when keys are pressed.
+                        exec: () => {
+                          handleSubmit(submitHandler)();
+                        },
                       },
-                    },
-                  ]}
-                  readOnly={fetching}
-                  ref={inputRef}
-                />
-              )}
-            />
-            <FormError>
-              <ErrorMessage errors={errors} name="ksql" />
-            </FormError>
-          </S.Fieldset>
-
-          <S.StreamPropertiesContainer>
-            Stream properties:
-            {fields.map((item, index) => (
-              <S.InputsContainer key={item.id}>
-                <S.StreamPropertiesInputWrapper>
-                  <Controller
-                    control={control}
+                    ]}
+                    readOnly={fetching}
+                    ref={inputRef}
+                  />
+                )}
+              />
+              <FormError>
+                <ErrorMessage errors={errors} name="ksql" />
+              </FormError>
+            </S.Fieldset>
+
+            <S.Fieldset>
+              Stream properties:
+              {fields.map((field, index) => (
+                <S.InputsContainer key={field.id}>
+                  <Input
                     name={`streamsProperties.${index}.key`}
-                    render={({ field }) => (
-                      <input
-                        {...field}
-                        placeholder="Key"
-                        aria-label="key"
-                        type="text"
-                        autoComplete="off"
-                      />
-                    )}
+                    placeholder="Key"
+                    type="text"
+                    autoComplete="off"
+                    withError
                   />
-                  <FormError>
-                    <ErrorMessage
-                      errors={errors}
-                      name={`streamsProperties.${index}.key`}
-                    />
-                  </FormError>
-                </S.StreamPropertiesInputWrapper>
-                <S.StreamPropertiesInputWrapper>
-                  <Controller
-                    control={control}
+                  <Input
                     name={`streamsProperties.${index}.value`}
-                    render={({ field }) => (
-                      <input
-                        {...field}
-                        placeholder="Value"
-                        aria-label="value"
-                        type="text"
-                        autoComplete="off"
-                      />
-                    )}
+                    placeholder="Value"
+                    type="text"
+                    autoComplete="off"
+                    withError
                   />
-                  <FormError>
-                    <ErrorMessage
-                      errors={errors}
-                      name={`streamsProperties.${index}.value`}
-                    />
-                  </FormError>
-                </S.StreamPropertiesInputWrapper>
-
-                <S.DeleteButtonWrapper onClick={() => remove(index)}>
-                  <IconButtonWrapper aria-label="deleteProperty">
+                  <IconButtonWrapper
+                    aria-label="deleteProperty"
+                    onClick={removeProperty(index)}
+                  >
                     <CloseIcon aria-hidden />
                   </IconButtonWrapper>
-                </S.DeleteButtonWrapper>
-              </S.InputsContainer>
-            ))}
+                </S.InputsContainer>
+              ))}
+              <Button
+                type="button"
+                buttonSize="M"
+                buttonType="secondary"
+                disabled={isAppendDisabled}
+                onClick={appendProperty}
+              >
+                <PlusIcon />
+                Add Stream Property
+              </Button>
+            </S.Fieldset>
+          </S.KSQLInputsWrapper>
+          <S.ButtonsContainer>
             <Button
-              type="button"
-              buttonSize="M"
               buttonType="secondary"
-              onClick={handleAddNewProperty}
+              buttonSize="M"
+              disabled={fetching || !isDirty || !hasResults}
+              onClick={handleClear}
+            >
+              Clear results
+            </Button>
+            <Button
+              buttonType="primary"
+              buttonSize="M"
+              type="submit"
+              disabled={fetching}
+              onClick={handleFocus}
             >
-              <PlusIcon />
-              Add Stream Property
+              Execute
             </Button>
-          </S.StreamPropertiesContainer>
-        </S.KSQLInputsWrapper>
-        <S.KSQLButtons>
-          <Button
-            buttonType="primary"
-            buttonSize="M"
-            type="submit"
-            disabled={fetching}
-            onClick={handleFocus}
-          >
-            Execute
-          </Button>
-          <Button
-            buttonType="secondary"
-            buttonSize="M"
-            disabled={!fetching}
-            onClick={handleSSECancel}
-          >
-            Stop query
-          </Button>
-          <Button
-            buttonType="secondary"
-            buttonSize="M"
-            disabled={fetching || !hasResults}
-            onClick={handleClearResults}
-          >
-            Clear results
-          </Button>
-        </S.KSQLButtons>
-      </form>
-    </S.QueryWrapper>
+          </S.ButtonsContainer>
+        </form>
+      </S.QueryWrapper>
+    </FormProvider>
   );
 };
 

+ 0 - 189
kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/__test__/QueryForm.spec.tsx

@@ -1,189 +0,0 @@
-import { render } from 'lib/testHelpers';
-import React from 'react';
-import QueryForm, { Props } from 'components/KsqlDb/Query/QueryForm/QueryForm';
-import { screen, waitFor, within } from '@testing-library/dom';
-import userEvent from '@testing-library/user-event';
-
-const renderComponent = (props: Props) => render(<QueryForm {...props} />);
-
-describe('QueryForm', () => {
-  it('renders', () => {
-    renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: jest.fn(),
-    });
-
-    const KSQLBlock = screen.getByLabelText('KSQL');
-    expect(KSQLBlock).toBeInTheDocument();
-    expect(within(KSQLBlock).getByText('KSQL')).toBeInTheDocument();
-    expect(
-      within(KSQLBlock).getByRole('button', { name: 'Clear' })
-    ).toBeInTheDocument();
-    // Represents SQL editor
-    expect(within(KSQLBlock).getByRole('textbox')).toBeInTheDocument();
-
-    const streamPropertiesBlock = screen.getByRole('textbox', { name: 'key' });
-    expect(streamPropertiesBlock).toBeInTheDocument();
-    expect(screen.getByText('Stream properties:')).toBeInTheDocument();
-    expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument();
-    expect(screen.queryAllByRole('textbox')[0]).toBeInTheDocument();
-
-    // Form controls
-    expect(screen.getByRole('button', { name: 'Execute' })).toBeInTheDocument();
-    expect(screen.getByRole('button', { name: 'Execute' })).toBeEnabled();
-    expect(
-      screen.getByRole('button', { name: 'Stop query' })
-    ).toBeInTheDocument();
-    expect(screen.getByRole('button', { name: 'Stop query' })).toBeDisabled();
-    expect(
-      screen.getByRole('button', { name: 'Clear results' })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('button', { name: 'Clear results' })
-    ).toBeDisabled();
-  });
-
-  it('renders error with empty input', async () => {
-    const submitFn = jest.fn();
-    renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: submitFn,
-    });
-
-    await userEvent.click(screen.getByRole('button', { name: 'Execute' }));
-
-    await waitFor(() => {
-      expect(screen.getByText('ksql is a required field')).toBeInTheDocument();
-      expect(submitFn).not.toBeCalled();
-    });
-  });
-
-  it('submits with correct inputs', async () => {
-    const submitFn = jest.fn();
-    renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: submitFn,
-    });
-
-    const textbox = screen.getAllByRole('textbox');
-    textbox[0].focus();
-    await userEvent.paste('show tables;');
-    const key = screen.getByRole('textbox', { name: 'key' });
-    key.focus();
-    await userEvent.paste('test');
-    const value = screen.getByRole('textbox', { name: 'value' });
-    value.focus();
-    await userEvent.paste('test');
-    await userEvent.click(screen.getByRole('button', { name: 'Execute' }));
-
-    expect(
-      screen.queryByText('ksql is a required field')
-    ).not.toBeInTheDocument();
-
-    expect(
-      screen.queryByText('streamsProperties is not JSON object')
-    ).not.toBeInTheDocument();
-
-    expect(submitFn).toBeCalled();
-  });
-
-  it('clear results is enabled when has results', async () => {
-    const clearFn = jest.fn();
-    renderComponent({
-      fetching: false,
-      hasResults: true,
-      handleClearResults: clearFn,
-      handleSSECancel: jest.fn(),
-      submitHandler: jest.fn(),
-    });
-
-    expect(screen.getByRole('button', { name: 'Clear results' })).toBeEnabled();
-
-    await userEvent.click(
-      screen.getByRole('button', { name: 'Clear results' })
-    );
-
-    expect(clearFn).toBeCalled();
-  });
-
-  it('stop query query is enabled when is fetching', async () => {
-    const cancelFn = jest.fn();
-    renderComponent({
-      fetching: true,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: cancelFn,
-      submitHandler: jest.fn(),
-    });
-
-    expect(screen.getByRole('button', { name: 'Stop query' })).toBeEnabled();
-
-    await userEvent.click(screen.getByRole('button', { name: 'Stop query' }));
-
-    expect(cancelFn).toBeCalled();
-  });
-
-  it('add new property', async () => {
-    renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: jest.fn(),
-    });
-
-    const textbox = screen.getByLabelText('key');
-    await userEvent.type(textbox, 'prop_name');
-    await userEvent.click(
-      screen.getByRole('button', { name: 'Add Stream Property' })
-    );
-    expect(screen.getAllByRole('textbox', { name: 'key' }).length).toEqual(2);
-  });
-
-  it("doesn't add new property", async () => {
-    renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: jest.fn(),
-    });
-
-    await userEvent.click(
-      screen.getByRole('button', { name: 'Add Stream Property' })
-    );
-    expect(screen.getAllByRole('textbox', { name: 'key' }).length).toEqual(1);
-  });
-
-  it('delete stream property', async () => {
-    await renderComponent({
-      fetching: false,
-      hasResults: false,
-      handleClearResults: jest.fn(),
-      handleSSECancel: jest.fn(),
-      submitHandler: jest.fn(),
-    });
-    const textBoxes = screen.getAllByRole('textbox', { name: 'key' });
-    textBoxes[0].focus();
-    await userEvent.paste('test');
-    await userEvent.click(
-      screen.getByRole('button', { name: 'Add Stream Property' })
-    );
-    await userEvent.click(screen.getAllByLabelText('deleteProperty')[0]);
-
-    await screen.getByRole('button', { name: 'Add Stream Property' });
-
-    await userEvent.click(screen.getAllByLabelText('deleteProperty')[0]);
-
-    expect(textBoxes.length).toEqual(1);
-  });
-});

+ 0 - 116
kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx

@@ -1,116 +0,0 @@
-import { render, EventSourceMock, WithRoute } from 'lib/testHelpers';
-import React from 'react';
-import Query, {
-  getFormattedErrorFromTableData,
-} from 'components/KsqlDb/Query/Query';
-import { screen } from '@testing-library/dom';
-import fetchMock from 'fetch-mock';
-import { clusterKsqlDbQueryPath } from 'lib/paths';
-import userEvent from '@testing-library/user-event';
-
-const clusterName = 'testLocal';
-const renderComponent = () =>
-  render(
-    <WithRoute path={clusterKsqlDbQueryPath()}>
-      <Query />
-    </WithRoute>,
-    {
-      initialEntries: [clusterKsqlDbQueryPath(clusterName)],
-    }
-  );
-
-describe('Query', () => {
-  it('renders', () => {
-    renderComponent();
-
-    expect(screen.getByLabelText('KSQL')).toBeInTheDocument();
-    expect(screen.getByLabelText('Stream properties:')).toBeInTheDocument();
-  });
-
-  afterEach(() => fetchMock.reset());
-  it('fetch on execute', async () => {
-    renderComponent();
-
-    const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, {
-      pipeId: 'testPipeID',
-    });
-
-    Object.defineProperty(window, 'EventSource', {
-      value: EventSourceMock,
-    });
-    const inputs = screen.getAllByRole('textbox');
-    const textAreaElement = inputs[0] as HTMLTextAreaElement;
-
-    textAreaElement.focus();
-    await userEvent.paste('show tables;');
-    await userEvent.click(screen.getByRole('button', { name: 'Execute' }));
-
-    expect(mock.calls().length).toBe(1);
-  });
-
-  it('fetch on execute with streamParams', async () => {
-    renderComponent();
-
-    const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, {
-      pipeId: 'testPipeID',
-    });
-
-    Object.defineProperty(window, 'EventSource', {
-      value: EventSourceMock,
-    });
-
-    const inputs = screen.getAllByRole('textbox');
-    const textAreaElement = inputs[0] as HTMLTextAreaElement;
-    textAreaElement.focus();
-    await userEvent.paste('show tables;');
-
-    const key = screen.getByLabelText('key');
-    key.focus();
-    await userEvent.paste('key');
-    const value = screen.getByLabelText('value');
-    value.focus();
-    await userEvent.paste('value');
-
-    await userEvent.click(screen.getByRole('button', { name: 'Execute' }));
-
-    expect(mock.calls().length).toBe(1);
-  });
-});
-
-describe('getFormattedErrorFromTableData', () => {
-  it('works', () => {
-    expect(getFormattedErrorFromTableData([['Test Error']])).toStrictEqual({
-      title: 'Test Error',
-      message: '',
-    });
-
-    expect(
-      getFormattedErrorFromTableData([
-        ['some_type', 'errorCode', 'messageText'],
-      ])
-    ).toStrictEqual({
-      title: '[Error #errorCode] some_type',
-      message: 'messageText',
-    });
-
-    expect(
-      getFormattedErrorFromTableData([
-        [
-          'some_type',
-          'errorCode',
-          'messageText',
-          'statementText',
-          ['test1', 'test2'],
-        ],
-      ])
-    ).toStrictEqual({
-      title: '[Error #errorCode] some_type',
-      message: '[test1, test2] "statementText" messageText',
-    });
-
-    expect(getFormattedErrorFromTableData([])).toStrictEqual({
-      title: 'Unknown error',
-      message: 'Recieved empty response',
-    });
-  });
-});

+ 6 - 12
kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx

@@ -6,13 +6,11 @@ import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled
 
 import * as S from './TableRenderer.styled';
 
-export interface Props {
+interface TableRendererProps {
   table: KsqlTableResponse;
 }
 
-export function hasJsonStructure(
-  str: string | Record<string, unknown>
-): boolean {
+function hasJsonStructure(str: string | Record<string, unknown>): boolean {
   if (typeof str === 'object') {
     return true;
   }
@@ -30,13 +28,7 @@ export function hasJsonStructure(
   return false;
 }
 
-const TableRenderer: React.FC<Props> = ({ table }) => {
-  const heading = React.useMemo(() => {
-    return table.header || '';
-  }, [table.header]);
-  const ths = React.useMemo(() => {
-    return table.columnNames || [];
-  }, [table.columnNames]);
+const TableRenderer: React.FC<TableRendererProps> = ({ table }) => {
   const rows = React.useMemo(() => {
     return (table.values || []).map((row) => {
       return {
@@ -53,9 +45,11 @@ const TableRenderer: React.FC<Props> = ({ table }) => {
     });
   }, [table.values]);
 
+  const ths = table.columnNames || [];
+
   return (
     <S.Wrapper>
-      <TableTitle>{heading}</TableTitle>
+      <TableTitle>{table.header}</TableTitle>
       <S.ScrollableTable>
         <thead>
           <tr>

+ 0 - 71
kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/__test__/TableRenderer.spec.tsx

@@ -1,71 +0,0 @@
-import { render } from 'lib/testHelpers';
-import React from 'react';
-import TableRenderer, {
-  Props,
-  hasJsonStructure,
-} from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer';
-import { screen } from '@testing-library/dom';
-
-const renderComponent = (props: Props) => render(<TableRenderer {...props} />);
-
-describe('TableRenderer', () => {
-  it('renders', () => {
-    renderComponent({
-      table: {
-        header: 'Test header',
-        columnNames: ['Test column name'],
-        values: [['Table row #1'], ['Table row #2'], ['{"jsonrow": "#3"}']],
-      },
-    });
-
-    expect(
-      screen.getByRole('heading', { name: 'Test header' })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('columnheader', { name: 'Test column name' })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('cell', { name: 'Table row #1' })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('cell', { name: 'Table row #2' })
-    ).toBeInTheDocument();
-  });
-
-  it('renders with empty arrays', () => {
-    renderComponent({
-      table: {},
-    });
-
-    expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
-  });
-});
-
-describe('hasJsonStructure', () => {
-  it('works', () => {
-    expect(hasJsonStructure('simplestring')).toBeFalsy();
-    expect(
-      hasJsonStructure("{'looksLikeJson': 'but has wrong quotes'}")
-    ).toBeFalsy();
-    expect(
-      hasJsonStructure('{"json": "but doesnt have closing brackets"')
-    ).toBeFalsy();
-    expect(hasJsonStructure('"string":"that looks like json"')).toBeFalsy();
-
-    expect(hasJsonStructure('1')).toBeFalsy();
-    expect(hasJsonStructure('{1:}')).toBeFalsy();
-    expect(hasJsonStructure('{1:"1"}')).toBeFalsy();
-
-    // @ts-expect-error We suppress error because this function works with unknown data from server
-    expect(hasJsonStructure(1)).toBeFalsy();
-
-    expect(hasJsonStructure('{}')).toBeTruthy();
-    expect(hasJsonStructure('{"correct": "json"}')).toBeTruthy();
-
-    expect(hasJsonStructure('[]')).toBeTruthy();
-    expect(hasJsonStructure('[{}]')).toBeTruthy();
-
-    expect(hasJsonStructure({})).toBeTruthy();
-    expect(hasJsonStructure({ correct: 'json' })).toBeTruthy();
-  });
-});

+ 39 - 0
kafka-ui-react-app/src/components/KsqlDb/TableView.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources';
+import Table from 'components/common/NewTable';
+import { ColumnDef } from '@tanstack/react-table';
+
+interface TableViewProps {
+  fetching: boolean;
+  rows: KsqlTableDescription[] | KsqlStreamDescription[];
+}
+
+const TableView: React.FC<TableViewProps> = ({ fetching, rows }) => {
+  const columns = React.useMemo<
+    ColumnDef<KsqlTableDescription | KsqlStreamDescription>[]
+  >(
+    () => [
+      { header: 'Name', accessorKey: 'name' },
+      { header: 'Topic', accessorKey: 'topic' },
+      { header: 'Key Format', accessorKey: 'keyFormat' },
+      { header: 'Value Format', accessorKey: 'valueFormat' },
+      {
+        header: 'Is Windowed',
+        accessorKey: 'isWindowed',
+        cell: ({ row }) =>
+          'isWindowed' in row.original ? String(row.original.isWindowed) : '-',
+      },
+    ],
+    []
+  );
+  return (
+    <Table
+      data={rows || []}
+      columns={columns}
+      emptyMessage={fetching ? 'Loading...' : 'No rows found'}
+      enableSorting={false}
+    />
+  );
+};
+
+export default TableView;

+ 0 - 42
kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx

@@ -1,42 +0,0 @@
-import React from 'react';
-import KsqlDb from 'components/KsqlDb/KsqlDb';
-import { render, WithRoute } from 'lib/testHelpers';
-import { screen } from '@testing-library/dom';
-import {
-  clusterKsqlDbPath,
-  clusterKsqlDbQueryPath,
-  getNonExactPath,
-} from 'lib/paths';
-
-const KSqLComponentText = {
-  list: 'list',
-  query: 'query',
-};
-
-jest.mock('components/KsqlDb/List/List', () => () => (
-  <div>{KSqLComponentText.list}</div>
-));
-jest.mock('components/KsqlDb/Query/Query', () => () => (
-  <div>{KSqLComponentText.query}</div>
-));
-
-describe('KsqlDb Component', () => {
-  const clusterName = 'clusterName';
-  const renderComponent = (path: string) =>
-    render(
-      <WithRoute path={getNonExactPath(clusterKsqlDbPath())}>
-        <KsqlDb />
-      </WithRoute>,
-      { initialEntries: [path] }
-    );
-
-  it('Renders the List', () => {
-    renderComponent(clusterKsqlDbPath(clusterName));
-    expect(screen.getByText(KSqLComponentText.list)).toBeInTheDocument();
-  });
-
-  it('Renders the List', () => {
-    renderComponent(clusterKsqlDbQueryPath(clusterName));
-    expect(screen.getByText(KSqLComponentText.query)).toBeInTheDocument();
-  });
-});

+ 0 - 6
kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts

@@ -12,12 +12,6 @@ export const versionPayload = [
 ];
 export const versionEmptyPayload = [];
 
-export const versions = [
-  schemaVersion1,
-  schemaVersion2,
-  schemaVersionWithNonAsciiChars,
-];
-
 export const jsonSchema: SchemaSubject = {
   subject: 'test',
   version: '15',

+ 1 - 1
kafka-ui-react-app/src/components/common/ActionComponent/__tests__/fixtures.ts

@@ -14,7 +14,7 @@ export const invalidPermission = {
   action: Action.DELETE,
 };
 
-export const roles = [
+const roles = [
   {
     ...validPermission,
     actions: [validPermission.action],

+ 1 - 0
kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx

@@ -1,4 +1,5 @@
 import { diff as DiffEditor } from 'react-ace';
+import 'ace-builds/src-noconflict/ace';
 import 'ace-builds/src-noconflict/mode-json5';
 import 'ace-builds/src-noconflict/mode-protobuf';
 import 'ace-builds/src-noconflict/theme-textmate';

+ 1 - 3
kafka-ui-react-app/src/components/common/Editor/Editor.tsx

@@ -1,11 +1,9 @@
-/* eslint-disable react/jsx-props-no-spreading */
 import AceEditor, { IAceEditorProps } from 'react-ace';
 import 'ace-builds/src-noconflict/mode-json5';
 import 'ace-builds/src-noconflict/mode-protobuf';
 import 'ace-builds/src-noconflict/theme-tomorrow';
 import { SchemaType } from 'generated-sources';
 import React from 'react';
-import ReactAce from 'react-ace/lib/ace';
 import styled from 'styled-components';
 
 interface EditorProps extends IAceEditorProps {
@@ -13,7 +11,7 @@ interface EditorProps extends IAceEditorProps {
   schemaType?: string;
 }
 
-const Editor = React.forwardRef<ReactAce | null, EditorProps>((props, ref) => {
+const Editor = React.forwardRef<AceEditor | null, EditorProps>((props, ref) => {
   const { isFixedHeight, schemaType, ...rest } = props;
   return (
     <AceEditor

+ 0 - 11
kafka-ui-react-app/src/components/common/NewTable/TimestampCell copy.tsx

@@ -1,11 +0,0 @@
-import { CellContext } from '@tanstack/react-table';
-import React from 'react';
-
-import * as S from './Table.styled';
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const TruncatedTextCell: React.FC<CellContext<any, unknown>> = ({
-  getValue,
-}) => <S.Ellipsis>{getValue<string>()}</S.Ellipsis>;
-
-export default TruncatedTextCell;

+ 2 - 2
kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx

@@ -1,15 +1,15 @@
 /* eslint-disable react/jsx-props-no-spreading */
 import AceEditor, { IAceEditorProps } from 'react-ace';
+import 'ace-builds/src-noconflict/ace';
 import 'ace-builds/src-noconflict/mode-sql';
 import 'ace-builds/src-noconflict/theme-textmate';
 import React from 'react';
-import ReactAce from 'react-ace/lib/ace';
 
 interface SQLEditorProps extends IAceEditorProps {
   isFixedHeight?: boolean;
 }
 
-const SQLEditor = React.forwardRef<ReactAce | null, SQLEditorProps>(
+const SQLEditor = React.forwardRef<AceEditor | null, SQLEditorProps>(
   (props, ref) => {
     const { isFixedHeight, ...rest } = props;
     return (

+ 2 - 2
kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx

@@ -8,13 +8,13 @@ import {
 
 import * as S from './Tooltip.styled';
 
-export interface PropsTypes {
+interface TooltipProps {
   value: React.ReactNode;
   content: string;
   placement?: Placement;
 }
 
-const Tooltip: React.FC<PropsTypes> = ({ value, content, placement }) => {
+const Tooltip: React.FC<TooltipProps> = ({ value, content, placement }) => {
   const [open, setOpen] = useState(false);
   const { x, y, refs, strategy, context } = useFloating({
     open,

+ 1 - 1
kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx

@@ -3,5 +3,5 @@ import Heading from 'components/common/heading/Heading.styled';
 import styled from 'styled-components';
 
 export const TableTitle = styled((props) => <Heading level={3} {...props} />)`
-  padding: 16px;
+  padding: 16px 16px 0;
 `;

+ 1 - 1
kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts

@@ -4,7 +4,7 @@ import { modifyRolesData } from 'lib/permissions';
 export const clusterName1 = 'local';
 export const clusterName2 = 'dev';
 
-export const userPermissionsMock = [
+const userPermissionsMock = [
   {
     clusters: [clusterName1],
     resource: ResourceType.TOPIC,

+ 1 - 1
kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts

@@ -109,7 +109,7 @@ export function useUpdateConnectorConfig(props: UseConnectorProps) {
     }
   );
 }
-export function useCreateConnectorMutation(clusterName: ClusterName) {
+function useCreateConnectorMutation(clusterName: ClusterName) {
   const client = useQueryClient();
   return useMutation(
     (props: CreateConnectorProps) =>

+ 184 - 0
kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx

@@ -0,0 +1,184 @@
+import { ksqlDbApiClient as api } from 'lib/api';
+import { useMutation, useQueries } from '@tanstack/react-query';
+import { ClusterName } from 'redux/interfaces';
+import { BASE_PARAMS } from 'lib/constants';
+import React from 'react';
+import { fetchEventSource } from '@microsoft/fetch-event-source';
+import {
+  showAlert,
+  showServerError,
+  showSuccessAlert,
+} from 'lib/errorHandling';
+import {
+  ExecuteKsqlRequest,
+  KsqlResponse,
+  KsqlTableResponse,
+} from 'generated-sources';
+import { StopLoading } from 'components/Topics/Topic/Messages/Messages.styled';
+import toast from 'react-hot-toast';
+
+export function useKsqlkDb(clusterName: ClusterName) {
+  return useQueries({
+    queries: [
+      {
+        queryKey: ['clusters', clusterName, 'ksqlDb', 'tables'],
+        queryFn: () => api.listTables({ clusterName }),
+        suspense: false,
+      },
+      {
+        queryKey: ['clusters', clusterName, 'ksqlDb', 'streams'],
+        queryFn: () => api.listStreams({ clusterName }),
+        suspense: false,
+      },
+    ],
+  });
+}
+
+export function useExecuteKsqlkDbQueryMutation() {
+  return useMutation((props: ExecuteKsqlRequest) => api.executeKsql(props));
+}
+
+const getFormattedErrorFromTableData = (
+  responseValues: KsqlTableResponse['values']
+): { title: string; message: string } => {
+  // We expect someting like that
+  // [[
+  //   "@type",
+  //   "error_code",
+  //   "message",
+  //   "statementText"?,
+  //   "entities"?
+  // ]],
+  // or
+  // [["message"]]
+
+  if (!responseValues || !responseValues.length) {
+    return {
+      title: 'Unknown error',
+      message: 'Recieved empty response',
+    };
+  }
+
+  let title = '';
+  let message = '';
+  if (responseValues[0].length < 2) {
+    const [messageText] = responseValues[0];
+    title = messageText;
+  } else {
+    const [type, errorCode, messageText, statementText, entities] =
+      responseValues[0];
+    title = `[Error #${errorCode}] ${type}`;
+    message =
+      (entities?.length ? `[${entities.join(', ')}] ` : '') +
+      (statementText ? `"${statementText}" ` : '') +
+      messageText;
+  }
+
+  return { title, message };
+};
+
+type UseKsqlkDbSSEProps = {
+  pipeId: string | false;
+  clusterName: ClusterName;
+};
+
+export const useKsqlkDbSSE = ({ clusterName, pipeId }: UseKsqlkDbSSEProps) => {
+  const [data, setData] = React.useState<KsqlTableResponse>();
+  const [isFetching, setIsFetching] = React.useState<boolean>(false);
+
+  const abortController = new AbortController();
+
+  React.useEffect(() => {
+    const fetchData = async () => {
+      const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/ksql/response`;
+      await fetchEventSource(
+        `${url}?${new URLSearchParams({ pipeId: pipeId || '' }).toString()}`,
+        {
+          method: 'GET',
+          signal: abortController.signal,
+          openWhenHidden: true,
+          async onopen(response) {
+            const { ok, status } = response;
+            if (ok) setData(undefined); // Reset
+            if (status >= 400 && status < 500 && status !== 429) {
+              showServerError(response);
+            }
+          },
+          onmessage(event) {
+            const { table }: KsqlResponse = JSON.parse(event.data);
+            if (!table) {
+              return;
+            }
+            switch (table?.header) {
+              case 'Execution error': {
+                showAlert('error', {
+                  ...getFormattedErrorFromTableData(table.values),
+                  id: `${url}-executionError`,
+                });
+                break;
+              }
+              case 'Schema':
+                setData(table);
+                break;
+              case 'Row':
+                setData((state) => ({
+                  header: state?.header,
+                  columnNames: state?.columnNames,
+                  values: [...(state?.values || []), ...(table?.values || [])],
+                }));
+                break;
+              case 'Query Result':
+                showSuccessAlert({
+                  id: `${url}-querySuccess`,
+                  title: 'Query succeed',
+                  message: '',
+                });
+                break;
+              case 'Source Description':
+              case 'properties':
+              default:
+                setData(table);
+                break;
+            }
+          },
+          onclose() {
+            setIsFetching(false);
+          },
+          onerror(err) {
+            setIsFetching(false);
+            showServerError(err);
+          },
+        }
+      );
+    };
+
+    const abortFetchData = () => {
+      setIsFetching(false);
+      if (pipeId) abortController.abort();
+    };
+    if (pipeId) {
+      toast.promise(
+        fetchData(),
+        {
+          loading: (
+            <>
+              <div>Consuming query execution result...</div>
+              &nbsp;
+              <StopLoading onClick={abortFetchData}>Abort</StopLoading>
+            </>
+          ),
+          success: 'Cancelled',
+          error: 'Something went wrong. Please try again.',
+        },
+        {
+          id: 'messages',
+          success: { duration: 20 },
+        }
+      );
+    }
+
+    return abortFetchData;
+  }, [pipeId]);
+
+  return { data, isFetching };
+};

+ 1 - 1
kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts

@@ -2,7 +2,7 @@ import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants';
 import create from 'zustand';
 import { persist } from 'zustand/middleware';
 
-export interface AdvancedFilter {
+interface AdvancedFilter {
   name: string;
   value: string;
 }

+ 2 - 2
kafka-ui-react-app/src/lib/paths.ts

@@ -204,7 +204,7 @@ export const clusterConnectorsRelativePath = 'connectors';
 export const clusterConnectorNewRelativePath = 'create-new';
 export const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`;
 export const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`;
-export const clusterConnectConnectorTasksRelativePath = 'tasks';
+const clusterConnectConnectorTasksRelativePath = 'tasks';
 export const clusterConnectConnectorConfigRelativePath = 'config';
 
 export const clusterConnectsPath = (
@@ -287,5 +287,5 @@ export const clusterConfigPath = (
   clusterName: ClusterName = RouteParams.clusterName
 ) => `${clusterPath(clusterName)}/${clusterConfigRelativePath}`;
 
-export const clusterNewConfigRelativePath = 'create-new-cluster';
+const clusterNewConfigRelativePath = 'create-new-cluster';
 export const clusterNewConfigPath = `/ui/clusters/${clusterNewConfigRelativePath}`;

+ 2 - 1
kafka-ui-react-app/src/lib/yupExtended.ts

@@ -41,7 +41,8 @@ const isJsonObject = () => {
 };
 
 /**
- * due to yup rerunning all the object validiation during any render, it makes sense to cache the async results
+ * due to yup rerunning all the object validiation during any render,
+ * it makes sense to cache the async results
  * */
 export function cacheTest(
   asyncValidate: (val?: string, ctx?: yup.AnyObject) => Promise<boolean>

+ 0 - 19
kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts

@@ -1,19 +0,0 @@
-import {
-  KsqlCommandV2Response,
-  KsqlStreamDescription,
-  KsqlTableDescription,
-} from 'generated-sources';
-
-export interface KsqlState {
-  tables: KsqlTableDescription[];
-  streams: KsqlStreamDescription[];
-  executionResult: KsqlCommandV2Response | null;
-}
-
-export interface KsqlDescription {
-  name?: string;
-  topic?: string;
-  keyFormat?: string;
-  valueFormat?: string;
-  isWindowed?: boolean;
-}

+ 0 - 2
kafka-ui-react-app/src/redux/reducers/index.ts

@@ -3,12 +3,10 @@ import loader from 'redux/reducers/loader/loaderSlice';
 import schemas from 'redux/reducers/schemas/schemasSlice';
 import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';
 import consumerGroups from 'redux/reducers/consumerGroups/consumerGroupsSlice';
-import ksqlDb from 'redux/reducers/ksqlDb/ksqlDbSlice';
 
 export default combineReducers({
   loader,
   topicMessages,
   consumerGroups,
   schemas,
-  ksqlDb,
 });

+ 0 - 43
kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/fixtures.ts

@@ -1,43 +0,0 @@
-type Dictionary<T> = Record<string, T>;
-
-export const fetchKsqlDbTablesPayload: {
-  tables: Dictionary<string>[];
-  streams: Dictionary<string>[];
-} = {
-  tables: [
-    {
-      type: 'TABLE',
-      name: 'USERS',
-      topic: 'users',
-      keyFormat: 'KAFKA',
-      valueFormat: 'AVRO',
-      isWindowed: 'false',
-    },
-    {
-      type: 'TABLE',
-      name: 'USERS2',
-      topic: 'users',
-      keyFormat: 'KAFKA',
-      valueFormat: 'AVRO',
-      isWindowed: 'false',
-    },
-  ],
-  streams: [
-    {
-      type: 'STREAM',
-      name: 'KSQL_PROCESSING_LOG',
-      topic: 'default_ksql_processing_log',
-      keyFormat: 'KAFKA',
-      valueFormat: 'JSON',
-      isWindowed: 'false',
-    },
-    {
-      type: 'STREAM',
-      name: 'PAGEVIEWS',
-      topic: 'pageviews',
-      keyFormat: 'KAFKA',
-      valueFormat: 'AVRO',
-      isWindowed: 'false',
-    },
-  ],
-};

+ 0 - 51
kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts

@@ -1,51 +0,0 @@
-import { store } from 'redux/store';
-import * as selectors from 'redux/reducers/ksqlDb/selectors';
-import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
-
-import { fetchKsqlDbTablesPayload } from './fixtures';
-
-describe('TopicMessages selectors', () => {
-  describe('Initial state', () => {
-    beforeAll(() => {
-      store.dispatch({
-        type: fetchKsqlDbTables.pending.type,
-        payload: fetchKsqlDbTablesPayload,
-      });
-    });
-
-    it('Returns empty state', () => {
-      expect(selectors.getKsqlDbTables(store.getState())).toEqual({
-        rows: {
-          streams: [],
-          tables: [],
-        },
-        fetched: false,
-        fetching: true,
-        tablesCount: 0,
-        streamsCount: 0,
-      });
-    });
-  });
-
-  describe('State', () => {
-    beforeAll(() => {
-      store.dispatch({
-        type: fetchKsqlDbTables.fulfilled.type,
-        payload: fetchKsqlDbTablesPayload,
-      });
-    });
-
-    it('Returns tables and streams', () => {
-      expect(selectors.getKsqlDbTables(store.getState())).toEqual({
-        rows: {
-          streams: [...fetchKsqlDbTablesPayload.streams],
-          tables: [...fetchKsqlDbTablesPayload.tables],
-        },
-        fetched: true,
-        fetching: false,
-        tablesCount: 2,
-        streamsCount: 2,
-      });
-    });
-  });
-});

+ 0 - 75
kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts

@@ -1,75 +0,0 @@
-import { KsqlState } from 'redux/interfaces/ksqlDb';
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
-import { ExecuteKsqlRequest } from 'generated-sources';
-import { ClusterName } from 'redux/interfaces';
-import { ksqlDbApiClient } from 'lib/api';
-
-const getTables = (clusterName: ClusterName) =>
-  ksqlDbApiClient.listTables({
-    clusterName,
-  });
-
-const getStreams = (clusterName: ClusterName) =>
-  ksqlDbApiClient.listStreams({
-    clusterName,
-  });
-
-export const fetchKsqlDbTables = createAsyncThunk(
-  'ksqlDb/fetchKsqlDbTables',
-  async (clusterName: ClusterName) => {
-    const [tables, streams] = await Promise.all([
-      getTables(clusterName),
-      getStreams(clusterName),
-    ]);
-
-    const processedTables = tables.map((table) => ({
-      type: 'TABLE',
-      ...table,
-    }));
-    const processedStreams = streams.map((stream) => ({
-      type: 'STREAM',
-      ...stream,
-    }));
-
-    return {
-      tables: processedTables,
-      streams: processedStreams,
-    };
-  }
-);
-
-export const executeKsql = createAsyncThunk(
-  'ksqlDb/executeKsql',
-  (params: ExecuteKsqlRequest) => ksqlDbApiClient.executeKsql(params)
-);
-
-const initialState: KsqlState = {
-  streams: [],
-  tables: [],
-  executionResult: null,
-};
-
-const ksqlDbSlice = createSlice({
-  name: 'ksqlDb',
-  initialState,
-  reducers: {
-    resetExecutionResult: (state) => ({
-      ...state,
-      executionResult: null,
-    }),
-  },
-  extraReducers: (builder) => {
-    builder.addCase(fetchKsqlDbTables.fulfilled, (state, action) => ({
-      ...state,
-      ...action.payload,
-    }));
-    builder.addCase(executeKsql.fulfilled, (state, action) => ({
-      ...state,
-      executionResult: action.payload,
-    }));
-  },
-});
-
-export const { resetExecutionResult } = ksqlDbSlice.actions;
-
-export default ksqlDbSlice.reducer;

+ 0 - 33
kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts

@@ -1,33 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { RootState } from 'redux/interfaces';
-import { createFetchingSelector } from 'redux/reducers/loader/selectors';
-import { KsqlState } from 'redux/interfaces/ksqlDb';
-import { AsyncRequestStatus } from 'lib/constants';
-
-const ksqlDbState = ({ ksqlDb }: RootState): KsqlState => ksqlDb;
-
-const getKsqlDbFetchTablesAndStreamsFetchingStatus = createFetchingSelector(
-  'ksqlDb/fetchKsqlDbTables'
-);
-
-const getKsqlExecutionStatus = createFetchingSelector('ksqlDb/executeKsql');
-
-export const getKsqlDbTables = createSelector(
-  [ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus],
-  (state, status) => ({
-    rows: { streams: [...state.streams], tables: [...state.tables] },
-    fetched: status === AsyncRequestStatus.fulfilled,
-    fetching: status === AsyncRequestStatus.pending,
-    tablesCount: state.tables.length,
-    streamsCount: state.streams.length,
-  })
-);
-
-export const getKsqlExecution = createSelector(
-  [ksqlDbState, getKsqlExecutionStatus],
-  (state, status) => ({
-    executionResult: state.executionResult,
-    fetched: status === AsyncRequestStatus.fulfilled,
-    fetching: status === AsyncRequestStatus.pending,
-  })
-);

+ 7 - 0
kafka-ui-react-app/vite.config.ts

@@ -25,6 +25,13 @@ export default defineConfig(({ mode }) => {
     },
     build: {
       outDir: 'build',
+      rollupOptions: {
+        output: {
+          manualChunks: {
+            ace: ['ace-builds', 'react-ace'],
+          },
+        },
+      },
     },
     experimental: {
       renderBuiltUrl(