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..3e609fb03 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; + } ` ); @@ -360,13 +288,15 @@ export const MessageLoading = styled.div.attrs({ font-size: ${({ theme }) => theme.heading.h3.fontSize}; display: ${({ isLive }) => (isLive ? 'flex' : 'none')}; justify-content: space-around; - width: 250px; + width: 260px; `; -export const StopLoading = styled.div` - color: ${({ theme }) => theme.pageLoader.borderColor}; +export const StopLoading = styled.button` + color: ${({ theme }) => theme.heading.base.color}; 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..b8fc307ed 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -1,645 +1,239 @@ 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; + consumptionStats?: 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 }, + consumptionStats, 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, - }); - - 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); + }, [topic?.partitions]); - source.current = sse; - setIsFetching(true); + const partitionValue = useMemo(() => { + return p.map((value) => partitions.dict[value]); + }, [p, partitions]); - 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(