Repository: provectus/kafka-ui
Issue: Live tailing improvements
Labels: type/enhancement
good first issue
scope/frontend
status/accepted

1. New messages should appear on top
2. Pagination buttons (e.g. "Next") shouldn't be visible.
3.
wrong parameters passed for live tailing: 
`https://www.kafka-ui.provectus.io/api/clusters/local/topics/7890/messages?filterQueryType=STRING_CONTAINS&attempt=1&limit=100&seekDirection=TAILING&seekType=OFFSET&seekTo=0::0`

this should be `https://www.kafka-ui.provectus.io/api/clusters/local/topics/7890/messages&seekDirection=TAILING&seekType=LATEST&seekTo=0::0`

`seekType` should be `LATEST`
`attempt` has no effect (can be removed from all endpoints)
`limit` has no effect in TAILING mode
(not critical) `filterQueryType` make sense only when `q` param passed
`seekTo` should be 0::0,1::0,... where first number is partition

#943

Issue comments:
1. While live mode is on (active loading) all the controls (search, offset, partitions select) should be disabled. 
2. Once "stop loading" is pressed, fetched messages should NOT disappear. And controls (p. 1) should be enabled back again.

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
index d753f33cfb5..57e7fc23b2b 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
@@ -129,6 +129,8 @@ const Filters: React.FC<FiltersProps> = ({
       : MessageFilterType.STRING_CONTAINS
   );
   const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');
+  const [isTailing, setIsTailing] = React.useState<boolean>(isLive);
+
   const isSeekTypeControlVisible = React.useMemo(
     () => selectedPartitions.length > 0,
     [selectedPartitions]
@@ -136,11 +138,13 @@ const Filters: React.FC<FiltersProps> = ({
 
   const isSubmitDisabled = React.useMemo(() => {
     if (isSeekTypeControlVisible) {
-      return currentSeekType === SeekType.TIMESTAMP && !timestamp;
+      return (
+        (currentSeekType === SeekType.TIMESTAMP && !timestamp) || isTailing
+      );
     }
 
     return false;
-  }, [isSeekTypeControlVisible, currentSeekType, timestamp]);
+  }, [isSeekTypeControlVisible, currentSeekType, timestamp, isTailing]);
 
   const partitionMap = React.useMemo(
     () =>
@@ -345,6 +349,10 @@ const Filters: React.FC<FiltersProps> = ({
     handleFiltersSubmit(offset);
   }, [handleFiltersSubmit, seekDirection]);
 
+  React.useEffect(() => {
+    setIsTailing(isLive);
+  }, [isLive]);
+
   return (
     <S.FiltersWrapper>
       <div>
@@ -352,6 +360,7 @@ const Filters: React.FC<FiltersProps> = ({
           <Search
             placeholder="Search"
             value={query}
+            disabled={isTailing}
             handleSearch={(value: string) => setQuery(value)}
           />
           <S.SeekTypeSelectorWrapper>
@@ -362,7 +371,7 @@ const Filters: React.FC<FiltersProps> = ({
               selectSize="M"
               minWidth="100px"
               options={SeekTypeOptions}
-              disabled={isLive}
+              disabled={isTailing}
             />
             {currentSeekType === SeekType.OFFSET ? (
               <Input
@@ -373,7 +382,7 @@ const Filters: React.FC<FiltersProps> = ({
                 className="offset-selector"
                 placeholder="Offset"
                 onChange={({ target: { value } }) => setOffset(value)}
-                disabled={isLive}
+                disabled={isTailing}
               />
             ) : (
               <DatePicker
@@ -384,7 +393,7 @@ const Filters: React.FC<FiltersProps> = ({
                 dateFormat="MMMM d, yyyy HH:mm"
                 className="date-picker"
                 placeholderText="Select timestamp"
-                disabled={isLive}
+                disabled={isTailing}
               />
             )}
           </S.SeekTypeSelectorWrapper>
@@ -397,6 +406,7 @@ const Filters: React.FC<FiltersProps> = ({
             value={selectedPartitions}
             onChange={setSelectedPartitions}
             labelledBy="Select partitions"
+            disabled={isTailing}
           />
           <S.ClearAll onClick={handleClearAllFilters}>Clear all</S.ClearAll>
           {isFetching ? (
@@ -465,13 +475,13 @@ const Filters: React.FC<FiltersProps> = ({
             isFetching &&
             phaseMessage}
         </p>
-        <S.MessageLoading isLive={isLive}>
+        <S.MessageLoading isLive={isTailing}>
           <S.MessageLoadingSpinner isFetching={isFetching} />
           Loading messages.
           <S.StopLoading
             onClick={() => {
-              changeSeekDirection(SeekDirection.FORWARD);
-              setIsFetching(false);
+              handleSSECancel();
+              setIsTailing(false);
             }}
           >
             Stop loading
diff --git a/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts b/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts
index ea596585b0a..f4767bd76df 100644
--- a/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts
+++ b/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts
@@ -9,8 +9,14 @@ const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>`
   & > .dropdown-container {
     height: 32px;
 
+    * {
+      cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
+    }
+
     & > .dropdown-heading {
       height: 32px;
+      color: ${({ disabled, theme }) =>
+        disabled ? theme.select.color.disabled : theme.select.color.active};
     }
   }
 `;
diff --git a/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx b/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx
new file mode 100644
index 00000000000..16b30e042d3
--- /dev/null
+++ b/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { render } from 'lib/testHelpers';
+import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
+import { ISelectProps } from 'react-multi-select-component/dist/lib/interfaces';
+
+const Option1 = { value: 1, label: 'option 1' };
+const Option2 = { value: 2, label: 'option 2' };
+
+interface IMultiSelectProps extends ISelectProps {
+  minWidth?: string;
+}
+
+const DefaultProps: IMultiSelectProps = {
+  options: [Option1, Option2],
+  labelledBy: 'multi-select',
+  value: [Option1, Option2],
+};
+
+describe('MultiSelect.Styled', () => {
+  const setUpComponent = (props: IMultiSelectProps = DefaultProps) => {
+    const { container } = render(<MultiSelect {...props} />);
+    const multiSelect = container.firstChild;
+    const dropdownContainer = multiSelect?.firstChild?.firstChild;
+
+    return { container, multiSelect, dropdownContainer };
+  };
+
+  it('should have 200px minWidth by default', () => {
+    const { container } = setUpComponent();
+    const multiSelect = container.firstChild;
+
+    expect(multiSelect).toHaveStyle('min-width: 200px');
+  });
+
+  it('should have the provided minWidth in styles', () => {
+    const minWidth = '400px';
+    const { container } = setUpComponent({ ...DefaultProps, minWidth });
+    const multiSelect = container.firstChild;
+
+    expect(multiSelect).toHaveStyle(`min-width: ${minWidth}`);
+  });
+
+  describe('when not disabled', () => {
+    it('should have cursor pointer', () => {
+      const { dropdownContainer } = setUpComponent();
+
+      expect(dropdownContainer).toHaveStyle(`cursor: pointer`);
+    });
+  });
+
+  describe('when disabled', () => {
+    it('should have cursor not-allowed', () => {
+      const { dropdownContainer } = setUpComponent({
+        ...DefaultProps,
+        disabled: true,
+      });
+
+      expect(dropdownContainer).toHaveStyle(`cursor: not-allowed`);
+    });
+  });
+});
diff --git a/kafka-ui-react-app/src/components/common/Search/Search.tsx b/kafka-ui-react-app/src/components/common/Search/Search.tsx
index 1369ea9f41c..81f513950d8 100644
--- a/kafka-ui-react-app/src/components/common/Search/Search.tsx
+++ b/kafka-ui-react-app/src/components/common/Search/Search.tsx
@@ -6,12 +6,14 @@ interface SearchProps {
   handleSearch: (value: string) => void;
   placeholder?: string;
   value: string;
+  disabled?: boolean;
 }
 
 const Search: React.FC<SearchProps> = ({
   handleSearch,
   placeholder = 'Search',
   value,
+  disabled = false,
 }) => {
   const onChange = useDebouncedCallback(
     (e) => handleSearch(e.target.value),
@@ -26,6 +28,7 @@ const Search: React.FC<SearchProps> = ({
       defaultValue={value}
       leftIcon="fas fa-search"
       inputSize="M"
+      disabled={disabled}
     />
   );
 };


Test patch: