From 32fe3232cc94521cba961ad405d9c0ddc88cd656 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 19 May 2022 08:43:10 +0400 Subject: [PATCH 1/2] #RI-2763-add common Range Filter, add tests --- .../range-filter/RangeFilter.spec.tsx | 76 +++++++ .../components/range-filter/RangeFilter.tsx | 148 +++++++++++++ .../range-filter/styles.module.scss | 126 +++++++++++ .../StreamDetails/StreamDetails.tsx | 58 +++++- .../StreamDetails/StreamRangeFilter.spec.tsx | 12 -- .../StreamDetails/StreamRangeFilter.tsx | 197 ------------------ .../StreamDetails/styles.module.scss | 116 +---------- 7 files changed, 409 insertions(+), 324 deletions(-) create mode 100644 redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx create mode 100644 redisinsight/ui/src/components/range-filter/RangeFilter.tsx create mode 100644 redisinsight/ui/src/components/range-filter/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.spec.tsx delete mode 100644 redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.tsx diff --git a/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx b/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx new file mode 100644 index 0000000000..c08961cd37 --- /dev/null +++ b/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import RangeFilter, { Props } from './RangeFilter' + +const mockedProps = mock() + +const startRangeTestId = 'range-start-input' +const endRangeTestId = 'range-end-input' +const resetBtnTestId = 'range-filter-btn' + +describe('StreamRangeFilter', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call handleChangeStart onChange start range thumb', () => { + const handleChangeStart = jest.fn() + render( + + ) + const startRangeInput = screen.getByTestId(startRangeTestId) + + fireEvent.change( + startRangeInput, + { target: { value: 123 } } + ) + expect(handleChangeStart).toBeCalledTimes(1) + }) + + it('should call handleChangeEnd onChange end range thumb', () => { + const handleChangeEnd = jest.fn() + render( + + ) + const endRangeInput = screen.getByTestId(endRangeTestId) + + fireEvent.change( + endRangeInput, + { target: { value: 15 } } + ) + expect(handleChangeEnd).toBeCalledTimes(1) + }) + it('should reset start and end values on press Reset buttons', () => { + const handleChangeEnd = jest.fn() + const handleChangeStart = jest.fn() + + render( + + ) + const resetBtn = screen.getByTestId(resetBtnTestId) + + fireEvent.click(resetBtn) + + expect(handleChangeEnd).toBeCalledWith(120) + expect(handleChangeStart).toBeCalledWith(1) + }) +}) diff --git a/redisinsight/ui/src/components/range-filter/RangeFilter.tsx b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx new file mode 100644 index 0000000000..73e705faae --- /dev/null +++ b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx @@ -0,0 +1,148 @@ +import React, { useCallback, useEffect, useRef } from 'react' +import cx from 'classnames' + +import { getFormatTime, } from 'uiSrc/utils/streamUtils' + +import styles from './styles.module.scss' + +const buttonString = 'Reset Filter' + +export interface Props { + max: number + min: number + start: number + end: number + handleChangeStart: (value: number) => void + handleChangeEnd: (value: number) => void +} + +function usePrevious(value: any) { + const ref = useRef() + useEffect(() => { + ref.current = value + }) + return ref.current +} + +const RangeFilter = ({ max, min, start, end, handleChangeStart, handleChangeEnd }: Props) => { + const getPercent = useCallback( + (value) => Math.round(((value - min) / (max - min)) * 100), + [min, max] + ) + + const minValRef = useRef(null) + const maxValRef = useRef(null) + const range = useRef(null) + + const prevValue = usePrevious({ max }) ?? { max: 0 } + + const resetFilter = useCallback( + () => { + handleChangeStart(min) + handleChangeEnd(max) + }, + [min, max] + ) + + useEffect(() => { + if (maxValRef.current) { + const minPercent = getPercent(start) + const maxPercent = getPercent(+maxValRef.current.value) + + if (range.current) { + range.current.style.left = `${minPercent}%` + range.current.style.width = `${maxPercent - minPercent}%` + } + } + }, [start, getPercent]) + + useEffect(() => { + if (minValRef.current) { + const minPercent = getPercent(+minValRef.current.value) + const maxPercent = getPercent(end) + + if (range.current) { + range.current.style.width = `${maxPercent - minPercent}%` + } + } + }, [end, getPercent]) + + useEffect(() => { + if (max && prevValue && prevValue.max !== max && end === prevValue.max) { + handleChangeEnd(max) + } + }, [prevValue]) + + if (start === 0 && end === 0) { + return ( +
+
+
+ ) + } + + if (start === end) { + return ( +
+
+
{getFormatTime(start?.toString())}
+
{getFormatTime(end?.toString())}
+
+
+ ) + } + + return ( + <> +
+ { + const value = Math.min(+event.target.value, end - 1) + handleChangeStart(value) + event.target.value = value.toString() + }} + className={cx(styles.thumb, styles.thumbZindex3)} + data-testid="range-start-input" + /> + { + const value = Math.max(+event.target.value, start + 1) + handleChangeEnd(value) + event.target.value = value.toString() + }} + className={cx(styles.thumb, styles.thumbZindex4)} + data-testid="range-end-input" + /> +
+
+
+
{getFormatTime(start?.toString())}
+
{getFormatTime(end?.toString())}
+
+
+
+ {(start !== min || end !== max) && ( + + )} + + ) +} + +export default RangeFilter diff --git a/redisinsight/ui/src/components/range-filter/styles.module.scss b/redisinsight/ui/src/components/range-filter/styles.module.scss new file mode 100644 index 0000000000..9260ac0b86 --- /dev/null +++ b/redisinsight/ui/src/components/range-filter/styles.module.scss @@ -0,0 +1,126 @@ +.rangeWrapper { + margin: 30px 30px 26px; + padding: 12px 0; +} + +.resetButton { + position: absolute; + right: 30px; + top: 80px; + z-index: 10; + text-decoration: underline; + color: var(--euiTextSubduedColor); + &:hover, + &:focus { + color: var(--euiTextColor); + } + font: normal normal 500 13px/18px Graphik, sans-serif; +} + +.slider { + position: relative; + width: 100%; +} + +.sliderTrack, +.sliderRange, +.sliderLeftValue, +.sliderRightValue { + position: absolute; +} + +.sliderTrack { + background-color: var(--separatorColor); + width: 100%; + height: 1px; + margin-top: 2px; + z-index: 1; +} + +.sliderRange { + height: 5px; + background-color: var(--euiColorPrimary); + z-index: 2; +} + +.sliderLeftValue, +.sliderRightValue { + width: max-content; + color: var(--euiColorMediumShade); + font: normal normal normal 12px/18px Graphik; + margin-top: 20px; +} + +.sliderLeftValue { + left: 0; + margin-top: -30px; +} + +.sliderRightValue { + right: -4px; +} + +.mockRange { + left: 30px; + width: calc(100% - 56px); +} + +.thumb, +.thumb::-webkit-slider-thumb { + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; +} + +.thumb { + pointer-events: none; + position: absolute; + height: 0; + width: calc(100% - 60px); + outline: none; +} + +.thumbZindex3 { + z-index: 3; +} + +.thumbZindex4 { + z-index: 4; +} + +.thumb::-moz-range-thumb { + width: 2px; + height: 12px; + background-color: var(--euiColorPrimary); + border: none; + cursor: pointer; + margin-top: 4px; + pointer-events: all; + position: relative; +} + +.thumbZindex3::-moz-range-thumb { + transform: translateY(-4px); +} + +.thumbZindex4::-moz-range-thumb { + transform: translateY(8px); +} + +input[type='range']::-webkit-slider-thumb { + width: 2px; + height: 12px; + background-color: var(--euiColorPrimary); + border: none; + cursor: pointer; + margin-top: 4px; + pointer-events: all; + position: relative; +} + +input[type='range']:first-child::-webkit-slider-thumb { + transform: translateY(-4px); +} + +input[type='range']:last-of-type::-webkit-slider-thumb { + transform: translateY(8px); +} diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx index def8e99e13..e41e4adfdd 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamDetails.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useCallback, useMemo, useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { last, isNull } from 'lodash' import cx from 'classnames' @@ -7,18 +7,20 @@ import { EuiButtonIcon, EuiProgress } from '@elastic/eui' import { fetchMoreStreamEntries, fetchStreamEntries, + updateStart, + updateEnd, streamDataSelector, streamSelector, streamRangeSelector, } from 'uiSrc/slices/browser/stream' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' +import RangeFilter from 'uiSrc/components/range-filter/RangeFilter' import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { SortOrder } from 'uiSrc/constants' import { getTimestampFromId } from 'uiSrc/utils/streamUtils' import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' -import StreamRangeFilter from './StreamRangeFilter' import styles from './styles.module.scss' @@ -95,10 +97,60 @@ const StreamDetails = (props: Props) => { dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order)) } + const handleChangeStartFilter = useCallback( + (value: number) => { + dispatch(updateStart(value.toString())) + }, + [] + ) + + const handleChangeEndFilter = useCallback( + (value: number) => { + dispatch(updateEnd(value.toString())) + }, + [] + ) + + const firstEntryTimeStamp = useMemo(() => getTimestampFromId(firstEntry?.id), [firstEntry?.id]) + const lastEntryTimeStamp = useMemo(() => getTimestampFromId(lastEntry?.id), [lastEntry?.id]) + + const startNumber = useMemo(() => (start === '' ? 0 : parseInt(start, 10)), [start]) + const endNumber = useMemo(() => (end === '' ? 0 : parseInt(end, 10)), [end]) + + useEffect(() => { + if (start === '' && firstEntry?.id !== '') { + dispatch(updateStart(firstEntryTimeStamp.toString())) + } + }, [firstEntryTimeStamp]) + + useEffect(() => { + if (end === '' && lastEntry?.id !== '') { + dispatch(updateEnd(lastEntryTimeStamp.toString())) + } + }, [lastEntryTimeStamp]) + + useEffect(() => { + if (start !== '' && end !== '') { + dispatch(fetchStreamEntries( + key, + SCAN_COUNT_DEFAULT, + sortedColumnOrder, + false + )) + } + }, [start, end]) + return ( <> {shouldFilterRender ? ( - + ) : (
diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.spec.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.spec.tsx deleted file mode 100644 index a3feafcca6..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' -import StreamRangeFilter, { Props } from './StreamDetails' - -const mockedProps = mock() - -describe('StreamRangeFilter', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.tsx deleted file mode 100644 index cd6b15c823..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/StreamRangeFilter.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React, { useCallback, useEffect, useRef } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import cx from 'classnames' - -import { - fetchStreamEntries, - streamRangeSelector, - streamDataSelector, - updateStart, - updateEnd, -} from 'uiSrc/slices/browser/stream' -import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' -import { getFormatTime, getTimestampFromId } from 'uiSrc/utils/streamUtils' -import { SortOrder } from 'uiSrc/constants' -import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' - -import styles from './styles.module.scss' - -const buttonString = 'Reset Filter' - -export interface Props { - sortedColumnOrder: SortOrder -} - -function usePrevious(value: any) { - const ref = useRef() - useEffect(() => { - ref.current = value - }) - return ref.current -} - -const StreamRangeFilter = ({ sortedColumnOrder }: Props) => { - const dispatch = useDispatch() - - const { - start, - end, - } = useSelector(streamRangeSelector) - - const { firstEntry, lastEntry } = useSelector(streamDataSelector) - - const startNumber = start === '' ? 0 : parseInt(start, 10) - const endNumber = end === '' ? 0 : parseInt(end, 10) - - const min = firstEntry?.id - const max = lastEntry?.id - - const firstEntryTimeStamp: number = getTimestampFromId(min) - const lastEntryTimeStamp: number = getTimestampFromId(max) - - const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } - - const getPercent = useCallback( - (value) => Math.round(((value - firstEntryTimeStamp) / (lastEntryTimeStamp - firstEntryTimeStamp)) * 100), - [firstEntryTimeStamp, lastEntryTimeStamp] - ) - - const minValRef = useRef(null) - const maxValRef = useRef(null) - const range = useRef(null) - - const prevMaxValue = usePrevious({ max }) ?? { max: '' } - - const resetFilter = useCallback( - () => { - dispatch(updateStart(firstEntryTimeStamp.toString())) - dispatch(updateEnd(lastEntryTimeStamp.toString())) - }, - [firstEntryTimeStamp, lastEntryTimeStamp] - ) - - useEffect(() => { - if (maxValRef.current) { - const minPercent = getPercent(startNumber) - const maxPercent = getPercent(+maxValRef.current.value) - - if (range.current) { - range.current.style.left = `${minPercent}%` - range.current.style.width = `${maxPercent - minPercent}%` - } - } - }, [startNumber, getPercent]) - - useEffect(() => { - if (minValRef.current) { - const minPercent = getPercent(+minValRef.current.value) - const maxPercent = getPercent(endNumber) - - if (range.current) { - range.current.style.width = `${maxPercent - minPercent}%` - } - } - }, [endNumber, getPercent]) - - useEffect(() => { - if (start === '') { - dispatch(updateStart(firstEntryTimeStamp.toString())) - } - }, [min]) - - useEffect(() => { - if (end === '') { - dispatch(updateEnd(lastEntryTimeStamp.toString())) - } - }, [max]) - - useEffect(() => { - if (max && prevMaxValue && prevMaxValue.max !== max && endNumber === getTimestampFromId(prevMaxValue.max)) { - dispatch(updateEnd(getTimestampFromId(max).toString())) - } - }, [prevMaxValue]) - - useEffect(() => { - if (start !== '' && end !== '') { - dispatch(fetchStreamEntries( - key, - SCAN_COUNT_DEFAULT, - sortedColumnOrder, - false - )) - } - }, [start, end]) - - if (start === '' && end === '') { - return ( -
-
-
- ) - } - - if (start === end) { - return ( -
-
-
{getFormatTime(start)}
-
{getFormatTime(end)}
-
-
- ) - } - - return ( - <> -
- { - const value = Math.min(+event.target.value, endNumber - 1) - dispatch(updateStart(value.toString())) - event.target.value = value.toString() - }} - className={cx(styles.thumb, styles.thumbZindex3)} - data-testid="range-start-input" - /> - { - const value = Math.max(+event.target.value, startNumber + 1) - dispatch(updateEnd(value.toString())) - event.target.value = value.toString() - }} - className={cx(styles.thumb, styles.thumbZindex4)} - data-testid="range-end-input" - /> -
-
-
-
{getFormatTime(start)}
-
{getFormatTime(end)}
-
-
-
- {(startNumber !== firstEntryTimeStamp || endNumber !== lastEntryTimeStamp) && ( - - )} - - ) -} - -export default StreamRangeFilter diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss index ecbd412021..5d60192b2b 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetails/styles.module.scss @@ -99,124 +99,16 @@ $maxWidthCell: 190px; padding: 12px 0; } -.resetButton { - position: absolute; - right: 30px; - top: 80px; - z-index: 10; - text-decoration: underline; - color: var(--euiTextSubduedColor); - &:hover, - &:focus { - color: var(--euiTextColor); - } - font: normal normal 500 13px/18px Graphik, sans-serif; -} - -.slider { - position: relative; - width: 100%; -} - -.sliderTrack, -.sliderRange, -.sliderLeftValue, -.sliderRightValue { - position: absolute; +.sliderTrack.mockRange { + left: 18px; + width: calc(100% - 36px); } .sliderTrack { + position: absolute; background-color: var(--separatorColor); width: 100%; height: 1px; margin-top: 2px; z-index: 1; } - -.sliderRange { - height: 5px; - background-color: var(--euiColorPrimary); - z-index: 2; -} - -.sliderLeftValue, -.sliderRightValue { - width: max-content; - color: var(--euiColorMediumShade); - font: normal normal normal 12px/18px Graphik; - margin-top: 20px; -} - -.sliderLeftValue { - left: 0; - margin-top: -30px; -} - -.sliderRightValue { - right: -4px; -} - -.mockRange { - left: 30px; - width: calc(100% - 56px); -} - -.thumb, -.thumb::-webkit-slider-thumb { - -webkit-appearance: none; - -webkit-tap-highlight-color: transparent; -} - -.thumb { - pointer-events: none; - position: absolute; - height: 0; - width: calc(100% - 60px); - outline: none; -} - -.thumbZindex3 { - z-index: 3; -} - -.thumbZindex4 { - z-index: 4; -} - -.thumb::-moz-range-thumb { - width: 2px; - height: 12px; - background-color: var(--euiColorPrimary); - border: none; - cursor: pointer; - margin-top: 4px; - pointer-events: all; - position: relative; -} - -.thumbZindex3::-moz-range-thumb { - transform: translateY(-4px); -} - -.thumbZindex4::-moz-range-thumb { - transform: translateY(8px); -} - -input[type='range']::-webkit-slider-thumb { - width: 2px; - height: 12px; - background-color: var(--euiColorPrimary); - border: none; - cursor: pointer; - margin-top: 4px; - pointer-events: all; - position: relative; -} - -input[type='range']:first-child::-webkit-slider-thumb { - transform: translateY(-4px); -} - -input[type='range']:last-of-type::-webkit-slider-thumb { - transform: translateY(8px); -} From 556d11566ae3a9a15b98629111eed7ec59553db8 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 19 May 2022 09:42:38 +0400 Subject: [PATCH 2/2] #RI-2763-resolve comments --- .../range-filter/RangeFilter.spec.tsx | 2 +- .../components/range-filter/RangeFilter.tsx | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx b/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx index c08961cd37..eebc8daff2 100644 --- a/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx +++ b/redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx @@ -9,7 +9,7 @@ const startRangeTestId = 'range-start-input' const endRangeTestId = 'range-end-input' const resetBtnTestId = 'range-filter-btn' -describe('StreamRangeFilter', () => { +describe('RangeFilter', () => { it('should render', () => { expect(render()).toBeTruthy() }) diff --git a/redisinsight/ui/src/components/range-filter/RangeFilter.tsx b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx index 73e705faae..dd8a83f94c 100644 --- a/redisinsight/ui/src/components/range-filter/RangeFilter.tsx +++ b/redisinsight/ui/src/components/range-filter/RangeFilter.tsx @@ -24,7 +24,9 @@ function usePrevious(value: any) { return ref.current } -const RangeFilter = ({ max, min, start, end, handleChangeStart, handleChangeEnd }: Props) => { +const RangeFilter = (props: Props) => { + const { max, min, start, end, handleChangeStart, handleChangeEnd } = props + const getPercent = useCallback( (value) => Math.round(((value - min) / (max - min)) * 100), [min, max] @@ -44,6 +46,22 @@ const RangeFilter = ({ max, min, start, end, handleChangeStart, handleChangeEnd [min, max] ) + const onChangeStart = useCallback( + (event) => { + const value = Math.min(+event.target.value, end - 1) + handleChangeStart(value) + }, + [end] + ) + + const onChangeEnd = useCallback( + (event) => { + const value = Math.max(+event.target.value, start + 1) + handleChangeEnd(value) + }, + [start] + ) + useEffect(() => { if (maxValRef.current) { const minPercent = getPercent(start) @@ -101,11 +119,7 @@ const RangeFilter = ({ max, min, start, end, handleChangeStart, handleChangeEnd max={max} value={start} ref={minValRef} - onChange={(event) => { - const value = Math.min(+event.target.value, end - 1) - handleChangeStart(value) - event.target.value = value.toString() - }} + onChange={onChangeStart} className={cx(styles.thumb, styles.thumbZindex3)} data-testid="range-start-input" /> @@ -115,11 +129,7 @@ const RangeFilter = ({ max, min, start, end, handleChangeStart, handleChangeEnd max={max} value={end} ref={maxValRef} - onChange={(event) => { - const value = Math.max(+event.target.value, start + 1) - handleChangeEnd(value) - event.target.value = value.toString() - }} + onChange={onChangeEnd} className={cx(styles.thumb, styles.thumbZindex4)} data-testid="range-end-input" />