From 9198263e4711720dbf60032fe8f1601477101c88 Mon Sep 17 00:00:00 2001 From: Mgrdich Date: Sat, 24 Feb 2024 22:10:42 +0400 Subject: [PATCH] Fix the Select Generic systems + adding minor messages for the other components add filter query params code fixes minor component fix Message Filters query params setting Filter code generic fixes useMessagesFilters code fixes useMessagesFilters code modifications code fixes useMessagesFilters adding partitions useMessageFilters code modifications Bunch of components changes , and active filter changes add Register Smart filter mutation hook Add FlexBox component and QuestionInfo remove unecessary components AddFilter EditFilter. add params to the useMessagesFilters and minor util fixes submit the removed testing components add setSmartFilter code fixes and minor fixes for the store Fix the messages code filtering and modifications Saved Filters code modifications , to incorporate the new design AddEditFilterContainer component code modifications Minor fix in the message Filter Store useMessagesFilters code fixes components code fixes , add filter functionality persistant issue fix Filters components and styled Filter components Fix the SelectOption typings FlexBox code fix FilterSiderBar code fix FilterSiderBar Test additions Fix the SavedFilter components + delete obsolete code Messages Components test suites fixes AddEditFilterContainer.spec test suites part one AddEditFilterContainer.spec test suites part two FilterMetrics component code modifications remove the redux enshitifications and add tests suites for the MessagesTable Fix the valueSerde code fixes , and Filter tests suites pipelines code fixes fix the time format Table spec typescript code ActionSelect component type system fix Fix unecessary test suites fixes in the tables Fix linter issues add utils test suites Topic messages code fixes add Messages live mode and cursor bug fix fix the spec files Fix select input + bunch of cosmetic changes --- frontend/src/components/Schemas/New/New.tsx | 6 +- .../Filters/AddEditFilterContainer.tsx | 187 +++-- .../Topic/Messages/Filters/AddFilter.tsx | 122 --- .../Topic/Messages/Filters/EditFilter.tsx | 37 - .../Topic/Messages/Filters/FilterModal.tsx | 84 -- .../Topic/Messages/Filters/Filters.styled.ts | 213 ++--- .../Topics/Topic/Messages/Filters/Filters.tsx | 729 ++++-------------- .../Messages/Filters/FiltersContainer.ts | 36 - .../Topic/Messages/Filters/FiltersMetrics.tsx | 68 ++ .../Topic/Messages/Filters/FiltersSideBar.tsx | 64 ++ .../Topic/Messages/Filters/InfoModal.tsx | 5 +- .../Topic/Messages/Filters/QuestionInfo.tsx | 20 + .../Topic/Messages/Filters/SavedFilters.tsx | 113 ++- .../__tests__/AddEditFilterContainer.spec.tsx | 150 ++-- .../Filters/__tests__/AddFilter.spec.tsx | 221 ------ .../Filters/__tests__/EditFilter.spec.tsx | 68 -- .../Filters/__tests__/FilterModal.spec.tsx | 48 -- .../Filters/__tests__/FilterSideBar.spec.tsx | 62 ++ .../Filters/__tests__/Filters.spec.tsx | 351 ++++----- .../Filters/__tests__/FiltersMetrics.spec.tsx | 119 +++ .../Filters/__tests__/SavedFilters.spec.tsx | 141 ++-- .../Topics/Topic/Messages/Filters/utils.ts | 25 +- .../Topics/Topic/Messages/Messages.tsx | 107 +-- .../Topics/Topic/Messages/MessagesTable.tsx | 67 +- .../__test__/FiltersContainer.spec.tsx | 15 - .../Topic/Messages/__test__/Messages.spec.tsx | 109 +-- .../Messages/__test__/MessagesTable.spec.tsx | 104 +-- .../Topic/Messages/__test__/utils.spec.ts | 43 +- .../Topics/Topic/SendMessage/SendMessage.tsx | 4 +- .../Topics/Topic/SendMessage/utils.ts | 13 +- .../src/components/Topics/Topic/Topic.tsx | 9 +- .../Topics/shared/Form/TopicForm.tsx | 10 +- .../ActionSelect/ActionSelect.tsx | 6 +- .../src/components/common/Button/Button.tsx | 6 +- .../src/components/common/FlexBox/FlexBox.tsx | 39 + .../common/NewTable/__test__/Table.spec.tsx | 10 +- .../src/components/common/Search/Search.tsx | 10 +- .../common/Select/ControlledSelect.tsx | 10 +- .../src/components/common/Select/Select.tsx | 182 +++-- .../common/Select/__tests__/Select.spec.tsx | 23 +- .../SlidingSidebar/SlidingSidebar.styled.ts | 20 + .../common/SlidingSidebar/SlidingSidebar.tsx | 15 +- .../contexts/TopicMessagesContext.ts | 14 - frontend/src/lib/constants.ts | 7 +- frontend/src/lib/hooks/api/topicMessages.tsx | 191 ++--- frontend/src/lib/hooks/api/topics.ts | 1 + frontend/src/lib/hooks/filterUtils.ts | 17 + .../src/lib/hooks/useMessageFiltersStore.ts | 75 +- frontend/src/lib/hooks/useMessagesFilters.ts | 249 ++++++ frontend/src/lib/types.ts | 4 + frontend/src/redux/reducers/index.ts | 2 - .../topicMessages/__test__/fixtures.ts | 29 - .../topicMessages/__test__/reducer.spec.ts | 79 -- .../topicMessages/__test__/selectors.spec.ts | 62 -- .../redux/reducers/topicMessages/selectors.ts | 30 - .../topicMessages/topicMessagesSlice.ts | 57 -- 56 files changed, 1867 insertions(+), 2621 deletions(-) delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts create mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/FiltersMetrics.tsx create mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/FiltersSideBar.tsx create mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/QuestionInfo.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/__tests__/AddFilter.spec.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/__tests__/EditFilter.spec.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/__tests__/FilterModal.spec.tsx create mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/__tests__/FilterSideBar.spec.tsx create mode 100644 frontend/src/components/Topics/Topic/Messages/Filters/__tests__/FiltersMetrics.spec.tsx delete mode 100644 frontend/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx create mode 100644 frontend/src/components/common/FlexBox/FlexBox.tsx delete mode 100644 frontend/src/components/contexts/TopicMessagesContext.ts create mode 100644 frontend/src/lib/hooks/filterUtils.ts create mode 100644 frontend/src/lib/hooks/useMessagesFilters.ts create mode 100644 frontend/src/lib/types.ts delete mode 100644 frontend/src/redux/reducers/topicMessages/__test__/fixtures.ts delete mode 100644 frontend/src/redux/reducers/topicMessages/__test__/reducer.spec.ts delete mode 100644 frontend/src/redux/reducers/topicMessages/__test__/selectors.spec.ts delete mode 100644 frontend/src/redux/reducers/topicMessages/selectors.ts delete mode 100644 frontend/src/redux/reducers/topicMessages/topicMessagesSlice.ts diff --git a/frontend/src/components/Schemas/New/New.tsx b/frontend/src/components/Schemas/New/New.tsx index 3e5d39c9d..81c987ccd 100644 --- a/frontend/src/components/Schemas/New/New.tsx +++ b/frontend/src/components/Schemas/New/New.tsx @@ -13,7 +13,7 @@ import { useNavigate } from 'react-router-dom'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import Input from 'components/common/Input/Input'; import { FormError } from 'components/common/Input/Input.styled'; -import Select, { SelectOption } from 'components/common/Select/Select'; +import Select from 'components/common/Select/Select'; import { Button } from 'components/common/Button/Button'; import { Textarea } from 'components/common/Textbox/Textarea.styled'; import PageHeading from 'components/common/PageHeading/PageHeading'; @@ -27,7 +27,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import * as S from './New.styled'; -const SchemaTypeOptions: Array = [ +const SchemaTypeOptions = [ { value: SchemaType.AVRO, label: 'AVRO' }, { value: SchemaType.JSON, label: 'JSON' }, { value: SchemaType.PROTOBUF, label: 'PROTOBUF' }, @@ -131,7 +131,7 @@ const New: React.FC = () => { ( Save this filter @@ -100,32 +187,34 @@ const AddEditFilterContainer: React.FC = ({ inputSize="M" placeholder="Enter Name" autoComplete="off" - name="name" - defaultValue={inputDisplayNameDefaultValue} + name="id" />
- +
- - - + + {!isEdit && } + + + + diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx deleted file mode 100644 index 035d98c3a..000000000 --- a/frontend/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled'; -import { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters'; -import { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal'; -import SavedFilters from 'components/Topics/Topic/Messages/Filters/SavedFilters'; -import SavedIcon from 'components/common/Icons/SavedIcon'; -import QuestionIcon from 'components/common/Icons/QuestionIcon'; -import useBoolean from 'lib/hooks/useBoolean'; -import { showAlert } from 'lib/errorHandling'; - -import AddEditFilterContainer from './AddEditFilterContainer'; -import InfoModal from './InfoModal'; - -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; - isSavedFiltersOpen: boolean; - onClickSavedFilters(newValue: boolean): void; - activeFilter?: MessageFilters; -} - -export interface AddMessageFilters extends MessageFilters { - saveFilter: boolean; -} - -const AddFilter: React.FC = ({ - toggleIsOpen, - filters, - addFilter, - deleteFilter, - activeFilterHandler, - toggleEditModal, - editFilter, - isSavedFiltersOpen, - onClickSavedFilters, - activeFilter, -}) => { - const { value: isOpen, toggle } = useBoolean(); - - const onSubmit = React.useCallback( - async (values: AddMessageFilters) => { - const isFilterExists = filters.some( - (filter) => filter.name === values.name - ); - - if (isFilterExists) { - showAlert('error', { - id: '', - title: 'Validation Error', - message: 'Filter with the same name already exists', - }); - return; - } - - const data = { ...values }; - if (data.saveFilter) { - addFilter(data); - } else { - // other case is not applying the filter - const dataCodeLabel = - data.code.length > 16 ? `${data.code.slice(0, 16)}...` : data.code; - data.name = data.name || dataCodeLabel; - - activeFilterHandler(data, -1); - toggleIsOpen(); - } - }, - [activeFilterHandler, addFilter, toggleIsOpen] - ); - return ( - <> - - Add filter -
- - - - {isOpen && } -
-
- {isSavedFiltersOpen ? ( - onClickSavedFilters(!onClickSavedFilters)} - filters={filters} - onEdit={(index: number, filter: MessageFilters) => { - toggleEditModal(); - editFilter({ index, filter }); - }} - activeFilter={activeFilter} - /> - ) : ( - <> - onClickSavedFilters(!isSavedFiltersOpen)} - > - Saved Filters - - - - )} - - ); -}; - -export default AddFilter; diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx deleted file mode 100644 index 04f9c47ee..000000000 --- a/frontend/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters'; -import { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal'; - -import AddEditFilterContainer from './AddEditFilterContainer'; -import * as S from './Filters.styled'; - -export interface EditFilterProps { - editFilter: FilterEdit; - toggleEditModal(): void; - editSavedFilter(filter: FilterEdit): void; -} - -const EditFilter: React.FC = ({ - editFilter, - toggleEditModal, - editSavedFilter, -}) => { - const onSubmit = (values: MessageFilters) => { - editSavedFilter({ index: editFilter.index, filter: values }); - toggleEditModal(); - }; - return ( - <> - Edit filter - toggleEditModal()} - submitBtnText="Save" - inputDisplayNameDefaultValue={editFilter.filter.name} - inputCodeDefaultValue={editFilter.filter.code} - submitCallback={onSubmit} - /> - - ); -}; - -export default EditFilter; diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx deleted file mode 100644 index 0ada15878..000000000 --- a/frontend/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled'; -import { - ActiveMessageFilter, - MessageFilters, -} from 'components/Topics/Topic/Messages/Filters/Filters'; -import AddFilter from 'components/Topics/Topic/Messages/Filters/AddFilter'; -import EditFilter from 'components/Topics/Topic/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; - activeFilter: ActiveMessageFilter; - quickEditMode?: boolean; -} - -export interface FilterEdit { - index: number; - filter: MessageFilters; -} - -const FilterModal: React.FC = ({ - toggleIsOpen, - filters, - addFilter, - deleteFilter, - activeFilterHandler, - editSavedFilter, - activeFilter, - quickEditMode = false, -}) => { - const [isInEditMode, setIsInEditMode] = - React.useState(quickEditMode); - const [isSavedFiltersOpen, setIsSavedFiltersOpen] = - React.useState(false); - - const toggleEditModal = () => { - setIsInEditMode(!isInEditMode); - }; - - const [editFilter, setEditFilter] = React.useState(() => { - const { index, name, code } = activeFilter; - return quickEditMode - ? { index, filter: { name, code } } - : { index: -1, filter: { name: '', code: '' } }; - }); - const editFilterHandler = (value: FilterEdit) => { - setEditFilter(value); - setIsInEditMode(!isInEditMode); - }; - - const toggleEditModalHandler = quickEditMode ? toggleIsOpen : toggleEditModal; - - return ( - - {isInEditMode ? ( - - ) : ( - setIsSavedFiltersOpen(!isSavedFiltersOpen)} - activeFilter={activeFilter} - /> - )} - - ); -}; - -export default FilterModal; diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts index 7ec8fbde0..333a6209d 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts +++ b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts @@ -4,6 +4,9 @@ import styled, { css } from 'styled-components'; import DatePicker from 'react-datepicker'; import EditIcon from 'components/common/Icons/EditIcon'; import closeIcon from 'components/common/Icons/CloseIcon'; +import { PollingMode } from 'generated-sources'; + +import { isModeOptionWithInput } from './utils'; interface SavedFilterProps { selected: boolean; @@ -16,32 +19,9 @@ interface MessageLoadingSpinnerProps { isFetching: boolean; } -export const FiltersWrapper = styled.div` - display: flex; - flex-direction: column; - padding-left: 16px; - padding-right: 16px; - - & > div:first-child { - display: flex; - justify-content: space-between; - padding-top: 2px; - align-items: flex-end; - } -`; - -export const FilterInputs = styled.div` - display: flex; - gap: 8px; - align-items: flex-end; - width: 90%; - flex-wrap: wrap; -`; - -export const SeekTypeSelectorWrapper = styled.div` +export const FilterModeTypeSelectorWrapper = styled.div` display: flex; & .select-wrapper { - width: 40% !important; & > select { border-radius: 4px 0 0 4px !important; } @@ -83,14 +63,6 @@ export const DatePickerInput = styled(DatePicker)` } `; -export const FiltersMetrics = styled.div` - display: flex; - justify-content: flex-end; - align-items: center; - gap: 22px; - padding-top: 16px; - padding-bottom: 16px; -`; export const Message = styled.div` font-size: 14px; color: ${({ theme }) => theme.metrics.filters.color.normal}; @@ -106,22 +78,6 @@ export const MetricsIcon = styled.div` padding-right: 6px; height: 12px; `; - -export const ClearAll = styled.div` - color: ${({ theme }) => theme.metrics.filters.color.normal}; - font-size: 12px; - cursor: pointer; - line-height: 32px; - margin-left: 8px; -`; - -export const ButtonContainer = styled.div` - width: 100%; - display: flex; - justify-content: center; - margin-top: 20px; -`; - export const ListItem = styled.li` font-size: 12px; font-weight: 400; @@ -138,19 +94,6 @@ export const InfoParagraph = styled.div` color: ${({ theme }) => theme.table.td.color.normal}; `; -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.modal.border.contrast}; - box-shadow: ${({ theme }) => theme.modal.shadow}; - padding: 16px; - z-index: 1; -`; - export const InfoModal = styled.div` height: auto; width: 560px; @@ -171,42 +114,16 @@ export const QuestionIconContainer = styled.button` border: none; `; -export const FilterTitle = styled.h3` - line-height: 32px; - font-size: 20px; - margin-bottom: 40px; - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - color: ${({ theme }) => theme.modal.color}; - &:after { - content: ''; - width: calc(100% + 32px); - height: 1px; - position: absolute; - top: 40px; - left: -16px; - display: inline-block; - background-color: ${({ theme }) => theme.modal.border.top}; - } -`; - -export const CreatedFilter = styled.p` - margin: 25px 0 10px; - font-size: 14px; - line-height: 20px; - color: ${({ theme }) => theme.savedFilter.color}; -`; - export const NoSavedFilter = styled.p` - color: ${({ theme }) => theme.savedFilter.color}; + color: ${({ theme }) => theme.default.color.normal}; + font-size: 16px; + margin-top: 10px; `; export const SavedFiltersContainer = styled.div` overflow-y: auto; height: 195px; - justify-content: space-around; - padding-left: 10px; + display: flex; + flex-direction: column; `; export const SavedFilterName = styled.div` @@ -215,9 +132,9 @@ export const SavedFilterName = styled.div` color: ${({ theme }) => theme.savedFilter.filterName}; `; -export const FilterButtonWrapper = styled.div` +export const FilterButtonWrapper = styled.div<{ isEdit: boolean }>` display: flex; - justify-content: flex-end; + justify-content: ${(props) => (props.isEdit ? 'flex-end' : 'space-between')}; margin-top: 10px; gap: 10px; padding-top: 16px; @@ -234,24 +151,20 @@ export const FilterButtonWrapper = styled.div` } `; -export const ActiveSmartFilterWrapper = styled.div` - padding: 8px 0 5px; - display: flex; - gap: 10px; - align-items: center; - justify-content: flex-start; -`; - -export const DeleteSavedFilter = styled.div.attrs({ role: 'deleteIcon' })` - margin-top: 2px; +export const DeleteSavedFilter = styled.button` cursor: pointer; color: ${({ theme }) => theme.icons.deleteIcon}; + background-color: transparent; + border: none; `; -export const FilterEdit = styled.div` +export const FilterEdit = styled.button` font-weight: 500; font-size: 14px; line-height: 20px; + background-color: transparent; + border: none; + cursor: pointer; `; export const FilterOptions = styled.div` @@ -266,19 +179,20 @@ export const SavedFilter = styled.div.attrs({ })` display: flex; justify-content: space-between; - padding-right: 5px; + padding: 5px; height: 32px; + border-radius: 4px; align-items: center; cursor: pointer; - border-top: 1px solid ${({ theme }) => theme.panelColor.borderTop}; &:hover ${FilterOptions} { display: flex; } &:hover { - background: ${({ theme }) => theme.layout.stuffColor}; + background-color: ${({ theme }) => theme.layout.stuffColor}; } - background: ${({ selected, theme }) => - selected ? theme.layout.stuffColor : theme.modal.backgroundColor}; + + background-color: ${({ selected, theme }) => + selected ? theme.layout.stuffColor : 'transparent'}; `; export const ActiveSmartFilter = styled.div` @@ -293,7 +207,7 @@ export const ActiveSmartFilter = styled.div` line-height: 20px; `; -export const EditSmartFilterIcon = styled.div( +export const EditSmartFilterIcon = styled.button( ({ theme: { icons } }) => css` color: ${icons.editIcon.normal}; display: flex; @@ -302,19 +216,26 @@ export const EditSmartFilterIcon = styled.div( height: 32px; width: 32px; cursor: pointer; + background-color: transparent; + border: none; border-left: 1px solid ${icons.editIcon.border}; - &:hover { + &:hover:not(:disabled) { ${EditIcon} { fill: ${icons.editIcon.hover}; } } - &:active { + &:active:not(:disabled) { ${EditIcon} { fill: ${icons.editIcon.active}; } } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } ` ); @@ -323,7 +244,7 @@ export const SmartFilterName = styled.div` min-width: 32px; `; -export const DeleteSmartFilterIcon = styled.div( +export const DeleteSmartFilterIcon = styled.button( ({ theme: { icons } }) => css` color: ${icons.closeIcon.normal}; display: flex; @@ -332,6 +253,8 @@ export const DeleteSmartFilterIcon = styled.div( height: 32px; width: 32px; cursor: pointer; + background-color: transparent; + border: none; border-left: 1px solid ${icons.closeIcon.border}; svg { @@ -339,17 +262,22 @@ export const DeleteSmartFilterIcon = styled.div( width: 14px; } - &:hover { + &:hover:not(:disabled) { ${closeIcon} { fill: ${icons.closeIcon.hover}; } } - &:active { + &:active:not(:disabled) { ${closeIcon} { fill: ${icons.closeIcon.active}; } } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } ` ); @@ -363,10 +291,12 @@ export const MessageLoading = styled.div.attrs({ width: 250px; `; -export const StopLoading = styled.div` +export const StopLoading = styled.button` color: ${({ theme }) => theme.pageLoader.borderColor}; font-size: ${({ theme }) => theme.heading.h3.fontSize}; cursor: pointer; + background-color: transparent; + border: none; `; export const MessageLoadingSpinner = styled.div` @@ -388,39 +318,30 @@ export const MessageLoadingSpinner = styled.div` } `; -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.editFilter.textColor}; - font-weight: 500; +// styled component lib bug it does not pick up the generic +export const FilterModeTypeSelect = styled(Select)` + border-top-right-radius: ${(props) => + !props.value || !isModeOptionWithInput(props.value) ? '4px' : '0'}; + border-bottom-right-radius: ${(props) => + !props.value || !isModeOptionWithInput(props.value) ? '4px' : '0'}; + user-select: none; `; -export const SavedFiltersText = styled.div` - ${textStyle}; - margin-left: 7px; +export const SavedFilterText = styled.div` + font-weight: 600; + color: ${({ theme }) => theme.default.color.normal}; `; -export const BackToCustomText = styled.div` - ${textStyle}; +export const SavedFilterClearAll = styled.button` + font-weight: 500; + color: ${({ theme }) => theme.link.color}; + background-color: transparent; + border: none; cursor: pointer; -`; + font-size: 16px; -export const SeekTypeSelect = styled(Select)` - border-top-right-radius: 0; - border-bottom-right-radius: 0; - user-select: none; -`; - -export const Serdes = styled.div` - display: flex; - gap: 24px; - padding: 8px 0; + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } `; diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx index 99eb6bac4..3416f9391 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -1,645 +1,236 @@ import 'react-datepicker/dist/react-datepicker.css'; -import { - MessageFilterType, - Partition, - SeekDirection, - SeekType, - SerdeUsage, - TopicMessage, - TopicMessageConsuming, - TopicMessageEvent, - TopicMessageEventTypeEnum, -} from 'generated-sources'; -import React, { useContext } from 'react'; -import omitBy from 'lodash/omitBy'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { SerdeUsage, TopicMessageConsuming } from 'generated-sources'; +import React, { useMemo, useState } from 'react'; import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; -import { Option } from 'react-multi-select-component'; -import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import { BASE_PARAMS } from 'lib/constants'; import Select from 'components/common/Select/Select'; import { Button } from 'components/common/Button/Button'; import Search from 'components/common/Search/Search'; -import FilterModal, { - FilterEdit, -} from 'components/Topics/Topic/Messages/Filters/FilterModal'; -import { SeekDirectionOptions } from 'components/Topics/Topic/Messages/Messages'; -import TopicMessagesContext from 'components/contexts/TopicMessagesContext'; -import useBoolean from 'lib/hooks/useBoolean'; -import { RouteParamsClusterTopic } from 'lib/paths'; -import useAppParams from 'lib/hooks/useAppParams'; import PlusIcon from 'components/common/Icons/PlusIcon'; -import EditIcon from 'components/common/Icons/EditIcon'; -import CloseIcon from 'components/common/Icons/CloseIcon'; -import ClockIcon from 'components/common/Icons/ClockIcon'; -import ArrowDownIcon from 'components/common/Icons/ArrowDownIcon'; -import FileIcon from 'components/common/Icons/FileIcon'; -import { useTopicDetails } from 'lib/hooks/api/topics'; -import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { getSerdeOptions } from 'components/Topics/Topic/SendMessage/utils'; import { useSerdes } from 'lib/hooks/api/topicMessages'; +import useAppParams from 'lib/hooks/useAppParams'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import { useMessagesFilters } from 'lib/hooks/useMessagesFilters'; +import { ModeOptions } from 'lib/hooks/filterUtils'; +import { useTopicDetails } from 'lib/hooks/api/topics'; +import EditIcon from 'components/common/Icons/EditIcon'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import FlexBox from 'components/common/FlexBox/FlexBox'; import * as S from './Filters.styled'; import { + ADD_FILTER_ID, filterOptions, - getOffsetFromSeekToParam, - getSelectedPartitionsFromSeekToParam, - getTimestampFromSeekToParam, + isLiveMode, + isModeOffsetSelector, + isModeOptionWithInput, } from './utils'; - -type Query = Record; +import FiltersSideBar from './FiltersSideBar'; +import FiltersMetrics from './FiltersMetrics'; export interface FiltersProps { phaseMessage?: string; - meta: TopicMessageConsuming; + meta?: TopicMessageConsuming; isFetching: boolean; - messageEventType?: string; - - addMessage(content: { message: TopicMessage; prepend: boolean }): void; - - resetMessages(): void; - - updatePhase(phase: string): void; - - updateMeta(meta: TopicMessageConsuming): void; - - setIsFetching(status: boolean): void; - - setMessageType(messageType: string): void; -} - -export interface MessageFilters { - name: string; - code: string; -} - -export interface ActiveMessageFilter { - index: number; - name: string; - code: string; + abortFetchData: () => void; } -const PER_PAGE = 100; - -export const SeekTypeOptions = [ - { value: SeekType.OFFSET, label: 'Offset' }, - { value: SeekType.TIMESTAMP, label: 'Timestamp' }, -]; - const Filters: React.FC = ({ - phaseMessage, - meta: { elapsedMs, bytesConsumed, messagesConsumed, filterApplyErrors }, + meta, isFetching, - addMessage, - resetMessages, - updatePhase, - updateMeta, - setIsFetching, - setMessageType, - messageEventType, + abortFetchData, + phaseMessage, }) => { const { clusterName, topicName } = useAppParams(); - const location = useLocation(); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const page = searchParams.get('page'); + const { + mode, + setMode, + date, + setTimeStamp, + keySerde, + setKeySerde, + valueSerde, + setValueSerde, + offset, + setOffsetValue, + search, + setSearch, + partitions: p, + setPartition, + smartFilter, + setSmartFilter, + refreshData, + } = useMessagesFilters(); const { data: topic } = useTopicDetails({ clusterName, topicName }); - - const partitions = topic?.partitions || []; - - const { seekDirection, isLive, changeSeekDirection } = - useContext(TopicMessagesContext); - - const { value: isOpen, toggle } = useBoolean(); - - const { value: isQuickEditOpen, toggle: toggleQuickEdit } = useBoolean(); - - const source = React.useRef(null); - - const [selectedPartitions, setSelectedPartitions] = React.useState( - getSelectedPartitionsFromSeekToParam(searchParams, partitions) - ); - - const [currentSeekType, setCurrentSeekType] = React.useState( - SeekTypeOptions.find( - (ele) => ele.value === (searchParams.get('seekType') as SeekType) - ) !== undefined - ? (searchParams.get('seekType') as SeekType) - : SeekType.OFFSET - ); - const [offset, setOffset] = React.useState( - getOffsetFromSeekToParam(searchParams) - ); - - const [timestamp, setTimestamp] = React.useState( - getTimestampFromSeekToParam(searchParams) - ); - const [keySerde, setKeySerde] = React.useState( - searchParams.get('keySerde') || '' - ); - const [valueSerde, setValueSerde] = React.useState( - searchParams.get('valueSerde') || '' - ); - - const [savedFilters, setSavedFilters] = React.useState( - JSON.parse(localStorage.getItem('savedFilters') ?? '[]') - ); - - let storageActiveFilter = localStorage.getItem('activeFilter'); - storageActiveFilter = - storageActiveFilter ?? JSON.stringify({ name: '', code: '', index: -1 }); - - const [activeFilter, setActiveFilter] = React.useState( - JSON.parse(storageActiveFilter) - ); - - const [queryType, setQueryType] = React.useState( - activeFilter.name - ? MessageFilterType.CEL_SCRIPT - : MessageFilterType.STRING_CONTAINS - ); - const [query, setQuery] = React.useState(searchParams.get('q') || ''); - const [isTailing, setIsTailing] = React.useState(isLive); - - const isSeekTypeControlVisible = React.useMemo( - () => selectedPartitions.length > 0, - [selectedPartitions] - ); - - const isSubmitDisabled = React.useMemo(() => { - if (isSeekTypeControlVisible) { - return ( - (currentSeekType === SeekType.TIMESTAMP && !timestamp) || isTailing - ); - } - - return false; - }, [isSeekTypeControlVisible, currentSeekType, timestamp, isTailing]); - - const partitionMap = React.useMemo( - () => - partitions.reduce>( - (acc, partition) => ({ - ...acc, - [partition.partition]: partition, - }), - {} - ), - [partitions] - ); - - const handleClearAllFilters = () => { - setCurrentSeekType(SeekType.OFFSET); - setOffset(''); - setTimestamp(null); - setQuery(''); - changeSeekDirection(SeekDirection.FORWARD); - getSelectedPartitionsFromSeekToParam(searchParams, partitions); - setSelectedPartitions( - partitions.map((partition: Partition) => { - return { - value: partition.partition, - label: `Partition #${partition.partition.toString()}`, + const [createdEditedSmartId, setCreatedEditedSmartId] = useState(); + + const partitions = useMemo(() => { + return (topic?.partitions || []).reduce<{ + dict: Record; + list: { label: string; value: number }[]; + }>( + (acc, currentValue) => { + const label = { + label: `Partition #${currentValue.partition.toString()}`, + value: currentValue.partition, }; - }) - ); - }; - - const handleFiltersSubmit = (currentOffset: string) => { - const nextAttempt = Number(searchParams.get('attempt') || 0) + 1; - const props: Query = { - q: queryType === MessageFilterType.CEL_SCRIPT ? activeFilter.code : query, - filterQueryType: queryType, - attempt: nextAttempt, - limit: PER_PAGE, - page: page || 0, - seekDirection, - keySerde: keySerde || searchParams.get('keySerde') || '', - valueSerde: valueSerde || searchParams.get('valueSerde') || '', - }; - - if (isSeekTypeControlVisible) { - switch (seekDirection) { - case SeekDirection.FORWARD: - props.seekType = SeekType.BEGINNING; - break; - case SeekDirection.BACKWARD: - case SeekDirection.TAILING: - props.seekType = SeekType.LATEST; - break; - default: - props.seekType = currentSeekType; - } - - if (offset && currentSeekType === SeekType.OFFSET) { - props.seekType = SeekType.OFFSET; - } - - if (timestamp && currentSeekType === SeekType.TIMESTAMP) { - props.seekType = SeekType.TIMESTAMP; - } - - const isSeekTypeWithSeekTo = - props.seekType === SeekType.TIMESTAMP || - props.seekType === SeekType.OFFSET; - - if ( - selectedPartitions.length !== partitions.length || - isSeekTypeWithSeekTo - ) { - // not everything in the partition is selected - props.seekTo = selectedPartitions.map(({ value }) => { - const offsetProperty = - seekDirection === SeekDirection.FORWARD ? 'offsetMin' : 'offsetMax'; - const offsetBasedSeekTo = - currentOffset || partitionMap[value][offsetProperty]; - const seekToOffset = - currentSeekType === SeekType.OFFSET - ? offsetBasedSeekTo - : timestamp?.getTime(); - - return `${value}::${seekToOffset || '0'}`; - }); - } - } - - const newProps = omitBy(props, (v) => v === undefined || v === ''); - const qs = Object.keys(newProps) - .map((key) => `${key}=${encodeURIComponent(newProps[key] as string)}`) - .join('&'); - navigate({ - search: `?${qs}`, - }); - }; - - const handleSSECancel = () => { - if (!source.current) return; - setIsFetching(false); - 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 }) + // eslint-disable-next-line no-param-reassign + acc.dict[label.value] = label; + acc.list.push(label); + return acc; + }, + { dict: {}, list: [] } ); - setActiveFilter({ index, ...newActiveFilter }); - setQueryType(MessageFilterType.CEL_SCRIPT); - }; - - const composeMessageFilter = (filter: FilterEdit): ActiveMessageFilter => ({ - index: filter.index, - name: filter.filter.name, - code: filter.filter.code, - }); + }, [topic?.partitions]); - const storeAsActiveFilter = (filter: FilterEdit) => { - const messageFilter = JSON.stringify(composeMessageFilter(filter)); - localStorage.setItem('activeFilter', messageFilter); - }; - - const editSavedFilter = (filter: FilterEdit) => { - const filters = [...savedFilters]; - filters[filter.index] = filter.filter; - if (activeFilter.name && activeFilter.index === filter.index) { - setActiveFilter(composeMessageFilter(filter)); - storeAsActiveFilter(filter); - } - localStorage.setItem('savedFilters', JSON.stringify(filters)); - setSavedFilters(filters); - }; - - const editCurrentFilter = (filter: FilterEdit) => { - if (filter.index < 0) { - setActiveFilter(composeMessageFilter(filter)); - storeAsActiveFilter(filter); - } else { - editSavedFilter(filter); - } - }; - // eslint-disable-next-line consistent-return - React.useEffect(() => { - if (location.search?.length !== 0) { - const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent( - clusterName - )}/topics/${topicName}/messages${location.search}`; - const sse = new EventSource(url); + const partitionValue = useMemo(() => { + return p.map((value) => partitions.dict[value]); + }, [p, partitions]); - source.current = sse; - setIsFetching(true); - - sse.onopen = () => { - resetMessages(); - setIsFetching(true); - }; - sse.onmessage = ({ data }) => { - const { type, message, phase, consuming }: TopicMessageEvent = - JSON.parse(data); - switch (type) { - case TopicMessageEventTypeEnum.MESSAGE: - if (message) { - addMessage({ - message, - prepend: isLive, - }); - } - break; - case TopicMessageEventTypeEnum.PHASE: - if (phase?.name) { - updatePhase(phase.name); - } - break; - case TopicMessageEventTypeEnum.CONSUMING: - if (consuming) updateMeta(consuming); - break; - case TopicMessageEventTypeEnum.DONE: - if (consuming && type) { - setMessageType(type); - updateMeta(consuming); - } - break; - default: - } - }; - - sse.onerror = () => { - setIsFetching(false); - sse.close(); - }; - - return () => { - setIsFetching(false); - sse.close(); - }; - } - }, [ - clusterName, - topicName, - seekDirection, - location, - addMessage, - resetMessages, - setIsFetching, - updateMeta, - updatePhase, - ]); - React.useEffect(() => { - if (location.search?.length === 0) { - handleFiltersSubmit(offset); - } - }, [ - seekDirection, - queryType, - activeFilter, - currentSeekType, - timestamp, - query, - location, - ]); - React.useEffect(() => { - handleFiltersSubmit(offset); - }, [ - seekDirection, - queryType, - activeFilter, - currentSeekType, - timestamp, - query, - seekDirection, - page, - ]); - - React.useEffect(() => { - setIsTailing(isLive); - }, [isLive]); - - const { data: serdes = {} } = useSerdes({ + const { data: serdes = {}, isLoading } = useSerdes({ clusterName, topicName, use: SerdeUsage.DESERIALIZE, }); + const handleRefresh = () => { + if (isLiveMode(mode) && isFetching) { + abortFetchData(); + } + refreshData(); + }; + return ( - -
- -
- Seek Type - - setCurrentSeekType(option as SeekType)} - value={currentSeekType} - selectSize="M" - minWidth="100px" - options={SeekTypeOptions} - disabled={isTailing} - /> + + + + + - {currentSeekType === SeekType.OFFSET ? ( + {isModeOptionWithInput(mode) && + (isModeOffsetSelector(mode) ? ( setOffset(value)} - disabled={isTailing} + onChange={({ target: { value } }) => { + setOffsetValue(value); + }} /> ) : ( setTimestamp(date)} + selected={date} + onChange={setTimeStamp} showTimeInput timeInputLabel="Time:" - dateFormat="MMM d, yyyy HH:mm" + dateFormat="MMM d, yyyy" placeholderText="Select timestamp" - disabled={isTailing} /> - )} - -
-
- Partitions - ({ - label: `Partition #${p.partition.toString()}`, - value: p.partition, - }))} - filterOptions={filterOptions} - value={selectedPartitions} - onChange={setSelectedPartitions} - labelledBy="Select partitions" - disabled={isTailing} - /> -
-
- Key Serde - setValueSerde(option as string)} - options={getSerdeOptions(serdes.value || [])} - value={searchParams.get('valueSerde') as string} - minWidth="170px" - selectSize="M" - disabled={isTailing} - /> -
- Clear all + ))} + + + -
- ); describe('Custom Select', () => { @@ -63,18 +60,4 @@ describe('Custom Select', () => { expect(getOption()).toHaveTextContent(normalOptionLabel); }); }); - - describe('when non-live', () => { - it('there is not live icon', () => { - renderComponent({ isLive: false }); - expect(screen.queryByTestId('liveIcon')).not.toBeInTheDocument(); - }); - }); - - describe('when live', () => { - it('there is live icon', () => { - render(