Procházet zdrojové kódy

Smart filters improvements (#1814)

* Add Filters add button and remove the Add Filter Icon

* Generalize DeleteFilterModal and write tests suites + add custom addModal hook

* add react-testing-hook-lib + add tests to useModal hook

* Add parameter to ConfirmationModal + remove delete Modal add generic modal in add filter page

* implementing the modal hook on add Filter

* Finalize the Smart Filters functionality

* Styling code modifications

* Styling code modifications

* Filters styling code modifcations

* minor modifications in the tests suites

* minor tests suites description modifications

* minor tests suites code modifications

* Adding unNamed Filter selection option + tests suites

* Fix typo

Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com>

* Adding tests Wuites to AddEditFilterContainer and to addFilter

* Add Filters add button and remove the Add Filter Icon

* Generalize DeleteFilterModal and write tests suites + add custom addModal hook

* add react-testing-hook-lib + add tests to useModal hook

* Add parameter to ConfirmationModal + remove delete Modal add generic modal in add filter page

* implementing the modal hook on add Filter

* Finalize the Smart Filters functionality

* Styling code modifications

* Styling code modifications

* Filters styling code modifcations

* minor modifications in the tests suites

* minor tests suites description modifications

* minor tests suites code modifications

* Adding unNamed Filter selection option + tests suites

* Fix typo

Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com>

* Adding tests Wuites to AddEditFilterContainer and to addFilter

* q parameter modifications in the SmartFilters

* Add popup close functionality after applying the filters

Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Mgrdich před 3 roky
rodič
revize
6eb6bb1d7e
20 změnil soubory, kde provedl 828 přidání a 473 odebrání
  1. 22 0
      kafka-ui-react-app/package-lock.json
  2. 1 0
      kafka-ui-react-app/package.json
  3. 76 90
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx
  4. 43 123
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddFilter.tsx
  5. 11 8
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx
  6. 51 49
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
  7. 7 6
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
  8. 109 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx
  9. 21 59
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddEditFilterContainer.spec.tsx
  10. 121 114
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddFilter.spec.tsx
  11. 6 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/EditFilter.spec.tsx
  12. 7 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/FilterModal.spec.tsx
  13. 50 16
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx
  14. 157 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx
  15. 5 3
      kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx
  16. 13 1
      kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx
  17. 29 0
      kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx
  18. 66 0
      kafka-ui-react-app/src/lib/hooks/__tests__/useModal.spec.ts
  19. 32 0
      kafka-ui-react-app/src/lib/hooks/useModal.ts
  20. 1 0
      kafka-ui-react-app/src/theme/theme.ts

+ 22 - 0
kafka-ui-react-app/package-lock.json

@@ -4869,6 +4869,19 @@
         "@testing-library/dom": "^8.0.0"
       }
     },
+    "@testing-library/react-hooks": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz",
+      "integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "@types/react": ">=16.9.0",
+        "@types/react-dom": ">=16.9.0",
+        "@types/react-test-renderer": ">=16.9.0",
+        "react-error-boundary": "^3.1.0"
+      }
+    },
     "@testing-library/user-event": {
       "version": "13.5.0",
       "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
@@ -17194,6 +17207,15 @@
         "scheduler": "^0.20.2"
       }
     },
+    "react-error-boundary": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+      "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.12.5"
+      }
+    },
     "react-error-overlay": {
       "version": "6.0.10",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",

+ 1 - 0
kafka-ui-react-app/package.json

@@ -82,6 +82,7 @@
     "@openapitools/openapi-generator-cli": "^2.4.15",
     "@testing-library/dom": "^8.11.1",
     "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/react-hooks": "^7.0.2",
     "@testing-library/user-event": "^13.5.0",
     "@types/classnames": "^2.2.11",
     "@types/enzyme": "^3.10.9",

+ 76 - 90
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx

@@ -7,41 +7,38 @@ import { FormProvider, Controller, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { Button } from 'components/common/Button/Button';
 import { FormError } from 'components/common/Input/Input.styled';
-import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
+import { AddMessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/AddFilter';
 import { yupResolver } from '@hookform/resolvers/yup';
 import yup from 'lib/yupExtended';
 
 const validationSchema = yup.object().shape({
-  name: yup.string().required(),
+  saveFilter: yup.boolean(),
   code: yup.string().required(),
+  name: yup.string().when('saveFilter', {
+    is: (value: boolean | undefined) => typeof value === 'undefined' || value,
+    then: (schema) => schema.required(),
+    otherwise: (schema) => schema.notRequired(),
+  }),
 });
 
 export interface AddEditFilterContainerProps {
-  title: string;
   cancelBtnHandler: () => void;
   submitBtnText: string;
   inputDisplayNameDefaultValue?: string;
   inputCodeDefaultValue?: string;
-  toggleSaveFilterValue?: boolean;
-  toggleSaveFilterSetter?: () => void;
-  createNewFilterText?: string;
-  submitCallback?: (values: MessageFilters) => void;
-  submitCallbackWithReset?: boolean;
+  isAdd?: boolean;
+  submitCallback?: (values: AddMessageFilters) => void;
 }
 
 const AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({
-  title,
   cancelBtnHandler,
   submitBtnText,
   inputDisplayNameDefaultValue = '',
   inputCodeDefaultValue = '',
-  toggleSaveFilterValue,
-  toggleSaveFilterSetter,
-  createNewFilterText,
   submitCallback,
-  submitCallbackWithReset,
+  isAdd,
 }) => {
-  const methods = useForm<MessageFilters>({
+  const methods = useForm<AddMessageFilters>({
     mode: 'onChange',
     resolver: yupResolver(validationSchema),
   });
@@ -53,88 +50,77 @@ const AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({
   } = methods;
 
   const onSubmit = React.useCallback(
-    (values: MessageFilters) => {
+    (values: AddMessageFilters) => {
       submitCallback?.(values);
-      if (submitCallbackWithReset) {
-        reset({ name: '', code: '' });
-      }
+      reset({ name: '', code: '', saveFilter: false });
     },
-    [reset, submitCallback, submitCallbackWithReset]
+    [isAdd, reset, submitCallback]
   );
 
   return (
-    <>
-      <S.FilterTitle>{title}</S.FilterTitle>
-      <FormProvider {...methods}>
-        {createNewFilterText && (
-          <S.CreatedFilter>{createNewFilterText}</S.CreatedFilter>
-        )}
-        <form
-          onSubmit={handleSubmit(onSubmit)}
-          aria-label="Filters submit Form"
-        >
-          <div>
-            <InputLabel>Display name</InputLabel>
-            <Input
-              inputSize="M"
-              placeholder="Enter Name"
-              autoComplete="off"
-              name="name"
-              defaultValue={inputDisplayNameDefaultValue}
-            />
-          </div>
-          <div>
-            <FormError>
-              <ErrorMessage errors={errors} name="name" />
-            </FormError>
-          </div>
-          <div>
-            <InputLabel>Filter code</InputLabel>
-            <Controller
-              control={control}
-              name="code"
-              defaultValue={inputCodeDefaultValue}
-              render={({ field: { onChange, ref } }) => (
-                <Textarea ref={ref} onChange={onChange} />
-              )}
+    <FormProvider {...methods}>
+      <form onSubmit={handleSubmit(onSubmit)} aria-label="Filters submit Form">
+        <div>
+          <InputLabel>Filter code</InputLabel>
+          <Controller
+            control={control}
+            name="code"
+            defaultValue={inputCodeDefaultValue}
+            render={({ field: { onChange, ref } }) => (
+              <Textarea ref={ref} onChange={onChange} />
+            )}
+          />
+        </div>
+        <div>
+          <FormError>
+            <ErrorMessage errors={errors} name="code" />
+          </FormError>
+        </div>
+        {isAdd && (
+          <S.CheckboxWrapper>
+            <input
+              {...methods.register('saveFilter')}
+              name="saveFilter"
+              type="checkbox"
             />
-          </div>
-          <div>
-            <FormError>
-              <ErrorMessage errors={errors} name="code" />
-            </FormError>
-          </div>
-          {!!toggleSaveFilterSetter && (
-            <S.CheckboxWrapper>
-              <input
-                type="checkbox"
-                checked={toggleSaveFilterValue}
-                onChange={toggleSaveFilterSetter}
-              />
-              <InputLabel>Save this filter</InputLabel>
-            </S.CheckboxWrapper>
-          )}
-          <S.FilterButtonWrapper>
-            <Button
-              buttonSize="M"
-              buttonType="secondary"
-              type="button"
-              onClick={cancelBtnHandler}
-            >
-              Cancel
-            </Button>
-            <Button
-              buttonSize="M"
-              buttonType="primary"
-              type="submit"
-              disabled={!isValid || isSubmitting || !isDirty}
-            >
-              {submitBtnText}
-            </Button>
-          </S.FilterButtonWrapper>
-        </form>
-      </FormProvider>
-    </>
+            <InputLabel>Save this filter</InputLabel>
+          </S.CheckboxWrapper>
+        )}
+        <div>
+          <InputLabel>Display name</InputLabel>
+          <Input
+            inputSize="M"
+            placeholder="Enter Name"
+            autoComplete="off"
+            name="name"
+            defaultValue={inputDisplayNameDefaultValue}
+          />
+        </div>
+        <div>
+          <FormError>
+            <ErrorMessage errors={errors} name="name" />
+          </FormError>
+        </div>
+        <S.FilterButtonWrapper>
+          <Button
+            buttonSize="M"
+            buttonType="secondary"
+            type="button"
+            onClick={cancelBtnHandler}
+          >
+            Cancel
+          </Button>
+          <Button
+            buttonSize="M"
+            buttonType="primary"
+            type="submit"
+            disabled={!isValid || isSubmitting || !isDirty}
+          >
+            {submitBtnText}
+          </Button>
+        </S.FilterButtonWrapper>
+      </form>
+    </FormProvider>
   );
 };
 

+ 43 - 123
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddFilter.tsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled';
-import { Button } from 'components/common/Button/Button';
 import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
 import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
+import SavedFilters from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
+import SavedIcon from 'components/common/Icons/SavedIcon';
 
 import AddEditFilterContainer from './AddEditFilterContainer';
 
@@ -16,6 +17,10 @@ export interface FilterModalProps {
   editFilter(value: FilterEdit): void;
 }
 
+export interface AddMessageFilters extends MessageFilters {
+  saveFilter: boolean;
+}
+
 const AddFilter: React.FC<FilterModalProps> = ({
   toggleIsOpen,
   filters,
@@ -25,139 +30,54 @@ const AddFilter: React.FC<FilterModalProps> = ({
   toggleEditModal,
   editFilter,
 }) => {
-  const [addNewFilter, setAddNewFilter] = React.useState(false);
-  const [toggleSaveFilter, setToggleSaveFilter] = React.useState(false);
-  const [selectedFilter, setSelectedFilter] = React.useState(-1);
-  const [toggleDeletionModal, setToggleDeletionModal] =
+  const [savedFilterState, setSavedFilterState] =
     React.useState<boolean>(false);
-  const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
-
-  const deleteFilterHandler = (index: number) => {
-    setToggleDeletionModal(!toggleDeletionModal);
-    setDeleteIndex(index);
-  };
-  const activeFilter = () => {
-    if (selectedFilter > -1) {
-      activeFilterHandler(filters[selectedFilter], selectedFilter);
-      toggleIsOpen();
-    }
-  };
 
   const onSubmit = React.useCallback(
-    async (values: MessageFilters) => {
-      if (!toggleSaveFilter) {
-        activeFilterHandler(values, -1);
+    async (values: AddMessageFilters) => {
+      const data = { ...values };
+      if (data.saveFilter) {
+        addFilter(data);
       } else {
-        addFilter(values);
+        // other case is not applying the filter
+        data.name = data.name ? data.name : 'Unsaved filter';
+        activeFilterHandler(data, -1);
+        toggleIsOpen();
       }
-      setAddNewFilter(!addNewFilter);
     },
-    [addNewFilter, toggleSaveFilter, activeFilterHandler, addFilter]
+    [activeFilterHandler, addFilter, toggleIsOpen]
   );
-  return !addNewFilter ? (
+  return (
     <>
       <S.FilterTitle>Add filter</S.FilterTitle>
-      <S.NewFilterIcon onClick={() => setAddNewFilter(!addNewFilter)}>
-        <i className="fas fa-plus fa-sm" /> New filter
-      </S.NewFilterIcon>
-      <S.CreatedFilter>Created filters</S.CreatedFilter>
-      {toggleDeletionModal && (
-        <S.ConfirmDeletionModal>
-          <S.ConfirmDeletionModalHeader>
-            <S.ConfirmDeletionTitle>Confirm deletion</S.ConfirmDeletionTitle>
-            <S.CloseDeletionModalIcon
-              data-testid="closeDeletionModalIcon"
-              onClick={() => setToggleDeletionModal(!toggleDeletionModal)}
-            >
-              <i className="fas fa-times-circle" />
-            </S.CloseDeletionModalIcon>
-          </S.ConfirmDeletionModalHeader>
-          <S.ConfirmDeletionText>
-            Are you sure want to remove {filters[deleteIndex].name}?
-          </S.ConfirmDeletionText>
-          <S.FilterButtonWrapper>
-            <Button
-              buttonSize="M"
-              buttonType="secondary"
-              type="button"
-              onClick={() => setToggleDeletionModal(!toggleDeletionModal)}
-            >
-              Cancel
-            </Button>
-            <Button
-              buttonSize="M"
-              buttonType="primary"
-              type="button"
-              onClick={() => {
-                deleteFilter(deleteIndex);
-                setToggleDeletionModal(!toggleDeletionModal);
-              }}
-            >
-              Delete
-            </Button>
-          </S.FilterButtonWrapper>
-        </S.ConfirmDeletionModal>
-      )}
-      <S.SavedFiltersContainer>
-        {filters.length === 0 && <p>no saved filter(s)</p>}
-        {filters.map((filter, index) => (
-          <S.SavedFilter
-            key={Symbol(filter.name).toString()}
-            selected={selectedFilter === index}
-            onClick={() => setSelectedFilter(index)}
+      {savedFilterState ? (
+        <SavedFilters
+          deleteFilter={deleteFilter}
+          activeFilterHandler={activeFilterHandler}
+          closeModal={toggleIsOpen}
+          onGoBack={() => setSavedFilterState(false)}
+          filters={filters}
+          onEdit={(index: number, filter: MessageFilters) => {
+            toggleEditModal();
+            editFilter({ index, filter });
+          }}
+        />
+      ) : (
+        <>
+          <S.SavedFiltersTextContainer
+            onClick={() => setSavedFilterState(true)}
           >
-            <S.SavedFilterName>{filter.name}</S.SavedFilterName>
-            <S.FilterOptions>
-              <S.FilterEdit
-                onClick={() => {
-                  toggleEditModal();
-                  editFilter({ index, filter });
-                }}
-              >
-                Edit
-              </S.FilterEdit>
-              <S.DeleteSavedFilter
-                data-testid="deleteIcon"
-                onClick={() => deleteFilterHandler(index)}
-              >
-                <i className="fas fa-times" />
-              </S.DeleteSavedFilter>
-            </S.FilterOptions>
-          </S.SavedFilter>
-        ))}
-      </S.SavedFiltersContainer>
-      <S.FilterButtonWrapper>
-        <Button
-          buttonSize="M"
-          buttonType="secondary"
-          type="button"
-          onClick={toggleIsOpen}
-          disabled={toggleDeletionModal}
-        >
-          Cancel
-        </Button>
-        <Button
-          buttonSize="M"
-          buttonType="primary"
-          type="button"
-          onClick={activeFilter}
-          disabled={toggleDeletionModal}
-        >
-          Select filter
-        </Button>
-      </S.FilterButtonWrapper>
+            <SavedIcon /> <S.SavedFiltersText>Saved Filters</S.SavedFiltersText>
+          </S.SavedFiltersTextContainer>
+          <AddEditFilterContainer
+            cancelBtnHandler={toggleIsOpen}
+            submitBtnText="Add filter"
+            submitCallback={onSubmit}
+            isAdd
+          />
+        </>
+      )}
     </>
-  ) : (
-    <AddEditFilterContainer
-      title="Add filter"
-      cancelBtnHandler={() => setAddNewFilter(!addNewFilter)}
-      submitBtnText="Add filter"
-      submitCallback={onSubmit}
-      submitCallbackWithReset
-      createNewFilterText="Create a new filter"
-      toggleSaveFilterValue={toggleSaveFilter}
-      toggleSaveFilterSetter={() => setToggleSaveFilter(!toggleSaveFilter)}
-    />
   );
 };
 

+ 11 - 8
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx

@@ -3,6 +3,7 @@ import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters
 import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
 
 import AddEditFilterContainer from './AddEditFilterContainer';
+import * as S from './Filters.styled';
 
 export interface EditFilterProps {
   editFilter: FilterEdit;
@@ -23,14 +24,16 @@ const EditFilter: React.FC<EditFilterProps> = ({
     [editSavedFilter, editFilter.index, toggleEditModal]
   );
   return (
-    <AddEditFilterContainer
-      title="Edit saved filter"
-      cancelBtnHandler={() => toggleEditModal()}
-      submitBtnText="Save"
-      inputDisplayNameDefaultValue={editFilter.filter.name}
-      inputCodeDefaultValue={editFilter.filter.code}
-      submitCallback={onSubmit}
-    />
+    <>
+      <S.FilterTitle>Edit saved filter</S.FilterTitle>
+      <AddEditFilterContainer
+        cancelBtnHandler={() => toggleEditModal()}
+        submitBtnText="Save"
+        inputDisplayNameDefaultValue={editFilter.filter.name}
+        inputCodeDefaultValue={editFilter.filter.code}
+        submitCallback={onSubmit}
+      />
+    </>
   );
 };
 

+ 51 - 49
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts

@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
 
 interface SavedFilterProps {
   selected: boolean;
@@ -99,7 +99,6 @@ export const ClearAll = styled.span`
   color: ${({ theme }) => theme.metrics.filters.color.normal};
   font-size: 12px;
   cursor: pointer;
-  font-family: Inter;
 `;
 
 export const MessageFilterModal = styled.div`
@@ -117,16 +116,19 @@ export const MessageFilterModal = styled.div`
 
 export const FilterTitle = styled.h3`
   line-height: 32px;
-  font-family: Inter;
   font-size: 20px;
   margin-bottom: 40px;
-`;
-
-export const NewFilterIcon = styled.div`
-  color: ${({ theme }) => theme.icons.newFilterIcon};
-  padding-right: 6px;
-  height: 12px;
-  cursor: pointer;
+  position: relative;
+  &:after {
+    content: '';
+    width: calc(100% + 32px);
+    height: 1px;
+    position: absolute;
+    top: 40px;
+    left: -16px;
+    display: inline-block;
+    background-color: #f1f2f3;
+  }
 `;
 
 export const CreatedFilter = styled.p`
@@ -154,15 +156,20 @@ export const SavedFilterName = styled.div`
 export const FilterButtonWrapper = styled.div`
   display: flex;
   justify-content: flex-end;
-  margin-top: 25px;
+  margin-top: 10px;
   gap: 10px;
-`;
-
-export const AddFiltersIcon = styled.div`
-  color: ${({ theme }) => theme.metrics.filters.color.icon};
-  padding-right: 6px;
-  height: 20px;
-  cursor: pointer;
+  padding-top: 16px;
+  position: relative;
+  &:before {
+    content: '';
+    width: calc(100% + 32px);
+    height: 1px;
+    position: absolute;
+    top: 0;
+    left: -16px;
+    display: inline-block;
+    background-color: #f1f2f3;
+  }
 `;
 
 export const ActiveSmartFilterWrapper = styled.div`
@@ -200,6 +207,7 @@ export const SavedFilter = styled.div.attrs({
   height: 32px;
   align-items: center;
   cursor: pointer;
+  border-top: 1px solid #f1f2f3;
   &:hover ${FilterOptions} {
     display: flex;
   }
@@ -240,37 +248,6 @@ export const DeleteSavedFilterIcon = styled.div`
   cursor: pointer;
 `;
 
-export const ConfirmDeletionModal = styled.div.attrs({ role: 'deletionModal' })`
-  height: auto;
-  width: 348px;
-  border-radius: 8px;
-  background: ${({ theme }) => theme.modal.backgroundColor};
-  position: absolute;
-  left: 20%;
-  border: 1px solid ${({ theme }) => theme.breadcrumb};
-  box-shadow: ${({ theme }) => theme.modal.shadow};
-  padding: 16px;
-  z-index: 2;
-`;
-
-export const ConfirmDeletionModalHeader = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  height: 48px;
-`;
-
-export const ConfirmDeletionTitle = styled.h3`
-  font-size: 20px;
-  line-height: 32px;
-`;
-
-export const CloseDeletionModalIcon = styled.div`
-  color: ${({ theme }) => theme.icons.closeModalIcon};
-  height: 20px;
-  cursor: pointer;
-`;
-
 export const ConfirmDeletionText = styled.h3`
   color: ${({ theme }) => theme.modal.deletionTextColor};
   font-size: 14px;
@@ -310,3 +287,28 @@ export const MessageLoadingSpinner = styled.div<MessageLoadingSpinnerProps>`
     }
   }
 `;
+
+export const SavedFiltersTextContainer = styled.div.attrs({
+  role: 'savedFilterText',
+})`
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  margin-bottom: 15px;
+`;
+
+const textStyle = css`
+  font-size: 14px;
+  color: ${({ theme }) => theme.editFilterText.color};
+  font-weight: 500;
+`;
+
+export const SavedFiltersText = styled.div`
+  ${textStyle};
+  margin-left: 7px;
+`;
+
+export const BackToCustomText = styled.div`
+  ${textStyle};
+  cursor: pointer;
+`;

+ 7 - 6
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx

@@ -27,6 +27,7 @@ import FilterModal, {
 } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
 import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
 import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
+import useModal from 'lib/hooks/useModal';
 
 import * as S from './Filters.styled';
 import {
@@ -89,8 +90,7 @@ const Filters: React.FC<FiltersProps> = ({
   const { searchParams, seekDirection, isLive, changeSeekDirection } =
     useContext(TopicMessagesContext);
 
-  const [isOpen, setIsOpen] = React.useState(false);
-  const toggleIsOpen = () => setIsOpen(!isOpen);
+  const { isOpen, toggle } = useModal();
 
   const source = React.useRef<EventSource | null>(null);
 
@@ -157,7 +157,7 @@ const Filters: React.FC<FiltersProps> = ({
     return {
       q:
         queryType === MessageFilterType.GROOVY_SCRIPT
-          ? `valueAsText.contains('${activeFilter.code}')`
+          ? activeFilter.code
           : query,
       filterQueryType: queryType,
       attempt,
@@ -445,9 +445,10 @@ const Filters: React.FC<FiltersProps> = ({
         />
       </div>
       <S.ActiveSmartFilterWrapper>
-        <S.AddFiltersIcon data-testid="addFilterIcon" onClick={toggleIsOpen}>
+        <Button buttonType="primary" buttonSize="M" onClick={toggle}>
           <i className="fas fa-plus fa-sm" />
-        </S.AddFiltersIcon>
+          Add Filters
+        </Button>
         {activeFilter.name && (
           <S.ActiveSmartFilter data-testid="activeSmartFilter">
             {activeFilter.name}
@@ -462,7 +463,7 @@ const Filters: React.FC<FiltersProps> = ({
       </S.ActiveSmartFilterWrapper>
       {isOpen && (
         <FilterModal
-          toggleIsOpen={toggleIsOpen}
+          toggleIsOpen={toggle}
           filters={savedFilters}
           addFilter={addFilter}
           deleteFilter={deleteFilter}

+ 109 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx

@@ -0,0 +1,109 @@
+import React, { FC } from 'react';
+import { Button } from 'components/common/Button/Button';
+import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
+import useModal from 'lib/hooks/useModal';
+
+import * as S from './Filters.styled';
+import { MessageFilters } from './Filters';
+
+export interface Props {
+  filters: MessageFilters[];
+  onEdit(index: number, filter: MessageFilters): void;
+  deleteFilter(index: number): void;
+  activeFilterHandler(activeFilter: MessageFilters, index: number): void;
+  closeModal(): void;
+  onGoBack(): void;
+}
+
+const SavedFilters: FC<Props> = ({
+  filters,
+  onEdit,
+  deleteFilter,
+  activeFilterHandler,
+  closeModal,
+  onGoBack,
+}) => {
+  const { isOpen, setOpen, setClose } = useModal();
+  const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
+  const [selectedFilter, setSelectedFilter] = React.useState(-1);
+
+  const activeFilter = () => {
+    if (selectedFilter > -1) {
+      activeFilterHandler(filters[selectedFilter], selectedFilter);
+    }
+    closeModal();
+  };
+
+  const deleteFilterHandler = (index: number) => {
+    setOpen();
+    setDeleteIndex(index);
+  };
+
+  return (
+    <>
+      <ConfirmationModal
+        isOpen={isOpen}
+        title="Confirm deletion"
+        onConfirm={() => {
+          deleteFilter(deleteIndex);
+          setClose();
+        }}
+        onCancel={setClose}
+        submitBtnText="Delete"
+      >
+        <S.ConfirmDeletionText>
+          Are you sure want to remove {filters[deleteIndex]?.name}?
+        </S.ConfirmDeletionText>
+      </ConfirmationModal>
+      <S.BackToCustomText onClick={onGoBack}>
+        Back To custom filters
+      </S.BackToCustomText>
+      <S.SavedFiltersContainer>
+        <S.CreatedFilter>Saved filters</S.CreatedFilter>
+        {filters.length === 0 && <p>No saved filter(s)</p>}
+        {filters.map((filter, index) => (
+          <S.SavedFilter
+            key={Symbol(filter.name).toString()}
+            selected={selectedFilter === index}
+            onClick={() => setSelectedFilter(index)}
+          >
+            <S.SavedFilterName>{filter.name}</S.SavedFilterName>
+            <S.FilterOptions>
+              <S.FilterEdit onClick={() => onEdit(index, filter)}>
+                Edit
+              </S.FilterEdit>
+              <S.DeleteSavedFilter
+                data-testid="deleteIcon"
+                onClick={() => deleteFilterHandler(index)}
+              >
+                <i className="fas fa-times" />
+              </S.DeleteSavedFilter>
+            </S.FilterOptions>
+          </S.SavedFilter>
+        ))}
+      </S.SavedFiltersContainer>
+      <S.FilterButtonWrapper>
+        <Button
+          buttonSize="M"
+          buttonType="secondary"
+          type="button"
+          onClick={closeModal}
+          disabled={isOpen}
+        >
+          Cancel
+        </Button>
+        <Button
+          buttonSize="M"
+          buttonType="primary"
+          type="button"
+          onClick={activeFilter}
+          disabled={isOpen}
+        >
+          Select filter
+        </Button>
+      </S.FilterButtonWrapper>
+    </>
+  );
+};
+
+export default SavedFilters;

+ 21 - 59
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddEditFilterContainer.spec.tsx

@@ -8,9 +8,7 @@ import userEvent from '@testing-library/user-event';
 import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
 
 describe('AddEditFilterContainer component', () => {
-  const defaultTitle = 'Test Title';
   const defaultSubmitBtn = 'Submit Button';
-  const defaultNewFilter = 'Create New Filters';
 
   const mockData: MessageFilters = {
     name: 'mockName',
@@ -18,14 +16,11 @@ describe('AddEditFilterContainer component', () => {
   };
 
   const setupComponent = (props: Partial<AddEditFilterContainerProps> = {}) => {
-    const { title, submitBtnText, createNewFilterText } = props;
+    const { submitBtnText } = props;
     return render(
       <AddEditFilterContainer
-        title={title || defaultTitle}
         cancelBtnHandler={jest.fn()}
         submitBtnText={submitBtnText || defaultSubmitBtn}
-        createNewFilterText={createNewFilterText || defaultNewFilter}
-        toggleSaveFilterSetter={jest.fn()}
         {...props}
       />
     );
@@ -35,14 +30,9 @@ describe('AddEditFilterContainer component', () => {
     beforeEach(() => {
       setupComponent();
     });
-    it('should render the components', () => {
-      expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
-    });
 
-    it('should check the default parameters values', () => {
-      expect(screen.getByText(defaultTitle)).toBeInTheDocument();
+    it('should check the default Button text', () => {
       expect(screen.getByText(defaultSubmitBtn)).toBeInTheDocument();
-      expect(screen.getByText(defaultNewFilter)).toBeInTheDocument();
     });
 
     it('should check whether the submit Button is disabled when the form is pristine and disabled if dirty', async () => {
@@ -51,12 +41,12 @@ describe('AddEditFilterContainer component', () => {
 
       const inputs = screen.getAllByRole('textbox');
 
-      const inputNameElement = inputs[0];
-      userEvent.type(inputNameElement, 'Hello World!');
-
-      const textAreaElement = inputs[1];
+      const textAreaElement = inputs[0];
       userEvent.type(textAreaElement, 'Hello World With TextArea');
 
+      const inputNameElement = inputs[1];
+      userEvent.type(inputNameElement, 'Hello World!');
+
       await waitFor(() => {
         expect(submitButtonElem).toBeEnabled();
       });
@@ -71,12 +61,12 @@ describe('AddEditFilterContainer component', () => {
     it('should view the error message after typing and clearing the input', async () => {
       const inputs = screen.getAllByRole('textbox');
 
-      const inputNameElement = inputs[0];
-      userEvent.type(inputNameElement, 'Hello World!');
-
-      const textAreaElement = inputs[1];
+      const textAreaElement = inputs[0];
       userEvent.type(textAreaElement, 'Hello World With TextArea');
 
+      const inputNameElement = inputs[1];
+      userEvent.type(inputNameElement, 'Hello World!');
+
       userEvent.clear(inputNameElement);
       userEvent.clear(textAreaElement);
 
@@ -96,8 +86,8 @@ describe('AddEditFilterContainer component', () => {
       });
 
       const inputs = screen.getAllByRole('textbox');
-      const inputNameElement = inputs[0];
-      const textAreaElement = inputs[1];
+      const textAreaElement = inputs[0];
+      const inputNameElement = inputs[1];
 
       expect(inputNameElement).toHaveValue(mockData.name);
       expect(textAreaElement).toHaveValue(mockData.code);
@@ -114,19 +104,19 @@ describe('AddEditFilterContainer component', () => {
     });
 
     it('should test whether the submit Callback is being called', async () => {
-      const submitCallback = jest.fn() as (v: MessageFilters) => void;
+      const submitCallback = jest.fn();
       setupComponent({
         submitCallback,
       });
 
       const inputs = screen.getAllByRole('textbox');
 
-      const inputNameElement = inputs[0];
-      userEvent.type(inputNameElement, 'Hello World!');
-
-      const textAreaElement = inputs[1];
+      const textAreaElement = inputs[0];
       userEvent.type(textAreaElement, 'Hello World With TextArea');
 
+      const inputNameElement = inputs[1];
+      userEvent.type(inputNameElement, 'Hello World!');
+
       const submitBtnElement = screen.getByText(defaultSubmitBtn);
 
       await waitFor(() => {
@@ -140,48 +130,20 @@ describe('AddEditFilterContainer component', () => {
       });
     });
 
-    it('should display the checkbox if the props is passed and click stay checking', async () => {
-      const setCheckboxMock = jest.fn();
-      setupComponent({
-        toggleSaveFilterSetter: setCheckboxMock,
-      });
-
-      const checkbox = screen.getByRole('checkbox');
-      expect(checkbox).toBeInTheDocument();
-
-      userEvent.click(checkbox);
-
-      await waitFor(() => {
-        expect(checkbox).toBeChecked();
-      });
-
-      await waitFor(() => {
-        expect(setCheckboxMock).toBeCalled();
-      });
-    });
-
     it('should display the checkbox if the props is passed and initially check state', () => {
-      setupComponent({
-        toggleSaveFilterSetter: jest.fn(),
-        toggleSaveFilterValue: true,
-      });
-
+      setupComponent({ isAdd: true });
       const checkbox = screen.getByRole('checkbox');
       expect(checkbox).toBeInTheDocument();
+      expect(checkbox).not.toBeChecked();
+      userEvent.click(checkbox);
       expect(checkbox).toBeChecked();
     });
 
-    it('should pass and render the view props', () => {
-      const title = 'titleTest';
-      const createNewFilterText = 'createNewFilterTextTest';
+    it('should pass and render the correct button text', () => {
       const submitBtnText = 'submitBtnTextTest';
       setupComponent({
-        title,
-        createNewFilterText,
         submitBtnText,
       });
-      expect(screen.getByText(title)).toBeInTheDocument();
-      expect(screen.getByText(createNewFilterText)).toBeInTheDocument();
       expect(screen.getByText(submitBtnText)).toBeInTheDocument();
     });
   });

+ 121 - 114
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddFilter.spec.tsx

@@ -7,8 +7,12 @@ import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters
 import { screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 
-const filters: MessageFilters[] = [{ name: 'name', code: 'code' }];
-const setupComponent = (props?: Partial<FilterModalProps>) =>
+const filters: MessageFilters[] = [
+  { name: 'name', code: 'code' },
+  { name: 'name2', code: 'code2' },
+];
+
+const setupComponent = (props: Partial<FilterModalProps> = {}) =>
   render(
     <AddFilter
       toggleIsOpen={jest.fn()}
@@ -17,151 +21,103 @@ const setupComponent = (props?: Partial<FilterModalProps>) =>
       activeFilterHandler={jest.fn()}
       toggleEditModal={jest.fn()}
       editFilter={jest.fn()}
-      filters={filters}
+      filters={props.filters || filters}
       {...props}
     />
   );
 
 describe('AddFilter component', () => {
-  it('renders component with filters', () => {
-    setupComponent({ filters });
-    expect(screen.getByRole('savedFilter')).toBeInTheDocument();
-  });
-  it('renders component without filters', () => {
-    setupComponent({ filters: [] });
-    expect(screen.getByText('no saved filter(s)')).toBeInTheDocument();
-  });
-  it('renders add filter modal with saved filters', () => {
+  it('should test click on Saved Filters redirects to Saved components', () => {
     setupComponent();
-    expect(screen.getByText('Created filters')).toBeInTheDocument();
+    userEvent.click(screen.getByRole('savedFilterText'));
+    expect(screen.getByText('Saved filters')).toBeInTheDocument();
+    expect(screen.getAllByRole('savedFilter')).toHaveLength(2);
   });
-  describe('Filter deletion', () => {
-    it('open deletion modal', () => {
-      setupComponent();
-      userEvent.hover(screen.getByRole('savedFilter'));
-      userEvent.click(screen.getByTestId('deleteIcon'));
-      expect(screen.getByRole('deletionModal')).toBeInTheDocument();
-    });
-    it('close deletion modal with button', () => {
-      setupComponent();
-      userEvent.hover(screen.getByRole('savedFilter'));
-      userEvent.click(screen.getByTestId('deleteIcon'));
-      expect(screen.getByRole('deletionModal')).toBeInTheDocument();
-      const cancelButton = screen.getAllByRole('button', { name: /Cancel/i });
-      userEvent.click(cancelButton[0]);
-      expect(screen.getByText('Created filters')).toBeInTheDocument();
-    });
-    it('close deletion modal with close icon', () => {
-      setupComponent();
-      userEvent.hover(screen.getByRole('savedFilter'));
-      userEvent.click(screen.getByTestId('deleteIcon'));
-      expect(screen.getByRole('deletionModal')).toBeInTheDocument();
-      userEvent.click(screen.getByTestId('closeDeletionModalIcon'));
-      expect(screen.getByText('Created filters')).toBeInTheDocument();
-    });
-    it('delete filter', () => {
-      const deleteFilter = jest.fn();
-      setupComponent({ filters, deleteFilter });
-      userEvent.hover(screen.getByRole('savedFilter'));
-      userEvent.click(screen.getByTestId('deleteIcon'));
-      userEvent.click(screen.getByRole('button', { name: /Delete/i }));
-      expect(deleteFilter).toHaveBeenCalledTimes(1);
-      expect(screen.getByText('Created filters')).toBeInTheDocument();
-    });
+
+  it('should test click on return to custom filter redirects to Add filters', () => {
+    setupComponent();
+    userEvent.click(screen.getByRole('savedFilterText'));
+    expect(screen.getByText('Saved filters')).toBeInTheDocument();
+    expect(screen.queryByRole('savedFilterText')).not.toBeInTheDocument();
+    expect(screen.getAllByRole('savedFilter')).toHaveLength(2);
+
+    userEvent.click(screen.getByText(/back to custom filters/i));
+    expect(screen.queryByText('Saved filters')).not.toBeInTheDocument();
+    expect(screen.getByRole('savedFilterText')).toBeInTheDocument();
   });
+
   describe('Add new filter', () => {
     beforeEach(() => {
       setupComponent();
     });
-    it('renders add new filter modal', async () => {
-      await waitFor(() => {
-        userEvent.click(screen.getByText('New filter'));
-      });
-      expect(screen.getByText('Create a new filter')).toBeInTheDocument();
-    });
+
     it('adding new filter', async () => {
+      const codeValue = 'filter code';
+      const nameValue = 'filter name';
+      const textBoxes = screen.getAllByRole('textbox');
+
+      const codeTextBox = textBoxes[0];
+      const nameTextBox = textBoxes[1];
+
+      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
+      expect(addFilterBtn).toBeDisabled();
+      expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();
       await waitFor(() => {
-        userEvent.click(screen.getByText('New filter'));
+        userEvent.type(codeTextBox, codeValue);
+        userEvent.type(nameTextBox, nameValue);
       });
-      expect(
-        screen.getByRole('button', { name: /Add filter/i })
-      ).toBeDisabled();
+      expect(addFilterBtn).toBeEnabled();
+      expect(codeTextBox).toHaveValue(codeValue);
+      expect(nameTextBox).toHaveValue(nameValue);
+    });
+
+    it('should check unSaved filter without name', async () => {
+      const codeTextBox = screen.getAllByRole('textbox')[0];
+      const code = 'filter code';
+      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
+      expect(addFilterBtn).toBeDisabled();
       expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();
       await waitFor(() => {
-        userEvent.type(screen.getAllByRole('textbox')[0], 'filter name');
-        userEvent.type(screen.getAllByRole('textbox')[1], 'filter code');
+        userEvent.type(codeTextBox, code);
       });
-      expect(screen.getAllByRole('textbox')[0]).toHaveValue('filter name');
-      expect(screen.getAllByRole('textbox')[1]).toHaveValue('filter code');
-    });
-    it('close add new filter modal', () => {
-      userEvent.click(screen.getByText('New filter'));
-      expect(screen.getByText('Save this filter')).toBeInTheDocument();
-      userEvent.click(screen.getByText('Cancel'));
-      expect(screen.getByText('Created filters')).toBeInTheDocument();
+      expect(addFilterBtn).toBeEnabled();
+      expect(codeTextBox).toHaveValue(code);
     });
   });
-  describe('Edit filter', () => {
-    it('opens editFilter modal', () => {
-      const editFilter = jest.fn();
-      const toggleEditModal = jest.fn();
-      setupComponent({ editFilter, toggleEditModal });
-      userEvent.click(screen.getByText('Edit'));
-      expect(editFilter).toHaveBeenCalledTimes(1);
-      expect(toggleEditModal).toHaveBeenCalledTimes(1);
-    });
-  });
-  describe('Selecting a filter', () => {
-    it('should mock the select function if the filter is check no otherwise', () => {
-      const toggleOpenMock = jest.fn();
-      const activeFilterMock = jest.fn() as (
-        activeFilter: MessageFilters,
-        index: number
-      ) => void;
-      setupComponent({
-        filters,
-        toggleIsOpen: toggleOpenMock,
-        activeFilterHandler: activeFilterMock,
-      });
-      const selectFilterButton = screen.getByText(/Select filter/i);
 
-      userEvent.click(selectFilterButton);
-      expect(activeFilterMock).not.toHaveBeenCalled();
-      expect(toggleOpenMock).not.toHaveBeenCalled();
+  describe('onSubmit with Filter being saved', () => {
+    const addFilterMock = jest.fn();
+    const activeFilterHandlerMock = jest.fn();
+    const toggleModelMock = jest.fn();
 
-      const savedFilterElement = screen.getByRole('savedFilter');
-      userEvent.click(savedFilterElement);
-      userEvent.click(selectFilterButton);
+    const codeValue = 'filter code';
+    const nameValue = 'filter name';
 
-      expect(activeFilterMock).toHaveBeenCalled();
-      expect(toggleOpenMock).toHaveBeenCalled();
-    });
-  });
-  describe('onSubmit with Filter being saved', () => {
-    let addFilterMock: (values: MessageFilters) => void;
-    let activeFilterHandlerMock: (
-      activeFilter: MessageFilters,
-      index: number
-    ) => void;
     beforeEach(async () => {
-      addFilterMock = jest.fn() as (values: MessageFilters) => void;
-      activeFilterHandlerMock = jest.fn() as (
-        activeFilter: MessageFilters,
-        index: number
-      ) => void;
       setupComponent({
         addFilter: addFilterMock,
         activeFilterHandler: activeFilterHandlerMock,
+        toggleIsOpen: toggleModelMock,
       });
-      userEvent.click(screen.getByText(/New filter/i));
+
       await waitFor(() => {
-        userEvent.type(screen.getAllByRole('textbox')[0], 'filter name');
-        userEvent.type(screen.getAllByRole('textbox')[1], 'filter code');
+        userEvent.type(screen.getAllByRole('textbox')[0], codeValue);
+        userEvent.type(screen.getAllByRole('textbox')[1], nameValue);
       });
     });
 
+    afterEach(() => {
+      addFilterMock.mockClear();
+      activeFilterHandlerMock.mockClear();
+      toggleModelMock.mockClear();
+    });
+
     it('OnSubmit condition with checkbox off functionality', async () => {
-      userEvent.click(screen.getAllByRole('button')[1]);
+      // since both values are in it
+      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
+      expect(addFilterBtn).toBeEnabled();
+      userEvent.click(addFilterBtn);
+
       await waitFor(() => {
         expect(activeFilterHandlerMock).toHaveBeenCalled();
         expect(addFilterMock).not.toHaveBeenCalled();
@@ -175,6 +131,57 @@ describe('AddFilter component', () => {
       await waitFor(() => {
         expect(activeFilterHandlerMock).not.toHaveBeenCalled();
         expect(addFilterMock).toHaveBeenCalled();
+        expect(toggleModelMock).not.toHaveBeenCalled();
+      });
+    });
+
+    it('should check the state submit button when checkbox state changes so is name input value', async () => {
+      const checkbox = screen.getByRole('checkbox');
+      const codeTextBox = screen.getAllByRole('textbox')[0];
+      const nameTextBox = screen.getAllByRole('textbox')[1];
+      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
+
+      userEvent.clear(nameTextBox);
+      expect(nameTextBox).toHaveValue('');
+
+      userEvent.click(addFilterBtn);
+      await waitFor(() => {
+        expect(activeFilterHandlerMock).toHaveBeenCalledTimes(1);
+        expect(activeFilterHandlerMock).toHaveBeenCalledWith(
+          {
+            name: 'Unsaved filter',
+            code: codeValue,
+            saveFilter: false,
+          },
+          -1
+        );
+        // get reset-ed
+        expect(codeTextBox).toHaveValue('');
+        expect(toggleModelMock).toHaveBeenCalled();
+      });
+
+      userEvent.type(codeTextBox, codeValue);
+      expect(codeTextBox).toHaveValue(codeValue);
+
+      userEvent.click(checkbox);
+      expect(addFilterBtn).toBeDisabled();
+
+      userEvent.type(nameTextBox, nameValue);
+      expect(nameTextBox).toHaveValue(nameValue);
+
+      await waitFor(() => {
+        expect(addFilterBtn).toBeEnabled();
+      });
+
+      userEvent.click(addFilterBtn);
+
+      await waitFor(() => {
+        expect(activeFilterHandlerMock).toHaveBeenCalledTimes(1);
+        expect(addFilterMock).toHaveBeenCalledWith({
+          name: nameValue,
+          code: codeValue,
+          saveFilter: true,
+        });
       });
     });
   });

+ 6 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/EditFilter.spec.tsx

@@ -25,13 +25,16 @@ const setupComponent = (props?: Partial<EditFilterProps>) =>
 describe('EditFilter component', () => {
   it('renders component', () => {
     setupComponent();
+    expect(screen.getByText(/edit saved filter/i)).toBeInTheDocument();
   });
+
   it('closes editFilter modal', () => {
     const toggleEditModal = jest.fn();
     setupComponent({ toggleEditModal });
     userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
     expect(toggleEditModal).toHaveBeenCalledTimes(1);
   });
+
   it('save edited fields and close modal', async () => {
     const toggleEditModal = jest.fn();
     const editSavedFilter = jest.fn();
@@ -40,13 +43,14 @@ describe('EditFilter component', () => {
     expect(toggleEditModal).toHaveBeenCalledTimes(1);
     expect(editSavedFilter).toHaveBeenCalledTimes(1);
   });
+
   it('checks input values to match', () => {
     setupComponent();
     expect(screen.getAllByRole('textbox')[0]).toHaveValue(
-      editFilter.filter.name
+      editFilter.filter.code
     );
     expect(screen.getAllByRole('textbox')[1]).toHaveValue(
-      editFilter.filter.code
+      editFilter.filter.name
     );
   });
 });

+ 7 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/FilterModal.spec.tsx

@@ -26,10 +26,15 @@ describe('FilterModal component', () => {
     setupWrapper();
   });
   it('renders component with add filter modal', () => {
-    expect(screen.getByText('Add filter')).toBeInTheDocument();
+    expect(
+      screen.getByRole('heading', { name: /add filter/i, level: 3 })
+    ).toBeInTheDocument();
   });
   it('renders component with edit filter modal', async () => {
+    await waitFor(() => userEvent.click(screen.getByRole('savedFilterText')));
     await waitFor(() => userEvent.click(screen.getByText('Edit')));
-    expect(screen.getByText('Edit saved filter')).toBeInTheDocument();
+    expect(
+      screen.getByRole('heading', { name: /edit saved filter/i, level: 3 })
+    ).toBeInTheDocument();
   });
 });

+ 50 - 16
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx

@@ -45,35 +45,42 @@ describe('Filters component', () => {
   it('renders component', () => {
     setupWrapper();
   });
+
   describe('when fetching', () => {
     it('shows cancel button while fetching', () => {
       setupWrapper({ isFetching: true });
       expect(screen.getByText('Cancel')).toBeInTheDocument();
     });
   });
+
   describe('when fetching is over', () => {
     it('shows submit button while fetching is over', () => {
       setupWrapper();
       expect(screen.getByText('Submit')).toBeInTheDocument();
     });
   });
+
   describe('Input elements', () => {
+    const inputValue = 'Hello World!';
+
     it('search input', () => {
       setupWrapper();
       const SearchInput = screen.getByPlaceholderText('Search');
       expect(SearchInput).toBeInTheDocument();
       expect(SearchInput).toHaveValue('');
-      userEvent.type(SearchInput, 'Hello World!');
-      expect(SearchInput).toHaveValue('Hello World!');
+      userEvent.type(SearchInput, inputValue);
+      expect(SearchInput).toHaveValue(inputValue);
     });
+
     it('offset input', () => {
       setupWrapper();
       const OffsetInput = screen.getByPlaceholderText('Offset');
       expect(OffsetInput).toBeInTheDocument();
       expect(OffsetInput).toHaveValue('');
-      userEvent.type(OffsetInput, 'Hello World!');
-      expect(OffsetInput).toHaveValue('Hello World!');
+      userEvent.type(OffsetInput, inputValue);
+      expect(OffsetInput).toHaveValue(inputValue);
     });
+
     it('timestamp input', () => {
       setupWrapper();
       const seekTypeSelect = screen.getAllByRole('listbox');
@@ -84,11 +91,12 @@ describe('Filters component', () => {
       const TimestampInput = screen.getByPlaceholderText('Select timestamp');
       expect(TimestampInput).toBeInTheDocument();
       expect(TimestampInput).toHaveValue('');
-      userEvent.type(TimestampInput, 'Hello World!');
-      expect(TimestampInput).toHaveValue('Hello World!');
+      userEvent.type(TimestampInput, inputValue);
+      expect(TimestampInput).toHaveValue(inputValue);
       expect(screen.getByText('Submit')).toBeInTheDocument();
     });
   });
+
   describe('Select elements', () => {
     let seekTypeSelects: HTMLElement[];
     let options: HTMLElement[];
@@ -137,7 +145,11 @@ describe('Filters component', () => {
   describe('add new filter modal', () => {
     it('renders addFilter modal', () => {
       setupWrapper();
-      userEvent.click(screen.getByTestId('addFilterIcon'));
+      userEvent.click(
+        screen.getByRole('button', {
+          name: /add filters/i,
+        })
+      );
       expect(screen.getByTestId('messageFilterModal')).toBeInTheDocument();
     });
   });
@@ -146,21 +158,43 @@ describe('Filters component', () => {
     beforeEach(async () => {
       setupWrapper();
 
-      await waitFor(() => userEvent.click(screen.getByTestId('addFilterIcon')));
-      userEvent.click(screen.getByText('New filter'));
-      await waitFor(() => {
-        userEvent.type(screen.getAllByRole('textbox')[2], 'filter name');
-        userEvent.type(screen.getAllByRole('textbox')[3], 'filter code');
-      });
-      expect(screen.getAllByRole('textbox')[2]).toHaveValue('filter name');
-      expect(screen.getAllByRole('textbox')[3]).toHaveValue('filter code');
       await waitFor(() =>
-        userEvent.click(screen.getByRole('button', { name: /Add Filter/i }))
+        userEvent.click(
+          screen.getByRole('button', {
+            name: /add filters/i,
+          })
+        )
       );
+
+      const filterName = 'filter name';
+      const filterCode = 'filter code';
+
+      const messageFilterModal = screen.getByTestId('messageFilterModal');
+
+      await waitFor(() => {
+        const textBoxElements =
+          within(messageFilterModal).getAllByRole('textbox');
+        userEvent.type(textBoxElements[0], filterName);
+        userEvent.type(textBoxElements[1], filterCode);
+      });
+      const textBoxElements =
+        within(messageFilterModal).getAllByRole('textbox');
+      expect(textBoxElements[0]).toHaveValue(filterName);
+      expect(textBoxElements[1]).toHaveValue(filterCode);
+
+      await waitFor(() => {
+        return userEvent.click(
+          within(messageFilterModal).getByRole('button', {
+            name: /add filter/i,
+          })
+        );
+      });
     });
+
     it('shows saved smart filter', () => {
       expect(screen.getByTestId('activeSmartFilter')).toBeInTheDocument();
     });
+
     it('delete the active smart Filter', async () => {
       const smartFilterElement = screen.getByTestId('activeSmartFilter');
       const deleteIcon = within(smartFilterElement).getByTestId(

+ 157 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx

@@ -0,0 +1,157 @@
+import React from 'react';
+import SavedFilters, {
+  Props,
+} from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
+import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from 'lib/testHelpers';
+
+describe('SavedFilter Component', () => {
+  const mockFilters: MessageFilters[] = [
+    { name: 'name', code: 'code' },
+    { name: 'name1', code: 'code1' },
+  ];
+
+  const setUpComponent = (props: Partial<Props> = {}) => {
+    return render(
+      <SavedFilters
+        filters={props.filters || mockFilters}
+        onEdit={props.onEdit || jest.fn()}
+        closeModal={props.closeModal || jest.fn()}
+        onGoBack={props.onGoBack || jest.fn()}
+        activeFilterHandler={props.activeFilterHandler || jest.fn()}
+        deleteFilter={props.deleteFilter || jest.fn()}
+      />
+    );
+  };
+
+  it('should check the Cancel button click', () => {
+    const cancelMock = jest.fn();
+    setUpComponent({ closeModal: cancelMock });
+    userEvent.click(screen.getByText(/cancel/i));
+    expect(cancelMock).toHaveBeenCalled();
+  });
+
+  it('should check on go back button click', () => {
+    const onGoBackMock = jest.fn();
+    setUpComponent({ onGoBack: onGoBackMock });
+    userEvent.click(screen.getByText(/back to custom filters/i));
+    expect(onGoBackMock).toHaveBeenCalled();
+  });
+
+  describe('Empty Filters Rendering', () => {
+    beforeEach(() => {
+      setUpComponent({ filters: [] });
+    });
+    it('should check the rendering of the empty filter', () => {
+      expect(screen.getByText(/no saved filter/i)).toBeInTheDocument();
+      expect(screen.queryByRole('savedFilter')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('Saved Filters Deleting Editing', () => {
+    const onEditMock = jest.fn();
+    const activeFilterMock = jest.fn();
+    const cancelMock = jest.fn();
+
+    beforeEach(() => {
+      setUpComponent({
+        onEdit: onEditMock,
+        activeFilterHandler: activeFilterMock,
+        closeModal: cancelMock,
+      });
+    });
+
+    afterEach(() => {
+      onEditMock.mockClear();
+      activeFilterMock.mockClear();
+      cancelMock.mockClear();
+    });
+
+    it('should check the normal data rendering', () => {
+      expect(screen.getAllByRole('savedFilter')).toHaveLength(
+        mockFilters.length
+      );
+      expect(screen.getByText(mockFilters[0].name)).toBeInTheDocument();
+      expect(screen.getByText(mockFilters[1].name)).toBeInTheDocument();
+    });
+
+    it('should check the Filter edit Button works', () => {
+      const savedFilters = screen.getAllByRole('savedFilter');
+      userEvent.hover(savedFilters[0]);
+      userEvent.click(within(savedFilters[0]).getByText(/edit/i));
+      expect(onEditMock).toHaveBeenCalled();
+
+      userEvent.hover(savedFilters[1]);
+      userEvent.click(within(savedFilters[1]).getByText(/edit/i));
+      expect(onEditMock).toHaveBeenCalledTimes(2);
+    });
+
+    it('should check the select filter', () => {
+      const selectFilterButton = screen.getByText(/Select filter/i);
+
+      userEvent.click(selectFilterButton);
+      expect(activeFilterMock).not.toHaveBeenCalled();
+
+      const savedFilterElement = screen.getAllByRole('savedFilter');
+      userEvent.click(savedFilterElement[0]);
+      userEvent.click(selectFilterButton);
+
+      expect(activeFilterMock).toHaveBeenCalled();
+      expect(cancelMock).toHaveBeenCalled();
+    });
+  });
+
+  describe('Saved Filters Deletion', () => {
+    const deleteMock = jest.fn();
+
+    beforeEach(() => {
+      setUpComponent({ deleteFilter: deleteMock });
+    });
+
+    afterEach(() => {
+      deleteMock.mockClear();
+    });
+
+    it('Open Confirmation for the deletion modal', () => {
+      const savedFilters = screen.getAllByRole('savedFilter');
+      const deleteIcons = screen.getAllByTestId('deleteIcon');
+      userEvent.hover(savedFilters[0]);
+      userEvent.click(deleteIcons[0]);
+      const modelDialog = screen.getByRole('dialog');
+      expect(modelDialog).toBeInTheDocument();
+      expect(
+        within(modelDialog).getByText(/Confirm deletion/i)
+      ).toBeInTheDocument();
+    });
+
+    it('Close Confirmations deletion modal with button', () => {
+      const savedFilters = screen.getAllByRole('savedFilter');
+      const deleteIcons = screen.getAllByTestId('deleteIcon');
+
+      userEvent.hover(savedFilters[0]);
+      userEvent.click(deleteIcons[0]);
+
+      const modelDialog = screen.getByRole('dialog');
+      expect(modelDialog).toBeInTheDocument();
+      const cancelButton = within(modelDialog).getByRole('button', {
+        name: /Cancel/i,
+      });
+      userEvent.click(cancelButton);
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+    });
+
+    it('Delete the saved filter', () => {
+      const savedFilters = screen.getAllByRole('savedFilter');
+      const deleteIcons = screen.getAllByTestId('deleteIcon');
+
+      userEvent.hover(savedFilters[0]);
+      userEvent.click(deleteIcons[0]);
+
+      userEvent.click(screen.getByRole('button', { name: /Delete/i }));
+      expect(deleteMock).toHaveBeenCalledTimes(1);
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+    });
+  });
+});

+ 5 - 3
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx

@@ -9,15 +9,17 @@ export interface ConfirmationModalProps {
   onConfirm(): void;
   onCancel(): void;
   isConfirming?: boolean;
+  submitBtnText?: string;
 }
 
 const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
   isOpen,
   children,
-  title,
+  title = 'Confirm the action',
   onCancel,
   onConfirm,
   isConfirming = false,
+  submitBtnText = 'Submit',
 }) => {
   const cancelHandler = React.useCallback(() => {
     if (!isConfirming) {
@@ -30,7 +32,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
       <div onClick={cancelHandler} aria-hidden="true" />
       <div>
         <header>
-          <p>{title || 'Confirm the action'}</p>
+          <p>{title}</p>
         </header>
         <section>{children}</section>
         <footer>
@@ -51,7 +53,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
             type="button"
             disabled={isConfirming}
           >
-            Submit
+            {submitBtnText}
           </Button>
         </footer>
       </div>

+ 13 - 1
kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx

@@ -9,7 +9,7 @@ import theme from 'theme/theme';
 const confirmMock = jest.fn();
 const cancelMock = jest.fn();
 const body = 'Please Confirm the action!';
-describe('ConfiramationModal', () => {
+describe('ConfirmationModal', () => {
   const setupWrapper = (props: Partial<ConfirmationModalProps> = {}) => (
     <ThemeProvider theme={theme}>
       <ConfirmationModal
@@ -49,6 +49,12 @@ describe('ConfiramationModal', () => {
       title
     );
   });
+
+  it('Check the text on the submit button default behavior', () => {
+    const wrapper = mount(setupWrapper({ isOpen: true }));
+    expect(wrapper.exists({ children: 'Submit' })).toBeTruthy();
+  });
+
   it('handles onConfirm when user clicks confirm button', () => {
     const wrapper = mount(setupWrapper({ isOpen: true }));
     const confirmBtn = wrapper.find({ children: 'Submit' });
@@ -57,6 +63,12 @@ describe('ConfiramationModal', () => {
     expect(confirmMock).toHaveBeenCalledTimes(1);
   });
 
+  it('Check the text on the submit button', () => {
+    const submitBtnText = 'Submit btn Text';
+    const wrapper = mount(setupWrapper({ isOpen: true, submitBtnText }));
+    expect(wrapper.exists({ children: submitBtnText })).toBeTruthy();
+  });
+
   describe('cancellation', () => {
     let wrapper: ReactWrapper;
 

+ 29 - 0
kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx

@@ -0,0 +1,29 @@
+import React, { FC } from 'react';
+import { useTheme } from 'styled-components';
+
+const SavedIcon: FC = () => {
+  const theme = useTheme();
+
+  return (
+    <svg
+      width="18"
+      height="20"
+      viewBox="0 0 18 20"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M16 2H2L2 17.9873L7.29945 15.4982C8.3767 14.9922 9.6233 14.9922 10.7005 15.4982L16 17.9873V2ZM2 0C0.895431 0 0 0.895431 0 2V17.9873C0 19.4527 1.5239 20.4206 2.85027 19.7976L8.14973 17.3085C8.68835 17.0555 9.31165 17.0555 9.85027 17.3085L15.1497 19.7976C16.4761 20.4206 18 19.4527 18 17.9873V2C18 0.895431 17.1046 0 16 0H2Z"
+        fill={theme.icons.savedIcon}
+      />
+      <path
+        d="M9 4L10.4401 7.01791L13.7553 7.45492L11.3301 9.75709L11.9389 13.0451L9 11.45L6.06107 13.0451L6.66991 9.75709L4.24472 7.45492L7.55993 7.01791L9 4Z"
+        fill={theme.icons.savedIcon}
+      />
+    </svg>
+  );
+};
+
+export default SavedIcon;

+ 66 - 0
kafka-ui-react-app/src/lib/hooks/__tests__/useModal.spec.ts

@@ -0,0 +1,66 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import useModal from 'lib/hooks/useModal';
+
+describe('useModal CustomHook', () => {
+  it('should check true initial values', () => {
+    let initialValue = true;
+    const { result, rerender } = renderHook(() => useModal(initialValue));
+    expect(result.current.isOpen).toBe(initialValue);
+    initialValue = false;
+    rerender();
+    // because state is in useState
+    expect(result.current.isOpen).not.toBe(initialValue);
+  });
+
+  it('should check false initial values', () => {
+    let initialValue = false;
+    const { result, rerender } = renderHook(() => useModal(initialValue));
+    expect(result.current.isOpen).toBe(initialValue);
+
+    initialValue = true;
+    rerender();
+    // because state is in useState
+    expect(result.current.isOpen).not.toBe(initialValue);
+  });
+
+  it('should check setOpen function', () => {
+    const { result } = renderHook(() => useModal());
+    expect(result.current.isOpen).toBeFalsy();
+    act(() => {
+      result.current.setOpen();
+    });
+    expect(result.current.isOpen).toBeTruthy();
+  });
+
+  it('should check setClose function', () => {
+    const { result } = renderHook(() => useModal());
+
+    expect(result.current.isOpen).toBeFalsy();
+    act(() => {
+      result.current.setOpen();
+    });
+
+    expect(result.current.isOpen).toBeTruthy();
+
+    act(() => {
+      result.current.setClose();
+    });
+    expect(result.current.isOpen).toBeFalsy();
+  });
+
+  it('should check setToggle function', () => {
+    const { result } = renderHook(() => useModal());
+
+    expect(result.current.isOpen).toBeFalsy();
+    act(() => {
+      result.current.toggle();
+    });
+
+    expect(result.current.isOpen).toBeTruthy();
+
+    act(() => {
+      result.current.toggle();
+    });
+    expect(result.current.isOpen).toBeFalsy();
+  });
+});

+ 32 - 0
kafka-ui-react-app/src/lib/hooks/useModal.ts

@@ -0,0 +1,32 @@
+import { useCallback, useState } from 'react';
+
+interface UseModalReturn {
+  isOpen: boolean;
+  setOpen(): void;
+  setClose(): void;
+  toggle(): void;
+}
+const useModal = (initialModalState?: boolean): UseModalReturn => {
+  const [modalOpen, setModalOpen] = useState<boolean>(!!initialModalState);
+
+  const setOpen = useCallback(() => {
+    setModalOpen(true);
+  }, []);
+
+  const setClose = useCallback(() => {
+    setModalOpen(false);
+  }, []);
+
+  const toggle = useCallback(() => {
+    setModalOpen((prev) => !prev);
+  }, []);
+
+  return {
+    isOpen: modalOpen,
+    setOpen,
+    setClose,
+    toggle,
+  };
+};
+
+export default useModal;

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

@@ -489,6 +489,7 @@ const theme = {
     },
     newFilterIcon: Colors.brand[50],
     closeModalIcon: Colors.neutral[25],
+    savedIcon: Colors.brand[50],
   },
   viewer: {
     wrapper: Colors.neutral[3],