소스 검색

Merge branch 'master' into ISSUE-3427_brokers_skew_stats

Ilya Kuramshin 2 년 전
부모
커밋
56cbfad56f
18개의 변경된 파일333개의 추가작업 그리고 135개의 파일을 삭제
  1. 103 10
      documentation/project/contributing/prerequisites.md
  2. 11 9
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java
  3. 1 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java
  4. 6 6
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java
  5. 14 0
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java
  6. 17 4
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java
  7. 1 1
      kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx
  8. 16 3
      kafka-ui-react-app/src/components/Topics/List/BatchActionsBar.tsx
  9. 14 1
      kafka-ui-react-app/src/components/common/Button/Button.tsx
  10. 6 0
      kafka-ui-react-app/src/components/common/Button/__tests__/Button.spec.tsx
  11. 1 21
      kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts
  12. 2 1
      kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx
  13. 1 0
      kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx
  14. 86 77
      kafka-ui-react-app/src/components/common/Select/Select.tsx
  15. 26 0
      kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts
  16. 20 0
      kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx
  17. 6 0
      kafka-ui-react-app/src/components/common/Spinner/types.ts
  18. 2 1
      kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx

+ 103 - 10
documentation/project/contributing/prerequisites.md

@@ -1,16 +1,71 @@
-### Prerequisites
+## Prerequisites
 
 
-This page explains how to get the software you need to use a Linux or macOS
-machine for local development.
+This page explains how to get the software you need to use on Linux or macOS for local development.
 
 
-Before you begin contributing you must have:
+* `java 17` package or newer 
+* `git` installed
+* `docker` installed
 
 
-* A GitHub account
-* `Java` 17 or newer
-* `Git`
-* `Docker`
+> Note: For contribution, you must have `github` account.
 
 
-### Installing prerequisites on macOS
+### For Linux
+
+1. Install `OpenJDK 17` package or newer: 
+
+```
+sudo apt update
+sudo apt install openjdk-17-jdk
+```
+
+* Check java version using command `java -version`.
+
+```
+openjdk version "17.0.5" 2022-10-18
+OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
+OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)
+```
+
+Note : In case OpenJDK 17 is not set as your default Java, run `sudo update-alternatives --config java` command to list all installed Java versions.
+
+```
+Selection    Path                                            Priority   Status
+------------------------------------------------------------
+* 0            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1111      auto mode
+  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1111      manual mode
+  2            /usr/lib/jvm/java-16-openjdk-amd64/bin/java      1051      manual mode
+  3            /usr/lib/jvm/java-17-openjdk-amd64/bin/java      1001      manual mode
+
+Press <enter> to keep the current choice[*], or type selection number:
+```
+
+you can set it as the default by entering the selection number for it in the list and pressing Enter. For example, to set Java 17 as the default, you would enter "3" and press **Enter**.
+
+2. Install `git`:
+
+```
+sudo apt install git
+```
+
+3. Install `docker`:
+
+```
+sudo apt update
+sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
+apt-cache policy docker-ce
+sudo apt -y install docker-ce
+```
+
+To execute the `docker` Command without `sudo`:
+
+```
+sudo usermod -aG docker ${USER}
+su - ${USER}
+sudo chmod 666 /var/run/docker.sock
+```
+
+### For macOS
 
 
 1. Install [brew](https://brew.sh/).
 1. Install [brew](https://brew.sh/).
 2. Install brew cask:
 2. Install brew cask:
@@ -37,6 +92,44 @@ export JAVA_HOME="$(/usr/libexec/java_home -v 17)"
 Consider allocating not less than 4GB of memory for your docker.
 Consider allocating not less than 4GB of memory for your docker.
 Otherwise, some apps within a stack (e.g. `kafka-ui.yaml`) might crash.
 Otherwise, some apps within a stack (e.g. `kafka-ui.yaml`) might crash.
 
 
+To check how much memory is allocated to docker, use `docker info`.
+
+You will find the total memory and used memory in the output. if you won't find used memory thats mean memory limits are not set for containers.
+
+#### To allocate 4GB of memory for Docker:
+
+#### For Ubuntu
+
+1. Open the Docker configuration file in a text editor using the following command:
+
+```
+sudo nano /etc/default/docker
+```
+
+2. Add the following line to the file to allocate 4GB of memory to Docker:
+
+```
+DOCKER_OPTS="--default-ulimit memlock=-1:-1 --memory=4g --memory-swap=-1"
+```
+
+3. Save the file and exit the text editor.
+
+4. Restart the Docker service using the following command:
+
+```
+sudo service docker restart
+```
+
+5. Verify that the memory limit has been set correctly by running the following command:
+
+```
+docker info | grep -i memory
+```
+
+Note that the warning messages are expected as they relate to the kernel not supporting cgroup memory limits.
+
+Now any containers you run in docker will be limited to this amount of memory. You can also increase the memory limit as your preference.
+
 ## Where to go next
 ## Where to go next
 
 
-In the next section, you'll [learn how to Build and Run kafka-ui](building.md).
+In the next section, you'll learn [how to Build and Run kafka-ui](building.md).

+ 11 - 9
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java

@@ -25,7 +25,8 @@ public class FeatureService {
 
 
   private final AdminClientService adminClientService;
   private final AdminClientService adminClientService;
 
 
-  public Mono<List<ClusterFeature>> getAvailableFeatures(KafkaCluster cluster, @Nullable Node controller) {
+  public Mono<List<ClusterFeature>> getAvailableFeatures(KafkaCluster cluster,
+                                                         ReactiveAdminClient.ClusterDescription clusterDescription) {
     List<Mono<ClusterFeature>> features = new ArrayList<>();
     List<Mono<ClusterFeature>> features = new ArrayList<>();
 
 
     if (Optional.ofNullable(cluster.getConnectsClients())
     if (Optional.ofNullable(cluster.getConnectsClients())
@@ -42,17 +43,15 @@ public class FeatureService {
       features.add(Mono.just(ClusterFeature.SCHEMA_REGISTRY));
       features.add(Mono.just(ClusterFeature.SCHEMA_REGISTRY));
     }
     }
 
 
-    if (controller != null) {
-      features.add(
-          isTopicDeletionEnabled(cluster, controller)
-              .flatMap(r -> Boolean.TRUE.equals(r) ? Mono.just(ClusterFeature.TOPIC_DELETION) : Mono.empty())
-      );
-    }
+    features.add(topicDeletionEnabled(cluster, clusterDescription.getController()));
 
 
     return Flux.fromIterable(features).flatMap(m -> m).collectList();
     return Flux.fromIterable(features).flatMap(m -> m).collectList();
   }
   }
 
 
-  private Mono<Boolean> isTopicDeletionEnabled(KafkaCluster cluster, Node controller) {
+  private Mono<ClusterFeature> topicDeletionEnabled(KafkaCluster cluster, @Nullable Node controller) {
+    if (controller == null) {
+      return Mono.just(ClusterFeature.TOPIC_DELETION); // assuming it is enabled by default
+    }
     return adminClientService.get(cluster)
     return adminClientService.get(cluster)
         .flatMap(ac -> ac.loadBrokersConfig(List.of(controller.id())))
         .flatMap(ac -> ac.loadBrokersConfig(List.of(controller.id())))
         .map(config ->
         .map(config ->
@@ -61,6 +60,9 @@ public class FeatureService {
                 .filter(e -> e.name().equals(DELETE_TOPIC_ENABLED_SERVER_PROPERTY))
                 .filter(e -> e.name().equals(DELETE_TOPIC_ENABLED_SERVER_PROPERTY))
                 .map(e -> Boolean.parseBoolean(e.value()))
                 .map(e -> Boolean.parseBoolean(e.value()))
                 .findFirst()
                 .findFirst()
-                .orElse(true));
+                .orElse(true))
+        .flatMap(enabled -> enabled
+            ? Mono.just(ClusterFeature.TOPIC_DELETION)
+            : Mono.empty());
   }
   }
 }
 }

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java

@@ -41,7 +41,7 @@ public class StatisticsService {
                     List.of(
                     List.of(
                         metricsCollector.getBrokerMetrics(cluster, description.getNodes()),
                         metricsCollector.getBrokerMetrics(cluster, description.getNodes()),
                         getLogDirInfo(description, ac),
                         getLogDirInfo(description, ac),
-                        featureService.getAvailableFeatures(cluster, description.getController()),
+                        featureService.getAvailableFeatures(cluster, description),
                         loadTopicConfigs(cluster),
                         loadTopicConfigs(cluster),
                         describeTopics(cluster)),
                         describeTopics(cluster)),
                     results ->
                     results ->

+ 6 - 6
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java

@@ -208,23 +208,23 @@ public class TopicsList extends BasePage {
             return new TopicsList();
             return new TopicsList();
         }
         }
 
 
+        private SelenideElement getNameElm() {
+            return element.$x("./td[2]");
+        }
+
         @Step
         @Step
         public boolean isInternal() {
         public boolean isInternal() {
             boolean internal = false;
             boolean internal = false;
             try {
             try {
-                internal = element.$x("./td[2]/a/span").isDisplayed();
+                internal = getNameElm().$x("./a/span").isDisplayed();
             } catch (Throwable ignored) {
             } catch (Throwable ignored) {
             }
             }
             return internal;
             return internal;
         }
         }
 
 
-        private SelenideElement getNameElm() {
-            return element.$x("./td[2]");
-        }
-
         @Step
         @Step
         public String getName() {
         public String getName() {
-            return getNameElm().getText().trim();
+            return getNameElm().$x("./a").getAttribute("title");
         }
         }
 
 
         @Step
         @Step

+ 14 - 0
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java

@@ -58,4 +58,18 @@ public class SmokeBacklog extends BaseManualTest {
     @Test
     @Test
     public void testCaseG() {
     public void testCaseG() {
     }
     }
+
+    @Automation(state = TO_BE_AUTOMATED)
+    @Suite(id = 5)
+    @QaseId(335)
+    @Test
+    public void testCaseH() {
+    }
+
+    @Automation(state = TO_BE_AUTOMATED)
+    @Suite(id = 5)
+    @QaseId(336)
+    @Test
+    public void testCaseI() {
+    }
 }
 }

+ 17 - 4
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java

@@ -359,7 +359,7 @@ public class TopicsTest extends BaseTest {
 
 
     @QaseId(11)
     @QaseId(11)
     @Test(priority = 15)
     @Test(priority = 15)
-    public void checkShowInternalTopicsButtonFunctionality() {
+    public void checkShowInternalTopicsButton() {
         navigateToTopics();
         navigateToTopics();
         SoftAssert softly = new SoftAssert();
         SoftAssert softly = new SoftAssert();
         softly.assertTrue(topicsList.isShowInternalRadioBtnSelected(), "isInternalRadioBtnSelected()");
         softly.assertTrue(topicsList.isShowInternalRadioBtnSelected(), "isInternalRadioBtnSelected()");
@@ -373,8 +373,21 @@ public class TopicsTest extends BaseTest {
         softly.assertAll();
         softly.assertAll();
     }
     }
 
 
-    @QaseId(56)
+    @QaseId(334)
     @Test(priority = 16)
     @Test(priority = 16)
+    public void checkInternalTopicsNaming() {
+        navigateToTopics();
+        SoftAssert softly = new SoftAssert();
+        topicsList
+                .setShowInternalRadioButton(true)
+                .getInternalTopics()
+                .forEach(topic -> softly.assertTrue(topic.getName().startsWith("_"),
+                        String.format("'%s' starts with '_'", topic.getName())));
+        softly.assertAll();
+    }
+
+    @QaseId(56)
+    @Test(priority = 17)
     public void checkRetentionBytesAccordingToMaxSizeOnDisk() {
     public void checkRetentionBytesAccordingToMaxSizeOnDisk() {
         navigateToTopics();
         navigateToTopics();
         topicsList
         topicsList
@@ -422,7 +435,7 @@ public class TopicsTest extends BaseTest {
     }
     }
 
 
     @QaseId(247)
     @QaseId(247)
-    @Test(priority = 17)
+    @Test(priority = 18)
     public void recreateTopicFromTopicProfile() {
     public void recreateTopicFromTopicProfile() {
         Topic topicToRecreate = new Topic()
         Topic topicToRecreate = new Topic()
                 .setName("topic-to-recreate-" + randomAlphabetic(5))
                 .setName("topic-to-recreate-" + randomAlphabetic(5))
@@ -450,7 +463,7 @@ public class TopicsTest extends BaseTest {
     }
     }
 
 
     @QaseId(8)
     @QaseId(8)
-    @Test(priority = 18)
+    @Test(priority = 19)
     public void checkCopyTopicPossibility() {
     public void checkCopyTopicPossibility() {
         Topic topicToCopy = new Topic()
         Topic topicToCopy = new Topic()
                 .setName("topic-to-copy-" + randomAlphabetic(5))
                 .setName("topic-to-copy-" + randomAlphabetic(5))

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

@@ -110,7 +110,7 @@ const Form: React.FC = () => {
   return (
   return (
     <FormProvider {...methods}>
     <FormProvider {...methods}>
       <PageHeading
       <PageHeading
-        text="Edit"
+        text={`${subject} Edit`}
         backText="Schema Registry"
         backText="Schema Registry"
         backTo={clusterSchemasPath(clusterName)}
         backTo={clusterSchemasPath(clusterName)}
       />
       />

+ 16 - 3
kafka-ui-react-app/src/components/Topics/List/BatchActionsBar.tsx

@@ -9,7 +9,6 @@ import {
   useDeleteTopic,
   useDeleteTopic,
 } from 'lib/hooks/api/topics';
 } from 'lib/hooks/api/topics';
 import { useConfirm } from 'lib/hooks/useConfirm';
 import { useConfirm } from 'lib/hooks/useConfirm';
-import { Button } from 'components/common/Button/Button';
 import { clusterTopicCopyRelativePath } from 'lib/paths';
 import { clusterTopicCopyRelativePath } from 'lib/paths';
 import { useQueryClient } from '@tanstack/react-query';
 import { useQueryClient } from '@tanstack/react-query';
 import { ActionCanButton } from 'components/common/ActionComponent';
 import { ActionCanButton } from 'components/common/ActionComponent';
@@ -108,6 +107,19 @@ const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
     );
     );
   }, [selectedTopics, clusterName, roles]);
   }, [selectedTopics, clusterName, roles]);
 
 
+  const canCopySelectedTopic = useMemo(() => {
+    return selectedTopics.every((value) =>
+      isPermitted({
+        roles,
+        resource: ResourceType.TOPIC,
+        action: Action.CREATE,
+        value,
+        clusterName,
+        rbacFlag,
+      })
+    );
+  }, [selectedTopics, clusterName, roles]);
+
   const canPurgeSelectedTopics = useMemo(() => {
   const canPurgeSelectedTopics = useMemo(() => {
     return selectedTopics.every((value) =>
     return selectedTopics.every((value) =>
       isPermitted({
       isPermitted({
@@ -132,14 +144,15 @@ const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
       >
       >
         Delete selected topics
         Delete selected topics
       </ActionCanButton>
       </ActionCanButton>
-      <Button
+      <ActionCanButton
         buttonSize="M"
         buttonSize="M"
         buttonType="secondary"
         buttonType="secondary"
         disabled={selectedTopics.length !== 1}
         disabled={selectedTopics.length !== 1}
+        canDoAction={canCopySelectedTopic}
         to={getCopyTopicPath()}
         to={getCopyTopicPath()}
       >
       >
         Copy selected topic
         Copy selected topic
-      </Button>
+      </ActionCanButton>
       <ActionCanButton
       <ActionCanButton
         buttonSize="M"
         buttonSize="M"
         buttonType="secondary"
         buttonType="secondary"

+ 14 - 1
kafka-ui-react-app/src/components/common/Button/Button.tsx

@@ -3,11 +3,13 @@ import StyledButton, {
 } from 'components/common/Button/Button.styled';
 } from 'components/common/Button/Button.styled';
 import React from 'react';
 import React from 'react';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
+import Spinner from 'components/common/Spinner/Spinner';
 
 
 export interface Props
 export interface Props
   extends React.ButtonHTMLAttributes<HTMLButtonElement>,
   extends React.ButtonHTMLAttributes<HTMLButtonElement>,
     ButtonProps {
     ButtonProps {
   to?: string | object;
   to?: string | object;
+  inProgress?: boolean;
 }
 }
 
 
 export const Button: React.FC<Props> = ({ to, ...props }) => {
 export const Button: React.FC<Props> = ({ to, ...props }) => {
@@ -20,5 +22,16 @@ export const Button: React.FC<Props> = ({ to, ...props }) => {
       </Link>
       </Link>
     );
     );
   }
   }
-  return <StyledButton type="button" {...props} />;
+  return (
+    <StyledButton
+      type="button"
+      disabled={props.disabled || props.inProgress}
+      {...props}
+    >
+      {props.children}{' '}
+      {props.inProgress ? (
+        <Spinner size={16} borderWidth={2} marginLeft={2} emptyBorderColor />
+      ) : null}
+    </StyledButton>
+  );
 };
 };

+ 6 - 0
kafka-ui-react-app/src/components/common/Button/__tests__/Button.spec.tsx

@@ -58,4 +58,10 @@ describe('Button', () => {
       theme.button.primary.invertedColors.normal
       theme.button.primary.invertedColors.normal
     );
     );
   });
   });
+  it('renders disabled button and spinner when inProgress truthy', () => {
+    render(<Button buttonType="primary" buttonSize="M" inProgress />);
+    expect(screen.getByRole('button')).toBeInTheDocument();
+    expect(screen.getByRole('progressbar')).toBeInTheDocument();
+    expect(screen.getByRole('button')).toBeDisabled();
+  });
 });
 });

+ 1 - 21
kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts

@@ -1,4 +1,4 @@
-import styled, { css } from 'styled-components';
+import styled from 'styled-components';
 
 
 export const Wrapper = styled.div`
 export const Wrapper = styled.div`
   display: flex;
   display: flex;
@@ -8,23 +8,3 @@ export const Wrapper = styled.div`
   height: 100%;
   height: 100%;
   width: 100%;
   width: 100%;
 `;
 `;
-
-export const Spinner = styled.div(
-  ({ theme }) => css`
-    border: 10px solid ${theme.pageLoader.borderColor};
-    border-bottom: 10px solid ${theme.pageLoader.borderBottomColor};
-    border-radius: 50%;
-    width: 80px;
-    height: 80px;
-    animation: spin 1.3s linear infinite;
-
-    @keyframes spin {
-      0% {
-        transform: rotate(0deg);
-      }
-      100% {
-        transform: rotate(360deg);
-      }
-    }
-  `
-);

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

@@ -1,10 +1,11 @@
 import React from 'react';
 import React from 'react';
+import Spinner from 'components/common/Spinner/Spinner';
 
 
 import * as S from './PageLoader.styled';
 import * as S from './PageLoader.styled';
 
 
 const PageLoader: React.FC = () => (
 const PageLoader: React.FC = () => (
   <S.Wrapper>
   <S.Wrapper>
-    <S.Spinner role="progressbar" />
+    <Spinner />
   </S.Wrapper>
   </S.Wrapper>
 );
 );
 
 

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

@@ -45,6 +45,7 @@ const ControlledSelect: React.FC<ControlledSelectProps> = ({
               options={options}
               options={options}
               placeholder={placeholder}
               placeholder={placeholder}
               disabled={disabled}
               disabled={disabled}
+              ref={field.ref}
             />
             />
           );
           );
         }}
         }}

+ 86 - 77
kafka-ui-react-app/src/components/common/Select/Select.tsx

@@ -27,90 +27,99 @@ export interface SelectOption {
   isLive?: boolean;
   isLive?: boolean;
 }
 }
 
 
-const Select: React.FC<SelectProps> = ({
-  options = [],
-  value,
-  defaultValue,
-  selectSize = 'L',
-  placeholder = '',
-  isLive,
-  disabled = false,
-  onChange,
-  isThemeMode,
-  ...props
-}) => {
-  const [selectedOption, setSelectedOption] = useState(value);
-  const [showOptions, setShowOptions] = useState(false);
+const Select = React.forwardRef<HTMLUListElement, SelectProps>(
+  (
+    {
+      options = [],
+      value,
+      defaultValue,
+      selectSize = 'L',
+      placeholder = '',
+      isLive,
+      disabled = false,
+      onChange,
+      isThemeMode,
+      ...props
+    },
+    ref
+  ) => {
+    const [selectedOption, setSelectedOption] = useState(value);
+    const [showOptions, setShowOptions] = useState(false);
 
 
-  const showOptionsHandler = () => {
-    if (!disabled) setShowOptions(!showOptions);
-  };
+    const showOptionsHandler = () => {
+      if (!disabled) setShowOptions(!showOptions);
+    };
 
 
-  const selectContainerRef = useRef(null);
-  const clickOutsideHandler = () => setShowOptions(false);
-  useClickOutside(selectContainerRef, clickOutsideHandler);
+    const selectContainerRef = useRef(null);
+    const clickOutsideHandler = () => setShowOptions(false);
+    useClickOutside(selectContainerRef, clickOutsideHandler);
 
 
-  const updateSelectedOption = (option: SelectOption) => {
-    if (!option.disabled) {
-      setSelectedOption(option.value);
+    const updateSelectedOption = (option: SelectOption) => {
+      if (!option.disabled) {
+        setSelectedOption(option.value);
 
 
-      if (onChange) {
-        onChange(option.value);
+        if (onChange) {
+          onChange(option.value);
+        }
+
+        setShowOptions(false);
       }
       }
+    };
 
 
-      setShowOptions(false);
-    }
-  };
+    React.useEffect(() => {
+      setSelectedOption(value);
+    }, [isLive, value]);
 
 
-  React.useEffect(() => {
-    setSelectedOption(value);
-  }, [isLive, value]);
+    return (
+      <div ref={selectContainerRef}>
+        <S.Select
+          role="listbox"
+          selectSize={selectSize}
+          isLive={isLive}
+          disabled={disabled}
+          onClick={showOptionsHandler}
+          onKeyDown={showOptionsHandler}
+          isThemeMode={isThemeMode}
+          ref={ref}
+          tabIndex={0}
+          {...props}
+        >
+          <S.SelectedOptionWrapper>
+            {isLive && <LiveIcon />}
+            <S.SelectedOption
+              role="option"
+              tabIndex={0}
+              isThemeMode={isThemeMode}
+            >
+              {options.find(
+                (option) => option.value === (defaultValue || selectedOption)
+              )?.label || placeholder}
+            </S.SelectedOption>
+          </S.SelectedOptionWrapper>
+          {showOptions && (
+            <S.OptionList>
+              {options?.map((option) => (
+                <S.Option
+                  value={option.value}
+                  key={option.value}
+                  disabled={option.disabled}
+                  onClick={() => updateSelectedOption(option)}
+                  tabIndex={0}
+                  role="option"
+                >
+                  {option.isLive && <LiveIcon />}
+                  {option.label}
+                </S.Option>
+              ))}
+            </S.OptionList>
+          )}
+          <DropdownArrowIcon isOpen={showOptions} />
+        </S.Select>
+      </div>
+    );
+  }
+);
 
 
-  return (
-    <div ref={selectContainerRef}>
-      <S.Select
-        role="listbox"
-        selectSize={selectSize}
-        isLive={isLive}
-        disabled={disabled}
-        onClick={showOptionsHandler}
-        onKeyDown={showOptionsHandler}
-        isThemeMode={isThemeMode}
-        {...props}
-      >
-        <S.SelectedOptionWrapper>
-          {isLive && <LiveIcon />}
-          <S.SelectedOption
-            role="option"
-            tabIndex={0}
-            isThemeMode={isThemeMode}
-          >
-            {options.find(
-              (option) => option.value === (defaultValue || selectedOption)
-            )?.label || placeholder}
-          </S.SelectedOption>
-        </S.SelectedOptionWrapper>
-        {showOptions && (
-          <S.OptionList>
-            {options?.map((option) => (
-              <S.Option
-                value={option.value}
-                key={option.value}
-                disabled={option.disabled}
-                onClick={() => updateSelectedOption(option)}
-                tabIndex={0}
-                role="option"
-              >
-                {option.isLive && <LiveIcon />}
-                {option.label}
-              </S.Option>
-            ))}
-          </S.OptionList>
-        )}
-        <DropdownArrowIcon isOpen={showOptions} />
-      </S.Select>
-    </div>
-  );
-};
+Select.displayName = 'Select';
 
 
 export default Select;
 export default Select;

+ 26 - 0
kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts

@@ -0,0 +1,26 @@
+import styled from 'styled-components';
+import { SpinnerProps } from 'components/common/Spinner/types';
+
+export const Spinner = styled.div<SpinnerProps>`
+  border-width: ${(props) => props.borderWidth}px;
+  border-style: solid;
+  border-color: ${({ theme }) => theme.pageLoader.borderColor};
+  border-bottom-color: ${(props) =>
+    props.emptyBorderColor
+      ? 'transparent'
+      : props.theme.pageLoader.borderBottomColor};
+  border-radius: 50%;
+  width: ${(props) => props.size}px;
+  height: ${(props) => props.size}px;
+  margin-left: ${(props) => props.marginLeft}px;
+  animation: spin 1.3s linear infinite;
+
+  @keyframes spin {
+    0% {
+      transform: rotate(0deg);
+    }
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+`;

+ 20 - 0
kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx

@@ -0,0 +1,20 @@
+/* eslint-disable react/default-props-match-prop-types */
+import React from 'react';
+import { SpinnerProps } from 'components/common/Spinner/types';
+
+import * as S from './Spinner.styled';
+
+const defaultProps: SpinnerProps = {
+  size: 80,
+  borderWidth: 10,
+  emptyBorderColor: false,
+  marginLeft: 0,
+};
+
+const Spinner: React.FC<SpinnerProps> = (props) => (
+  <S.Spinner role="progressbar" {...props} />
+);
+
+Spinner.defaultProps = defaultProps;
+
+export default Spinner;

+ 6 - 0
kafka-ui-react-app/src/components/common/Spinner/types.ts

@@ -0,0 +1,6 @@
+export interface SpinnerProps {
+  size?: number;
+  borderWidth?: number;
+  emptyBorderColor?: boolean;
+  marginLeft?: number;
+}

+ 2 - 1
kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx

@@ -75,7 +75,7 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
   const onReset = () => methods.reset();
   const onReset = () => methods.reset();
 
 
   const onValidate = async () => {
   const onValidate = async () => {
-    await trigger();
+    await trigger(undefined, { shouldFocus: true });
     if (!methods.formState.isValid) return;
     if (!methods.formState.isValid) return;
     disableForm();
     disableForm();
     const data = methods.getValues();
     const data = methods.getValues();
@@ -142,6 +142,7 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
               buttonSize="L"
               buttonSize="L"
               buttonType="primary"
               buttonType="primary"
               disabled={isSubmitDisabled}
               disabled={isSubmitDisabled}
+              inProgress={isSubmitting}
             >
             >
               Submit
               Submit
             </Button>
             </Button>