Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#RI-2752 - Auto-refresh #633

Merged
merged 2 commits into from May 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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