Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Props>()

const startRangeTestId = 'range-start-input'
const endRangeTestId = 'range-end-input'
const resetBtnTestId = 'range-filter-btn'

describe('RangeFilter', () => {
it('should render', () => {
expect(render(<RangeFilter {...instance(mockedProps)} />)).toBeTruthy()
})

it('should call handleChangeStart onChange start range thumb', () => {
const handleChangeStart = jest.fn()
render(
<RangeFilter
{...instance(mockedProps)}
handleChangeStart={handleChangeStart}
start={1}
end={1000}
/>
)
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(
<RangeFilter
{...instance(mockedProps)}
handleChangeEnd={handleChangeEnd}
start={1}
end={100}
/>
)
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(
<RangeFilter
{...instance(mockedProps)}
handleChangeStart={handleChangeStart}
handleChangeEnd={handleChangeEnd}
start={1}
end={100}
min={1}
max={120}
/>
)
const resetBtn = screen.getByTestId(resetBtnTestId)

fireEvent.click(resetBtn)

expect(handleChangeEnd).toBeCalledWith(120)
expect(handleChangeStart).toBeCalledWith(1)
})
})
158 changes: 158 additions & 0 deletions redisinsight/ui/src/components/range-filter/RangeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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 = (props: Props) => {
const { max, min, start, end, handleChangeStart, handleChangeEnd } = props

const getPercent = useCallback(
(value) => Math.round(((value - min) / (max - min)) * 100),
[min, max]
)

const minValRef = useRef<HTMLInputElement>(null)
const maxValRef = useRef<HTMLInputElement>(null)
const range = useRef<HTMLInputElement>(null)

const prevValue = usePrevious({ max }) ?? { max: 0 }

const resetFilter = useCallback(
() => {
handleChangeStart(min)
handleChangeEnd(max)
},
[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)
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 (
<div data-testid="mock-blank-range" className={styles.rangeWrapper}>
<div className={cx(styles.sliderTrack, styles.mockRange)} />
</div>
)
}

if (start === end) {
return (
<div data-testid="mock-fill-range" className={styles.rangeWrapper}>
<div className={cx(styles.sliderRange, styles.mockRange)}>
<div className={styles.sliderLeftValue}>{getFormatTime(start?.toString())}</div>
<div className={styles.sliderRightValue}>{getFormatTime(end?.toString())}</div>
</div>
</div>
)
}

return (
<>
<div className={styles.rangeWrapper}>
<input
type="range"
min={min}
max={max}
value={start}
ref={minValRef}
onChange={onChangeStart}
className={cx(styles.thumb, styles.thumbZindex3)}
data-testid="range-start-input"
/>
<input
type="range"
min={min}
max={max}
value={end}
ref={maxValRef}
onChange={onChangeEnd}
className={cx(styles.thumb, styles.thumbZindex4)}
data-testid="range-end-input"
/>
<div className={styles.slider}>
<div className={styles.sliderTrack} />
<div ref={range} className={styles.sliderRange}>
<div className={styles.sliderLeftValue}>{getFormatTime(start?.toString())}</div>
<div className={styles.sliderRightValue}>{getFormatTime(end?.toString())}</div>
</div>
</div>
</div>
{(start !== min || end !== max) && (
<button
data-testid="range-filter-btn"
className={styles.resetButton}
type="button"
onClick={resetFilter}
>
{buttonString}
</button>
)}
</>
)
}

export default RangeFilter
126 changes: 126 additions & 0 deletions redisinsight/ui/src/components/range-filter/styles.module.scss
Original file line number Diff line number Diff line change
@@ -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);
}
Loading