mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-17 05:11:27 +08:00
Feat: back navigation support throughout the app (#6701)
* feat: custom hook to prevent redundant navigation and handle default params with URL comparison * feat: implement useSafeNavigation to QB, to ensure that the back navigation works properly * fix: handle syncing the relativeTime param with the time picker selected relative time * feat: add support for absolute and relative time sync with time picker component * refactor: integrate safeNavigate in LogsExplorerChart and deprecate the existing back navigation * feat: update pagination query params on pressing next/prev page * fix: fix the issue of NOOP getting converted to Count on coming back from alert creation page * refactor: replace history navigation with safeNavigate in DateTimeSelectionV2 component it also fixes the issue of relativeTime not being added to the url on mounting * feat: integrate useSafeNavigate across service details tabs * fix: fix duplicate redirections by converting the timestamp to milliseconds * fix: replace history navigation with useSafeNavigate in LogsExplorerViews and useUrlQueryData * fix: replace history navigation with useSafeNavigate across dashboard components * fix: use safeNavigate in alert components * fix: fix the issue of back navigation in alert table and sync the pagination with url param * fix: handle back navigation for resource filter and sync the state with url query * fix: fix the issue of double redirection from top operations to traces * fix: replace history.push with safeNavigate in TracesExplorer's updateDashboard * fix: prevent unnecessary query re-runs by checking stagedQuery before redirecting in NewWidget * chore: cleanup * fix: fix the failing tests * fix: fix the documentation redirection failing tests * test: mock useSafeNavigate hook in WidgetGraphComponent test * test: mock useSafeNavigate hook in ExplorerCard test
This commit is contained in:
parent
ef635b6b60
commit
82d84c041c
@ -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'),
|
||||
}));
|
||||
|
@ -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<ColumnsType | undefined>(
|
||||
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 (
|
||||
<div className="DynamicColumnTable">
|
||||
<Flex justify="flex-end" align="center" gap={8}>
|
||||
@ -116,6 +144,7 @@ function DynamicColumnTable({
|
||||
<ResizeTable
|
||||
columns={columnsData}
|
||||
onDragColumn={onDragColumn}
|
||||
pagination={enhancedPagination}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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<any> {
|
||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||
facingIssueBtn?: LaunchChatSupportProps;
|
||||
shouldSendAlertsLogEvent?: boolean;
|
||||
pagination?: PaginationProps;
|
||||
}
|
||||
|
||||
export type GetVisibleColumnsFunction = (
|
||||
|
@ -27,6 +27,12 @@ jest.mock('uplot', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
let mockWindowOpen: jest.Mock;
|
||||
|
||||
window.ResizeObserver =
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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<
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
});
|
||||
|
@ -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(() => {
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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"
|
||||
>
|
||||
<Link
|
||||
to={getLink()}
|
||||
<div
|
||||
className="title-link"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
safeNavigate(getLink());
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={dashboard?.image || Base64Icons[0]}
|
||||
@ -460,7 +462,7 @@ function DashboardsList(): JSX.Element {
|
||||
>
|
||||
{dashboard.name}
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
@ -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<Record<string, unknown>>();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
const [isUploadJSONError, setIsUploadJSONError] = useState<boolean>(false);
|
||||
@ -97,7 +98,7 @@ function ImportJSON({
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
history.push(
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
|
@ -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<GetChartDataProps>['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({
|
||||
|
@ -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<any>;
|
||||
}): 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,
|
||||
|
@ -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({
|
||||
|
@ -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 (
|
||||
<Row gutter={24}>
|
||||
@ -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
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<>
|
||||
<GraphControlsPanel
|
||||
@ -96,6 +99,7 @@ function ServiceOverview({
|
||||
apmToTraceQuery: apmToLogQuery,
|
||||
isViewLogsClicked: true,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
@ -103,6 +107,7 @@ function ServiceOverview({
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
<Card data-testid="service_latency">
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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<InputRef>(null);
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(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',
|
||||
|
@ -21,6 +21,7 @@ export interface NavigateToTraceProps {
|
||||
maxTime: number;
|
||||
selectedTraceTags: string;
|
||||
apmToTraceQuery: Query;
|
||||
safeNavigate: (path: string) => void;
|
||||
}
|
||||
|
||||
export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
|
@ -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 = (
|
||||
|
@ -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 = {
|
||||
|
@ -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 (
|
||||
|
@ -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(() => {
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
|
@ -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(<QueryTable {...QueryTableProps} />);
|
||||
|
@ -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<boolean>(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]);
|
||||
|
||||
|
@ -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 <div>{SOMETHING_WENT_WRONG}</div>;
|
||||
}
|
||||
@ -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,
|
||||
|
@ -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<IResourceAttribute[]>(
|
||||
getResourceAttributeQueriesFromURL(),
|
||||
);
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [optionsData, setOptionsData] = useState<OptionsData>({
|
||||
@ -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, {
|
||||
|
@ -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({
|
||||
|
136
frontend/src/hooks/useSafeNavigate.ts
Normal file
136
frontend/src/hooks/useSafeNavigate.ts
Normal file
@ -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 };
|
||||
};
|
@ -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 = <T>(
|
||||
queryKey: string,
|
||||
defaultData?: T,
|
||||
): UseUrlQueryData<T> => {
|
||||
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 = <T>(
|
||||
// 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 {
|
||||
|
@ -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);
|
||||
|
@ -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' : ''
|
||||
|
@ -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<DashboardWidgetPageParams>();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const [selectedGraph, setSelectedGraph] = useState<PANEL_TYPES>();
|
||||
|
||||
@ -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 <Spinner tip="Loading.." />;
|
||||
|
@ -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 () => {
|
||||
|
@ -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) ||
|
||||
|
@ -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) =>
|
||||
|
@ -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],
|
||||
|
@ -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.
|
||||
|
@ -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)) {
|
||||
|
@ -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<boolean>(false);
|
||||
|
||||
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
|
||||
@ -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<Dispatch<AppActions>>();
|
||||
|
@ -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<string | null>(location.pathname);
|
||||
@ -763,6 +763,8 @@ export function QueryBuilderProvider({
|
||||
[panelType, stagedQuery],
|
||||
);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const redirectWithQueryBuilderData = useCallback(
|
||||
(
|
||||
query: Partial<Query>,
|
||||
@ -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(
|
||||
|
@ -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<IAppContext>,
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user