diff --git a/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx b/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx index 5fe90b283b1e..dd41a0498b94 100644 --- a/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx +++ b/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx @@ -20,6 +20,12 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({ useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'), })); diff --git a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx index d8b68387885d..9b40000e9bdf 100644 --- a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx +++ b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx @@ -6,6 +6,8 @@ import { ColumnGroupType, ColumnType } from 'antd/es/table'; import { ColumnsType } from 'antd/lib/table'; import logEvent from 'api/common/logEvent'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; +import useUrlQuery from 'hooks/useUrlQuery'; import { SlidersHorizontal } from 'lucide-react'; import { memo, useEffect, useState } from 'react'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -25,8 +27,12 @@ function DynamicColumnTable({ onDragColumn, facingIssueBtn, shouldSendAlertsLogEvent, + pagination, ...restProps }: DynamicColumnTableProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); + const urlQuery = useUrlQuery(); + const [columnsData, setColumnsData] = useState( columns, ); @@ -93,6 +99,28 @@ function DynamicColumnTable({ type: 'checkbox', })) || []; + // Get current page from URL or default to 1 + const currentPage = Number(urlQuery.get('page')) || 1; + + const handlePaginationChange = (page: number, pageSize?: number): void => { + // Update URL with new page number while preserving other params + urlQuery.set('page', page.toString()); + + const newUrl = `${window.location.pathname}?${urlQuery.toString()}`; + safeNavigate(newUrl); + + // Call original pagination handler if provided + if (pagination?.onChange && !!pageSize) { + pagination.onChange(page, pageSize); + } + }; + + const enhancedPagination = { + ...pagination, + current: currentPage, // Ensure the pagination component shows the correct page + onChange: handlePaginationChange, + }; + return (
@@ -116,6 +144,7 @@ function DynamicColumnTable({
diff --git a/frontend/src/components/ResizeTable/types.ts b/frontend/src/components/ResizeTable/types.ts index 1ad3c3318afe..b912ca44d93e 100644 --- a/frontend/src/components/ResizeTable/types.ts +++ b/frontend/src/components/ResizeTable/types.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TableProps } from 'antd'; import { ColumnsType } from 'antd/es/table'; +import { PaginationProps } from 'antd/lib'; import { ColumnGroupType, ColumnType } from 'antd/lib/table'; import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport'; @@ -15,6 +16,7 @@ export interface DynamicColumnTableProps extends TableProps { onDragColumn?: (fromIndex: number, toIndex: number) => void; facingIssueBtn?: LaunchChatSupportProps; shouldSendAlertsLogEvent?: boolean; + pagination?: PaginationProps; } export type GetVisibleColumnsFunction = ( diff --git a/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx b/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx index ea878ee748db..1dc088d3d8f0 100644 --- a/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx +++ b/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx @@ -27,6 +27,12 @@ jest.mock('uplot', () => { }; }); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + let mockWindowOpen: jest.Mock; window.ResizeObserver = diff --git a/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx b/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx index ea892014d3ac..498595d183ad 100644 --- a/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx +++ b/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx @@ -36,6 +36,11 @@ window.ResizeObserver = unobserve: jest.fn(), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); describe('Anomaly Alert Documentation Redirection', () => { let mockWindowOpen: jest.Mock; diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 348e5075f987..511c43dca118 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -17,8 +17,8 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; import { isEqual } from 'lodash-es'; @@ -87,7 +87,7 @@ function FormAlertRules({ // init namespace for translations const { t } = useTranslation('alerts'); const { featureFlags } = useAppContext(); - + const { safeNavigate } = useSafeNavigate(); const { selectedTime: globalSelectedInterval } = useSelector< AppState, GlobalReducer @@ -224,7 +224,7 @@ function FormAlertRules({ const generatedUrl = `${location.pathname}?${queryParams.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); // eslint-disable-next-line react-hooks/exhaustive-deps }, [detectionMethod]); @@ -295,8 +295,8 @@ function FormAlertRules({ urlQuery.delete(QueryParams.panelTypes); urlQuery.delete(QueryParams.ruleId); urlQuery.delete(QueryParams.relativeTime); - history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`); - }, [urlQuery]); + safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`); + }, [safeNavigate, urlQuery]); // onQueryCategoryChange handles changes to query category // in state as well as sets additional defaults @@ -515,7 +515,7 @@ function FormAlertRules({ urlQuery.delete(QueryParams.panelTypes); urlQuery.delete(QueryParams.ruleId); urlQuery.delete(QueryParams.relativeTime); - history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`); + safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`); }, 2000); } else { logData = { diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index d682af12a8d1..ad97b3feacc3 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -20,11 +20,11 @@ import { import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useChartMutable } from 'hooks/useChartMutable'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import GetMinMax from 'lib/getMinMax'; -import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -49,6 +49,7 @@ function FullView({ isDependedDataLoaded = false, onToggleModelHandler, }: FullViewProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const { selectedTime: globalSelectedTime } = useSelector< AppState, GlobalReducer @@ -137,9 +138,9 @@ function FullView({ urlQuery.set(QueryParams.startTime, minTime.toString()); urlQuery.set(QueryParams.endTime, maxTime.toString()); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.push(generatedUrl); + safeNavigate(generatedUrl); }, - [dispatch, location.pathname, urlQuery], + [dispatch, location.pathname, safeNavigate, urlQuery], ); const [graphsVisibilityStates, setGraphsVisibilityStates] = useState< diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx index cc8d35ad9c54..deb92aa4f508 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx @@ -23,6 +23,12 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + jest.mock('uplot', () => { const paths = { spline: jest.fn(), diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 74022b2c0ad6..c4b9f09ac44c 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -10,9 +10,9 @@ import { placeWidgetAtBottom } from 'container/NewWidget/utils'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; -import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { @@ -51,6 +51,7 @@ function WidgetGraphComponent({ customSeries, customErrorMessage, }: WidgetGraphComponentProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const [deleteModal, setDeleteModal] = useState(false); const [hovered, setHovered] = useState(false); const { notifications } = useNotifications(); @@ -173,7 +174,7 @@ function WidgetGraphComponent({ graphType: widget?.panelTypes, widgetId: uuid, }; - history.push(`${pathname}/new?${createQueryParams(queryParams)}`); + safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`); }, }, ); @@ -194,7 +195,7 @@ function WidgetGraphComponent({ const separator = existingSearch.toString() ? '&' : ''; const newSearch = `${existingSearch}${separator}${updatedSearch}`; - history.push({ + safeNavigate({ pathname, search: newSearch, }); @@ -221,7 +222,7 @@ function WidgetGraphComponent({ }); setGraphVisibility(localStoredVisibilityState); } - history.push({ + safeNavigate({ pathname, search: createQueryParams(updatedQueryParams), }); diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 3d33fe799e32..27a3cd0dd6da 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -15,8 +15,8 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { defaultTo, isUndefined } from 'lodash-es'; import isEqual from 'lodash-es/isEqual'; import { @@ -55,6 +55,7 @@ interface GraphLayoutProps { // eslint-disable-next-line sonarjs/cognitive-complexity function GraphLayout(props: GraphLayoutProps): JSX.Element { const { handle } = props; + const { safeNavigate } = useSafeNavigate(); const { selectedDashboard, layouts, @@ -215,13 +216,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.push(generatedUrl); + safeNavigate(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); } }, - [dispatch, pathname, urlQuery], + [dispatch, pathname, safeNavigate, urlQuery], ); useEffect(() => { diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 8dd92d8b4e47..9956ebc16097 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -18,8 +18,8 @@ import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import useComponentPermission from 'hooks/useComponentPermission'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { isEmpty } from 'lodash-es'; import { CircleX, X } from 'lucide-react'; @@ -72,6 +72,7 @@ function WidgetHeader({ setSearchTerm, }: IWidgetHeaderProps): JSX.Element | null { const urlQuery = useUrlQuery(); + const { safeNavigate } = useSafeNavigate(); const onEditHandler = useCallback((): void => { const widgetId = widget.id; urlQuery.set(QueryParams.widgetId, widgetId); @@ -81,8 +82,8 @@ function WidgetHeader({ encodeURIComponent(JSON.stringify(widget.query)), ); const generatedUrl = `${window.location.pathname}/new?${urlQuery}`; - history.push(generatedUrl); - }, [urlQuery, widget.id, widget.panelTypes, widget.query]); + safeNavigate(generatedUrl); + }, [safeNavigate, urlQuery, widget.id, widget.panelTypes, widget.query]); const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView'); diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 110b1e921a40..6cd1468dcffd 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -35,7 +35,7 @@ import dayjs from 'dayjs'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; -import history from 'lib/history'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { get, isEmpty, isUndefined } from 'lodash-es'; import { ArrowDownWideNarrow, @@ -74,7 +74,7 @@ import { } from 'react'; import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; -import { generatePath, Link } from 'react-router-dom'; +import { generatePath } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; import { Dashboard, @@ -105,7 +105,7 @@ function DashboardsList(): JSX.Element { } = useGetAllDashboard(); const { user } = useAppContext(); - + const { safeNavigate } = useSafeNavigate(); const { listSortOrder: sortOrder, setListSortOrder: setSortOrder, @@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element { }); if (response.statusCode === 200) { - history.push( + safeNavigate( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, }), @@ -313,7 +313,7 @@ function DashboardsList(): JSX.Element { errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } - }, [newDashboardState, t]); + }, [newDashboardState, safeNavigate, t]); const onModalHandler = (uploadedGrafana: boolean): void => { logEvent('Dashboard List: Import JSON clicked', {}); @@ -418,7 +418,7 @@ function DashboardsList(): JSX.Element { if (event.metaKey || event.ctrlKey) { window.open(getLink(), '_blank'); } else { - history.push(getLink()); + safeNavigate(getLink()); } logEvent('Dashboard List: Clicked on dashboard', { dashboardId: dashboard.id, @@ -444,10 +444,12 @@ function DashboardsList(): JSX.Element { placement="left" overlayClassName="title-toolip" > - e.stopPropagation()} + onClick={(e): void => { + e.stopPropagation(); + safeNavigate(getLink()); + }} > {dashboard.name} - + diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 535711629b47..1ea39e023942 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -18,8 +18,8 @@ import createDashboard from 'api/dashboard/create'; import ROUTES from 'constants/routes'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; -import history from 'lib/history'; import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react'; // #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/ // See more: https://github.com/lucide-icons/lucide/issues/94 @@ -33,6 +33,7 @@ function ImportJSON({ uploadedGrafana, onModalHandler, }: ImportJSONProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const [jsonData, setJsonData] = useState>(); const { t } = useTranslation(['dashboard', 'common']); const [isUploadJSONError, setIsUploadJSONError] = useState(false); @@ -97,7 +98,7 @@ function ImportJSON({ }); if (response.statusCode === 200) { - history.push( + safeNavigate( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, }), diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index 7ac1934bb7a6..d0acc3f2fda4 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -2,14 +2,12 @@ import Graph from 'components/Graph'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import { themeColors } from 'constants/theme'; -import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import getChartData, { GetChartDataProps } from 'lib/getChartData'; import GetMinMax from 'lib/getMinMax'; import { colors } from 'lib/getRandomColor'; -import getTimeString from 'lib/getTimeString'; -import history from 'lib/history'; -import { memo, useCallback, useEffect, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; @@ -28,6 +26,7 @@ function LogsExplorerChart({ const dispatch = useDispatch(); const urlQuery = useUrlQuery(); const location = useLocation(); + const { safeNavigate } = useSafeNavigate(); const handleCreateDatasets: Required['createDataset'] = useCallback( (element, index, allLabels) => ({ data: element, @@ -62,41 +61,13 @@ function LogsExplorerChart({ urlQuery.set(QueryParams.startTime, minTime.toString()); urlQuery.set(QueryParams.endTime, maxTime.toString()); + urlQuery.delete(QueryParams.relativeTime); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.push(generatedUrl); + safeNavigate(generatedUrl); }, - [dispatch, location.pathname, urlQuery], + [dispatch, location.pathname, safeNavigate, urlQuery], ); - const handleBackNavigation = (): void => { - const searchParams = new URLSearchParams(window.location.search); - const startTime = searchParams.get(QueryParams.startTime); - const endTime = searchParams.get(QueryParams.endTime); - const relativeTime = searchParams.get( - QueryParams.relativeTime, - ) as CustomTimeType; - - if (relativeTime) { - dispatch(UpdateTimeInterval(relativeTime)); - } else if (startTime && endTime && startTime !== endTime) { - dispatch( - UpdateTimeInterval('custom', [ - parseInt(getTimeString(startTime), 10), - parseInt(getTimeString(endTime), 10), - ]), - ); - } - }; - - useEffect(() => { - window.addEventListener('popstate', handleBackNavigation); - - return (): void => { - window.removeEventListener('popstate', handleBackNavigation); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const graphData = useMemo( () => getChartData({ diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 1c8259d6563e..5da30af83a7a 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -38,6 +38,7 @@ import useAxiosError from 'hooks/useAxiosError'; import useClickOutside from 'hooks/useClickOutside'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { FlatLogData } from 'lib/logs/flatLogData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; @@ -62,7 +63,6 @@ import { useState, } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import { ILog } from 'types/api/logs/log'; @@ -98,7 +98,7 @@ function LogsExplorerViews({ chartQueryKeyRef: MutableRefObject; }): JSX.Element { const { notifications } = useNotifications(); - const history = useHistory(); + const { safeNavigate } = useSafeNavigate(); // this is to respect the panel type present in the URL rather than defaulting it to list always. const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); @@ -486,7 +486,7 @@ function LogsExplorerViews({ widgetId, }); - history.push(dashboardEditView); + safeNavigate(dashboardEditView); }, onError: handleAxisError, }); @@ -495,7 +495,7 @@ function LogsExplorerViews({ getUpdatedQueryForExport, exportDefaultQuery, options.selectColumns, - history, + safeNavigate, notifications, panelType, updateDashboard, diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 90bffab2da3d..da70cd6746c4 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -75,6 +75,12 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({ useGetExplorerQueryRange: jest.fn(), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + // Set up the specific behavior for useGetExplorerQueryRange in individual test cases beforeEach(() => { (useGetExplorerQueryRange as jest.Mock).mockReturnValue({ diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 9ce242582b9f..277e9d7dd621 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -13,6 +13,7 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import getStep from 'lib/getStep'; import history from 'lib/history'; @@ -157,6 +158,7 @@ function DBCall(): JSX.Element { servicename, isDBCall: true, }); + const { safeNavigate } = useSafeNavigate(); return ( @@ -171,6 +173,7 @@ function DBCall(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces @@ -206,6 +209,7 @@ function DBCall(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 328a5444729c..c3d86cc3b253 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -15,6 +15,7 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import getStep from 'lib/getStep'; import history from 'lib/history'; @@ -220,6 +221,8 @@ function External(): JSX.Element { isExternalCall: true, }); + const { safeNavigate } = useSafeNavigate(); + return ( <> @@ -234,6 +237,7 @@ function External(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery: errorApmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces @@ -270,6 +274,7 @@ function External(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces @@ -309,6 +314,7 @@ function External(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces @@ -345,6 +351,7 @@ function External(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 5fae3b9c6ebe..43f7d71c212d 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -13,6 +13,7 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import getStep from 'lib/getStep'; import history from 'lib/history'; @@ -290,6 +291,7 @@ function Application(): JSX.Element { }, ], }); + const { safeNavigate } = useSafeNavigate(); return ( <> @@ -317,6 +319,7 @@ function Application(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces @@ -346,6 +349,7 @@ function Application(): JSX.Element { timestamp: selectedTimeStamp, apmToTraceQuery, stepInterval, + safeNavigate, })} > View Traces diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index 78b0d576aa58..0b05246accc4 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -12,6 +12,7 @@ import { latency } from 'container/MetricsApplication/MetricsPageQueries/Overvie import { Card, GraphContainer } from 'container/MetricsApplication/styles'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { useAppContext } from 'providers/App/App'; import { useMemo } from 'react'; @@ -85,6 +86,8 @@ function ServiceOverview({ const apmToLogQuery = useGetAPMToLogsQueries({ servicename }); + const { safeNavigate } = useSafeNavigate(); + return ( <> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TableRenderer/ColumnWithLink.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TableRenderer/ColumnWithLink.tsx index 02dc0f9cf4c1..87870a6d1ee8 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TableRenderer/ColumnWithLink.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TableRenderer/ColumnWithLink.tsx @@ -1,5 +1,6 @@ import { Tooltip, Typography } from 'antd'; import { navigateToTrace } from 'container/MetricsApplication/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { v4 as uuid } from 'uuid'; @@ -14,6 +15,7 @@ function ColumnWithLink({ record, }: LinkColumnProps): JSX.Element { const text = record.toString(); + const { safeNavigate } = useSafeNavigate(); const apmToTraceQuery = useGetAPMToTracesQueries({ servicename, @@ -42,6 +44,7 @@ function ColumnWithLink({ maxTime, selectedTraceTags, apmToTraceQuery, + safeNavigate, }); }; diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts index e1f8164b0933..f38e48edfb18 100644 --- a/frontend/src/container/MetricsApplication/Tabs/util.ts +++ b/frontend/src/container/MetricsApplication/Tabs/util.ts @@ -6,7 +6,6 @@ import { getQueryString } from 'container/SideNav/helper'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils'; -import history from 'lib/history'; import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils'; import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils'; import { Dispatch, SetStateAction, useMemo } from 'react'; @@ -36,6 +35,7 @@ interface OnViewTracePopupClickProps { apmToTraceQuery: Query; isViewLogsClicked?: boolean; stepInterval?: number; + safeNavigate: (url: string) => void; } export function generateExplorerPath( @@ -63,6 +63,7 @@ export function onViewTracePopupClick({ apmToTraceQuery, isViewLogsClicked, stepInterval, + safeNavigate, }: OnViewTracePopupClickProps): VoidFunction { return (): void => { const endTime = timestamp; @@ -88,7 +89,7 @@ export function onViewTracePopupClick({ queryString, ); - history.push(newPath); + safeNavigate(newPath); }; } @@ -111,7 +112,7 @@ export function onGraphClickHandler( buttonElement.style.display = 'block'; buttonElement.style.left = `${mouseX}px`; buttonElement.style.top = `${mouseY}px`; - setSelectedTimeStamp(xValue); + setSelectedTimeStamp(Math.floor(xValue * 1_000)); } } else if (buttonElement && buttonElement.style.display === 'block') { buttonElement.style.display = 'none'; diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index da90045b6edb..14c46f7ff3d2 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -8,6 +8,7 @@ import Download from 'container/Download/Download'; import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useRef } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -31,7 +32,7 @@ function TopOperationsTable({ }: TopOperationsTableProps): JSX.Element { const searchInput = useRef(null); const { servicename: encodedServiceName } = useParams(); - + const { safeNavigate } = useSafeNavigate(); const servicename = decodeURIComponent(encodedServiceName); const { minTime, maxTime } = useSelector( (state) => state.globalTime, @@ -87,6 +88,7 @@ function TopOperationsTable({ maxTime, selectedTraceTags, apmToTraceQuery: preparedQuery, + safeNavigate, }); }; @@ -126,7 +128,7 @@ function TopOperationsTable({ key: 'p50', width: 50, sorter: (a: TopOperationList, b: TopOperationList): number => a.p50 - b.p50, - render: (value: number): string => (value / 1000000).toFixed(2), + render: (value: number): string => (value / 1_000_000).toFixed(2), }, { title: 'P95 (in ms)', @@ -134,7 +136,7 @@ function TopOperationsTable({ key: 'p95', width: 50, sorter: (a: TopOperationList, b: TopOperationList): number => a.p95 - b.p95, - render: (value: number): string => (value / 1000000).toFixed(2), + render: (value: number): string => (value / 1_000_000).toFixed(2), }, { title: 'P99 (in ms)', @@ -142,7 +144,7 @@ function TopOperationsTable({ key: 'p99', width: 50, sorter: (a: TopOperationList, b: TopOperationList): number => a.p99 - b.p99, - render: (value: number): string => (value / 1000000).toFixed(2), + render: (value: number): string => (value / 1_000_000).toFixed(2), }, { title: 'Number of Calls', diff --git a/frontend/src/container/MetricsApplication/types.ts b/frontend/src/container/MetricsApplication/types.ts index 60e7f269341d..5f53204d273b 100644 --- a/frontend/src/container/MetricsApplication/types.ts +++ b/frontend/src/container/MetricsApplication/types.ts @@ -21,6 +21,7 @@ export interface NavigateToTraceProps { maxTime: number; selectedTraceTags: string; apmToTraceQuery: Query; + safeNavigate: (path: string) => void; } export interface DatabaseCallsRPSProps extends DatabaseCallProps { diff --git a/frontend/src/container/MetricsApplication/utils.ts b/frontend/src/container/MetricsApplication/utils.ts index 303632b06f7a..d0e56f242e31 100644 --- a/frontend/src/container/MetricsApplication/utils.ts +++ b/frontend/src/container/MetricsApplication/utils.ts @@ -1,6 +1,5 @@ import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; -import history from 'lib/history'; import { TopOperationList } from './TopOperationsTable'; import { NavigateToTraceProps } from './types'; @@ -19,10 +18,14 @@ export const navigateToTrace = ({ maxTime, selectedTraceTags, apmToTraceQuery, + safeNavigate, }: NavigateToTraceProps): void => { const urlParams = new URLSearchParams(); - urlParams.set(QueryParams.startTime, (minTime / 1000000).toString()); - urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString()); + urlParams.set( + QueryParams.startTime, + Math.floor(minTime / 1_000_000).toString(), + ); + urlParams.set(QueryParams.endTime, Math.floor(maxTime / 1_000_000).toString()); const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery)); @@ -32,7 +35,7 @@ export const navigateToTrace = ({ QueryParams.compositeQuery }=${JSONCompositeQuery}`; - history.push(newTraceExplorerPath); + safeNavigate(newTraceExplorerPath); }; export const getNearestHighestBucketValue = ( diff --git a/frontend/src/container/NewDashboard/DashboardDescription/__tests__/DashboardDescription.test.tsx b/frontend/src/container/NewDashboard/DashboardDescription/__tests__/DashboardDescription.test.tsx index 8e302042a3c1..eac398d42d4b 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/__tests__/DashboardDescription.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/__tests__/DashboardDescription.test.tsx @@ -25,6 +25,12 @@ jest.mock( }, ); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('Dashboard landing page actions header tests', () => { it('unlock dashboard should be disabled for integrations created dashboards', async () => { const mockLocation = { diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 8725e7f43edc..8e116522be9b 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -21,8 +21,8 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { isEmpty } from 'lodash-es'; import { Check, @@ -89,6 +89,7 @@ export function sanitizeDashboardData( // eslint-disable-next-line sonarjs/cognitive-complexity function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const { handle } = props; const { selectedDashboard, @@ -311,7 +312,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { urlQuery.delete(QueryParams.relativeTime); const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); } return ( diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index 9262a4a76601..2d6e46f1f806 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -3,11 +3,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import GetMinMax from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; -import history from 'lib/history'; import { Dispatch, SetStateAction, @@ -33,6 +33,7 @@ function WidgetGraph({ const dispatch = useDispatch(); const urlQuery = useUrlQuery(); const location = useLocation(); + const { safeNavigate } = useSafeNavigate(); const handleBackNavigation = (): void => { const searchParams = new URLSearchParams(window.location.search); @@ -71,9 +72,9 @@ function WidgetGraph({ urlQuery.set(QueryParams.startTime, minTime.toString()); urlQuery.set(QueryParams.endTime, maxTime.toString()); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.push(generatedUrl); + safeNavigate(generatedUrl); }, - [dispatch, location.pathname, urlQuery], + [dispatch, location.pathname, safeNavigate, urlQuery], ); useEffect(() => { diff --git a/frontend/src/container/NewWidget/RightContainer/ColumnUnitSelector/__tests__/ColumnSelector.test.tsx b/frontend/src/container/NewWidget/RightContainer/ColumnUnitSelector/__tests__/ColumnSelector.test.tsx index 03b0f00cc51e..0f392caea226 100644 --- a/frontend/src/container/NewWidget/RightContainer/ColumnUnitSelector/__tests__/ColumnSelector.test.tsx +++ b/frontend/src/container/NewWidget/RightContainer/ColumnUnitSelector/__tests__/ColumnSelector.test.tsx @@ -87,6 +87,12 @@ jest.mock('hooks/queryBuilder/useGetCompositeQueryParam', () => ({ useGetCompositeQueryParam: (): Query => compositeQueryParam as Query, })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('Column unit selector panel unit test', () => { it('unit selectors should be rendered for queries and formula', () => { const mockLocation = { diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index c553f8b15801..5d85eb365207 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -20,10 +20,10 @@ import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useAxiosError from 'hooks/useAxiosError'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; -import history from 'lib/history'; import { defaultTo, isEmpty, isUndefined } from 'lodash-es'; import { Check, X } from 'lucide-react'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; @@ -67,6 +67,7 @@ import { } from './utils'; function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const { selectedDashboard, setSelectedDashboard, @@ -328,7 +329,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { } const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) }; updatedQuery.builder.queryData[0].pageSize = 10; - redirectWithQueryBuilderData(updatedQuery); + + // If stagedQuery exists, don't re-run the query (e.g. when clicking on Add to Dashboard from logs and traces explorer) + if (!stagedQuery) { + redirectWithQueryBuilderData(updatedQuery); + } return { query: updatedQuery, graphType: PANEL_TYPES.LIST, @@ -469,7 +474,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { setSelectedRowWidgetId(null); setSelectedDashboard(dashboard); setToScrollWidgetId(selectedWidget?.id || ''); - history.push({ + safeNavigate({ pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }), }); }, @@ -492,6 +497,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { setSelectedDashboard, setToScrollWidgetId, setSelectedRowWidgetId, + safeNavigate, dashboardId, ]); @@ -500,12 +506,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { setDiscardModal(true); return; } - history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); - }, [dashboardId, isQueryModified]); + safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId })); + }, [dashboardId, isQueryModified, safeNavigate]); const discardChanges = useCallback(() => { - history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); - }, [dashboardId]); + safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId })); + }, [dashboardId, safeNavigate]); const setGraphHandler = (type: PANEL_TYPES): void => { setIsLoadingPanelData(true); diff --git a/frontend/src/container/QueryTable/__test__/QueryTable.test.tsx b/frontend/src/container/QueryTable/__test__/QueryTable.test.tsx index ef00a58cebf4..d4baba732183 100644 --- a/frontend/src/container/QueryTable/__test__/QueryTable.test.tsx +++ b/frontend/src/container/QueryTable/__test__/QueryTable.test.tsx @@ -23,6 +23,12 @@ jest.mock('providers/Dashboard/Dashboard', () => ({ }), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('QueryTable -', () => { it('should render correctly with all the data rows', () => { const { container } = render(); diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index b75500b4b8ad..81fd282f1368 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -23,17 +23,18 @@ import { QueryHistoryState } from 'container/LiveLogs/types'; import NewExplorerCTA from 'container/NewExplorerCTA'; import dayjs, { Dayjs } from 'dayjs'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; -import history from 'lib/history'; import { isObject } from 'lodash-es'; import { Check, Copy, Info, Send, Undo } from 'lucide-react'; import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useQueryClient } from 'react-query'; -import { connect, useSelector } from 'react-redux'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { useNavigationType } from 'react-router-dom-v5-compat'; import { useCopyToClipboard } from 'react-use'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -43,6 +44,7 @@ import AppActions from 'types/actions'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { GlobalReducer } from 'types/reducer/globalTime'; +import { normalizeTimeToMs } from 'utils/timeUtils'; import AutoRefresh from '../AutoRefreshV2'; import { DateTimeRangeType } from '../CustomDateTimeModal'; @@ -75,6 +77,9 @@ function DateTimeSelection({ modalSelectedInterval, }: Props): JSX.Element { const [formSelector] = Form.useForm(); + const { safeNavigate } = useSafeNavigate(); + const navigationType = useNavigationType(); // Returns 'POP' for back/forward navigation + const dispatch = useDispatch(); const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -189,8 +194,8 @@ function DateTimeSelection({ const path = `${ROUTES.LIVE_LOGS}?${QueryParams.compositeQuery}=${JSONCompositeQuery}`; - history.push(path, queryHistoryState); - }, [panelType, queryClient, stagedQuery]); + safeNavigate(path, { state: queryHistoryState }); + }, [panelType, queryClient, safeNavigate, stagedQuery]); const { maxTime, minTime, selectedTime } = useSelector< AppState, @@ -349,7 +354,7 @@ function DateTimeSelection({ urlQuery.set(QueryParams.relativeTime, value); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); } // For logs explorer - time range handling is managed in useCopyLogLink.ts:52 @@ -368,6 +373,7 @@ function DateTimeSelection({ location.pathname, onTimeChange, refreshButtonHidden, + safeNavigate, stagedQuery, updateLocalStorageForRoutes, updateTimeInterval, @@ -440,7 +446,7 @@ function DateTimeSelection({ urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString()); urlQuery.delete(QueryParams.relativeTime); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); } } } @@ -467,7 +473,7 @@ function DateTimeSelection({ urlQuery.set(QueryParams.relativeTime, dateTimeStr); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); } if (!stagedQuery) { @@ -509,6 +515,77 @@ function DateTimeSelection({ return time; }; + const handleAbsoluteTimeSync = useCallback( + ( + startTime: string, + endTime: string, + currentMinTime: number, + currentMaxTime: number, + ): void => { + const startTs = normalizeTimeToMs(startTime); + const endTs = normalizeTimeToMs(endTime); + + const timeComparison = { + url: { + start: dayjs(startTs).startOf('minute'), + end: dayjs(endTs).startOf('minute'), + }, + current: { + start: dayjs(normalizeTimeToMs(currentMinTime)).startOf('minute'), + end: dayjs(normalizeTimeToMs(currentMaxTime)).startOf('minute'), + }, + }; + + const hasTimeChanged = + !timeComparison.current.start.isSame(timeComparison.url.start) || + !timeComparison.current.end.isSame(timeComparison.url.end); + + if (hasTimeChanged) { + dispatch(UpdateTimeInterval('custom', [startTs, endTs])); + } + }, + [dispatch], + ); + + const handleRelativeTimeSync = useCallback( + (relativeTime: string): void => { + updateTimeInterval(relativeTime as Time); + setIsValidteRelativeTime(true); + setRefreshButtonHidden(false); + }, + [updateTimeInterval], + ); + + // Sync time picker state with URL on browser navigation + useEffect(() => { + if (navigationType !== 'POP') return; + + if (searchStartTime && searchEndTime) { + handleAbsoluteTimeSync(searchStartTime, searchEndTime, minTime, maxTime); + return; + } + + if ( + relativeTimeFromUrl && + isValidTimeFormat(relativeTimeFromUrl) && + relativeTimeFromUrl !== selectedTime + ) { + handleRelativeTimeSync(relativeTimeFromUrl); + } + }, [ + navigationType, + searchStartTime, + searchEndTime, + relativeTimeFromUrl, + selectedTime, + minTime, + maxTime, + dispatch, + updateTimeInterval, + handleAbsoluteTimeSync, + handleRelativeTimeSync, + ]); + // this is triggred when we change the routes and based on that we are changing the default options useEffect(() => { const metricsTimeDuration = getLocalStorageKey( @@ -524,6 +601,16 @@ function DateTimeSelection({ const currentRoute = location.pathname; + // Give priority to relativeTime from URL if it exists and start /end time are not present in the url, to sync the relative time in URL param with the time picker + if ( + !searchStartTime && + !searchEndTime && + relativeTimeFromUrl && + isValidTimeFormat(relativeTimeFromUrl) + ) { + handleRelativeTimeSync(relativeTimeFromUrl); + } + // set the default relative time for alert history and overview pages if relative time is not specified if ( (!urlQuery.has(QueryParams.startTime) || @@ -535,7 +622,7 @@ function DateTimeSelection({ updateTimeInterval(defaultRelativeTime); urlQuery.set(QueryParams.relativeTime, defaultRelativeTime); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); return; } @@ -573,7 +660,7 @@ function DateTimeSelection({ const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname, updateTimeInterval, globalTimeLoading]); diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx index c7fd171a6d5d..182aa1d8816c 100644 --- a/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx @@ -12,6 +12,7 @@ import { transformDataWithDate, } from 'container/TracesExplorer/ListView/utils'; import { Pagination } from 'hooks/queryPagination'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; @@ -39,6 +40,7 @@ function TracesTableComponent({ offset: 0, limit: 10, }); + const { safeNavigate } = useSafeNavigate(); useEffect(() => { setRequestData((prev) => ({ @@ -87,6 +89,25 @@ function TracesTableComponent({ [], ); + const handlePaginationChange = useCallback( + (newPagination: Pagination) => { + const urlQuery = new URLSearchParams(window.location.search); + + // Update URL with new pagination values + urlQuery.set('offset', newPagination.offset.toString()); + urlQuery.set('limit', newPagination.limit.toString()); + + // Update URL without page reload + safeNavigate({ + search: urlQuery.toString(), + }); + + // Update component state + setPagination(newPagination); + }, + [safeNavigate], + ); + if (queryResponse.isError) { return
{SOMETHING_WENT_WRONG}
; } @@ -116,19 +137,19 @@ function TracesTableComponent({ offset={pagination.offset} countPerPage={pagination.limit} handleNavigatePrevious={(): void => { - setPagination({ + handlePaginationChange({ ...pagination, offset: pagination.offset - pagination.limit, }); }} handleNavigateNext={(): void => { - setPagination({ + handlePaginationChange({ ...pagination, offset: pagination.offset + pagination.limit, }); }} handleCountItemsPerPageChange={(value): void => { - setPagination({ + handlePaginationChange({ ...pagination, limit: value, offset: 0, diff --git a/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx b/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx index 895f67ac3451..d7f712f56361 100644 --- a/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx +++ b/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx @@ -1,10 +1,10 @@ import { useMachine } from '@xstate/react'; import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { encode } from 'js-base64'; -import history from 'lib/history'; -import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { whilelistedKeys } from './config'; @@ -32,6 +32,7 @@ function ResourceProvider({ children }: Props): JSX.Element { const [queries, setQueries] = useState( getResourceAttributeQueriesFromURL(), ); + const { safeNavigate } = useSafeNavigate(); const urlQuery = useUrlQuery(); const [optionsData, setOptionsData] = useState({ @@ -39,6 +40,12 @@ function ResourceProvider({ children }: Props): JSX.Element { options: [], }); + // Watch for URL query changes + useEffect(() => { + const queriesFromUrl = getResourceAttributeQueriesFromURL(); + setQueries(queriesFromUrl); + }, [urlQuery]); + const handleLoading = (isLoading: boolean): void => { setLoading(isLoading); if (isLoading) { @@ -53,10 +60,10 @@ function ResourceProvider({ children }: Props): JSX.Element { encode(JSON.stringify(queries)), ); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); setQueries(queries); }, - [pathname, urlQuery], + [pathname, safeNavigate, urlQuery], ); const [state, send] = useMachine(ResourceAttributesFilterMachine, { diff --git a/frontend/src/hooks/useResourceAttribute/__tests__/useResourceAttribute.test.tsx b/frontend/src/hooks/useResourceAttribute/__tests__/useResourceAttribute.test.tsx index 653cf48e531b..2c4b3731a1fb 100644 --- a/frontend/src/hooks/useResourceAttribute/__tests__/useResourceAttribute.test.tsx +++ b/frontend/src/hooks/useResourceAttribute/__tests__/useResourceAttribute.test.tsx @@ -5,6 +5,12 @@ import { Router } from 'react-router-dom'; import ResourceProvider from '../ResourceProvider'; import useResourceAttribute from '../useResourceAttribute'; +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('useResourceAttribute component hook', () => { it('should not change other query params except for resourceAttribute', async () => { const history = createMemoryHistory({ diff --git a/frontend/src/hooks/useSafeNavigate.ts b/frontend/src/hooks/useSafeNavigate.ts new file mode 100644 index 000000000000..516782ad0e31 --- /dev/null +++ b/frontend/src/hooks/useSafeNavigate.ts @@ -0,0 +1,136 @@ +import { cloneDeep, isEqual } from 'lodash-es'; +import { useCallback } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; + +interface NavigateOptions { + replace?: boolean; + state?: any; +} + +interface SafeNavigateParams { + pathname?: string; + search?: string; +} + +const areUrlsEffectivelySame = (url1: URL, url2: URL): boolean => { + if (url1.pathname !== url2.pathname) return false; + + const params1 = new URLSearchParams(url1.search); + const params2 = new URLSearchParams(url2.search); + + const allParams = new Set([ + ...Array.from(params1.keys()), + ...Array.from(params2.keys()), + ]); + + return Array.from(allParams).every((param) => { + if (param === 'compositeQuery') { + try { + const query1 = params1.get('compositeQuery'); + const query2 = params2.get('compositeQuery'); + + if (!query1 || !query2) return false; + + const decoded1 = JSON.parse(decodeURIComponent(query1)); + const decoded2 = JSON.parse(decodeURIComponent(query2)); + + const filtered1 = cloneDeep(decoded1); + const filtered2 = cloneDeep(decoded2); + + delete filtered1.id; + delete filtered2.id; + + return isEqual(filtered1, filtered2); + } catch (error) { + console.warn('Error comparing compositeQuery:', error); + return false; + } + } + + return params1.get(param) === params2.get(param); + }); +}; + +/** + * Determines if this navigation is adding default/initial parameters + * Returns true if: + * 1. We're staying on the same page (same pathname) + * 2. Either: + * - Current URL has no params and target URL has params, or + * - Target URL has new params that didn't exist in current URL + */ +const isDefaultNavigation = (currentUrl: URL, targetUrl: URL): boolean => { + // Different pathnames means it's not a default navigation + if (currentUrl.pathname !== targetUrl.pathname) return false; + + const currentParams = new URLSearchParams(currentUrl.search); + const targetParams = new URLSearchParams(targetUrl.search); + + // Case 1: Clean URL getting params for the first time + if (!currentParams.toString() && targetParams.toString()) return true; + + // Case 2: Check for new params that didn't exist before + const currentKeys = new Set(Array.from(currentParams.keys())); + const targetKeys = new Set(Array.from(targetParams.keys())); + + // Find keys that exist in target but not in current + const newKeys = Array.from(targetKeys).filter((key) => !currentKeys.has(key)); + + return newKeys.length > 0; +}; +export const useSafeNavigate = (): { + safeNavigate: ( + to: string | SafeNavigateParams, + options?: NavigateOptions, + ) => void; +} => { + const navigate = useNavigate(); + const location = useLocation(); + + const safeNavigate = useCallback( + (to: string | SafeNavigateParams, options?: NavigateOptions) => { + const currentUrl = new URL( + `${location.pathname}${location.search}`, + window.location.origin, + ); + + let targetUrl: URL; + + if (typeof to === 'string') { + targetUrl = new URL(to, window.location.origin); + } else { + targetUrl = new URL( + `${to.pathname || location.pathname}${to.search || ''}`, + window.location.origin, + ); + } + + const urlsAreSame = areUrlsEffectivelySame(currentUrl, targetUrl); + const isDefaultParamsNavigation = isDefaultNavigation(currentUrl, targetUrl); + + if (urlsAreSame) { + return; + } + + const navigationOptions = { + ...options, + replace: isDefaultParamsNavigation || options?.replace, + }; + + if (typeof to === 'string') { + navigate(to, navigationOptions); + } else { + navigate( + { + pathname: to.pathname || location.pathname, + search: to.search, + }, + navigationOptions, + ); + } + }, + [navigate, location.pathname, location.search], + ); + + return { safeNavigate }; +}; diff --git a/frontend/src/hooks/useUrlQueryData.ts b/frontend/src/hooks/useUrlQueryData.ts index 2945ba96b240..314703b776ba 100644 --- a/frontend/src/hooks/useUrlQueryData.ts +++ b/frontend/src/hooks/useUrlQueryData.ts @@ -1,15 +1,16 @@ import { useCallback, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { useSafeNavigate } from './useSafeNavigate'; import useUrlQuery from './useUrlQuery'; const useUrlQueryData = ( queryKey: string, defaultData?: T, ): UseUrlQueryData => { - const history = useHistory(); const location = useLocation(); const urlQuery = useUrlQuery(); + const { safeNavigate } = useSafeNavigate(); const query = useMemo(() => urlQuery.get(queryKey), [urlQuery, queryKey]); @@ -32,9 +33,9 @@ const useUrlQueryData = ( // Construct the new URL by combining the current pathname with the updated query string const generatedUrl = `${location.pathname}?${currentUrlQuery.toString()}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); }, - [history, location.pathname, queryKey], + [location.pathname, queryKey, safeNavigate], ); return { diff --git a/frontend/src/pages/AlertDetails/hooks.tsx b/frontend/src/pages/AlertDetails/hooks.tsx index c159d2169b2e..6e143ac18f13 100644 --- a/frontend/src/pages/AlertDetails/hooks.tsx +++ b/frontend/src/pages/AlertDetails/hooks.tsx @@ -20,6 +20,7 @@ import { urlKey } from 'container/AllError/utils'; import { RelativeTimeMap } from 'container/TopNav/DateTimeSelection/config'; import useAxiosError from 'hooks/useAxiosError'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import GetMinMax from 'lib/getMinMax'; @@ -321,6 +322,8 @@ export const useTimelineTable = ({ extra: any, ) => void; } => { + const { safeNavigate } = useSafeNavigate(); + const { pathname } = useLocation(); const { search } = useLocation(); @@ -343,7 +346,7 @@ export const useTimelineTable = ({ const updatedOrder = order === 'ascend' ? 'asc' : 'desc'; const params = new URLSearchParams(window.location.search); - history.replace( + safeNavigate( `${pathname}?${createQueryParams({ ...Object.fromEntries(params), order: updatedOrder, @@ -353,7 +356,7 @@ export const useTimelineTable = ({ ); } }, - [pathname], + [pathname, safeNavigate], ); const offsetInt = parseInt(offset, 10); diff --git a/frontend/src/pages/AlertList/index.tsx b/frontend/src/pages/AlertList/index.tsx index 19d746e8f0d6..d32df3c02b24 100644 --- a/frontend/src/pages/AlertList/index.tsx +++ b/frontend/src/pages/AlertList/index.tsx @@ -5,8 +5,8 @@ import ROUTES from 'constants/routes'; import AllAlertRules from 'container/ListAlertRules'; import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime'; import TriggeredAlerts from 'container/TriggeredAlerts'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { GalleryVerticalEnd, Pyramid } from 'lucide-react'; import AlertDetails from 'pages/AlertDetails'; import { useLocation } from 'react-router-dom'; @@ -14,6 +14,7 @@ import { useLocation } from 'react-router-dom'; function AllAlertList(): JSX.Element { const urlQuery = useUrlQuery(); const location = useLocation(); + const { safeNavigate } = useSafeNavigate(); const tab = urlQuery.get('tab'); const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY; @@ -67,7 +68,7 @@ function AllAlertList(): JSX.Element { if (search) { params += `&search=${search}`; } - history.replace(`/alerts?${params}`); + safeNavigate(`/alerts?${params}`); }} className={`${ isAlertHistory || isAlertOverview ? 'alert-details-tabs' : '' diff --git a/frontend/src/pages/DashboardWidget/index.tsx b/frontend/src/pages/DashboardWidget/index.tsx index 2e161497a259..c5dda71c8561 100644 --- a/frontend/src/pages/DashboardWidget/index.tsx +++ b/frontend/src/pages/DashboardWidget/index.tsx @@ -4,8 +4,8 @@ import { SOMETHING_WENT_WRONG } from 'constants/api'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import NewWidget from 'container/NewWidget'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useEffect, useState } from 'react'; import { generatePath, useLocation, useParams } from 'react-router-dom'; @@ -14,6 +14,7 @@ import { Widgets } from 'types/api/dashboard/getAll'; function DashboardWidget(): JSX.Element | null { const { search } = useLocation(); const { dashboardId } = useParams(); + const { safeNavigate } = useSafeNavigate(); const [selectedGraph, setSelectedGraph] = useState(); @@ -32,11 +33,11 @@ function DashboardWidget(): JSX.Element | null { const graphType = params.get('graphType') as PANEL_TYPES | null; if (graphType === null) { - history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); + safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId })); } else { setSelectedGraph(graphType); } - }, [dashboardId, search]); + }, [dashboardId, safeNavigate, search]); if (selectedGraph === undefined || dashboardResponse.isLoading) { return ; diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index d075316422b0..f2c63d5088b1 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -29,6 +29,12 @@ jest.mock('react-router-dom', () => ({ const mockWindowOpen = jest.fn(); window.open = mockWindowOpen; +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('dashboard list page', () => { // should render on updatedAt and descend when the column key and order is messed up it('should render the list even when the columnKey or the order is mismatched', async () => { diff --git a/frontend/src/pages/EditRules/index.tsx b/frontend/src/pages/EditRules/index.tsx index 372a8a199eb9..4d9d2b02977e 100644 --- a/frontend/src/pages/EditRules/index.tsx +++ b/frontend/src/pages/EditRules/index.tsx @@ -8,6 +8,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import ROUTES from 'constants/routes'; import EditRulesContainer from 'container/EditRules'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; import { useEffect } from 'react'; @@ -21,6 +22,7 @@ import { } from './constants'; function EditRules(): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const params = useUrlQuery(); const ruleId = params.get(QueryParams.ruleId); const { t } = useTranslation('common'); @@ -55,9 +57,9 @@ function EditRules(): JSX.Element { notifications.error({ message: 'Rule Id is required', }); - history.replace(ROUTES.LIST_ALL_ALERT); + safeNavigate(ROUTES.LIST_ALL_ALERT); } - }, [isValidRuleId, ruleId, notifications]); + }, [isValidRuleId, ruleId, notifications, safeNavigate]); if ( (isError && !isValidRuleId) || diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx index f22bfbac8036..1b1e3d841722 100644 --- a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -67,6 +67,12 @@ jest.mock('d3-interpolate', () => ({ interpolate: jest.fn(), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + const logsQueryServerRequest = (): void => server.use( rest.post(queryRangeURL, (req, res, ctx) => diff --git a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx index 6fbbd49191ee..932300e2ab60 100644 --- a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx +++ b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx @@ -11,7 +11,7 @@ import logEvent from 'api/common/logEvent'; import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { isArray, isEmpty, isEqual } from 'lodash-es'; +import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es'; import { Dispatch, SetStateAction, @@ -177,6 +177,21 @@ export function Filter(props: FilterProps): JSX.Element { return items as TagFilterItem[]; }; + const removeFilterItemIds = (query: Query): Query => { + const clonedQuery = cloneDeep(query); + clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({ + ...data, + filters: { + ...data.filters, + items: data.filters?.items?.map((item) => ({ + ...item, + id: '', + })), + }, + })); + return clonedQuery; + }; + const handleRun = useCallback( (props?: HandleRunProps): void => { const preparedQuery: Query = { @@ -204,9 +219,16 @@ export function Filter(props: FilterProps): JSX.Element { }); } - if (isEqual(currentQuery, preparedQuery) && !props?.resetAll) { + const currentQueryWithoutIds = removeFilterItemIds(currentQuery); + const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery); + + if ( + isEqual(currentQueryWithoutIds, preparedQueryWithoutIds) && + !props?.resetAll + ) { return; } + redirectWithQueryBuilderData(preparedQuery); }, [currentQuery, redirectWithQueryBuilderData, selectedFilters], diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index d5e89feb20d6..2c1f80e07f19 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -116,6 +116,12 @@ jest.mock('react-redux', () => ({ }), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + describe('TracesExplorer - Filters', () => { // Initial filter panel rendering // Test the initial state like which filters section are opened, default state of duration slider, etc. diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index c61a3fc583f4..448e41bf337c 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -24,7 +24,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useNotifications } from 'hooks/useNotifications'; -import history from 'lib/history'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { cloneDeep, isEmpty, set } from 'lodash-es'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -61,6 +61,7 @@ function TracesExplorer(): JSX.Element { const currentPanelType = useGetPanelTypesQueryParam(); const { handleExplorerTabChange } = useHandleExplorerTabChange(); + const { safeNavigate } = useSafeNavigate(); const currentTab = panelType || PANEL_TYPES.LIST; @@ -197,7 +198,7 @@ function TracesExplorer(): JSX.Element { widgetId, }); - history.push(dashboardEditView); + safeNavigate(dashboardEditView); }, onError: (error) => { if (axios.isAxiosError(error)) { diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx index aff7b7371c06..5bd6d4617387 100644 --- a/frontend/src/providers/Dashboard/Dashboard.tsx +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -9,10 +9,10 @@ import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import dayjs, { Dayjs } from 'dayjs'; import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage'; import useAxiosError from 'hooks/useAxiosError'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useTabVisibility from 'hooks/useTabFocus'; import useUrlQuery from 'hooks/useUrlQuery'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; -import history from 'lib/history'; import { defaultTo } from 'lodash-es'; import isEqual from 'lodash-es/isEqual'; import isUndefined from 'lodash-es/isUndefined'; @@ -84,6 +84,7 @@ interface Props { export function DashboardProvider({ children, }: PropsWithChildren): JSX.Element { + const { safeNavigate } = useSafeNavigate(); const [isDashboardSliderOpen, setIsDashboardSlider] = useState(false); const [toScrollWidgetId, setToScrollWidgetId] = useState(''); @@ -145,7 +146,7 @@ export function DashboardProvider({ params.set('order', sortOrder.order as string); params.set('page', sortOrder.pagination || '1'); params.set('search', sortOrder.search || ''); - history.replace({ search: params.toString() }); + safeNavigate({ search: params.toString() }); } const dispatch = useDispatch>(); diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 2c157c0e1f68..6233bf569c7c 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -23,6 +23,7 @@ import { import { OptionsQuery } from 'container/OptionsMenu/types'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; @@ -39,7 +40,7 @@ import { useState, } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; // ** Types import { @@ -96,7 +97,6 @@ export function QueryBuilderProvider({ children, }: PropsWithChildren): JSX.Element { const urlQuery = useUrlQuery(); - const history = useHistory(); const location = useLocation(); const currentPathnameRef = useRef(location.pathname); @@ -763,6 +763,8 @@ export function QueryBuilderProvider({ [panelType, stagedQuery], ); + const { safeNavigate } = useSafeNavigate(); + const redirectWithQueryBuilderData = useCallback( ( query: Partial, @@ -833,9 +835,9 @@ export function QueryBuilderProvider({ ? `${redirectingUrl}?${urlQuery}` : `${location.pathname}?${urlQuery}`; - history.replace(generatedUrl); + safeNavigate(generatedUrl); }, - [history, location.pathname, urlQuery], + [location.pathname, safeNavigate, urlQuery], ); const handleSetConfig = useCallback( diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx index 1091005a2ac1..b1423d7b3ace 100644 --- a/frontend/src/tests/test-utils.tsx +++ b/frontend/src/tests/test-utils.tsx @@ -88,6 +88,17 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: jest.fn(), + }), +})); + +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useNavigationType: (): any => 'PUSH', +})); + export function getAppContextMock( role: string, appContextOverrides?: Partial, diff --git a/frontend/src/utils/timeUtils.ts b/frontend/src/utils/timeUtils.ts index e93a96b4d253..e64284a34f44 100644 --- a/frontend/src/utils/timeUtils.ts +++ b/frontend/src/utils/timeUtils.ts @@ -134,3 +134,21 @@ export const epochToTimeString = (epochMs: number): string => { }; return date.toLocaleTimeString('en-US', options); }; + +/** + * Converts nanoseconds to milliseconds + * @param timestamp - The timestamp to convert + * @returns The timestamp in milliseconds + */ +export const normalizeTimeToMs = (timestamp: number | string): number => { + let ts = timestamp; + if (typeof timestamp === 'string') { + ts = Math.trunc(parseInt(timestamp, 10)); + } + ts = Number(ts); + + // Check if timestamp is in nanoseconds (19+ digits) + const isNanoSeconds = ts.toString().length >= 19; + + return isNanoSeconds ? Math.floor(ts / 1_000_000) : ts; +};