Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into issues/2844

Roman Zabaluev 2 лет назад
Родитель
Сommit
c027708075
26 измененных файлов с 330 добавлено и 127 удалено
  1. 1 1
      kafka-ui-api/pom.xml
  2. 3 4
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java
  3. 1 1
      kafka-ui-e2e-checks/pom.xml
  4. 2 1
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java
  5. 15 5
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java
  6. 9 16
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java
  7. 13 1
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java
  8. 55 3
      kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx
  9. 11 0
      kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts
  10. 17 0
      kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx
  11. 1 1
      kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx
  12. 19 5
      kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx
  13. 17 5
      kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
  14. 1 1
      kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx
  15. 15 2
      kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx
  16. 3 1
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
  17. 10 0
      kafka-ui-react-app/src/components/common/Input/Input.styled.ts
  18. 21 15
      kafka-ui-react-app/src/components/common/Input/Input.tsx
  19. 41 0
      kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx
  20. 9 2
      kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx
  21. 32 1
      kafka-ui-react-app/src/components/common/Search/Search.tsx
  22. 20 0
      kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx
  23. 1 32
      kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts
  24. 1 2
      kafka-ui-react-app/src/lib/dateTimeHelpers.ts
  25. 0 28
      kafka-ui-react-app/src/lib/yupExtended.ts
  26. 12 0
      kafka-ui-react-app/src/theme/theme.ts

+ 1 - 1
kafka-ui-api/pom.xml

@@ -12,7 +12,7 @@
     <artifactId>kafka-ui-api</artifactId>
 
     <properties>
-        <jacoco.version>0.8.8</jacoco.version>
+        <jacoco.version>0.8.10</jacoco.version>
         <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
         <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
         <sonar.jacoco.reportPath>${project.basedir}/target/jacoco.exec</sonar.jacoco.reportPath>

+ 3 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java

@@ -1,7 +1,7 @@
 package com.provectus.kafka.ui.model;
 
 import java.math.BigDecimal;
-import java.math.MathContext;
+import java.math.RoundingMode;
 import java.util.HashMap;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -21,8 +21,6 @@ public class PartitionDistributionStats {
   // avg skew will show unuseful results on low number of partitions
   private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50;
 
-  private static final MathContext ROUNDING_MATH_CTX = new MathContext(3);
-
   private final Map<Node, Integer> partitionLeaders;
   private final Map<Node, Integer> partitionsCount;
   private final Map<Node, Integer> inSyncPartitions;
@@ -88,6 +86,7 @@ public class PartitionDistributionStats {
       return null;
     }
     value = value == null ? 0 : value;
-    return new BigDecimal((value - avgValue) / avgValue * 100.0).round(ROUNDING_MATH_CTX);
+    return new BigDecimal((value - avgValue) / avgValue * 100.0)
+        .setScale(1, RoundingMode.HALF_UP);
   }
 }

+ 1 - 1
kafka-ui-e2e-checks/pom.xml

@@ -18,7 +18,7 @@
         <httpcomponents.version>5.2.1</httpcomponents.version>
         <selenium.version>4.8.1</selenium.version>
         <selenide.version>6.12.3</selenide.version>
-        <testng.version>7.7.0</testng.version>
+        <testng.version>7.7.1</testng.version>
         <allure.version>2.21.0</allure.version>
         <qase.io.version>3.0.4</qase.io.version>
         <aspectj.version>1.9.9.1</aspectj.version>

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

@@ -48,7 +48,8 @@ public class BrokersList extends BasePage {
   }
 
   private List<SelenideElement> getEnabledColumnHeaders() {
-    return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host")
+    return Stream.of("Broker ID", "Disk usage", "Partitions skew",
+            "Leaders", "Leader skew", "Online partitions", "Port", "Host")
         .map(name -> $x(String.format(columnHeaderLocator, name)))
         .collect(Collectors.toList());
   }

+ 15 - 5
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java

@@ -17,11 +17,12 @@ import java.util.List;
 public class KsqlQueryForm extends BasePage {
   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']");
   protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']");
   protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']");
   protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']");
   protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']");
+  protected SelenideElement abortButton = $x("//div[@role='status']/div[text()='Abort']");
+  protected SelenideElement cancelledAlert = $x("//div[@role='status'][text()='Cancelled']");
   protected ElementsCollection ksqlGridItems = $$x("//tbody//tr");
   protected ElementsCollection keyField = $$x("//input[@aria-label='key']");
   protected ElementsCollection valueField = $$x("//input[@aria-label='value']");
@@ -48,7 +49,7 @@ public class KsqlQueryForm extends BasePage {
   public KsqlQueryForm clickExecuteBtn(String query) {
     clickByActions(executeBtn);
     if (query.contains("EMIT CHANGES")) {
-      loadingSpinner.shouldBe(Condition.visible);
+      abortButton.shouldBe(Condition.visible);
     } else {
       waitUntilSpinnerDisappear();
     }
@@ -56,12 +57,21 @@ public class KsqlQueryForm extends BasePage {
   }
 
   @Step
-  public KsqlQueryForm clickStopQueryBtn() {
-    clickByActions(stopQueryBtn);
-    waitUntilSpinnerDisappear();
+  public boolean isAbortBtnVisible() {
+    return isVisible(abortButton);
+  }
+
+  @Step
+  public KsqlQueryForm clickAbortBtn() {
+    clickByActions(abortButton);
     return this;
   }
 
+  @Step
+  public boolean isCancelledAlertVisible() {
+    return isVisible(cancelledAlert);
+  }
+
   @Step
   public KsqlQueryForm clickClearResultsBtn() {
     clickByActions(clearResultsBtn);

+ 9 - 16
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java

@@ -21,73 +21,66 @@ public class SmokeBacklog extends BaseManualTest {
   public void testCaseA() {
   }
 
-  @Automation(state = TO_BE_AUTOMATED)
-  @Suite(id = KSQL_DB_SUITE_ID)
-  @QaseId(277)
-  @Test
-  public void testCaseB() {
-  }
-
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = BROKERS_SUITE_ID)
   @QaseId(331)
   @Test
-  public void testCaseC() {
+  public void testCaseB() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = BROKERS_SUITE_ID)
   @QaseId(332)
   @Test
-  public void testCaseD() {
+  public void testCaseC() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = TOPICS_PROFILE_SUITE_ID)
   @QaseId(335)
   @Test
-  public void testCaseE() {
+  public void testCaseD() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = TOPICS_PROFILE_SUITE_ID)
   @QaseId(336)
   @Test
-  public void testCaseF() {
+  public void testCaseE() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = TOPICS_PROFILE_SUITE_ID)
   @QaseId(343)
   @Test
-  public void testCaseG() {
+  public void testCaseF() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = KSQL_DB_SUITE_ID)
   @QaseId(344)
   @Test
-  public void testCaseH() {
+  public void testCaseG() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = SCHEMAS_SUITE_ID)
   @QaseId(345)
   @Test
-  public void testCaseI() {
+  public void testCaseH() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = SCHEMAS_SUITE_ID)
   @QaseId(346)
   @Test
-  public void testCaseJ() {
+  public void testCaseI() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = TOPICS_PROFILE_SUITE_ID)
   @QaseId(347)
   @Test
-  public void testCaseK() {
+  public void testCaseJ() {
   }
 }

+ 13 - 1
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.smokesuite.ksqldb;
 
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS;
+import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SELECT_ALL_FROM;
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS;
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
 import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
@@ -87,7 +88,8 @@ public class KsqlDbTest extends BaseTest {
     navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery());
     SoftAssert softly = new SoftAssert();
     softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()");
-    softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(), "getItemByName()");
+    softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(),
+        String.format("getItemByName(%s)", FIRST_TABLE.getName()));
     softly.assertAll();
   }
 
@@ -104,6 +106,16 @@ public class KsqlDbTest extends BaseTest {
     softly.assertAll();
   }
 
+  @QaseId(277)
+  @Test(priority = 6)
+  public void stopQueryFunctionalCheck() {
+    navigateToKsqlDbAndExecuteRequest(String.format(SELECT_ALL_FROM.getQuery(), FIRST_TABLE.getName()));
+    Assert.assertTrue(ksqlQueryForm.isAbortBtnVisible(), "isAbortBtnVisible()");
+    ksqlQueryForm
+        .clickAbortBtn();
+    Assert.assertTrue(ksqlQueryForm.isCancelledAlertVisible(), "isCancelledAlertVisible()");
+  }
+
   @AfterClass(alwaysRun = true)
   public void afterClass() {
     TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName));

+ 55 - 3
kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx

@@ -11,7 +11,9 @@ import CheckMarkRoundIcon from 'components/common/Icons/CheckMarkRoundIcon';
 import { ColumnDef } from '@tanstack/react-table';
 import { clusterBrokerPath } from 'lib/paths';
 import Tooltip from 'components/common/Tooltip/Tooltip';
+import ColoredCell from 'components/common/NewTable/ColoredCell';
 
+import SkewHeader from './SkewHeader/SkewHeader';
 import * as S from './BrokersList.styled';
 
 const NA = 'N/A';
@@ -57,11 +59,15 @@ const BrokersList: React.FC = () => {
         count: segmentCount || NA,
         port: broker?.port,
         host: broker?.host,
+        partitionsLeader: broker?.partitionsLeader,
+        partitionsSkew: broker?.partitionsSkew,
+        leadersSkew: broker?.leadersSkew,
+        inSyncPartitions: broker?.inSyncPartitions,
       };
     });
   }, [diskUsage, brokers]);
 
-  const columns = React.useMemo<ColumnDef<typeof rows>[]>(
+  const columns = React.useMemo<ColumnDef<(typeof rows)[number]>[]>(
     () => [
       {
         header: 'Broker ID',
@@ -84,7 +90,7 @@ const BrokersList: React.FC = () => {
         ),
       },
       {
-        header: 'Segment Size',
+        header: 'Disk usage',
         accessorKey: 'size',
         // eslint-disable-next-line react/no-unstable-nested-components
         cell: ({ getValue, table, cell, column, renderValue, row }) =>
@@ -98,10 +104,56 @@ const BrokersList: React.FC = () => {
               cell={cell}
               getValue={getValue}
               renderValue={renderValue}
+              renderSegments
             />
           ),
       },
-      { header: 'Segment Count', accessorKey: 'count' },
+      {
+        // eslint-disable-next-line react/no-unstable-nested-components
+        header: () => <SkewHeader />,
+        accessorKey: 'partitionsSkew',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => {
+          const value = getValue<number>();
+          return (
+            <ColoredCell
+              value={value ? `${value.toFixed(2)}%` : '-'}
+              warn={value >= 10 && value < 20}
+              attention={value >= 20}
+            />
+          );
+        },
+      },
+      { header: 'Leaders', accessorKey: 'partitionsLeader' },
+      {
+        header: 'Leader skew',
+        accessorKey: 'leadersSkew',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => {
+          const value = getValue<number>();
+          return (
+            <ColoredCell
+              value={value ? `${value.toFixed(2)}%` : '-'}
+              warn={value >= 10 && value < 20}
+              attention={value >= 20}
+            />
+          );
+        },
+      },
+      {
+        header: 'Online partitions',
+        accessorKey: 'inSyncPartitions',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue, row }) => {
+          const value = getValue<number>();
+          return (
+            <ColoredCell
+              value={value}
+              attention={value !== row.original.count}
+            />
+          );
+        },
+      },
       { header: 'Port', accessorKey: 'port' },
       {
         header: 'Host',

+ 11 - 0
kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts

@@ -0,0 +1,11 @@
+import styled from 'styled-components';
+import { MessageTooltip } from 'components/common/Tooltip/Tooltip.styled';
+
+export const CellWrapper = styled.div`
+  display: flex;
+  gap: 10px;
+
+  ${MessageTooltip} {
+    max-height: unset;
+  }
+`;

+ 17 - 0
kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import Tooltip from 'components/common/Tooltip/Tooltip';
+import InfoIcon from 'components/common/Icons/InfoIcon';
+
+import * as S from './SkewHeader.styled';
+
+const SkewHeader: React.FC = () => (
+  <S.CellWrapper>
+    Partitions skew
+    <Tooltip
+      value={<InfoIcon />}
+      content="The divergence from the average brokers' value"
+    />
+  </S.CellWrapper>
+);
+
+export default SkewHeader;

+ 1 - 1
kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx

@@ -37,7 +37,7 @@ const Config: React.FC = () => {
     formState: { isDirty, isSubmitting, isValid, errors },
     setValue,
   } = useForm<FormValues>({
-    mode: 'onTouched',
+    mode: 'onChange',
     resolver: yupResolver(validationSchema),
     defaultValues: {
       config: JSON.stringify(config, null, '\t'),

+ 19 - 5
kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx

@@ -1,17 +1,31 @@
-import React from 'react';
-import { Cluster } from 'generated-sources';
+import React, { useMemo } from 'react';
+import { Cluster, ResourceType } from 'generated-sources';
 import { CellContext } from '@tanstack/react-table';
-import { Button } from 'components/common/Button/Button';
 import { clusterConfigPath } from 'lib/paths';
+import { useGetUserInfo } from 'lib/hooks/api/roles';
+import { ActionCanButton } from 'components/common/ActionComponent';
 
 type Props = CellContext<Cluster, unknown>;
 
 const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
   const { name } = row.original;
+  const { data } = useGetUserInfo();
+
+  const isApplicationConfig = useMemo(() => {
+    return !!data?.userInfo?.permissions.some(
+      (permission) => permission.resource === ResourceType.APPLICATIONCONFIG
+    );
+  }, [data]);
+
   return (
-    <Button buttonType="secondary" buttonSize="S" to={clusterConfigPath(name)}>
+    <ActionCanButton
+      buttonType="secondary"
+      buttonSize="S"
+      to={clusterConfigPath(name)}
+      canDoAction={isApplicationConfig}
+    >
       Configure
-    </Button>
+    </ActionCanButton>
   );
 };
 

+ 17 - 5
kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx

@@ -1,23 +1,25 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useMemo } from 'react';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import Switch from 'components/common/Switch/Switch';
 import { useClusters } from 'lib/hooks/api/clusters';
-import { Cluster, ServerStatus } from 'generated-sources';
+import { Cluster, ResourceType, ServerStatus } from 'generated-sources';
 import { ColumnDef } from '@tanstack/react-table';
 import Table, { SizeCell } from 'components/common/NewTable';
 import useBoolean from 'lib/hooks/useBoolean';
-import { Button } from 'components/common/Button/Button';
 import { clusterNewConfigPath } from 'lib/paths';
 import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
 import { useNavigate } from 'react-router-dom';
+import { ActionCanButton } from 'components/common/ActionComponent';
+import { useGetUserInfo } from 'lib/hooks/api/roles';
 
 import * as S from './Dashboard.styled';
 import ClusterName from './ClusterName';
 import ClusterTableActionsCell from './ClusterTableActionsCell';
 
 const Dashboard: React.FC = () => {
+  const { data } = useGetUserInfo();
   const clusters = useClusters();
   const { value: showOfflineOnly, toggle } = useBoolean(false);
   const appInfo = React.useContext(GlobalSettingsContext);
@@ -62,6 +64,11 @@ const Dashboard: React.FC = () => {
     }
   }, [clusters, appInfo.hasDynamicConfig]);
 
+  const isApplicationConfig = useMemo(() => {
+    return !!data?.userInfo?.permissions.some(
+      (permission) => permission.resource === ResourceType.APPLICATIONCONFIG
+    );
+  }, [data]);
   return (
     <>
       <PageHeading text="Dashboard" />
@@ -87,9 +94,14 @@ const Dashboard: React.FC = () => {
           <label>Only offline clusters</label>
         </div>
         {appInfo.hasDynamicConfig && (
-          <Button buttonType="primary" buttonSize="M" to={clusterNewConfigPath}>
+          <ActionCanButton
+            buttonType="primary"
+            buttonSize="M"
+            to={clusterNewConfigPath}
+            canDoAction={isApplicationConfig}
+          >
             Configure new cluster
-          </Button>
+          </ActionCanButton>
         )}
       </S.Toolbar>
       <Table

+ 1 - 1
kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx

@@ -55,7 +55,7 @@ const Form: React.FC = () => {
     yup.object().shape({
       newSchema:
         schema?.schemaType === SchemaType.PROTOBUF
-          ? yup.string().required().isEnum('Schema syntax is not valid')
+          ? yup.string().required()
           : yup.string().required().isJsonObject('Schema syntax is not valid'),
     });
   const methods = useForm<NewSchemaSubjectRaw>({

+ 15 - 2
kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx

@@ -8,6 +8,7 @@ import { formatTimestamp } from 'lib/dateTimeHelpers';
 import { JSONPath } from 'jsonpath-plus';
 import Ellipsis from 'components/common/Ellipsis/Ellipsis';
 import WarningRedIcon from 'components/common/Icons/WarningRedIcon';
+import Tooltip from 'components/common/Tooltip/Tooltip';
 
 import MessageContent from './MessageContent/MessageContent';
 import * as S from './MessageContent/MessageContent.styled';
@@ -110,14 +111,26 @@ const Message: React.FC<Props> = ({
         </td>
         <S.DataCell title={key}>
           <Ellipsis text={renderFilteredJson(key, keyFilters)}>
-            {keySerde === 'Fallback' && <WarningRedIcon />}
+            {keySerde === 'Fallback' && (
+              <Tooltip
+                value={<WarningRedIcon />}
+                content="Fallback serde was used"
+                placement="left"
+              />
+            )}
           </Ellipsis>
         </S.DataCell>
         <S.DataCell title={content}>
           <S.Metadata>
             <S.MetadataValue>
               <Ellipsis text={renderFilteredJson(content, contentFilters)}>
-                {valueSerde === 'Fallback' && <WarningRedIcon />}
+                {valueSerde === 'Fallback' && (
+                  <Tooltip
+                    value={<WarningRedIcon />}
+                    content="Fallback serde was used"
+                    placement="left"
+                  />
+                )}
               </Ellipsis>
             </S.MetadataValue>
           </S.Metadata>

+ 3 - 1
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx

@@ -210,6 +210,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
                   name={name}
                   onChange={onChange}
                   value={value}
+                  height="40px"
                 />
               )}
             />
@@ -225,6 +226,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
                   name={name}
                   onChange={onChange}
                   value={value}
+                  height="280px"
                 />
               )}
             />
@@ -242,7 +244,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
                   defaultValue="{}"
                   name={name}
                   onChange={onChange}
-                  height="200px"
+                  height="40px"
                 />
               )}
             />

+ 10 - 0
kafka-ui-react-app/src/components/common/Input/Input.styled.ts

@@ -29,6 +29,16 @@ export const Wrapper = styled.div`
     width: 16px;
     fill: ${({ theme }) => theme.input.icon.color};
   }
+  svg:last-child {
+    position: absolute;
+    top: 8px;
+    line-height: 0;
+    z-index: 1;
+    left: unset;
+    right: 12px;
+    height: 16px;
+    width: 16px;
+  }
 `;
 
 export const Input = styled.input<InputProps>(

+ 21 - 15
kafka-ui-react-app/src/components/common/Input/Input.tsx

@@ -16,6 +16,7 @@ export interface InputProps
   withError?: boolean;
   label?: React.ReactNode;
   hint?: React.ReactNode;
+  clearIcon?: React.ReactNode;
 
   // Some may only accept integer, like `Number of Partitions`
   // some may accept decimal
@@ -99,19 +100,22 @@ function pasteNumberCheck(
   return value;
 }
 
-const Input: React.FC<InputProps> = ({
-  name,
-  hookFormOptions,
-  search,
-  inputSize = 'L',
-  type,
-  positiveOnly,
-  integerOnly,
-  withError = false,
-  label,
-  hint,
-  ...rest
-}) => {
+const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
+  const {
+    name,
+    hookFormOptions,
+    search,
+    inputSize = 'L',
+    type,
+    positiveOnly,
+    integerOnly,
+    withError = false,
+    label,
+    hint,
+    clearIcon,
+    ...rest
+  } = props;
+
   const methods = useFormContext();
 
   const fieldId = React.useId();
@@ -168,7 +172,6 @@ const Input: React.FC<InputProps> = ({
     // if the field is a part of react-hook-form form
     inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
   }
-
   return (
     <div>
       {label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
@@ -181,8 +184,11 @@ const Input: React.FC<InputProps> = ({
           type={type}
           onKeyPress={keyPressEventHandler}
           onPaste={pasteEventHandler}
+          ref={ref}
           {...inputOptions}
         />
+        {clearIcon}
+
         {withError && isHookFormField && (
           <S.FormError>
             <ErrorMessage name={name} />
@@ -192,6 +198,6 @@ const Input: React.FC<InputProps> = ({
       </S.Wrapper>
     </div>
   );
-};
+});
 
 export default Input;

+ 41 - 0
kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import styled from 'styled-components';
+
+interface CellProps {
+  isWarning?: boolean;
+  isAttention?: boolean;
+}
+
+interface ColoredCellProps {
+  value: number | string;
+  warn?: boolean;
+  attention?: boolean;
+}
+
+const Cell = styled.div<CellProps>`
+  color: ${(props) => {
+    if (props.isAttention) {
+      return props.theme.table.colored.color.attention;
+    }
+
+    if (props.isWarning) {
+      return props.theme.table.colored.color.warning;
+    }
+
+    return 'inherit';
+  }};
+`;
+
+const ColoredCell: React.FC<ColoredCellProps> = ({
+  value,
+  warn,
+  attention,
+}) => {
+  return (
+    <Cell isWarning={warn} isAttention={attention}>
+      {value}
+    </Cell>
+  );
+};
+
+export default ColoredCell;

+ 9 - 2
kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx

@@ -3,8 +3,15 @@ import { CellContext } from '@tanstack/react-table';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-const SizeCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
-  <BytesFormatted value={getValue<string | number>()} />
+type AsAny = any;
+
+const SizeCell: React.FC<
+  CellContext<AsAny, unknown> & { renderSegments?: boolean }
+> = ({ getValue, row, renderSegments = false }) => (
+  <>
+    <BytesFormatted value={getValue<string | number>()} />
+    {renderSegments ? `, ${row?.original.count} segment(s)` : null}
+  </>
 );
 
 export default SizeCell;

+ 32 - 1
kafka-ui-react-app/src/components/common/Search/Search.tsx

@@ -1,7 +1,9 @@
-import React from 'react';
+import React, { useRef } from 'react';
 import { useDebouncedCallback } from 'use-debounce';
 import Input from 'components/common/Input/Input';
 import { useSearchParams } from 'react-router-dom';
+import CloseIcon from 'components/common/Icons/CloseIcon';
+import styled from 'styled-components';
 
 interface SearchProps {
   placeholder?: string;
@@ -10,6 +12,16 @@ interface SearchProps {
   value?: string;
 }
 
+const IconButtonWrapper = styled.span.attrs(() => ({
+  role: 'button',
+  tabIndex: '0',
+}))`
+  height: 16px !important;
+  display: inline-block;
+  &:hover {
+    cursor: pointer;
+  }
+`;
 const Search: React.FC<SearchProps> = ({
   placeholder = 'Search',
   disabled = false,
@@ -17,7 +29,11 @@ const Search: React.FC<SearchProps> = ({
   onChange,
 }) => {
   const [searchParams, setSearchParams] = useSearchParams();
+  const ref = useRef<HTMLInputElement>(null);
   const handleChange = useDebouncedCallback((e) => {
+    if (ref.current != null) {
+      ref.current.value = e.target.value;
+    }
     if (onChange) {
       onChange(e.target.value);
     } else {
@@ -28,6 +44,15 @@ const Search: React.FC<SearchProps> = ({
       setSearchParams(searchParams);
     }
   }, 500);
+  const clearSearchValue = () => {
+    if (searchParams.get('q')) {
+      searchParams.set('q', '');
+      setSearchParams(searchParams);
+    }
+    if (ref.current != null) {
+      ref.current.value = '';
+    }
+  };
 
   return (
     <Input
@@ -37,7 +62,13 @@ const Search: React.FC<SearchProps> = ({
       defaultValue={value || searchParams.get('q') || ''}
       inputSize="M"
       disabled={disabled}
+      ref={ref}
       search
+      clearIcon={
+        <IconButtonWrapper onClick={clearSearchValue}>
+          <CloseIcon />
+        </IconButtonWrapper>
+      }
     />
   );
 };

+ 20 - 0
kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx

@@ -41,4 +41,24 @@ describe('Search', () => {
     render(<Search />);
     expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
   });
+
+  it('Clear button is visible', () => {
+    render(<Search placeholder={placeholder} />);
+
+    const clearButton = screen.getByRole('button');
+    expect(clearButton).toBeInTheDocument();
+  });
+
+  it('Clear button should clear text from input', async () => {
+    render(<Search placeholder={placeholder} />);
+
+    const searchField = screen.getAllByRole('textbox')[0];
+    await userEvent.type(searchField, 'some text');
+    expect(searchField).toHaveValue('some text');
+
+    const clearButton = screen.getByRole('button');
+    await userEvent.click(clearButton);
+
+    expect(searchField).toHaveValue('');
+  });
 });

+ 1 - 32
kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts

@@ -1,19 +1,5 @@
-import { isValidEnum, isValidJsonObject } from 'lib/yupExtended';
+import { isValidJsonObject } from 'lib/yupExtended';
 
-const invalidEnum = `
-ennum SchemType {
-  AVRO = 0;
-  JSON = 1;
-  PROTOBUF = 3;
-}
-`;
-const validEnum = `
-enum SchemType {
-  AVRO = 0;
-  JSON = 1;
-  PROTOBUF = 3;
-}
-`;
 describe('yup extended', () => {
   describe('isValidJsonObject', () => {
     it('returns false for no value', () => {
@@ -35,21 +21,4 @@ describe('yup extended', () => {
       expect(isValidJsonObject('{ "foo": "bar" }')).toBeTruthy();
     });
   });
-
-  describe('isValidEnum', () => {
-    it('returns false for invalid enum', () => {
-      expect(isValidEnum(invalidEnum)).toBeFalsy();
-    });
-    it('returns false for no value', () => {
-      expect(isValidEnum()).toBeFalsy();
-    });
-    it('returns true should trim value', () => {
-      expect(
-        isValidEnum(`  enum SchemType {AVRO = 0; PROTOBUF = 3;}   `)
-      ).toBeTruthy();
-    });
-    it('returns true for valid enum', () => {
-      expect(isValidEnum(validEnum)).toBeTruthy();
-    });
-  });
 });

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

@@ -1,6 +1,6 @@
 export const formatTimestamp = (
   timestamp?: number | string | Date,
-  format: Intl.DateTimeFormatOptions = { hour12: false }
+  format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' }
 ): string => {
   if (!timestamp) {
     return '';
@@ -8,7 +8,6 @@ export const formatTimestamp = (
 
   // empty array gets the default one from the browser
   const date = new Date(timestamp);
-
   // invalid date
   if (Number.isNaN(date.getTime())) {
     return '';

+ 0 - 28
kafka-ui-react-app/src/lib/yupExtended.ts

@@ -10,7 +10,6 @@ declare module 'yup' {
     TFlags extends yup.Flags = ''
   > extends yup.Schema<TType, TContext, TDefault, TFlags> {
     isJsonObject(message?: string): StringSchema<TType, TContext>;
-    isEnum(message?: string): StringSchema<TType, TContext>;
   }
 }
 
@@ -40,32 +39,6 @@ const isJsonObject = (message?: string) => {
     isValidJsonObject
   );
 };
-
-export const isValidEnum = (value?: string) => {
-  try {
-    if (!value) return false;
-    const trimmedValue = value.trim();
-    if (
-      trimmedValue.indexOf('enum') === 0 &&
-      trimmedValue.lastIndexOf('}') === trimmedValue.length - 1
-    ) {
-      return true;
-    }
-  } catch {
-    // do nothing
-  }
-  return false;
-};
-
-const isEnum = (message?: string) => {
-  return yup.string().test(
-    'isEnum',
-    // eslint-disable-next-line no-template-curly-in-string
-    message || '${path} is not Enum object',
-    isValidEnum
-  );
-};
-
 /**
  * due to yup rerunning all the object validiation during any render,
  * it makes sense to cache the async results
@@ -88,7 +61,6 @@ export function cacheTest(
 }
 
 yup.addMethod(yup.StringSchema, 'isJsonObject', isJsonObject);
-yup.addMethod(yup.StringSchema, 'isEnum', isEnum);
 
 export const topicFormValidationSchema = yup.object().shape({
   name: yup

+ 12 - 0
kafka-ui-react-app/src/theme/theme.ts

@@ -533,6 +533,12 @@ export const theme = {
         active: Colors.neutral[90],
       },
     },
+    colored: {
+      color: {
+        attention: Colors.red[50],
+        warning: Colors.yellow[20],
+      },
+    },
     expander: {
       normal: Colors.brand[30],
       hover: Colors.brand[40],
@@ -928,6 +934,12 @@ export const darkTheme: ThemeType = {
         active: Colors.neutral[0],
       },
     },
+    colored: {
+      color: {
+        attention: Colors.red[50],
+        warning: Colors.yellow[20],
+      },
+    },
     expander: {
       normal: Colors.brand[30],
       hover: Colors.brand[40],