diff --git a/frontend/src/container/GoToTop/index.tsx b/frontend/src/container/GoToTop/index.tsx new file mode 100644 index 0000000000..c1cc57973c --- /dev/null +++ b/frontend/src/container/GoToTop/index.tsx @@ -0,0 +1,29 @@ +import { ArrowUpOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +// hooks +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useScrollToTop from 'hooks/useScrollToTop'; + +function GoToTop(): JSX.Element | null { + const { isVisible, scrollToTop } = useScrollToTop(); + + const { panelType } = useQueryBuilder(); + + if (!isVisible) return null; + + if (panelType === PANEL_TYPES.LIST) { + return ( + } + /> + ); + } + + return null; +} + +export default GoToTop; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 45cbceaa45..4536b1be49 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,5 +1,4 @@ import { TabsProps } from 'antd'; -import axios from 'axios'; import LogDetail from 'components/LogDetail'; import TabLabel from 'components/TabLabel'; import { QueryParams } from 'constants/query'; @@ -13,6 +12,7 @@ import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; import ExportPanel from 'container/ExportPanel'; +import GoToTop from 'container/GoToTop'; import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerTable from 'container/LogsExplorerTable'; @@ -22,6 +22,7 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useAxiosError from 'hooks/useAxiosError'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; @@ -81,6 +82,8 @@ function LogsExplorerViews(): JSX.Element { const [logs, setLogs] = useState([]); const [requestData, setRequestData] = useState(null); + const handleAxisError = useAxiosError(); + const currentStagedQueryData = useMemo(() => { if (!stagedQuery || stagedQuery.builder.queryData.length !== 1) return null; @@ -357,16 +360,16 @@ function LogsExplorerViews(): JSX.Element { history.push(dashboardEditView); }, - onError: (error) => { - if (axios.isAxiosError(error)) { - notifications.error({ - message: error.message, - }); - } - }, + onError: handleAxisError, }); }, - [exportDefaultQuery, history, notifications, updateDashboard], + [ + exportDefaultQuery, + history, + notifications, + updateDashboard, + handleAxisError, + ], ); useEffect(() => { @@ -511,6 +514,8 @@ function LogsExplorerViews(): JSX.Element { onAddToQuery={handleAddToQuery} onClickActionItem={handleAddToQuery} /> + + ); } diff --git a/frontend/src/hooks/useScrollToTop/index.tsx b/frontend/src/hooks/useScrollToTop/index.tsx new file mode 100644 index 0000000000..f28fbd66eb --- /dev/null +++ b/frontend/src/hooks/useScrollToTop/index.tsx @@ -0,0 +1,29 @@ +import throttle from 'lodash-es/throttle'; +import { useEffect, useState } from 'react'; + +import { UseScrollToTop } from './types'; + +function useScrollToTop(visibleOffset = 200): UseScrollToTop { + const [isVisible, setIsVisible] = useState(false); + + const scrollToTop = (): void => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }; + + useEffect(() => { + const toggleVisibility = throttle(() => { + setIsVisible(window.pageYOffset > visibleOffset); + }, 300); + + window.addEventListener('scroll', toggleVisibility); + + return (): void => window.removeEventListener('scroll', toggleVisibility); + }, [visibleOffset]); + + return { isVisible, scrollToTop }; +} + +export default useScrollToTop; diff --git a/frontend/src/hooks/useScrollToTop/types.ts b/frontend/src/hooks/useScrollToTop/types.ts new file mode 100644 index 0000000000..6c106a2be9 --- /dev/null +++ b/frontend/src/hooks/useScrollToTop/types.ts @@ -0,0 +1,4 @@ +export interface UseScrollToTop { + isVisible: boolean; + scrollToTop: VoidFunction; +} diff --git a/frontend/src/hooks/useScrollToTop/useScrollToTop.test.ts b/frontend/src/hooks/useScrollToTop/useScrollToTop.test.ts new file mode 100644 index 0000000000..03820f5963 --- /dev/null +++ b/frontend/src/hooks/useScrollToTop/useScrollToTop.test.ts @@ -0,0 +1,58 @@ +import { act, renderHook } from '@testing-library/react'; + +import useScrollToTop from './index'; + +// Mocking window.scrollTo method +global.scrollTo = jest.fn(); + +describe('useScrollToTop hook', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('should change visibility and scroll to top on call', () => { + const { result } = renderHook(() => useScrollToTop(100)); + + // Simulate scrolling 150px down + act(() => { + global.pageYOffset = 150; + global.dispatchEvent(new Event('scroll')); + jest.advanceTimersByTime(300); + }); + + expect(result.current.isVisible).toBe(true); + + // Simulate scrolling to top + act(() => { + result.current.scrollToTop(); + }); + + expect(global.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }); + }); + + it('should be invisible when scrolled less than offset', () => { + const { result } = renderHook(() => useScrollToTop(100)); + + // Simulate scrolling 50px down + act(() => { + global.pageYOffset = 50; + global.dispatchEvent(new Event('scroll')); + jest.advanceTimersByTime(300); + }); + + expect(result.current.isVisible).toBe(false); + }); + + it('should be visible when scrolled more than offset', () => { + const { result } = renderHook(() => useScrollToTop(100)); + + // Simulate scrolling 50px down + act(() => { + global.pageYOffset = 200; + global.dispatchEvent(new Event('scroll')); + jest.advanceTimersByTime(300); + }); + + expect(result.current.isVisible).toBe(true); + }); +});