Skip to content

Commit

Permalink
Merge pull request #633 from RedisInsight/feature/RI-2752_Auto-refresh
Browse files Browse the repository at this point in the history
#RI-2752 - Auto-refresh
  • Loading branch information
romansergeenkosofteq committed May 16, 2022
2 parents af114bd + 14d79a2 commit 4806ff6
Show file tree
Hide file tree
Showing 45 changed files with 880 additions and 302 deletions.
5 changes: 3 additions & 2 deletions redisinsight/ui/src/components/virtual-table/VirtualTable.tsx
Expand Up @@ -43,7 +43,8 @@ const VirtualTable = (props: IProps) => {
setScrollTopPosition = () => {},
scrollTopProp = 0,
hideFooter = false,
tableWidth = 0
tableWidth = 0,
hideProgress,
} = props
const scrollTopRef = useRef<number>(0)
const [selectedRowIndex, setSelectedRowIndex] = useState<Nullable<number>>(null)
Expand Down Expand Up @@ -291,7 +292,7 @@ const VirtualTable = (props: IProps) => {
onWheel={onWheel}
data-testid="virtual-table-container"
>
{loading ? (
{loading && !hideProgress ? (
<EuiProgress
color="primary"
size="xs"
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/components/virtual-table/interfaces.ts
Expand Up @@ -68,6 +68,7 @@ export interface IProps {
scrollTopProp?: number
hideFooter?: boolean
tableWidth?: number
hideProgress?: boolean
}

export interface ISortedColumn {
Expand Down
Expand Up @@ -163,6 +163,7 @@ $footerHeight: 38px;

:global(.key-details-table) {
height: calc(100% - 38px);
position: relative;
&:global(.footerOpened) {
:global(.ReactVirtualized__Table__Grid) {
padding-bottom: 254px;
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/storage.ts
Expand Up @@ -11,6 +11,7 @@ enum BrowserStorageItem {
wbInputHistory = 'wbInputHistory',
isEnablementAreaMinimized = 'isEnablementAreaMinimized',
treeViewDelimiter = 'treeViewDelimiter',
autoRefreshRate = 'autoRefreshRate',
}

export default BrowserStorageItem
@@ -0,0 +1,82 @@
import React from 'react'
import { instance, mock } from 'ts-mockito'
import { fireEvent, screen, render } from 'uiSrc/utils/test-utils'
import AutoRefresh, { Props } from './AutoRefresh'
import { DEFAULT_REFRESH_RATE } from './utils'

const mockedProps = mock<Props>()

const INLINE_ITEM_EDITOR = 'inline-item-editor'

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

it('prop "displayText = true" should show Refresh text', () => {
const { queryByTestId } = render(<AutoRefresh {...instance(mockedProps)} displayText />)

expect(queryByTestId('refresh-message-label')).toBeInTheDocument()
})

it('prop "displayText = false" should hide Refresh text', () => {
const { queryByTestId } = render(<AutoRefresh {...instance(mockedProps)} displayText={false} />)

expect(queryByTestId('refresh-message-label')).not.toBeInTheDocument()
})

it('should call onRefresh', () => {
const onRefresh = jest.fn()
render(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} testid="refresh-key-btn" />)

fireEvent.click(screen.getByTestId('refresh-key-btn'))
expect(onRefresh).toBeCalled()
})

it('refresh text should contain "Last refresh" time with disabled auto-refresh', async () => {
render(<AutoRefresh {...instance(mockedProps)} displayText />)

expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(/Last refresh:/i)
expect(screen.getByTestId('refresh-message')).toHaveTextContent('now')
})

it('refresh text should contain "Auto-refresh" time with enabled auto-refresh', async () => {
render(<AutoRefresh {...instance(mockedProps)} displayText />)

fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))
fireEvent.click(screen.getByTestId('auto-refresh-switch'))

expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(/Auto refresh:/i)
expect(screen.getByTestId('refresh-message')).toHaveTextContent(DEFAULT_REFRESH_RATE)
})

describe('AutoRefresh Config', () => {
it('Auto refresh config should render', () => {
const { queryByTestId } = render(<AutoRefresh {...instance(mockedProps)} />)

fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))
expect(queryByTestId('auto-refresh-switch')).toBeInTheDocument()
})

it('should call onRefresh after enable auto-refresh and set 1 sec', async () => {
const onRefresh = jest.fn()
render(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} />)

fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))
fireEvent.click(screen.getByTestId('auto-refresh-switch'))
fireEvent.click(screen.getByTestId('refresh-rate'))

fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: '1' } })
expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1')

screen.getByTestId(/apply-btn/).click()

await new Promise((r) => setTimeout(r, 1100))
expect(onRefresh).toBeCalledTimes(1)
await new Promise((r) => setTimeout(r, 1100))
expect(onRefresh).toBeCalledTimes(2)
await new Promise((r) => setTimeout(r, 1100))
expect(onRefresh).toBeCalledTimes(3)
})
})
})
@@ -0,0 +1,232 @@
import React, { useEffect, useState } from 'react'
import { EuiButtonIcon, EuiPopover, EuiSwitch, EuiTextColor, EuiToolTip } from '@elastic/eui'
import cx from 'classnames'

import { MIN_REFRESH_RATE, Nullable, validateRefreshRateNumber } from 'uiSrc/utils'
import InlineItemEditor from 'uiSrc/components/inline-item-editor'
import { localStorageService } from 'uiSrc/services'
import { BrowserStorageItem } from 'uiSrc/constants'
import {
getTextByRefreshTime,
DEFAULT_REFRESH_RATE,
DURATION_FIRST_REFRESH_TIME,
MINUTE,
NOW,
} from './utils'

import styles from './styles.module.scss'

export interface Props {
postfix: string
loading: boolean
displayText: boolean
lastRefreshTime: Nullable<number>
testid?: string
containerClassName?: string
turnOffAutoRefresh?: boolean
onRefresh: (enableAutoRefresh: boolean, refreshRate: string) => void
}

const TIMEOUT_TO_UPDATE_REFRESH_TIME = 1_000 * MINUTE // once a minute

const AutoRefresh = ({
postfix,
loading,
displayText,
lastRefreshTime,
containerClassName = '',
testid = '',
turnOffAutoRefresh,
onRefresh,
}: Props) => {
let intervalText: NodeJS.Timeout
let timeoutRefresh: NodeJS.Timeout

const [refreshMessage, setRefreshMessage] = useState(NOW)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [refreshRate, setRefreshRate] = useState<string>('')
const [refreshRateMessage, setRefreshRateMessage] = useState<string>('')
const [enableAutoRefresh, setEnableAutoRefresh] = useState(false)
const [editingRate, setEditingRate] = useState(false)

const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)
const closePopover = () => {
setEnableAutoRefresh(enableAutoRefresh)
setIsPopoverOpen(false)
}

useEffect(() => {
const refreshRateStorage = localStorageService.get(BrowserStorageItem.autoRefreshRate + postfix)
|| DEFAULT_REFRESH_RATE

setRefreshRate(refreshRateStorage)
}, [postfix])

useEffect(() => {
if (turnOffAutoRefresh && enableAutoRefresh) {
setEnableAutoRefresh(false)
clearInterval(timeoutRefresh)
}
}, [turnOffAutoRefresh])

// update refresh label text
useEffect(() => {
const delta = getLastRefreshDelta(lastRefreshTime)
updateLastRefresh()

intervalText = setInterval(() => {
if (document.hidden) return

updateLastRefresh()
}, delta < DURATION_FIRST_REFRESH_TIME ? DURATION_FIRST_REFRESH_TIME : TIMEOUT_TO_UPDATE_REFRESH_TIME)
return () => clearInterval(intervalText)
}, [lastRefreshTime])

// refresh interval
useEffect(() => {
updateLastRefresh()

if (enableAutoRefresh && !loading) {
timeoutRefresh = setInterval(() => {
if (document.hidden) return

handleRefresh()
}, +refreshRate * 1_000)
} else {
clearInterval(timeoutRefresh)
}

if (enableAutoRefresh) {
updateAutoRefreshText(refreshRate)
}

return () => clearInterval(timeoutRefresh)
}, [enableAutoRefresh, refreshRate, loading, lastRefreshTime])

const getLastRefreshDelta = (time:Nullable<number>) => (Date.now() - (time || 0)) / 1_000

const updateLastRefresh = () => {
const delta = getLastRefreshDelta(lastRefreshTime)
const text = getTextByRefreshTime(delta, lastRefreshTime ?? 0)

lastRefreshTime && setRefreshMessage(text)
}

const updateAutoRefreshText = (refreshRate: string) => {
enableAutoRefresh && setRefreshRateMessage(
// more than 1 minute
+refreshRate > MINUTE ? `${Math.floor(+refreshRate / MINUTE)} min` : `${refreshRate} s`
)
}

const handleApplyAutoRefreshRate = (value: string) => {
setRefreshRate(+value > MIN_REFRESH_RATE ? value : `${MIN_REFRESH_RATE}`)
setEditingRate(false)
localStorageService.set(BrowserStorageItem.autoRefreshRate + postfix, value)
}

const handleDeclineAutoRefreshRate = () => {
setEditingRate(false)
}

const handleRefresh = () => {
onRefresh(enableAutoRefresh, refreshRate)
}

const onChangeEnableAutoRefresh = (value: boolean) => {
setEnableAutoRefresh(value)
}

return (
<div className={cx(styles.container, containerClassName, { [styles.enable]: enableAutoRefresh })}>
<EuiTextColor className={styles.summary}>
{displayText && (
<span data-testid="refresh-message-label">{`${enableAutoRefresh ? 'Auto refresh:' : 'Last refresh:'}`}</span>
)}
<span className={styles.time} data-testid="refresh-message">
{` ${enableAutoRefresh ? refreshRateMessage : refreshMessage}`}
</span>
</EuiTextColor>

<EuiToolTip
title="Last Refresh"
className={styles.tooltip}
position="top"
content={refreshMessage}
>
<EuiButtonIcon
iconType="refresh"
disabled={loading}
onClick={handleRefresh}
onMouseEnter={updateLastRefresh}
className={cx(styles.btn, { [styles.rolling]: enableAutoRefresh })}
aria-labelledby={testid?.replaceAll?.('-', ' ') || 'Refresh button'}
data-testid={testid || 'refresh-btn'}
/>
</EuiToolTip>

<EuiPopover
ownFocus={false}
anchorPosition="downRight"
isOpen={isPopoverOpen}
anchorClassName={styles.anchorWrapper}
panelClassName={cx('popover-without-top-tail', styles.popoverWrapper)}
closePopover={closePopover}
button={(
<EuiButtonIcon
iconType="arrowDown"
color="subdued"
aria-label="Auto-refresh config popover"
className={cx(styles.anchorBtn, { [styles.anchorBtnOpen]: isPopoverOpen })}
onClick={onButtonClick}
data-testid="auto-refresh-config-btn"
/>
)}
>
<div className={styles.switch}>
<EuiSwitch
compressed
label="Auto Refresh"
checked={enableAutoRefresh}
onChange={(e) => onChangeEnableAutoRefresh(e.target.checked)}
className={styles.switchOption}
data-testid="auto-refresh-switch"
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.inputLabel}>Refresh rate:</div>
{!editingRate && (
<EuiTextColor
color="subdued"
style={{ cursor: 'pointer' }}
onClick={() => setEditingRate(true)}
data-testid="refresh-rate"
>
{`${refreshRate} s`}
</EuiTextColor>
)}
{editingRate && (
<>
<div className={styles.input} data-testid="auto-refresh-rate-input">
<InlineItemEditor
initialValue={refreshRate}
fieldName="refreshRate"
placeholder={DEFAULT_REFRESH_RATE}
isLoading={loading}
validation={validateRefreshRateNumber}
onDecline={() => handleDeclineAutoRefreshRate()}
onApply={(value) => handleApplyAutoRefreshRate(value)}
/>
</div>
<EuiTextColor color="subdued">{' s'}</EuiTextColor>
</>
)}
</div>

</EuiPopover>

</div>
)
}

export default AutoRefresh
@@ -0,0 +1,5 @@
import AutoRefresh from './AutoRefresh'

export * from './AutoRefresh'

export default AutoRefresh

0 comments on commit 4806ff6

Please sign in to comment.