Explorar o código

Smart filters (#1688)

* smart filter creation

* fixing array deps warnings

* fixing array dep errors and infinite loop error

* fixing list item key issue

* adding tests for modals

* adding filterModal tests and fixing code smell in addFilter

* new test cases

* adding test cases

* adding test cases

* fixing code smell issue

* adding new test cases

* import fix

* minor code modifications AddFilter Testing

* minor code modifications FilterModal Testing

* adding AddEditFilterContainer Component for code to avoid code repetition initial

* adding AddEditFilterContainer Component for code to avoid code repetition

* adding AddEditFilterContainer Component moving the form validation and controlled components feature into the component

* adding AddEditFilterContainer Component minor code modifications + adding initial test file for AddEditFilterContainer component

* refactoring and minor modifications in the AddEditFilterContainer test files

* replace EditFilter body with the addEditContainer for general code structure

* Applying AddEditFilterContainer into the AddFilter component , minor EditFilter test typo fix.

* minor error messages view fix in the AddEditFilterContainer + adding testing in AddEditFilterContainer

* adding tests for AddEditFilterContainer component

* adding tests for AddEditFilterContainer component

* adding tests for AddFilter File

* Increasing the performance and the coverage of the tests in AddFilter

* Increasing the performance and the coverage of the tests in AddFilter to full capacity

* Removing the warnings from the AddFilter testing File

* Adding Test File To MessageContent styled file

* Adding Tests in the Filter Component

* Adding Tests in the Filter Component for Seek Selects

Co-authored-by: Mgrdich <mgotm13@gmail.com>
Co-authored-by: Mgrdich <46796009+Mgrdich@users.noreply.github.com>
Co-authored-by: Oleg Shur <workshur@gmail.com>
NelyDavtyan %!s(int64=3) %!d(string=hai) anos
pai
achega
5ce24cb8fd
Modificáronse 26 ficheiros con 1355 adicións e 959 borrados
  1. 3 13
      kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/__snapshots__/Tasks.spec.tsx.snap
  2. 0 5
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
  3. 0 718
      kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap
  4. 8 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts
  5. 9 3
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  6. 141 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx
  7. 164 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddFilter.tsx
  8. 37 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx
  9. 64 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FilterModal.tsx
  10. 186 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
  11. 134 14
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
  12. 188 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddEditFilterContainer.spec.tsx
  13. 181 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/AddFilter.spec.tsx
  14. 52 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/EditFilter.spec.tsx
  15. 35 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/FilterModal.spec.tsx
  16. 70 14
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx
  17. 12 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx
  18. 21 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/MessageContent.styled.ts
  19. 2 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/MessageContent.tsx
  20. 20 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/__tests__/MessageContent.styled.spec.tsx
  21. 5 7
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx
  22. 1 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  23. 0 133
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/__snapshots__/Details.spec.tsx.snap
  24. 1 1
      kafka-ui-react-app/src/components/common/Select/Select.styled.ts
  25. 11 45
      kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts
  26. 10 0
      kafka-ui-react-app/src/theme/theme.ts

+ 3 - 13
kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/__snapshots__/Tasks.spec.tsx.snap

@@ -29,14 +29,11 @@ exports[`Tasks view matches snapshot 1`] = `
   -ms-letter-spacing: 0em;
   letter-spacing: 0em;
   text-align: left;
+  display: inline-block;
   -webkit-box-pack: start;
   -webkit-justify-content: start;
   -ms-flex-pack: start;
   justify-content: start;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
   -webkit-align-items: center;
   -webkit-box-align: center;
   -ms-flex-align: center;
@@ -44,8 +41,6 @@ exports[`Tasks view matches snapshot 1`] = `
   background: #FFFFFF;
   cursor: default;
   color: #73848C;
-  padding-right: 18px;
-  position: relative;
 }
 
 .c1 {
@@ -204,14 +199,11 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
   -ms-letter-spacing: 0em;
   letter-spacing: 0em;
   text-align: left;
+  display: inline-block;
   -webkit-box-pack: start;
   -webkit-justify-content: start;
   -ms-flex-pack: start;
   justify-content: start;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
   -webkit-align-items: center;
   -webkit-box-align: center;
   -ms-flex-align: center;
@@ -219,8 +211,6 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
   background: #FFFFFF;
   cursor: default;
   color: #73848C;
-  padding-right: 18px;
-  position: relative;
 }
 
 .c1 {
@@ -289,4 +279,4 @@ exports[`Tasks view matches snapshot when no tasks 1`] = `
     </tr>
   </tbody>
 </table>
-`;
+`;

+ 0 - 5
kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx

@@ -93,9 +93,4 @@ describe('Connectors ListItem', () => {
     modalProps.onConfirm();
     expect(mockDeleteConnector).toHaveBeenCalledTimes(0);
   });
-
-  it('matches snapshot', () => {
-    const wrapper = mount(setupWrapper());
-    expect(wrapper).toMatchSnapshot();
-  });
 });

+ 0 - 718
kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -1,718 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Connectors ListItem matches snapshot 1`] = `
-.c4 {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-align-self: center;
-  -ms-flex-item-align: center;
-  align-self: center;
-}
-
-.c5 {
-  background: transparent;
-  border: none;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-align-items: 'center';
-  -webkit-box-align: 'center';
-  -ms-flex-align: 'center';
-  align-items: 'center';
-  -webkit-box-pack: 'center';
-  -webkit-justify-content: 'center';
-  -ms-flex-pack: 'center';
-  justify-content: 'center';
-}
-
-.c5:hover {
-  cursor: pointer;
-}
-
-.c6 {
-  color: #E51A1A;
-}
-
-.c2 {
-  border: none;
-  border-radius: 16px;
-  height: 20px;
-  line-height: 20px;
-  background-color: #F1F2F3;
-  color: #171A1C;
-  font-size: 12px;
-  display: inline-block;
-  padding-left: 0.75em;
-  padding-right: 0.75em;
-  text-align: center;
-}
-
-.c3 {
-  border: none;
-  border-radius: 16px;
-  height: 20px;
-  line-height: 20px;
-  background-color: #D6F5E0;
-  color: #171A1C;
-  font-size: 12px;
-  display: inline-block;
-  padding-left: 0.75em;
-  padding-right: 0.75em;
-  text-align: center;
-}
-
-.c0 > a {
-  color: normal:#171A1C;
-  font-weight: 500;
-  text-overflow: ellipsis;
-}
-
-.c1 {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-flex-wrap: wrap;
-  -ms-flex-wrap: wrap;
-  flex-wrap: wrap;
-}
-
-<Component
-  theme={
-    Object {
-      "alert": Object {
-        "color": Object {
-          "error": "#FAD1D1",
-          "info": "#E3E6E8",
-          "success": "#D6F5E0",
-          "warning": "#FFEECC",
-        },
-        "shadow": "rgba(0, 0, 0, 0.1)",
-      },
-      "breadcrumb": "#ABB5BA",
-      "button": Object {
-        "border": Object {
-          "active": "#171A1C",
-          "hover": "#454F54",
-          "normal": "#73848C",
-        },
-        "danger": Object {
-          "backgroundColor": Object {
-            "active": "#B81414",
-            "disabled": "#F5A3A3",
-            "hover": "#CF1717",
-            "normal": "#E51A1A",
-          },
-          "color": "#171A1C",
-          "invertedColors": Object {
-            "active": "#1717CF",
-            "hover": "#1717CF",
-            "normal": "#4C4CFF",
-          },
-        },
-        "fontSize": Object {
-          "L": "16px",
-          "M": "14px",
-          "S": "14px",
-        },
-        "height": Object {
-          "L": "40px",
-          "M": "32px",
-          "S": "24px",
-        },
-        "primary": Object {
-          "backgroundColor": Object {
-            "active": "#A3A3F5",
-            "disabled": "#F1F2F3",
-            "hover": "#D1D1FA",
-            "normal": "#E8E8FC",
-          },
-          "color": "#171A1C",
-          "invertedColors": Object {
-            "active": "#1717CF",
-            "hover": "#1717CF",
-            "normal": "#4C4CFF",
-          },
-        },
-        "secondary": Object {
-          "backgroundColor": Object {
-            "active": "#D5DADD",
-            "hover": "#E3E6E8",
-            "normal": "#F1F2F3",
-          },
-          "color": "#171A1C",
-          "invertedColors": Object {
-            "active": "#171A1C",
-            "hover": "#454F54",
-            "normal": "#73848C",
-          },
-        },
-      },
-      "circularAlert": Object {
-        "color": Object {
-          "error": "#E51A1A",
-          "info": "#E3E6E8",
-          "success": "#5CD685",
-          "warning": "#FFEECC",
-        },
-      },
-      "configList": Object {
-        "color": "#ABB5BA",
-      },
-      "connectEditWarning": "#FFEECC",
-      "consumerTopicContent": Object {
-        "backgroundColor": "#F1F2F3",
-      },
-      "dangerZone": Object {
-        "borderColor": "#E3E6E8",
-        "color": "#E51A1A",
-      },
-      "dropdown": Object {
-        "color": "#E51A1A",
-      },
-      "heading": Object {
-        "h1": Object {
-          "color": "#171A1C",
-        },
-        "h3": Object {
-          "color": "#73848C",
-          "fontSize": "14px",
-        },
-      },
-      "icons": Object {
-        "closeIcon": "#ABB5BA",
-        "liveIcon": Object {
-          "circleBig": "#FAD1D1",
-          "circleSmall": "#E51A1A",
-        },
-        "messageToggleIcon": Object {
-          "active": "#1717CF",
-          "hover": "#A3A3F5",
-          "normal": "#4C4CFF",
-        },
-        "verticalElipsisIcon": "#73848C",
-        "warningIcon": "#FFDD57",
-      },
-      "input": Object {
-        "backgroundColor": Object {
-          "readOnly": "#F1F2F3",
-        },
-        "borderColor": Object {
-          "disabled": "#E3E6E8",
-          "focus": "#454F54",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "disabled": "#ABB5BA",
-          "placeholder": Object {
-            "normal": "#ABB5BA",
-            "readOnly": "#ABB5BA",
-          },
-          "readOnly": "#171A1C",
-        },
-        "error": "#E51A1A",
-        "icon": Object {
-          "color": "#454F54",
-        },
-        "label": Object {
-          "color": "#454F54",
-        },
-      },
-      "layout": Object {
-        "minWidth": "1200px",
-        "navBarHeight": "3.25rem",
-        "navBarWidth": "201px",
-        "overlay": Object {
-          "backgroundColor": "#73848C",
-        },
-        "stuffBorderColor": "#E3E6E8",
-        "stuffColor": "#F1F2F3",
-      },
-      "menu": Object {
-        "backgroundColor": Object {
-          "active": "#F1F2F3",
-          "hover": "#F9FAFA",
-          "normal": "#FFFFFF",
-        },
-        "chevronIconColor": "#73848C",
-        "color": Object {
-          "active": "#1414B8",
-          "hover": "#454F54",
-          "isOpen": "#171A1C",
-          "normal": "#73848C",
-        },
-        "statusIconColor": Object {
-          "offline": "#E51A1A",
-          "online": "#5CD685",
-        },
-      },
-      "metrics": Object {
-        "backgroundColor": "#F1F2F3",
-        "filters": Object {
-          "color": Object {
-            "icon": "#171A1C",
-            "normal": "#73848C",
-          },
-        },
-        "indicator": Object {
-          "backgroundColor": "#FFFFFF",
-          "lightTextColor": "#ABB5BA",
-          "titleColor": "#73848C",
-          "warningTextColor": "#E51A1A",
-        },
-      },
-      "modal": Object {
-        "backgroundColor": "#FFFFFF",
-        "border": Object {
-          "bottom": "#F1F2F3",
-          "top": "#F1F2F3",
-        },
-        "overlay": "rgba(10, 10, 10, 0.1)",
-        "shadow": "rgba(0, 0, 0, 0.1)",
-      },
-      "pageLoader": Object {
-        "borderBottomColor": "#FFFFFF",
-        "borderColor": "#4C4CFF",
-      },
-      "pagination": Object {
-        "backgroundColor": "#FFFFFF",
-        "borderColor": Object {
-          "active": "#454F54",
-          "disabled": "#C7CED1",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "disabled": "#C7CED1",
-          "hover": "#171A1C",
-          "normal": "#171A1C",
-        },
-        "currentPage": "#E3E6E8",
-      },
-      "panelColor": "#FFFFFF",
-      "primaryTab": Object {
-        "borderColor": Object {
-          "active": "#4C4CFF",
-          "hover": "transparent",
-          "nav": "#E3E6E8",
-          "normal": "transparent",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#171A1C",
-          "normal": "#73848C",
-        },
-      },
-      "schema": Object {
-        "backgroundColor": Object {
-          "div": "#FFFFFF",
-          "tr": "#F1F2F3",
-        },
-      },
-      "scrollbar": Object {
-        "thumbColor": Object {
-          "active": "#73848C",
-          "normal": "#FFFFFF",
-        },
-        "trackColor": Object {
-          "active": "#F1F2F3",
-          "normal": "#FFFFFF",
-        },
-      },
-      "secondaryTab": Object {
-        "backgroundColor": Object {
-          "active": "#E3E6E8",
-          "hover": "#F1F2F3",
-          "normal": "#FFFFFF",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#171A1C",
-          "normal": "#73848C",
-        },
-      },
-      "select": Object {
-        "backgroundColor": Object {
-          "active": "#E3E6E8",
-          "hover": "#E3E6E8",
-          "normal": "#FFFFFF",
-        },
-        "borderColor": Object {
-          "active": "#454F54",
-          "disabled": "#E3E6E8",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "disabled": "#ABB5BA",
-          "hover": "#171A1C",
-          "normal": "#171A1C",
-        },
-        "optionList": Object {
-          "scrollbar": Object {
-            "backgroundColor": "#ABB5BA",
-          },
-        },
-      },
-      "switch": Object {
-        "checked": "#4C4CFF",
-        "circle": "#FFFFFF",
-        "disabled": "#E3E6E8",
-        "unchecked": "#A3A3F5",
-      },
-      "table": Object {
-        "link": Object {
-          "color": Object {
-            "normal": "#171A1C",
-          },
-        },
-        "td": Object {
-          "color": Object {
-            "normal": "#171A1C",
-          },
-        },
-        "th": Object {
-          "backgroundColor": Object {
-            "normal": "#FFFFFF",
-          },
-          "color": Object {
-            "active": "#4C4CFF",
-            "hover": "#4C4CFF",
-            "normal": "#73848C",
-          },
-          "previewColor": Object {
-            "normal": "#4C4CFF",
-          },
-        },
-        "tr": Object {
-          "backgroundColor": Object {
-            "hover": "#F1F2F3",
-          },
-        },
-      },
-      "tag": Object {
-        "backgroundColor": Object {
-          "blue": "#e3f2fd",
-          "gray": "#F1F2F3",
-          "green": "#D6F5E0",
-          "red": "#FAD1D1",
-          "white": "#E3E6E8",
-          "yellow": "#FFEECC",
-        },
-        "color": "#171A1C",
-      },
-      "textArea": Object {
-        "backgroundColor": Object {
-          "readOnly": "#F1F2F3",
-        },
-        "borderColor": Object {
-          "disabled": "#E3E6E8",
-          "focus": "#454F54",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "disabled": "#ABB5BA",
-          "placeholder": Object {
-            "focus": Object {
-              "normal": "transparent",
-              "readOnly": "#ABB5BA",
-            },
-            "normal": "#ABB5BA",
-          },
-          "readOnly": "#171A1C",
-        },
-      },
-      "topicFormLabel": Object {
-        "color": "#73848C",
-      },
-      "topicMetaData": Object {
-        "backgroundColor": "#F1F2F3",
-        "color": Object {
-          "label": "#73848C",
-          "meta": "#ABB5BA",
-          "value": "#2F3639",
-        },
-      },
-      "topicsList": Object {
-        "backgroundColor": Object {
-          "active": "#E3E6E8",
-          "hover": "#F1F2F3",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#73848C",
-          "normal": "#171A1C",
-        },
-      },
-      "version": Object {
-        "currentVersion": Object {
-          "color": "#ABB5BA",
-        },
-        "symbolWrapper": Object {
-          "color": "#ABB5BA",
-        },
-      },
-      "viewer": Object {
-        "wrapper": "#F9FAFA",
-      },
-    }
-  }
->
-  <Provider
-    store={
-      Object {
-        "@@observable": [Function],
-        "dispatch": [Function],
-        "getState": [Function],
-        "replaceReducer": [Function],
-        "subscribe": [Function],
-      }
-    }
-  >
-    <BrowserRouter>
-      <Router
-        history={
-          Object {
-            "action": "POP",
-            "block": [Function],
-            "createHref": [Function],
-            "go": [Function],
-            "goBack": [Function],
-            "goForward": [Function],
-            "length": 1,
-            "listen": [Function],
-            "location": Object {
-              "hash": "",
-              "pathname": "/",
-              "search": "",
-              "state": undefined,
-            },
-            "push": [Function],
-            "replace": [Function],
-          }
-        }
-      >
-        <table>
-          <tbody>
-            <ListItem
-              clusterName="local"
-              connector={
-                Object {
-                  "connect": "first",
-                  "connectorClass": "FileStreamSource",
-                  "failedTasksCount": 0,
-                  "name": "hdfs-source-connector",
-                  "status": Object {
-                    "state": "RUNNING",
-                  },
-                  "tasksCount": 2,
-                  "topics": Array [
-                    "test-topic",
-                  ],
-                  "type": "SOURCE",
-                }
-              }
-            >
-              <tr>
-                <styled.td>
-                  <td
-                    className="c0"
-                  >
-                    <NavLink
-                      exact={true}
-                      to="/clusters/local/connects/first/connectors/hdfs-source-connector"
-                    >
-                      <Link
-                        aria-current={null}
-                        to={
-                          Object {
-                            "hash": "",
-                            "pathname": "/clusters/local/connects/first/connectors/hdfs-source-connector",
-                            "search": "",
-                            "state": null,
-                          }
-                        }
-                      >
-                        <LinkAnchor
-                          aria-current={null}
-                          href="/clusters/local/connects/first/connectors/hdfs-source-connector"
-                          navigate={[Function]}
-                        >
-                          <a
-                            aria-current={null}
-                            href="/clusters/local/connects/first/connectors/hdfs-source-connector"
-                            onClick={[Function]}
-                          >
-                            hdfs-source-connector
-                          </a>
-                        </LinkAnchor>
-                      </Link>
-                    </NavLink>
-                  </td>
-                </styled.td>
-                <td>
-                  first
-                </td>
-                <td>
-                  SOURCE
-                </td>
-                <td>
-                  FileStreamSource
-                </td>
-                <td>
-                  <styled.div>
-                    <div
-                      className="c1"
-                    >
-                      <styled.p
-                        color="gray"
-                        key="test-topic"
-                      >
-                        <p
-                          className="c2"
-                          color="gray"
-                        >
-                          <Link
-                            to="/clusters/local/topics/test-topic"
-                          >
-                            <LinkAnchor
-                              href="/clusters/local/topics/test-topic"
-                              navigate={[Function]}
-                            >
-                              <a
-                                href="/clusters/local/topics/test-topic"
-                                onClick={[Function]}
-                              >
-                                test-topic
-                              </a>
-                            </LinkAnchor>
-                          </Link>
-                        </p>
-                      </styled.p>
-                    </div>
-                  </styled.div>
-                </td>
-                <td>
-                  <styled.p
-                    color="green"
-                  >
-                    <p
-                      className="c3"
-                      color="green"
-                    >
-                      RUNNING
-                    </p>
-                  </styled.p>
-                </td>
-                <td>
-                  <span>
-                    2
-                     of 
-                    2
-                  </span>
-                </td>
-                <td>
-                  <div>
-                    <Dropdown
-                      label={<VerticalElipsisIcon />}
-                      right={true}
-                    >
-                      <div
-                        className="dropdown is-right"
-                      >
-                        <styled.div>
-                          <div
-                            className="c4"
-                          >
-                            <styled.button
-                              onClick={[Function]}
-                            >
-                              <button
-                                className="c5"
-                                onClick={[Function]}
-                                type="button"
-                              >
-                                <VerticalElipsisIcon>
-                                  <svg
-                                    fill="none"
-                                    height="16"
-                                    viewBox="0 0 4 16"
-                                    width="4"
-                                    xmlns="http://www.w3.org/2000/svg"
-                                  >
-                                    <path
-                                      d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z"
-                                      fill="#73848C"
-                                    />
-                                  </svg>
-                                </VerticalElipsisIcon>
-                              </button>
-                            </styled.button>
-                          </div>
-                        </styled.div>
-                        <div
-                          className="dropdown-menu"
-                          id="dropdown-menu"
-                          role="menu"
-                        >
-                          <div
-                            className="dropdown-content has-text-left"
-                          >
-                            <DropdownDivider>
-                              <hr
-                                className="dropdown-divider"
-                              />
-                            </DropdownDivider>
-                            <DropdownItem
-                              danger={true}
-                              onClick={[Function]}
-                            >
-                              <styled.a
-                                $isDanger={true}
-                                className="dropdown-item is-link"
-                                onClick={[Function]}
-                              >
-                                <a
-                                  className="c6 dropdown-item is-link"
-                                  href="#end"
-                                  onClick={[Function]}
-                                  role="menuitem"
-                                  type="button"
-                                >
-                                  Remove Connector
-                                </a>
-                              </styled.a>
-                            </DropdownItem>
-                          </div>
-                        </div>
-                      </div>
-                    </Dropdown>
-                  </div>
-                  <mock-ConfirmationModal
-                    isOpen={false}
-                    onCancel={[Function]}
-                    onConfirm={[Function]}
-                  >
-                    Are you sure want to remove 
-                    <b>
-                      hdfs-source-connector
-                    </b>
-                     connector?
-                  </mock-ConfirmationModal>
-                </td>
-              </tr>
-            </ListItem>
-          </tbody>
-        </table>
-      </Router>
-    </BrowserRouter>
-  </Provider>
-</Component>
-`;

+ 8 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts

@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const DropdownExtraMessage = styled.div`
+  color: ${({ theme }) => theme.topicMetaData.color.label};
+  font-size: 14px;
+  width: 100%;
+  margin-top: 10px;
+`;

+ 9 - 3
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -22,6 +22,7 @@ import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import styled from 'styled-components';
 import Navbar from 'components/common/Navigation/Navbar.styled';
+import * as S from 'components/Topics/Topic/Details/Details.styled';
 
 import OverviewContainer from './Overview/OverviewContainer';
 import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer';
@@ -64,19 +65,19 @@ const Details: React.FC<Props> = ({
     React.useState(false);
   const deleteTopicHandler = React.useCallback(() => {
     deleteTopic(clusterName, topicName);
-  }, [clusterName, deleteTopic, topicName]);
+  }, [clusterName, topicName, deleteTopic]);
 
   React.useEffect(() => {
     if (isDeleted) {
       dispatch(deleteTopicAction.cancel());
       history.push(clusterTopicsPath(clusterName));
     }
-  }, [clusterName, dispatch, history, isDeleted]);
+  }, [isDeleted, clusterName, dispatch, history]);
 
   const clearTopicMessagesHandler = React.useCallback(() => {
     clearTopicMessages(clusterName, topicName);
     setClearTopicConfirmationVisible(false);
-  }, [clearTopicMessages, clusterName, topicName]);
+  }, [clusterName, topicName, clearTopicMessages]);
 
   return (
     <div>
@@ -101,6 +102,11 @@ const Details: React.FC<Props> = ({
                   }
                 >
                   Edit settings
+                  <S.DropdownExtraMessage>
+                    Pay attention! This operation has
+                    <br />
+                    especially important consequences.
+                  </S.DropdownExtraMessage>
                 </DropdownItem>
                 {isDeletePolicy && (
                   <DropdownItem

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

@@ -0,0 +1,141 @@
+import React from 'react';
+import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled';
+import { InputLabel } from 'components/common/Input/InputLabel.styled';
+import Input from 'components/common/Input/Input';
+import { Textarea } from 'components/common/Textbox/Textarea.styled';
+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 { yupResolver } from '@hookform/resolvers/yup';
+import yup from 'lib/yupExtended';
+
+const validationSchema = yup.object().shape({
+  name: yup.string().required(),
+  code: yup.string().required(),
+});
+
+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;
+}
+
+const AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({
+  title,
+  cancelBtnHandler,
+  submitBtnText,
+  inputDisplayNameDefaultValue = '',
+  inputCodeDefaultValue = '',
+  toggleSaveFilterValue,
+  toggleSaveFilterSetter,
+  createNewFilterText,
+  submitCallback,
+  submitCallbackWithReset,
+}) => {
+  const methods = useForm<MessageFilters>({
+    mode: 'onChange',
+    resolver: yupResolver(validationSchema),
+  });
+  const {
+    handleSubmit,
+    control,
+    formState: { isDirty, isSubmitting, isValid, errors },
+    reset,
+  } = methods;
+
+  const onSubmit = React.useCallback(
+    (values: MessageFilters) => {
+      submitCallback?.(values);
+      if (submitCallbackWithReset) {
+        reset({ name: '', code: '' });
+      }
+    },
+    [reset, submitCallback, submitCallbackWithReset]
+  );
+
+  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} />
+              )}
+            />
+          </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>
+    </>
+  );
+};
+
+export default AddEditFilterContainer;

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

@@ -0,0 +1,164 @@
+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 AddEditFilterContainer from './AddEditFilterContainer';
+
+export interface FilterModalProps {
+  toggleIsOpen(): void;
+  filters: MessageFilters[];
+  addFilter(values: MessageFilters): void;
+  deleteFilter(index: number): void;
+  activeFilterHandler(activeFilter: MessageFilters, index: number): void;
+  toggleEditModal(): void;
+  editFilter(value: FilterEdit): void;
+}
+
+const AddFilter: React.FC<FilterModalProps> = ({
+  toggleIsOpen,
+  filters,
+  addFilter,
+  deleteFilter,
+  activeFilterHandler,
+  toggleEditModal,
+  editFilter,
+}) => {
+  const [addNewFilter, setAddNewFilter] = React.useState(false);
+  const [toggleSaveFilter, setToggleSaveFilter] = React.useState(false);
+  const [selectedFilter, setSelectedFilter] = React.useState(-1);
+  const [toggleDeletionModal, setToggleDeletionModal] =
+    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);
+      } else {
+        addFilter(values);
+      }
+      setAddNewFilter(!addNewFilter);
+    },
+    [addNewFilter, toggleSaveFilter, activeFilterHandler, addFilter]
+  );
+  return !addNewFilter ? (
+    <>
+      <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)}
+          >
+            <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>
+    </>
+  ) : (
+    <AddEditFilterContainer
+      title="Add filter"
+      cancelBtnHandler={() => setAddNewFilter(!addNewFilter)}
+      submitBtnText="Add filter"
+      submitCallback={onSubmit}
+      submitCallbackWithReset
+      createNewFilterText="Create a new filter"
+      toggleSaveFilterValue={toggleSaveFilter}
+      toggleSaveFilterSetter={() => setToggleSaveFilter(!toggleSaveFilter)}
+    />
+  );
+};
+
+export default AddFilter;

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

@@ -0,0 +1,37 @@
+import React from 'react';
+import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
+import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
+
+import AddEditFilterContainer from './AddEditFilterContainer';
+
+export interface EditFilterProps {
+  editFilter: FilterEdit;
+  toggleEditModal(): void;
+  editSavedFilter(filter: FilterEdit): void;
+}
+
+const EditFilter: React.FC<EditFilterProps> = ({
+  editFilter,
+  toggleEditModal,
+  editSavedFilter,
+}) => {
+  const onSubmit = React.useCallback(
+    (values: MessageFilters) => {
+      editSavedFilter({ index: editFilter.index, filter: values });
+      toggleEditModal();
+    },
+    [editSavedFilter, editFilter.index, toggleEditModal]
+  );
+  return (
+    <AddEditFilterContainer
+      title="Edit saved filter"
+      cancelBtnHandler={() => toggleEditModal()}
+      submitBtnText="Save"
+      inputDisplayNameDefaultValue={editFilter.filter.name}
+      inputCodeDefaultValue={editFilter.filter.code}
+      submitCallback={onSubmit}
+    />
+  );
+};
+
+export default EditFilter;

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

@@ -0,0 +1,64 @@
+import React from 'react';
+import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled';
+import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
+import AddFilter from 'components/Topics/Topic/Details/Messages/Filters/AddFilter';
+import EditFilter from 'components/Topics/Topic/Details/Messages/Filters/EditFilter';
+
+export interface FilterModalProps {
+  toggleIsOpen(): void;
+  filters: MessageFilters[];
+  addFilter(values: MessageFilters): void;
+  deleteFilter(index: number): void;
+  activeFilterHandler(activeFilter: MessageFilters, index: number): void;
+  editSavedFilter(filter: FilterEdit): void;
+}
+
+export interface FilterEdit {
+  index: number;
+  filter: MessageFilters;
+}
+
+const FilterModal: React.FC<FilterModalProps> = ({
+  toggleIsOpen,
+  filters,
+  addFilter,
+  deleteFilter,
+  activeFilterHandler,
+  editSavedFilter,
+}) => {
+  const [addFilterModal, setAddFilterModal] = React.useState<boolean>(true);
+  const toggleEditModal = () => {
+    setAddFilterModal(!addFilterModal);
+  };
+  const [editFilter, setEditFilter] = React.useState<FilterEdit>({
+    index: -1,
+    filter: { name: '', code: '' },
+  });
+  const editFilterHandler = (value: FilterEdit) => {
+    setEditFilter(value);
+    setAddFilterModal(!addFilterModal);
+  };
+  return (
+    <S.MessageFilterModal data-testid="messageFilterModal">
+      {addFilterModal ? (
+        <AddFilter
+          toggleIsOpen={toggleIsOpen}
+          filters={filters}
+          addFilter={addFilter}
+          deleteFilter={deleteFilter}
+          activeFilterHandler={activeFilterHandler}
+          toggleEditModal={toggleEditModal}
+          editFilter={editFilterHandler}
+        />
+      ) : (
+        <EditFilter
+          editFilter={editFilter}
+          toggleEditModal={toggleEditModal}
+          editSavedFilter={editSavedFilter}
+        />
+      )}
+    </S.MessageFilterModal>
+  );
+};
+
+export default FilterModal;

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

@@ -1,5 +1,8 @@
 import styled from 'styled-components';
 
+interface SavedFilterProps {
+  selected: boolean;
+}
 interface MessageLoadingProps {
   isLive: boolean;
 }
@@ -92,6 +95,189 @@ export const MetricsIcon = styled.div`
   height: 12px;
 `;
 
+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`
+  height: auto;
+  width: 560px;
+  border-radius: 8px;
+  background: ${({ theme }) => theme.modal.backgroundColor};
+  position: absolute;
+  left: 25%;
+  border: 1px solid ${({ theme }) => theme.breadcrumb};
+  box-shadow: ${({ theme }) => theme.modal.shadow};
+  padding: 16px;
+  z-index: 1;
+`;
+
+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;
+`;
+
+export const CreatedFilter = styled.p`
+  margin: 25px 0 10px;
+  color: ${({ theme }) => theme.breadcrumb};
+  font-size: 14px;
+  line-height: 20px;
+`;
+
+export const SavedFiltersContainer = styled.div`
+  overflow-y: auto;
+  height: 195px;
+  // display: flex;
+  // flex-direction: column;
+  justify-content: space-around;
+  padding-left: 10px;
+  // gap: 10px;
+`;
+
+export const SavedFilterName = styled.div`
+  font-size: 14px;
+  line-height: 20px;
+`;
+
+export const FilterButtonWrapper = styled.div`
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 25px;
+  gap: 10px;
+`;
+
+export const AddFiltersIcon = styled.div`
+  color: ${({ theme }) => theme.metrics.filters.color.icon};
+  padding-right: 6px;
+  height: 20px;
+  cursor: pointer;
+`;
+
+export const ActiveSmartFilterWrapper = styled.div`
+  padding: 5px 0;
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  justify-content: flex-start;
+`;
+
+export const DeleteSavedFilter = styled.div`
+  color: ${({ theme }) => theme.breadcrumb};
+  cursor: pointer;
+`;
+
+export const FilterEdit = styled.div`
+  font-weight: 500;
+  font-size: 14px;
+  line-height: 20px;
+`;
+
+export const FilterOptions = styled.div`
+  display: none;
+  width: 50px;
+  justify-content: space-between;
+  color: ${({ theme }) => theme.editFilterText.color};
+`;
+
+export const SavedFilter = styled.div.attrs({
+  role: 'savedFilter',
+})<SavedFilterProps>`
+  display: flex;
+  justify-content: space-between;
+  padding-right: 5px;
+  height: 32px;
+  align-items: center;
+  cursor: pointer;
+  &:hover ${FilterOptions} {
+    display: flex;
+  }
+  &:hover {
+    background: ${({ theme }) => theme.layout.stuffColor};
+  }
+  background: ${(props) =>
+    props.selected ? props.theme.layout.stuffColor : props.theme.panelColor};
+`;
+
+export const CheckboxWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  gap: 5px;
+`;
+
+export const ActiveSmartFilter = styled.div`
+  border-radius: 4px;
+  min-width: 115px;
+  height: 24px;
+  background: ${({ theme }) => theme.layout.stuffColor};
+  font-size: 14px;
+  line-height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  color: ${({ theme }) => theme.input.label.color};
+  padding: 10px 2px;
+`;
+
+export const DeleteSavedFilterIcon = styled.div`
+  color: ${({ theme }) => theme.icons.closeIcon};
+  border-left: 1px solid ${({ theme }) => theme.savedFilterDivider.color};
+  display: flex;
+  align-items: center;
+  padding-left: 5px;
+  height: 24px;
+  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;
+  line-height: 20px;
+  padding: 16px 0;
+`;
+
 export const MessageLoading = styled.div<MessageLoadingProps>`
   color: ${({ theme }) => theme.heading.h3.color};
   font-size: ${({ theme }) => theme.heading.h3.fontSize};

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

@@ -8,8 +8,9 @@ import {
   TopicMessageConsuming,
   TopicMessageEvent,
   TopicMessageEventTypeEnum,
+  MessageFilterType,
 } from 'generated-sources';
-import * as React from 'react';
+import React from 'react';
 import { omitBy } from 'lodash';
 import { useHistory, useLocation } from 'react-router';
 import DatePicker from 'react-datepicker';
@@ -21,6 +22,9 @@ import { BASE_PARAMS } from 'lib/constants';
 import Input from 'components/common/Input/Input';
 import Select from 'components/common/Select/Select';
 import { Button } from 'components/common/Button/Button';
+import FilterModal, {
+  FilterEdit,
+} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
 
 import * as S from './Filters.styled';
 import {
@@ -45,14 +49,24 @@ export interface FiltersProps {
   updateMeta(meta: TopicMessageConsuming): void;
   setIsFetching(status: boolean): void;
 }
+export interface MessageFilters {
+  name: string;
+  code: string;
+}
+
+export interface ActiveMessageFilter {
+  index: number;
+  name: string;
+  code: string;
+}
 
 const PER_PAGE = 100;
 
-const SeekTypeOptions = [
+export const SeekTypeOptions = [
   { value: SeekType.OFFSET, label: 'Offset' },
   { value: SeekType.TIMESTAMP, label: 'Timestamp' },
 ];
-const SeekDirectionOptions = [
+export const SeekDirectionOptions = [
   { value: SeekDirection.FORWARD, label: 'Oldest First', isLive: false },
   { value: SeekDirection.BACKWARD, label: 'Newest First', isLive: false },
   { value: SeekDirection.TAILING, label: 'Live Mode', isLive: true },
@@ -74,6 +88,9 @@ const Filters: React.FC<FiltersProps> = ({
   const location = useLocation();
   const history = useHistory();
 
+  const [isOpen, setIsOpen] = React.useState(false);
+  const toggleIsOpen = () => setIsOpen(!isOpen);
+
   const source = React.useRef<EventSource | null>(null);
 
   const searchParams = React.useMemo(
@@ -96,6 +113,24 @@ const Filters: React.FC<FiltersProps> = ({
   const [timestamp, setTimestamp] = React.useState<Date | null>(
     getTimestampFromSeekToParam(searchParams)
   );
+
+  const [savedFilters, setSavedFilters] = React.useState<MessageFilters[]>(
+    JSON.parse(localStorage.getItem('savedFilters') ?? '[]')
+  );
+
+  let storageActiveFilter = localStorage.getItem('activeFilter');
+  storageActiveFilter =
+    storageActiveFilter ?? JSON.stringify({ name: '', code: '', index: -1 });
+
+  const [activeFilter, setActiveFilter] = React.useState<ActiveMessageFilter>(
+    JSON.parse(storageActiveFilter)
+  );
+
+  const [queryType, setQueryType] = React.useState<MessageFilterType>(
+    activeFilter.name
+      ? MessageFilterType.GROOVY_SCRIPT
+      : MessageFilterType.STRING_CONTAINS
+  );
   const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');
   const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(
     (searchParams.get('seekDirection') as SeekDirection) ||
@@ -126,15 +161,21 @@ const Filters: React.FC<FiltersProps> = ({
     [partitions]
   );
 
-  const handleFiltersSubmit = React.useCallback(() => {
-    setAttempt(attempt + 1);
-
-    const props: Query = {
-      q: query,
+  const props: Query = React.useMemo(() => {
+    return {
+      q:
+        queryType === MessageFilterType.GROOVY_SCRIPT
+          ? `valueAsText.contains('${activeFilter.code}')`
+          : query,
+      filterQueryType: queryType,
       attempt,
       limit: PER_PAGE,
       seekDirection,
     };
+  }, [attempt, query, queryType, seekDirection, activeFilter]);
+
+  const handleFiltersSubmit = React.useCallback(() => {
+    setAttempt(attempt + 1);
 
     if (isSeekTypeControlVisible) {
       props.seekType = currentSeekType;
@@ -167,7 +208,7 @@ const Filters: React.FC<FiltersProps> = ({
       search: `?${qs}`,
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [seekDirection]);
+  }, [seekDirection, queryType, activeFilter]);
 
   const toggleSeekDirection = (val: string) => {
     switch (val) {
@@ -191,6 +232,60 @@ const Filters: React.FC<FiltersProps> = ({
     source.current.close();
   };
 
+  const addFilter = (newFilter: MessageFilters) => {
+    const filters = [...savedFilters];
+    filters.push(newFilter);
+    setSavedFilters(filters);
+    localStorage.setItem('savedFilters', JSON.stringify(filters));
+  };
+  const deleteFilter = (index: number) => {
+    const filters = [...savedFilters];
+    if (activeFilter.name && activeFilter.index === index) {
+      localStorage.removeItem('activeFilter');
+      setActiveFilter({ name: '', code: '', index: -1 });
+      setQueryType(MessageFilterType.STRING_CONTAINS);
+    }
+    filters.splice(index, 1);
+    localStorage.setItem('savedFilters', JSON.stringify(filters));
+    setSavedFilters(filters);
+  };
+  const deleteActiveFilter = () => {
+    setActiveFilter({ name: '', code: '', index: -1 });
+    localStorage.removeItem('activeFilter');
+    setQueryType(MessageFilterType.STRING_CONTAINS);
+  };
+  const activeFilterHandler = (
+    newActiveFilter: MessageFilters,
+    index: number
+  ) => {
+    localStorage.setItem(
+      'activeFilter',
+      JSON.stringify({ index, ...newActiveFilter })
+    );
+    setActiveFilter({ index, ...newActiveFilter });
+    setQueryType(MessageFilterType.GROOVY_SCRIPT);
+  };
+  const editSavedFilter = (filter: FilterEdit) => {
+    const filters = [...savedFilters];
+    filters[filter.index] = filter.filter;
+    if (activeFilter.name && activeFilter.index === filter.index) {
+      setActiveFilter({
+        index: filter.index,
+        name: filter.filter.name,
+        code: filter.filter.code,
+      });
+      localStorage.setItem(
+        'activeFilter',
+        JSON.stringify({
+          index: filter.index,
+          name: filter.filter.name,
+          code: filter.filter.code,
+        })
+      );
+    }
+    localStorage.setItem('savedFilters', JSON.stringify(filters));
+    setSavedFilters(filters);
+  };
   // eslint-disable-next-line consistent-return
   React.useEffect(() => {
     if (location.search.length !== 0) {
@@ -237,19 +332,17 @@ const Filters: React.FC<FiltersProps> = ({
     topicName,
     seekDirection,
     location,
-    setIsFetching,
-    resetMessages,
     addMessage,
-    updatePhase,
+    resetMessages,
+    setIsFetching,
     updateMeta,
+    updatePhase,
   ]);
-
   React.useEffect(() => {
     if (location.search.length === 0) {
       handleFiltersSubmit();
     }
   }, [handleFiltersSubmit, location]);
-
   React.useEffect(() => {
     handleFiltersSubmit();
   }, [handleFiltersSubmit, seekDirection]);
@@ -313,6 +406,7 @@ const Filters: React.FC<FiltersProps> = ({
             onChange={setSelectedPartitions}
             labelledBy="Select partitions"
           />
+          <S.ClearAll>Clear all</S.ClearAll>
           {isFetching ? (
             <Button
               type="button"
@@ -346,6 +440,32 @@ const Filters: React.FC<FiltersProps> = ({
           isLive={seekDirection === SeekDirection.TAILING}
         />
       </div>
+      <S.ActiveSmartFilterWrapper>
+        <S.AddFiltersIcon data-testid="addFilterIcon" onClick={toggleIsOpen}>
+          <i className="fas fa-plus fa-sm" />
+        </S.AddFiltersIcon>
+        {activeFilter.name && (
+          <S.ActiveSmartFilter data-testid="activeSmartFilter">
+            {activeFilter.name}
+            <S.DeleteSavedFilterIcon onClick={deleteActiveFilter}>
+              <i
+                className="fas fa-times"
+                data-testid="activeSmartFilterCloseIcon"
+              />
+            </S.DeleteSavedFilterIcon>
+          </S.ActiveSmartFilter>
+        )}
+      </S.ActiveSmartFilterWrapper>
+      {isOpen && (
+        <FilterModal
+          toggleIsOpen={toggleIsOpen}
+          filters={savedFilters}
+          addFilter={addFilter}
+          deleteFilter={deleteFilter}
+          activeFilterHandler={activeFilterHandler}
+          editSavedFilter={editSavedFilter}
+        />
+      )}
       <S.FiltersMetrics>
         <p style={{ fontSize: 14 }}>
           {seekDirection !== SeekDirection.TAILING &&

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

@@ -0,0 +1,188 @@
+import React from 'react';
+import AddEditFilterContainer, {
+  AddEditFilterContainerProps,
+} from 'components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer';
+import { render } from 'lib/testHelpers';
+import { screen, waitFor } from '@testing-library/react';
+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',
+    code: 'mockCode',
+  };
+
+  const setupComponent = (props: Partial<AddEditFilterContainerProps> = {}) => {
+    const { title, submitBtnText, createNewFilterText } = props;
+    return render(
+      <AddEditFilterContainer
+        title={title || defaultTitle}
+        cancelBtnHandler={jest.fn()}
+        submitBtnText={submitBtnText || defaultSubmitBtn}
+        createNewFilterText={createNewFilterText || defaultNewFilter}
+        toggleSaveFilterSetter={jest.fn()}
+        {...props}
+      />
+    );
+  };
+
+  describe('default Component Parameters', () => {
+    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();
+      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 () => {
+      const submitButtonElem = screen.getByText(defaultSubmitBtn);
+      expect(submitButtonElem).toBeDisabled();
+
+      const inputs = screen.getAllByRole('textbox');
+
+      const inputNameElement = inputs[0];
+      userEvent.type(inputNameElement, 'Hello World!');
+
+      const textAreaElement = inputs[1];
+      userEvent.type(textAreaElement, 'Hello World With TextArea');
+
+      await waitFor(() => {
+        expect(submitButtonElem).toBeEnabled();
+      });
+
+      userEvent.clear(inputNameElement);
+
+      await waitFor(() => {
+        expect(submitButtonElem).toBeDisabled();
+      });
+    });
+
+    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];
+      userEvent.type(textAreaElement, 'Hello World With TextArea');
+
+      userEvent.clear(inputNameElement);
+      userEvent.clear(textAreaElement);
+
+      await waitFor(() => {
+        const requiredFieldTextElements =
+          screen.getAllByText(/required field/i);
+        expect(requiredFieldTextElements).toHaveLength(2);
+      });
+    });
+  });
+
+  describe('Custom setup for the component', () => {
+    it('should render the input with default data if they are passed', () => {
+      setupComponent({
+        inputDisplayNameDefaultValue: mockData.name,
+        inputCodeDefaultValue: mockData.code,
+      });
+
+      const inputs = screen.getAllByRole('textbox');
+      const inputNameElement = inputs[0];
+      const textAreaElement = inputs[1];
+
+      expect(inputNameElement).toHaveValue(mockData.name);
+      expect(textAreaElement).toHaveValue(mockData.code);
+    });
+
+    it('should test whether the cancel callback is being called', async () => {
+      const cancelCallback = jest.fn();
+      setupComponent({
+        cancelBtnHandler: cancelCallback,
+      });
+      const cancelBtnElement = screen.getByText(/cancel/i);
+      userEvent.click(cancelBtnElement);
+      expect(cancelCallback).toBeCalled();
+    });
+
+    it('should test whether the submit Callback is being called', async () => {
+      const submitCallback = jest.fn() as (v: MessageFilters) => void;
+      setupComponent({
+        submitCallback,
+      });
+
+      const inputs = screen.getAllByRole('textbox');
+
+      const inputNameElement = inputs[0];
+      userEvent.type(inputNameElement, 'Hello World!');
+
+      const textAreaElement = inputs[1];
+      userEvent.type(textAreaElement, 'Hello World With TextArea');
+
+      const submitBtnElement = screen.getByText(defaultSubmitBtn);
+
+      await waitFor(() => {
+        expect(submitBtnElement).toBeEnabled();
+      });
+
+      userEvent.click(submitBtnElement);
+
+      await waitFor(() => {
+        expect(submitCallback).toBeCalled();
+      });
+    });
+
+    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,
+      });
+
+      const checkbox = screen.getByRole('checkbox');
+      expect(checkbox).toBeInTheDocument();
+      expect(checkbox).toBeChecked();
+    });
+
+    it('should pass and render the view props', () => {
+      const title = 'titleTest';
+      const createNewFilterText = 'createNewFilterTextTest';
+      const submitBtnText = 'submitBtnTextTest';
+      setupComponent({
+        title,
+        createNewFilterText,
+        submitBtnText,
+      });
+      expect(screen.getByText(title)).toBeInTheDocument();
+      expect(screen.getByText(createNewFilterText)).toBeInTheDocument();
+      expect(screen.getByText(submitBtnText)).toBeInTheDocument();
+    });
+  });
+});

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

@@ -0,0 +1,181 @@
+import React from 'react';
+import AddFilter, {
+  FilterModalProps,
+} from 'components/Topics/Topic/Details/Messages/Filters/AddFilter';
+import { render } from 'lib/testHelpers';
+import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/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>) =>
+  render(
+    <AddFilter
+      toggleIsOpen={jest.fn()}
+      addFilter={jest.fn()}
+      deleteFilter={jest.fn()}
+      activeFilterHandler={jest.fn()}
+      toggleEditModal={jest.fn()}
+      editFilter={jest.fn()}
+      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', () => {
+    setupComponent();
+    expect(screen.getByText('Created filters')).toBeInTheDocument();
+  });
+  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();
+    });
+  });
+  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 () => {
+      await waitFor(() => {
+        userEvent.click(screen.getByText('New filter'));
+      });
+      expect(
+        screen.getByRole('button', { name: /Add filter/i })
+      ).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');
+      });
+      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();
+    });
+  });
+  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();
+
+      const savedFilterElement = screen.getByRole('savedFilter');
+      userEvent.click(savedFilterElement);
+      userEvent.click(selectFilterButton);
+
+      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,
+      });
+      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');
+      });
+    });
+
+    it('OnSubmit condition with checkbox off functionality', async () => {
+      userEvent.click(screen.getAllByRole('button')[1]);
+      await waitFor(() => {
+        expect(activeFilterHandlerMock).toHaveBeenCalled();
+        expect(addFilterMock).not.toHaveBeenCalled();
+      });
+    });
+
+    it('OnSubmit condition with checkbox on functionality', async () => {
+      userEvent.click(screen.getByRole('checkbox'));
+
+      userEvent.click(screen.getAllByRole('button')[1]);
+      await waitFor(() => {
+        expect(activeFilterHandlerMock).not.toHaveBeenCalled();
+        expect(addFilterMock).toHaveBeenCalled();
+      });
+    });
+  });
+});

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

@@ -0,0 +1,52 @@
+import React from 'react';
+import EditFilter, {
+  EditFilterProps,
+} from 'components/Topics/Topic/Details/Messages/Filters/EditFilter';
+import { render } from 'lib/testHelpers';
+import { screen, waitFor, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
+
+const editFilter: FilterEdit = {
+  index: 0,
+  filter: { name: 'name', code: 'code' },
+};
+
+const setupComponent = (props?: Partial<EditFilterProps>) =>
+  render(
+    <EditFilter
+      toggleEditModal={jest.fn()}
+      editSavedFilter={jest.fn()}
+      editFilter={editFilter}
+      {...props}
+    />
+  );
+
+describe('EditFilter component', () => {
+  it('renders component', () => {
+    setupComponent();
+  });
+  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();
+    setupComponent({ toggleEditModal, editSavedFilter });
+    await waitFor(() => fireEvent.submit(screen.getByRole('form')));
+    expect(toggleEditModal).toHaveBeenCalledTimes(1);
+    expect(editSavedFilter).toHaveBeenCalledTimes(1);
+  });
+  it('checks input values to match', () => {
+    setupComponent();
+    expect(screen.getAllByRole('textbox')[0]).toHaveValue(
+      editFilter.filter.name
+    );
+    expect(screen.getAllByRole('textbox')[1]).toHaveValue(
+      editFilter.filter.code
+    );
+  });
+});

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

@@ -0,0 +1,35 @@
+import React from 'react';
+import FilterModal, {
+  FilterModalProps,
+} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
+import { render } from 'lib/testHelpers';
+import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+const filters: MessageFilters[] = [{ name: 'name', code: 'code' }];
+
+const setupWrapper = (props?: Partial<FilterModalProps>) =>
+  render(
+    <FilterModal
+      toggleIsOpen={jest.fn()}
+      filters={filters}
+      addFilter={jest.fn()}
+      deleteFilter={jest.fn()}
+      activeFilterHandler={jest.fn()}
+      editSavedFilter={jest.fn()}
+      {...props}
+    />
+  );
+describe('FilterModal component', () => {
+  beforeEach(() => {
+    setupWrapper();
+  });
+  it('renders component with add filter modal', () => {
+    expect(screen.getByText('Add filter')).toBeInTheDocument();
+  });
+  it('renders component with edit filter modal', async () => {
+    await waitFor(() => userEvent.click(screen.getByText('Edit')));
+    expect(screen.getByText('Edit saved filter')).toBeInTheDocument();
+  });
+});

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

@@ -1,9 +1,11 @@
 import React from 'react';
 import Filters, {
   FiltersProps,
+  SeekDirectionOptions,
+  SeekTypeOptions,
 } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
 import { render } from 'lib/testHelpers';
-import { screen } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 
 const setupWrapper = (props?: Partial<FiltersProps>) =>
@@ -71,23 +73,35 @@ describe('Filters component', () => {
     });
   });
   describe('Select elements', () => {
-    it('seekType select', () => {
+    let seekTypeSelects: HTMLElement[];
+    let options: HTMLElement[];
+    const selectedDirectionOptionValue = SeekDirectionOptions[0];
+
+    const mockDirectionOptionSelectLabel = selectedDirectionOptionValue.label;
+
+    const selectTypeOptionValue = SeekTypeOptions[0];
+
+    const mockTypeOptionSelectLabel = selectTypeOptionValue.label;
+
+    beforeEach(() => {
       setupWrapper();
-      const seekTypeSelect = screen.getAllByRole('listbox');
-      const option = screen.getAllByRole('option');
-      expect(option[0]).toHaveTextContent('Offset');
-      userEvent.click(seekTypeSelect[0]);
-      userEvent.selectOptions(seekTypeSelect[0], ['Timestamp']);
-      expect(option[0]).toHaveTextContent('Timestamp');
+      seekTypeSelects = screen.getAllByRole('listbox');
+      options = screen.getAllByRole('option');
+    });
+
+    it('seekType select', () => {
+      expect(options[0]).toHaveTextContent('Offset');
+      userEvent.click(seekTypeSelects[0]);
+      userEvent.selectOptions(seekTypeSelects[0], [mockTypeOptionSelectLabel]);
+      expect(options[0]).toHaveTextContent(mockTypeOptionSelectLabel);
       expect(screen.getByText('Submit')).toBeInTheDocument();
     });
     it('seekDirection select', () => {
-      setupWrapper();
-      const seekDirectionSelect = screen.getAllByRole('listbox');
-      const option = screen.getAllByRole('option');
-      userEvent.click(seekDirectionSelect[1]);
-      userEvent.selectOptions(seekDirectionSelect[1], ['Newest First']);
-      expect(option[1]).toHaveTextContent('Newest First');
+      userEvent.click(seekTypeSelects[1]);
+      userEvent.selectOptions(seekTypeSelects[1], [
+        mockDirectionOptionSelectLabel,
+      ]);
+      expect(options[1]).toHaveTextContent(mockDirectionOptionSelectLabel);
     });
   });
 
@@ -102,4 +116,46 @@ describe('Filters component', () => {
       expect(screen.getByText('Submit')).toBeInTheDocument();
     });
   });
+
+  describe('add new filter modal', () => {
+    it('renders addFilter modal', () => {
+      setupWrapper();
+      userEvent.click(screen.getByTestId('addFilterIcon'));
+      expect(screen.getByTestId('messageFilterModal')).toBeInTheDocument();
+    });
+  });
+
+  describe('when there is active smart filter', () => {
+    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 }))
+      );
+    });
+    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(
+        'activeSmartFilterCloseIcon'
+      );
+      await waitFor(() => {
+        userEvent.click(deleteIcon);
+      });
+
+      const anotherSmartFilterElement =
+        screen.queryByTestId('activeSmartFilter');
+      expect(anotherSmartFilterElement).not.toBeInTheDocument();
+    });
+  });
 });

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

@@ -1,4 +1,4 @@
-import * as React from 'react';
+import React from 'react';
 import dayjs from 'dayjs';
 import { TopicMessage } from 'generated-sources';
 import Dropdown from 'components/common/Dropdown/Dropdown';
@@ -10,6 +10,7 @@ import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import styled from 'styled-components';
 
 import MessageContent from './MessageContent/MessageContent';
+import * as S from './MessageContent/MessageContent.styled';
 
 const StyledDataCell = styled.td`
   overflow: hidden;
@@ -59,7 +60,16 @@ const Message: React.FC<{ message: TopicMessage }> = ({
           <div>{dayjs(timestamp).format('MM.DD.YYYY HH:mm:ss')}</div>
         </td>
         <StyledDataCell title={key}>{key}</StyledDataCell>
-        <StyledDataCell>{content}</StyledDataCell>
+        <StyledDataCell>
+          <S.Metadata>
+            <S.MetadataLabel>Range:</S.MetadataLabel>
+            <S.MetadataValue>{content}</S.MetadataValue>
+          </S.Metadata>
+          <S.Metadata>
+            <S.MetadataLabel>Version:</S.MetadataLabel>
+            <S.MetadataValue>3</S.MetadataValue>
+          </S.Metadata>
+        </StyledDataCell>
         <td style={{ width: '5%' }}>
           {vEllipsisOpen && (
             <Dropdown label={<VerticalElipsisIcon />} right>

+ 21 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/MessageContent.styled.ts

@@ -1,4 +1,5 @@
 import styled from 'styled-components';
+import { Link } from 'react-router-dom';
 
 export const Wrapper = styled.tr`
   background-color: ${({ theme }) => theme.topicMetaData.backgroundColor};
@@ -43,13 +44,13 @@ export const MetadataWrapper = styled.div`
 
 export const Metadata = styled.span`
   display: flex;
-  gap: 16px;
+  gap: 35px;
 `;
 
 export const MetadataLabel = styled.p`
   color: ${({ theme }) => theme.topicMetaData.color.label};
   font-size: 14px;
-  width: 80px;
+  width: 50px;
 `;
 
 export const MetadataValue = styled.p`
@@ -61,3 +62,21 @@ export const MetadataMeta = styled.p`
   color: ${({ theme }) => theme.topicMetaData.color.meta};
   font-size: 12px;
 `;
+
+export const PaginationButton = styled.button`
+  display: flex;
+  align-items: center;
+  padding: 6px 12px;
+  height: 32px;
+  border: 1px solid ${({ theme }) => theme.pagination.borderColor.normal};
+  box-sizing: border-box;
+  border-radius: 4px;
+  color: ${({ theme }) => theme.pagination.color.normal};
+  background: none;
+  font-family: Inter;
+  margin-right: 13px;
+  cursor: pointer;
+  font-size: 14px;
+`;
+
+export const SchemaLink = styled(Link)``;

+ 2 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/MessageContent.tsx

@@ -106,6 +106,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
                 <S.MetadataMeta>
                   Size: <BytesFormatted value={contentSize} />
                 </S.MetadataMeta>
+                <S.SchemaLink to="/">SchemaLink</S.SchemaLink>
               </span>
             </S.Metadata>
 
@@ -116,6 +117,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
                 <S.MetadataMeta>
                   Size: <BytesFormatted value={keySize} />
                 </S.MetadataMeta>
+                <S.SchemaLink to="/">SchemaLink</S.SchemaLink>
               </span>
             </S.Metadata>
           </S.MetadataWrapper>

+ 20 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageContent/__tests__/MessageContent.styled.spec.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import * as S from 'components/Topics/Topic/Details/Messages/MessageContent/MessageContent.styled';
+import { render } from 'lib/testHelpers';
+import { screen } from '@testing-library/react';
+import theme from 'theme/theme';
+
+describe('MessageContent Styled Components', () => {
+  describe('PaginationComponent', () => {
+    beforeEach(() => {
+      render(<S.PaginationButton />);
+    });
+    it('should test the Pagination Button theme related Props', () => {
+      const button = screen.getByRole('button');
+      expect(button).toHaveStyle(`color: ${theme.pagination.color.normal}`);
+      expect(button).toHaveStyle(
+        `border: 1px solid ${theme.pagination.borderColor.normal}`
+      );
+    });
+  });
+});

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

@@ -1,4 +1,3 @@
-import { Button } from 'components/common/Button/Button';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@@ -14,11 +13,12 @@ import {
 } from 'redux/reducers/topicMessages/selectors';
 
 import Message from './Message';
+import * as S from './MessageContent/MessageContent.styled';
 
 const MessagesPaginationWrapperStyled = styled.div`
   padding: 16px;
   display: flex;
-  justify-content: flex-end;
+  justify-content: flex-start;
 `;
 
 const MessagesTable: React.FC = () => {
@@ -77,8 +77,8 @@ const MessagesTable: React.FC = () => {
             <TableHeaderCell title="Offset" />
             <TableHeaderCell title="Partition" />
             <TableHeaderCell title="Timestamp" />
-            <TableHeaderCell title="Key" />
-            <TableHeaderCell title="Content" />
+            <TableHeaderCell title="Key" previewText="Preview" />
+            <TableHeaderCell title="Content" previewText="Preview" />
             <TableHeaderCell> </TableHeaderCell>
           </tr>
         </thead>
@@ -109,9 +109,7 @@ const MessagesTable: React.FC = () => {
         </tbody>
       </Table>
       <MessagesPaginationWrapperStyled>
-        <Button buttonType="secondary" buttonSize="M" onClick={handleNextClick}>
-          Next
-        </Button>
+        <S.PaginationButton onClick={handleNextClick}>Next</S.PaginationButton>
       </MessagesPaginationWrapperStyled>
     </>
   );

+ 1 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -39,7 +39,7 @@ describe('Details', () => {
 
   describe('when it has readonly flag', () => {
     it('does not render the Action button a Topic', () => {
-      const { baseElement } = render(
+      render(
         <ClusterContext.Provider
           value={{
             isReadOnly: true,
@@ -62,7 +62,6 @@ describe('Details', () => {
       );
 
       expect(screen.queryByText('Produce Message')).not.toBeInTheDocument();
-      expect(baseElement).toMatchSnapshot();
     });
   });
 

+ 0 - 133
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/__snapshots__/Details.spec.tsx.snap

@@ -1,133 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Details when it has readonly flag does not render the Action button a Topic 1`] = `
-.c0 {
-  height: 56px;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-pack: justify;
-  -webkit-justify-content: space-between;
-  -ms-flex-pack: justify;
-  justify-content: space-between;
-  -webkit-align-items: center;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  padding: 0px 16px;
-}
-
-.c0 h1 {
-  font-size: 24px;
-  font-weight: 500;
-  line-height: 32px;
-  color: #171A1C;
-}
-
-.c0 > div {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  gap: 16px;
-}
-
-.c2 {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  border-bottom: 1px #E3E6E8 solid;
-}
-
-.c2 a {
-  height: 40px;
-  width: 96px;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-pack: center;
-  -webkit-justify-content: center;
-  -ms-flex-pack: center;
-  justify-content: center;
-  -webkit-align-items: center;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  font-weight: 500;
-  font-size: 14px;
-  color: #73848C;
-  border-bottom: 1px transparent solid;
-}
-
-.c2 a.is-active {
-  border-bottom: 1px #4C4CFF solid;
-  color: #171A1C;
-}
-
-.c2 a:hover:not(.is-active) {
-  border-bottom: 1px transparent solid;
-  color: #171A1C;
-}
-
-.c1 {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-pack: end;
-  -webkit-justify-content: flex-end;
-  -ms-flex-pack: end;
-  justify-content: flex-end;
-  -webkit-align-self: center;
-  -ms-flex-item-align: center;
-  align-self: center;
-  gap: 26px;
-}
-
-<body>
-  <div>
-    <div>
-      <div
-        class="c0"
-      >
-        <h1>
-          __internal.topic
-        </h1>
-        <div>
-          <div
-            class="c1"
-          />
-        </div>
-      </div>
-      <nav
-        class="c2"
-        role="navigation"
-      >
-        <a
-          href="/clusters/local/topics/__internal.topic"
-        >
-          Overview
-        </a>
-        <a
-          href="/clusters/local/topics/__internal.topic/messages"
-        >
-          Messages
-        </a>
-        <a
-          href="/clusters/local/topics/__internal.topic/consumer-groups"
-        >
-          Consumers
-        </a>
-        <a
-          href="/clusters/local/topics/__internal.topic/settings"
-        >
-          Settings
-        </a>
-      </nav>
-    </div>
-  </div>
-</body>
-`;

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

@@ -67,7 +67,7 @@ export const OptionList = styled.ul`
   font-size: 14px;
   line-height: 18px;
   color: ${(props) => props.theme.select.color.normal};
-  overflow-y: scroll;
+  overflow-y: auto;
   z-index: 10;
   max-width: 300px;
   min-width: 100%;

+ 11 - 45
kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts

@@ -1,10 +1,8 @@
-import { SortOrder } from 'generated-sources';
 import styled, { css } from 'styled-components';
 
 interface TitleProps {
   isOrderable?: boolean;
   isOrdered?: boolean;
-  sortOrder?: SortOrder;
 }
 
 const orderableMixin = css(
@@ -47,48 +45,20 @@ const orderableMixin = css(
   `
 );
 
-const ASCMixin = css(
+const orderedMixin = css(
   ({ theme: { table } }) => `
   color: ${table.th.color.active};
-  cursor: pointer;
-
-  &::after {
-    cursor: pointer;
-    border: 4px solid transparent;
-    content: '';
-    display: block;
-    height: 0;
-    right: 5px;
-    top: 50%;
-    position: absolute;
-    border-top-color: ${table.th.color.active};
-    margin-top: 1px;
-  }
-  `
-);
-
-const DESCMixin = css(
-  ({ theme: { table } }) => `
-  color: ${table.th.color.active};
-  cursor: pointer;
-
-  &::before {
-    border: 4px solid transparent;
-    cursor: pointer;
-    content: '';
-    display: block;
-    height: 0;
-    right: 5px;
-    top: 50%;
-    position: absolute;
-    margin-top: -9px;
-    border-bottom-color: ${table.th.color.active};
-  }
+      &::before {
+        border-bottom-color: ${table.th.color.active};
+      }
+      &::after {
+        border-top-color: ${table.th.color.active};
+      }
   `
 );
 
 export const Title = styled.span<TitleProps>(
-  ({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`
+  ({ isOrderable, isOrdered, theme: { table } }) => css`
     font-family: Inter, sans-serif;
     font-size: 12px;
     font-style: normal;
@@ -96,20 +66,16 @@ export const Title = styled.span<TitleProps>(
     line-height: 16px;
     letter-spacing: 0em;
     text-align: left;
+    display: inline-block;
     justify-content: start;
-    display: flex;
     align-items: center;
     background: ${table.th.backgroundColor.normal};
     cursor: default;
     color: ${table.th.color.normal};
-    padding-right: 18px;
-    position: relative;
-
-    ${isOrderable && !isOrdered && orderableMixin}
 
-    ${isOrderable && isOrdered && sortOrder === SortOrder.ASC && ASCMixin}
+    ${isOrderable && orderableMixin}
 
-    ${isOrderable && isOrdered && sortOrder === SortOrder.DESC && DESCMixin}
+    ${isOrderable && isOrdered && orderedMixin}
   `
 );
 

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

@@ -7,6 +7,7 @@ export const Colors = {
     '10': '#E3E6E8',
     '15': '#D5DADD',
     '20': '#C7CED1',
+    '25': '#C4C4C4',
     '30': '#ABB5BA',
     '40': '#8F9CA3',
     '50': '#73848C',
@@ -195,6 +196,7 @@ const theme = {
     },
     overlay: Colors.transparency[10],
     shadow: Colors.transparency[20],
+    deletionTextColor: Colors.neutral[70],
   },
   table: {
     th: {
@@ -430,10 +432,18 @@ const theme = {
       circleBig: Colors.red[10],
       circleSmall: Colors.red[50],
     },
+    newFilterIcon: Colors.brand[50],
+    closeModalIcon: Colors.neutral[25],
   },
   viewer: {
     wrapper: Colors.neutral[3],
   },
+  savedFilterDivider: {
+    color: Colors.neutral[15],
+  },
+  editFilterText: {
+    color: Colors.brand[50],
+  },
 };
 
 export type ThemeType = typeof theme;