mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 13:55:54 +08:00
feat: added 10 minute time range for traces co-relation in api monitoring (#7815)
* feat: added 10 minute time range for traces co-relation in api monitoring * feat: url sharing for domain details drawer w/o filters for endpoint stats * feat: added endpoint details persistence * feat: external services to api monitoring co relation - 0 * feat: added api co relations to other panels on external services * fix: cosmetic fix * feat: addded tests for url sharing utils * feat: minor cosmetic changes * fix: changed traces co relation window from 10 minutes to 5 minutes * fix: minor copy changes * fix: pr comments * fix: minor bug fix * fix: pr comments improvements
This commit is contained in:
parent
81b8f93177
commit
16938c6cc0
@ -69,5 +69,5 @@
|
|||||||
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
||||||
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
||||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||||
"API_MONITORING": "SigNoz | API Monitoring"
|
"API_MONITORING": "SigNoz | Third Party API"
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,12 @@ import {
|
|||||||
import GridCard from 'container/GridCardLayout/GridCard';
|
import GridCard from 'container/GridCardLayout/GridCard';
|
||||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { useApiMonitoringParams } from '../../../queryParams';
|
||||||
import { SPAN_ATTRIBUTES, VIEWS } from './constants';
|
import { SPAN_ATTRIBUTES, VIEWS } from './constants';
|
||||||
|
|
||||||
function AllEndPoints({
|
function AllEndPoints({
|
||||||
@ -35,6 +37,7 @@ function AllEndPoints({
|
|||||||
initialFilters: IBuilderQuery['filters'];
|
initialFilters: IBuilderQuery['filters'];
|
||||||
setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void;
|
setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
const [groupBySearchValue, setGroupBySearchValue] = useState<string>('');
|
const [groupBySearchValue, setGroupBySearchValue] = useState<string>('');
|
||||||
const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{
|
const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -55,87 +58,38 @@ function AllEndPoints({
|
|||||||
{ value: string; label: string }[]
|
{ value: string; label: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const handleGroupByChange = useCallback(
|
// --- FILTERS STATE SYNC ---
|
||||||
(value: IBuilderQuery['groupBy']) => {
|
|
||||||
const newGroupBy = [];
|
|
||||||
|
|
||||||
for (let index = 0; index < value.length; index++) {
|
|
||||||
const element = (value[index] as unknown) as string;
|
|
||||||
|
|
||||||
// Check if the key exists in our cached options first
|
|
||||||
if (allAvailableGroupByOptions[element]) {
|
|
||||||
newGroupBy.push(allAvailableGroupByOptions[element]);
|
|
||||||
} else {
|
|
||||||
// If not found in cache, check the current filtered results
|
|
||||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
|
||||||
(key) => key.key === element,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
newGroupBy.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setGroupBy(newGroupBy);
|
|
||||||
setGroupBySearchValue('');
|
|
||||||
},
|
|
||||||
[groupByFiltersData, setGroupBy, allAvailableGroupByOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (groupByFiltersData?.payload) {
|
|
||||||
// Update dropdown options
|
|
||||||
setGroupByOptions(
|
|
||||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
|
||||||
value: filter.key,
|
|
||||||
label: filter.key,
|
|
||||||
})) || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cache all available options to preserve selected values using functional update
|
|
||||||
// to avoid dependency on allAvailableGroupByOptions
|
|
||||||
setAllAvailableGroupByOptions((prevOptions) => {
|
|
||||||
const newOptions = { ...prevOptions };
|
|
||||||
groupByFiltersData?.payload?.attributeKeys?.forEach((filter) => {
|
|
||||||
newOptions[filter.key] = filter;
|
|
||||||
});
|
|
||||||
return newOptions;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [groupByFiltersData]); // Only depends on groupByFiltersData now
|
|
||||||
|
|
||||||
// Cache existing selected options on component mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (groupBy && groupBy.length > 0) {
|
|
||||||
setAllAvailableGroupByOptions((prevOptions) => {
|
|
||||||
const newOptions = { ...prevOptions };
|
|
||||||
groupBy.forEach((option) => {
|
|
||||||
newOptions[option.key] = option;
|
|
||||||
});
|
|
||||||
return newOptions;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [groupBy]); // Removed allAvailableGroupByOptions from dependencies
|
|
||||||
|
|
||||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
|
||||||
|
|
||||||
// Local state for filters, combining endpoint filter and search filters
|
|
||||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||||
|
if (params.allEndpointsLocalFilters) {
|
||||||
|
return params.allEndpointsLocalFilters;
|
||||||
|
}
|
||||||
// Initialize filters based on the initial endPointName prop
|
// Initialize filters based on the initial endPointName prop
|
||||||
const initialItems = [...initialFilters.items];
|
const initialItems = [...initialFilters.items];
|
||||||
return { op: 'AND', items: initialItems };
|
return { op: 'AND', items: initialItems };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync params to local filters state on param change
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
params.allEndpointsLocalFilters &&
|
||||||
|
!isEqual(params.allEndpointsLocalFilters, filters)
|
||||||
|
) {
|
||||||
|
setFilters(params.allEndpointsLocalFilters);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [params.allEndpointsLocalFilters]);
|
||||||
|
|
||||||
// Handler for changes from the QueryBuilderSearchV2 component
|
// Handler for changes from the QueryBuilderSearchV2 component
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(newFilters: IBuilderQuery['filters']): void => {
|
(newFilters: IBuilderQuery['filters']): void => {
|
||||||
// 1. Update local filters state immediately
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
|
setParams({ allEndpointsLocalFilters: newFilters });
|
||||||
},
|
},
|
||||||
[], // Dependencies for the callback
|
[setParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||||
|
|
||||||
const updatedCurrentQuery = useMemo(
|
const updatedCurrentQuery = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
@ -160,6 +114,91 @@ function AllEndPoints({
|
|||||||
[groupBy, domainName, filters],
|
[groupBy, domainName, filters],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- GROUP BY STATE SYNC (existing) ---
|
||||||
|
const handleGroupByChange = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
const newGroupBy = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const element = (value[index] as unknown) as string;
|
||||||
|
|
||||||
|
// Check if the key exists in our cached options first
|
||||||
|
if (allAvailableGroupByOptions[element]) {
|
||||||
|
newGroupBy.push(allAvailableGroupByOptions[element]);
|
||||||
|
} else {
|
||||||
|
// If not found in cache, check the current filtered results
|
||||||
|
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||||
|
(key) => key.key === element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
newGroupBy.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupBy(newGroupBy);
|
||||||
|
setGroupBySearchValue('');
|
||||||
|
// Update params
|
||||||
|
setParams({ groupBy: newGroupBy.map((g) => g.key) });
|
||||||
|
},
|
||||||
|
[groupByFiltersData, setGroupBy, allAvailableGroupByOptions, setParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync params to local groupBy state on param change
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
params.groupBy &&
|
||||||
|
Array.isArray(params.groupBy) &&
|
||||||
|
!isEqual(
|
||||||
|
params.groupBy,
|
||||||
|
groupBy.map((g) => g.key),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Only update if different
|
||||||
|
const newGroupBy = params.groupBy
|
||||||
|
.map((key) => allAvailableGroupByOptions[key])
|
||||||
|
.filter(Boolean);
|
||||||
|
if (newGroupBy.length === params.groupBy.length) {
|
||||||
|
setGroupBy(newGroupBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [params.groupBy, allAvailableGroupByOptions, groupBy, setGroupBy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupByFiltersData?.payload) {
|
||||||
|
// Update dropdown options
|
||||||
|
setGroupByOptions(
|
||||||
|
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||||
|
value: filter.key,
|
||||||
|
label: filter.key,
|
||||||
|
})) || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache all available options to preserve selected values using functional update
|
||||||
|
setAllAvailableGroupByOptions((prevOptions) => {
|
||||||
|
const newOptions = { ...prevOptions };
|
||||||
|
groupByFiltersData?.payload?.attributeKeys?.forEach((filter) => {
|
||||||
|
newOptions[filter.key] = filter;
|
||||||
|
});
|
||||||
|
return newOptions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [groupByFiltersData]); // Only depends on groupByFiltersData now
|
||||||
|
|
||||||
|
// Cache existing selected options on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupBy && groupBy.length > 0) {
|
||||||
|
setAllAvailableGroupByOptions((prevOptions) => {
|
||||||
|
const newOptions = { ...prevOptions };
|
||||||
|
groupBy.forEach((option) => {
|
||||||
|
newOptions[option.key] = option;
|
||||||
|
});
|
||||||
|
return newOptions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [groupBy]);
|
||||||
|
|
||||||
const onRowClick = useCallback(
|
const onRowClick = useCallback(
|
||||||
(props: any): void => {
|
(props: any): void => {
|
||||||
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
||||||
@ -172,6 +211,14 @@ function AllEndPoints({
|
|||||||
items: initialItems,
|
items: initialItems,
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
});
|
});
|
||||||
|
setParams({
|
||||||
|
selectedEndPointName: props[SPAN_ATTRIBUTES.URL_PATH] as string,
|
||||||
|
selectedView: VIEWS.ENDPOINT_STATS,
|
||||||
|
endPointDetailsLocalFilters: {
|
||||||
|
items: initialItems,
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
filters,
|
filters,
|
||||||
@ -179,6 +226,7 @@ function AllEndPoints({
|
|||||||
setSelectedEndPointName,
|
setSelectedEndPointName,
|
||||||
setSelectedView,
|
setSelectedView,
|
||||||
groupBy,
|
groupBy,
|
||||||
|
setParams,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -11,12 +11,13 @@ import {
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { useApiMonitoringParams } from '../../../queryParams';
|
||||||
import AllEndPoints from './AllEndPoints';
|
import AllEndPoints from './AllEndPoints';
|
||||||
import DomainMetrics from './components/DomainMetrics';
|
import DomainMetrics from './components/DomainMetrics';
|
||||||
import { VIEW_TYPES, VIEWS } from './constants';
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
@ -40,8 +41,13 @@ function DomainDetails({
|
|||||||
domainListLength: number;
|
domainListLength: number;
|
||||||
domainListFilters: IBuilderQuery['filters'];
|
domainListFilters: IBuilderQuery['filters'];
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
|
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||||
|
(params.selectedView as VIEWS) || VIEWS.ALL_ENDPOINTS,
|
||||||
|
);
|
||||||
|
const [selectedEndPointName, setSelectedEndPointName] = useState<string>(
|
||||||
|
params.selectedEndPointName || '',
|
||||||
|
);
|
||||||
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
||||||
IBuilderQuery['groupBy']
|
IBuilderQuery['groupBy']
|
||||||
>([]);
|
>([]);
|
||||||
@ -52,8 +58,27 @@ function DomainDetails({
|
|||||||
|
|
||||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
setSelectedView(e.target.value);
|
setSelectedView(e.target.value);
|
||||||
|
setParams({ selectedView: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEndPointChange = (name: string): void => {
|
||||||
|
setSelectedEndPointName(name);
|
||||||
|
setParams({ selectedEndPointName: name });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.selectedView && params.selectedView !== selectedView) {
|
||||||
|
setSelectedView(params.selectedView as VIEWS);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
params.selectedEndPointName !== undefined &&
|
||||||
|
params.selectedEndPointName !== selectedEndPointName
|
||||||
|
) {
|
||||||
|
setSelectedEndPointName(params.selectedEndPointName);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [params.selectedView, params.selectedEndPointName]);
|
||||||
|
|
||||||
const { maxTime, minTime, selectedTime } = useSelector<
|
const { maxTime, minTime, selectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
@ -67,34 +92,62 @@ function DomainDetails({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||||
selectedTime as Time,
|
(params.selectedInterval as Time) || (selectedTime as Time),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
// Sync params to local selectedInterval state on param change
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.selectedInterval && params.selectedInterval !== selectedInterval) {
|
||||||
|
setSelectedInterval(params.selectedInterval as Time);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [params.selectedInterval]);
|
||||||
|
|
||||||
|
const [modalTimeRange, setModalTimeRange] = useState(() => {
|
||||||
|
if (params.modalTimeRange) {
|
||||||
|
return params.modalTimeRange;
|
||||||
|
}
|
||||||
|
return {
|
||||||
startTime: startMs,
|
startTime: startMs,
|
||||||
endTime: endMs,
|
endTime: endMs,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync params to local modalTimeRange state on param change
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
params.modalTimeRange &&
|
||||||
|
JSON.stringify(params.modalTimeRange) !== JSON.stringify(modalTimeRange)
|
||||||
|
) {
|
||||||
|
setModalTimeRange(params.modalTimeRange);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [params.modalTimeRange]);
|
||||||
|
|
||||||
const handleTimeChange = useCallback(
|
const handleTimeChange = useCallback(
|
||||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||||
setSelectedInterval(interval as Time);
|
setSelectedInterval(interval as Time);
|
||||||
|
setParams({ selectedInterval: interval as string });
|
||||||
|
|
||||||
if (interval === 'custom' && dateTimeRange) {
|
if (interval === 'custom' && dateTimeRange) {
|
||||||
setModalTimeRange({
|
const newRange = {
|
||||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||||
});
|
};
|
||||||
|
setModalTimeRange(newRange);
|
||||||
|
setParams({ modalTimeRange: newRange });
|
||||||
} else {
|
} else {
|
||||||
const { maxTime, minTime } = GetMinMax(interval);
|
const { maxTime, minTime } = GetMinMax(interval);
|
||||||
|
|
||||||
setModalTimeRange({
|
const newRange = {
|
||||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||||
});
|
};
|
||||||
|
setModalTimeRange(newRange);
|
||||||
|
setParams({ modalTimeRange: newRange });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[setParams],
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -174,7 +227,6 @@ function DomainDetails({
|
|||||||
>
|
>
|
||||||
<Radio.Button
|
<Radio.Button
|
||||||
className={
|
className={
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
||||||
}
|
}
|
||||||
value={VIEW_TYPES.ALL_ENDPOINTS}
|
value={VIEW_TYPES.ALL_ENDPOINTS}
|
||||||
@ -204,7 +256,7 @@ function DomainDetails({
|
|||||||
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
||||||
<AllEndPoints
|
<AllEndPoints
|
||||||
domainName={domainData.domainName}
|
domainName={domainData.domainName}
|
||||||
setSelectedEndPointName={setSelectedEndPointName}
|
setSelectedEndPointName={handleEndPointChange}
|
||||||
setSelectedView={setSelectedView}
|
setSelectedView={setSelectedView}
|
||||||
groupBy={endPointsGroupBy}
|
groupBy={endPointsGroupBy}
|
||||||
setGroupBy={setEndPointsGroupBy}
|
setGroupBy={setEndPointsGroupBy}
|
||||||
@ -218,7 +270,7 @@ function DomainDetails({
|
|||||||
<EndPointDetails
|
<EndPointDetails
|
||||||
domainName={domainData.domainName}
|
domainName={domainData.domainName}
|
||||||
endPointName={selectedEndPointName}
|
endPointName={selectedEndPointName}
|
||||||
setSelectedEndPointName={setSelectedEndPointName}
|
setSelectedEndPointName={handleEndPointChange}
|
||||||
initialFilters={initialFiltersEndPointStats}
|
initialFilters={initialFiltersEndPointStats}
|
||||||
timeRange={modalTimeRange}
|
timeRange={modalTimeRange}
|
||||||
handleTimeChange={handleTimeChange}
|
handleTimeChange={handleTimeChange}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
|
||||||
import {
|
import {
|
||||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||||
extractPortAndEndpoint,
|
extractPortAndEndpoint,
|
||||||
@ -59,13 +60,16 @@ function EndPointDetails({
|
|||||||
) => void;
|
) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||||
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
|
|
||||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||||
|
|
||||||
// Local state for filters, combining endpoint filter and search filters
|
// Local state for filters, combining endpoint filter and search filters
|
||||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||||
// Initialize filters based on the initial endPointName prop
|
// Initialize filters based on the initial endPointName prop
|
||||||
const initialItems = [...initialFilters.items];
|
const initialItems = params.endPointDetailsLocalFilters
|
||||||
|
? [...params.endPointDetailsLocalFilters.items]
|
||||||
|
: [...initialFilters.items];
|
||||||
if (endPointName) {
|
if (endPointName) {
|
||||||
initialItems.push({
|
initialItems.push({
|
||||||
id: '92b8a1c1',
|
id: '92b8a1c1',
|
||||||
@ -107,11 +111,26 @@ function EndPointDetails({
|
|||||||
});
|
});
|
||||||
}, [endPointName]);
|
}, [endPointName]);
|
||||||
|
|
||||||
|
// Separate effect to update params when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
const filtersWithoutHttpUrl = {
|
||||||
|
op: 'AND',
|
||||||
|
items: filters.items.filter((item) => item.key?.key !== httpUrlKey.key),
|
||||||
|
};
|
||||||
|
setParams({ endPointDetailsLocalFilters: filtersWithoutHttpUrl });
|
||||||
|
}, [filters, setParams]);
|
||||||
|
|
||||||
// Handler for changes from the QueryBuilderSearchV2 component
|
// Handler for changes from the QueryBuilderSearchV2 component
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(newFilters: IBuilderQuery['filters']): void => {
|
(newFilters: IBuilderQuery['filters']): void => {
|
||||||
// 1. Update local filters state immediately
|
// 1. Update local filters state immediately
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
|
// Filter out http.url filter before saving to params
|
||||||
|
const filteredNewFilters = {
|
||||||
|
op: 'AND',
|
||||||
|
items: newFilters.items.filter((item) => item.key?.key !== httpUrlKey.key),
|
||||||
|
};
|
||||||
|
setParams({ endPointDetailsLocalFilters: filteredNewFilters });
|
||||||
|
|
||||||
// 2. Derive the endpoint name from the *new* filters state
|
// 2. Derive the endpoint name from the *new* filters state
|
||||||
const httpUrlFilter = newFilters.items.find(
|
const httpUrlFilter = newFilters.items.find(
|
||||||
@ -126,7 +145,7 @@ function EndPointDetails({
|
|||||||
setSelectedEndPointName(derivedEndPointName);
|
setSelectedEndPointName(derivedEndPointName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[endPointName, setSelectedEndPointName], // Dependencies for the callback
|
[endPointName, setSelectedEndPointName, setParams], // Dependencies for the callback
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedCurrentQuery = useMemo(
|
const updatedCurrentQuery = useMemo(
|
||||||
|
@ -215,7 +215,9 @@ function TopErrors({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography.Text className="no-filtered-endpoints-message">
|
<Typography.Text className="no-filtered-endpoints-message">
|
||||||
This query had no results. Edit your query and try again!
|
{showStatusCodeErrors
|
||||||
|
? 'Please disable "Status Message Exists" toggle to see all errors'
|
||||||
|
: 'This query had no results. Edit your query and try again!'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,7 @@ function MetricOverTimeGraph({
|
|||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
customOnDragSelect={(): void => {}}
|
customOnDragSelect={(): void => {}}
|
||||||
customTimeRange={timeRange}
|
customTimeRange={timeRange}
|
||||||
|
customTimeRangeWindowForCoRelation="5m"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -19,6 +19,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
@ -150,7 +151,12 @@ function StatusCodeBarCharts({
|
|||||||
metric?: { [key: string]: string },
|
metric?: { [key: string]: string },
|
||||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||||
): void => {
|
): void => {
|
||||||
|
const TWO_AND_HALF_MINUTES_IN_MILLISECONDS = 2.5 * 60 * 1000; // 150,000 milliseconds
|
||||||
const customFilters = getCustomFiltersForBarChart(metric);
|
const customFilters = getCustomFiltersForBarChart(metric);
|
||||||
|
const { start, end } = getStartAndEndTimesInMilliseconds(
|
||||||
|
xValue,
|
||||||
|
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
|
||||||
|
);
|
||||||
handleGraphClick({
|
handleGraphClick({
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
@ -164,6 +170,10 @@ function StatusCodeBarCharts({
|
|||||||
notifications,
|
notifications,
|
||||||
graphClick,
|
graphClick,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
customTracesTimeRange: {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
@ -16,7 +16,7 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -25,6 +25,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { DEFAULT_PARAMS, useApiMonitoringParams } from '../../queryParams';
|
||||||
import {
|
import {
|
||||||
columnsConfig,
|
columnsConfig,
|
||||||
formatDataForTable,
|
formatDataForTable,
|
||||||
@ -32,7 +33,9 @@ import {
|
|||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import DomainDetails from './DomainDetails/DomainDetails';
|
import DomainDetails from './DomainDetails/DomainDetails';
|
||||||
|
|
||||||
function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
function DomainList(): JSX.Element {
|
||||||
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
|
const { showIP, selectedDomain } = params;
|
||||||
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
|
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
@ -129,6 +132,16 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
|||||||
[data],
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Open drawer if selectedDomain is set in URL
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedDomain && formattedDataForTable?.length > 0) {
|
||||||
|
const idx = formattedDataForTable.findIndex(
|
||||||
|
(item) => item.domainName === selectedDomain,
|
||||||
|
);
|
||||||
|
setSelectedDomainIndex(idx);
|
||||||
|
}
|
||||||
|
}, [selectedDomain, formattedDataForTable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={cx('api-module-right-section')}>
|
<section className={cx('api-module-right-section')}>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
@ -179,6 +192,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
|||||||
(item) => item.key === record.key,
|
(item) => item.key === record.key,
|
||||||
);
|
);
|
||||||
setSelectedDomainIndex(dataIndex);
|
setSelectedDomainIndex(dataIndex);
|
||||||
|
setParams({ selectedDomain: record.domainName });
|
||||||
logEvent('API Monitoring: Domain name row clicked', {});
|
logEvent('API Monitoring: Domain name row clicked', {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -196,6 +210,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
|||||||
domainListLength={formattedDataForTable.length}
|
domainListLength={formattedDataForTable.length}
|
||||||
handleClose={(): void => {
|
handleClose={(): void => {
|
||||||
setSelectedDomainIndex(-1);
|
setSelectedDomainIndex(-1);
|
||||||
|
setParams(DEFAULT_PARAMS);
|
||||||
}}
|
}}
|
||||||
domainListFilters={query?.filters}
|
domainListFilters={query?.filters}
|
||||||
/>
|
/>
|
||||||
|
@ -8,13 +8,15 @@ import cx from 'classnames';
|
|||||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useApiMonitoringParams } from '../queryParams';
|
||||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||||
import DomainList from './Domains/DomainList';
|
import DomainList from './Domains/DomainList';
|
||||||
|
|
||||||
function Explorer(): JSX.Element {
|
function Explorer(): JSX.Element {
|
||||||
const [showIP, setShowIP] = useState<boolean>(true);
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
|
const showIP = params.showIP ?? true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent('API Monitoring: Landing page visited', {});
|
logEvent('API Monitoring: Landing page visited', {});
|
||||||
@ -34,14 +36,12 @@ function Explorer(): JSX.Element {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginLeft: 'auto' }}
|
style={{ marginLeft: 'auto' }}
|
||||||
checked={showIP}
|
checked={showIP ?? true}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setShowIP((showIP): boolean => {
|
|
||||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||||
showIP: !showIP,
|
showIP: !(showIP ?? true),
|
||||||
});
|
|
||||||
return !showIP;
|
|
||||||
});
|
});
|
||||||
|
setParams({ showIP });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -52,7 +52,7 @@ function Explorer(): JSX.Element {
|
|||||||
handleFilterVisibilityChange={(): void => {}}
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<DomainList showIP={showIP} />
|
<DomainList />
|
||||||
</div>
|
</div>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -79,6 +79,24 @@ jest.mock('antd', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock useApiMonitoringParams hook
|
||||||
|
jest.mock('container/ApiMonitoring/queryParams', () => ({
|
||||||
|
useApiMonitoringParams: jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
showIP: true,
|
||||||
|
selectedDomain: '',
|
||||||
|
selectedView: 'all_endpoints',
|
||||||
|
selectedEndPointName: '',
|
||||||
|
groupBy: [],
|
||||||
|
allEndpointsLocalFilters: undefined,
|
||||||
|
endPointDetailsLocalFilters: undefined,
|
||||||
|
modalTimeRange: undefined,
|
||||||
|
selectedInterval: undefined,
|
||||||
|
},
|
||||||
|
jest.fn(),
|
||||||
|
]),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AllEndPoints', () => {
|
describe('AllEndPoints', () => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
domainName: 'test-domain',
|
domainName: 'test-domain',
|
||||||
|
@ -25,6 +25,24 @@ jest.mock('react-query', () => ({
|
|||||||
useQueries: jest.fn(),
|
useQueries: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock useApiMonitoringParams hook
|
||||||
|
jest.mock('container/ApiMonitoring/queryParams', () => ({
|
||||||
|
useApiMonitoringParams: jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
showIP: true,
|
||||||
|
selectedDomain: '',
|
||||||
|
selectedView: 'all_endpoints',
|
||||||
|
selectedEndPointName: '',
|
||||||
|
groupBy: [],
|
||||||
|
allEndpointsLocalFilters: undefined,
|
||||||
|
endPointDetailsLocalFilters: undefined,
|
||||||
|
modalTimeRange: undefined,
|
||||||
|
selectedInterval: undefined,
|
||||||
|
},
|
||||||
|
jest.fn(),
|
||||||
|
]),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
|
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
|
||||||
'endPointMetricsData',
|
'endPointMetricsData',
|
||||||
|
@ -0,0 +1,276 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import {
|
||||||
|
ApiMonitoringParams,
|
||||||
|
DEFAULT_PARAMS,
|
||||||
|
getApiMonitoringParams,
|
||||||
|
setApiMonitoringParams,
|
||||||
|
} from 'container/ApiMonitoring/queryParams';
|
||||||
|
|
||||||
|
// Mock react-router-dom hooks
|
||||||
|
jest.mock('react-router-dom', () => {
|
||||||
|
const originalModule = jest.requireActual('react-router-dom');
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
useLocation: jest.fn(),
|
||||||
|
useHistory: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API Monitoring Query Params', () => {
|
||||||
|
describe('getApiMonitoringParams', () => {
|
||||||
|
it('returns default params when no query param exists', () => {
|
||||||
|
const search = '';
|
||||||
|
expect(getApiMonitoringParams(search)).toEqual(DEFAULT_PARAMS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses URL query params correctly', () => {
|
||||||
|
const mockParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: false,
|
||||||
|
selectedDomain: 'test-domain',
|
||||||
|
selectedView: 'test-view',
|
||||||
|
selectedEndPointName: '/api/test',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a URL search string with encoded params
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set(
|
||||||
|
'apiMonitoringParams',
|
||||||
|
encodeURIComponent(JSON.stringify(mockParams)),
|
||||||
|
);
|
||||||
|
const search = `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
const result = getApiMonitoringParams(search);
|
||||||
|
|
||||||
|
// Only check specific properties that we set, not all DEFAULT_PARAMS
|
||||||
|
expect(result.showIP).toBe(mockParams.showIP);
|
||||||
|
expect(result.selectedDomain).toBe(mockParams.selectedDomain);
|
||||||
|
expect(result.selectedView).toBe(mockParams.selectedView);
|
||||||
|
expect(result.selectedEndPointName).toBe(mockParams.selectedEndPointName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default params when parsing fails', () => {
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set('apiMonitoringParams', 'invalid-json');
|
||||||
|
const search = `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
expect(getApiMonitoringParams(search)).toEqual(DEFAULT_PARAMS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setApiMonitoringParams', () => {
|
||||||
|
it('updates URL with new params (push mode)', () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
};
|
||||||
|
const search = '';
|
||||||
|
const newParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: false,
|
||||||
|
selectedDomain: 'updated-domain',
|
||||||
|
};
|
||||||
|
|
||||||
|
setApiMonitoringParams(newParams, search, history as any, false);
|
||||||
|
|
||||||
|
expect(history.push).toHaveBeenCalledWith({
|
||||||
|
search: expect.stringContaining('apiMonitoringParams'),
|
||||||
|
});
|
||||||
|
expect(history.replace).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify that the search string contains the expected encoded params
|
||||||
|
const searchArg = history.push.mock.calls[0][0].search;
|
||||||
|
const params = new URLSearchParams(searchArg);
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test only the specific fields we set
|
||||||
|
expect(decoded.showIP).toBe(newParams.showIP);
|
||||||
|
expect(decoded.selectedDomain).toBe(newParams.selectedDomain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates URL with new params (replace mode)', () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
};
|
||||||
|
const search = '';
|
||||||
|
const newParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: false,
|
||||||
|
selectedDomain: 'updated-domain',
|
||||||
|
};
|
||||||
|
|
||||||
|
setApiMonitoringParams(newParams, search, history as any, true);
|
||||||
|
|
||||||
|
expect(history.replace).toHaveBeenCalledWith({
|
||||||
|
search: expect.stringContaining('apiMonitoringParams'),
|
||||||
|
});
|
||||||
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges new params with existing params', () => {
|
||||||
|
const history = {
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start with some existing params
|
||||||
|
const existingParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: true,
|
||||||
|
selectedDomain: 'domain-1',
|
||||||
|
selectedView: 'view-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set(
|
||||||
|
'apiMonitoringParams',
|
||||||
|
encodeURIComponent(JSON.stringify(existingParams)),
|
||||||
|
);
|
||||||
|
const search = `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
// Add some new params
|
||||||
|
const newParams: Partial<ApiMonitoringParams> = {
|
||||||
|
selectedDomain: 'domain-2',
|
||||||
|
selectedEndPointName: '/api/test',
|
||||||
|
};
|
||||||
|
|
||||||
|
setApiMonitoringParams(newParams, search, history as any, false);
|
||||||
|
|
||||||
|
// Verify merged params
|
||||||
|
const searchArg = history.push.mock.calls[0][0].search;
|
||||||
|
const params = new URLSearchParams(searchArg);
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test only the specific fields
|
||||||
|
expect(decoded.showIP).toBe(existingParams.showIP);
|
||||||
|
expect(decoded.selectedView).toBe(existingParams.selectedView);
|
||||||
|
expect(decoded.selectedDomain).toBe(newParams.selectedDomain); // This should be overwritten
|
||||||
|
expect(decoded.selectedEndPointName).toBe(newParams.selectedEndPointName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useApiMonitoringParams hook without calling hook directly', () => {
|
||||||
|
// Instead of using the hook directly, We are testing the individual functions that make up the hook
|
||||||
|
// as the original hook contains react core hooks
|
||||||
|
const mockUseLocationAndHistory = (initialSearch = ''): any => {
|
||||||
|
// Create mock location object
|
||||||
|
const location = {
|
||||||
|
search: initialSearch,
|
||||||
|
pathname: '/some-path',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock history object
|
||||||
|
const history = {
|
||||||
|
push: jest.fn((args) => {
|
||||||
|
// Simulate updating the location search
|
||||||
|
location.search = args.search;
|
||||||
|
}),
|
||||||
|
replace: jest.fn((args) => {
|
||||||
|
location.search = args.search;
|
||||||
|
}),
|
||||||
|
length: 1,
|
||||||
|
location,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up mocks for useLocation and useHistory
|
||||||
|
const useLocationMock = jest.requireMock('react-router-dom').useLocation;
|
||||||
|
const useHistoryMock = jest.requireMock('react-router-dom').useHistory;
|
||||||
|
|
||||||
|
useLocationMock.mockReturnValue(location);
|
||||||
|
useHistoryMock.mockReturnValue(history);
|
||||||
|
|
||||||
|
return { location, history };
|
||||||
|
};
|
||||||
|
|
||||||
|
it('retrieves URL params correctly from location', () => {
|
||||||
|
const testParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: false,
|
||||||
|
selectedDomain: 'test-domain',
|
||||||
|
selectedView: 'custom-view',
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set(
|
||||||
|
'apiMonitoringParams',
|
||||||
|
encodeURIComponent(JSON.stringify(testParams)),
|
||||||
|
);
|
||||||
|
const search = `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
const result = getApiMonitoringParams(search);
|
||||||
|
|
||||||
|
// Test only specific fields
|
||||||
|
expect(result.showIP).toBe(testParams.showIP);
|
||||||
|
expect(result.selectedDomain).toBe(testParams.selectedDomain);
|
||||||
|
expect(result.selectedView).toBe(testParams.selectedView);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates URL correctly with new params', () => {
|
||||||
|
const { location, history } = mockUseLocationAndHistory();
|
||||||
|
|
||||||
|
const newParams: Partial<ApiMonitoringParams> = {
|
||||||
|
selectedDomain: 'new-domain',
|
||||||
|
showIP: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manually execute the core logic of the hook's setParams function
|
||||||
|
setApiMonitoringParams(newParams, location.search, history as any);
|
||||||
|
|
||||||
|
expect(history.push).toHaveBeenCalledWith({
|
||||||
|
search: expect.stringContaining('apiMonitoringParams'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchArg = history.push.mock.calls[0][0].search;
|
||||||
|
const params = new URLSearchParams(searchArg);
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test only specific fields
|
||||||
|
expect(decoded.selectedDomain).toBe(newParams.selectedDomain);
|
||||||
|
expect(decoded.showIP).toBe(newParams.showIP);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing params when updating', () => {
|
||||||
|
// Create a search string with existing params
|
||||||
|
const initialParams: Partial<ApiMonitoringParams> = {
|
||||||
|
showIP: false,
|
||||||
|
selectedDomain: 'initial-domain',
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set(
|
||||||
|
'apiMonitoringParams',
|
||||||
|
encodeURIComponent(JSON.stringify(initialParams)),
|
||||||
|
);
|
||||||
|
const initialSearch = `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
// Set up mocks
|
||||||
|
const { location, history } = mockUseLocationAndHistory(initialSearch);
|
||||||
|
|
||||||
|
// Manually execute the core logic
|
||||||
|
setApiMonitoringParams(
|
||||||
|
{ selectedView: 'new-view' },
|
||||||
|
location.search,
|
||||||
|
history as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify history was updated
|
||||||
|
expect(history.push).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Parse the new query params from the URL
|
||||||
|
const searchArg = history.push.mock.calls[0][0].search;
|
||||||
|
const params = new URLSearchParams(searchArg);
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test only specific fields
|
||||||
|
expect(decoded.showIP).toBe(initialParams.showIP);
|
||||||
|
expect(decoded.selectedDomain).toBe(initialParams.selectedDomain);
|
||||||
|
expect(decoded.selectedView).toBe('new-view');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
88
frontend/src/container/ApiMonitoring/queryParams.ts
Normal file
88
frontend/src/container/ApiMonitoring/queryParams.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
// --- Types for all API Monitoring query params ---
|
||||||
|
export interface ApiMonitoringParams {
|
||||||
|
showIP?: boolean;
|
||||||
|
selectedDomain?: string;
|
||||||
|
selectedView?: string;
|
||||||
|
selectedEndPointName?: string;
|
||||||
|
groupBy?: string[];
|
||||||
|
allEndpointsLocalFilters?: any;
|
||||||
|
endPointDetailsLocalFilters?: any;
|
||||||
|
modalTimeRange?: { startTime: number; endTime: number };
|
||||||
|
selectedInterval?: string;
|
||||||
|
// Add more params as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PARAMS: ApiMonitoringParams = {
|
||||||
|
showIP: true,
|
||||||
|
selectedDomain: '',
|
||||||
|
selectedView: 'all_endpoints',
|
||||||
|
selectedEndPointName: '',
|
||||||
|
groupBy: [],
|
||||||
|
allEndpointsLocalFilters: undefined,
|
||||||
|
endPointDetailsLocalFilters: undefined,
|
||||||
|
modalTimeRange: undefined,
|
||||||
|
selectedInterval: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PARAM_KEY = 'apiMonitoringParams';
|
||||||
|
|
||||||
|
// --- Parse and serialize helpers ---
|
||||||
|
function encodeParams(params: ApiMonitoringParams): string {
|
||||||
|
return encodeURIComponent(JSON.stringify(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeParams(value: string | null): ApiMonitoringParams {
|
||||||
|
if (!value) return DEFAULT_PARAMS;
|
||||||
|
try {
|
||||||
|
return JSON.parse(decodeURIComponent(value));
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_PARAMS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Read query params from URL ---
|
||||||
|
export function getApiMonitoringParams(search: string): ApiMonitoringParams {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
return decodeParams(params.get(PARAM_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Set query params in URL (replace or push) ---
|
||||||
|
export function setApiMonitoringParams(
|
||||||
|
newParams: Partial<ApiMonitoringParams>,
|
||||||
|
search: string,
|
||||||
|
history: ReturnType<typeof useHistory>,
|
||||||
|
replace = false,
|
||||||
|
): void {
|
||||||
|
const urlParams = new URLSearchParams(search);
|
||||||
|
const current = decodeParams(urlParams.get(PARAM_KEY));
|
||||||
|
const merged = { ...current, ...newParams };
|
||||||
|
urlParams.set(PARAM_KEY, encodeParams(merged));
|
||||||
|
const newSearch = `?${urlParams.toString()}`;
|
||||||
|
if (replace) {
|
||||||
|
history.replace({ search: newSearch });
|
||||||
|
} else {
|
||||||
|
history.push({ search: newSearch });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- React hook to use query params in a component ---
|
||||||
|
export function useApiMonitoringParams(): [
|
||||||
|
ApiMonitoringParams,
|
||||||
|
(newParams: Partial<ApiMonitoringParams>, replace?: boolean) => void,
|
||||||
|
] {
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
const params = getApiMonitoringParams(location.search);
|
||||||
|
|
||||||
|
const setParams = useCallback(
|
||||||
|
(newParams: Partial<ApiMonitoringParams>, replace = false) => {
|
||||||
|
setApiMonitoringParams(newParams, location.search, history, replace);
|
||||||
|
},
|
||||||
|
[location.search, history],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [params, setParams];
|
||||||
|
}
|
@ -3796,9 +3796,9 @@ export const getRateOverTimeWidgetData = (
|
|||||||
): Widgets => {
|
): Widgets => {
|
||||||
let legend = domainName;
|
let legend = domainName;
|
||||||
if (endPointName) {
|
if (endPointName) {
|
||||||
const { endpoint, port } = extractPortAndEndpoint(endPointName);
|
const { endpoint } = extractPortAndEndpoint(endPointName);
|
||||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||||
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
|
legend = `${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getWidgetQueryBuilder(
|
return getWidgetQueryBuilder(
|
||||||
@ -3861,9 +3861,9 @@ export const getLatencyOverTimeWidgetData = (
|
|||||||
): Widgets => {
|
): Widgets => {
|
||||||
let legend = domainName;
|
let legend = domainName;
|
||||||
if (endPointName) {
|
if (endPointName) {
|
||||||
const { endpoint, port } = extractPortAndEndpoint(endPointName);
|
const { endpoint } = extractPortAndEndpoint(endPointName);
|
||||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||||
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
|
legend = `${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getWidgetQueryBuilder(
|
return getWidgetQueryBuilder(
|
||||||
|
@ -15,6 +15,10 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import {
|
||||||
|
getCustomTimeRangeWindowSweepInMS,
|
||||||
|
getStartAndEndTimesInMilliseconds,
|
||||||
|
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -57,6 +61,7 @@ function WidgetGraphComponent({
|
|||||||
customSeries,
|
customSeries,
|
||||||
customErrorMessage,
|
customErrorMessage,
|
||||||
customOnRowClick,
|
customOnRowClick,
|
||||||
|
customTimeRangeWindowForCoRelation,
|
||||||
}: WidgetGraphComponentProps): JSX.Element {
|
}: WidgetGraphComponentProps): JSX.Element {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
@ -263,6 +268,13 @@ function WidgetGraphComponent({
|
|||||||
metric?: { [key: string]: string },
|
metric?: { [key: string]: string },
|
||||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||||
): void => {
|
): void => {
|
||||||
|
const customTracesTimeRange = getCustomTimeRangeWindowSweepInMS(
|
||||||
|
customTimeRangeWindowForCoRelation,
|
||||||
|
);
|
||||||
|
const { start, end } = getStartAndEndTimesInMilliseconds(
|
||||||
|
xValue,
|
||||||
|
customTracesTimeRange,
|
||||||
|
);
|
||||||
handleGraphClick({
|
handleGraphClick({
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
@ -275,6 +287,9 @@ function WidgetGraphComponent({
|
|||||||
navigateToExplorer,
|
navigateToExplorer,
|
||||||
notifications,
|
notifications,
|
||||||
graphClick,
|
graphClick,
|
||||||
|
...(customTimeRangeWindowForCoRelation
|
||||||
|
? { customTracesTimeRange: { start, end } }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -393,6 +408,7 @@ WidgetGraphComponent.defaultProps = {
|
|||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
setLayout: undefined,
|
setLayout: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
|
customTimeRangeWindowForCoRelation: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WidgetGraphComponent;
|
export default WidgetGraphComponent;
|
||||||
|
@ -49,6 +49,7 @@ function GridCardGraph({
|
|||||||
analyticsEvent,
|
analyticsEvent,
|
||||||
customTimeRange,
|
customTimeRange,
|
||||||
customOnRowClick,
|
customOnRowClick,
|
||||||
|
customTimeRangeWindowForCoRelation,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
@ -289,6 +290,7 @@ function GridCardGraph({
|
|||||||
customSeries={customSeries}
|
customSeries={customSeries}
|
||||||
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
|
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
|
||||||
customOnRowClick={customOnRowClick}
|
customOnRowClick={customOnRowClick}
|
||||||
|
customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -303,6 +305,7 @@ GridCardGraph.defaultProps = {
|
|||||||
headerMenuList: [MenuItemKeys.View],
|
headerMenuList: [MenuItemKeys.View],
|
||||||
version: 'v3',
|
version: 'v3',
|
||||||
analyticsEvent: undefined,
|
analyticsEvent: undefined,
|
||||||
|
customTimeRangeWindowForCoRelation: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(GridCardGraph);
|
export default memo(GridCardGraph);
|
||||||
|
@ -40,6 +40,7 @@ export interface WidgetGraphComponentProps {
|
|||||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||||
customErrorMessage?: string;
|
customErrorMessage?: string;
|
||||||
customOnRowClick?: (record: RowData) => void;
|
customOnRowClick?: (record: RowData) => void;
|
||||||
|
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridCardGraphProps {
|
export interface GridCardGraphProps {
|
||||||
@ -67,6 +68,7 @@ export interface GridCardGraphProps {
|
|||||||
endTime: number;
|
endTime: number;
|
||||||
};
|
};
|
||||||
customOnRowClick?: (record: RowData) => void;
|
customOnRowClick?: (record: RowData) => void;
|
||||||
|
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
@ -179,6 +179,7 @@ interface HandleGraphClickParams {
|
|||||||
notifications: NotificationInstance;
|
notifications: NotificationInstance;
|
||||||
graphClick: (props: GraphClickProps) => void;
|
graphClick: (props: GraphClickProps) => void;
|
||||||
customFilters?: TagFilterItem[];
|
customFilters?: TagFilterItem[];
|
||||||
|
customTracesTimeRange?: { start: number; end: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleGraphClick = async ({
|
export const handleGraphClick = async ({
|
||||||
@ -194,6 +195,7 @@ export const handleGraphClick = async ({
|
|||||||
notifications,
|
notifications,
|
||||||
graphClick,
|
graphClick,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
customTracesTimeRange,
|
||||||
}: HandleGraphClickParams): Promise<void> => {
|
}: HandleGraphClickParams): Promise<void> => {
|
||||||
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
||||||
|
|
||||||
@ -225,8 +227,10 @@ export const handleGraphClick = async ({
|
|||||||
navigateToExplorer({
|
navigateToExplorer({
|
||||||
filters: [...result[key].filters, ...(customFilters || [])],
|
filters: [...result[key].filters, ...(customFilters || [])],
|
||||||
dataSource: result[key].dataSource as DataSource,
|
dataSource: result[key].dataSource as DataSource,
|
||||||
startTime: xValue,
|
startTime: customTracesTimeRange ? customTracesTimeRange?.start : xValue,
|
||||||
endTime: xValue + (stepInterval ?? 60),
|
endTime: customTracesTimeRange
|
||||||
|
? customTracesTimeRange?.end
|
||||||
|
: xValue + (stepInterval ?? 60),
|
||||||
shouldResolveQuery: true,
|
shouldResolveQuery: true,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
@ -31,10 +31,11 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
||||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, Row } from '../styles';
|
import { Card, GraphContainer, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
|
||||||
import { IServiceName } from './types';
|
import { IServiceName } from './types';
|
||||||
import {
|
import {
|
||||||
handleNonInQueryRange,
|
handleNonInQueryRange,
|
||||||
|
onViewAPIMonitoringPopupClick,
|
||||||
onViewTracePopupClick,
|
onViewTracePopupClick,
|
||||||
useGetAPMToTracesQueries,
|
useGetAPMToTracesQueries,
|
||||||
useGraphClickHandler,
|
useGraphClickHandler,
|
||||||
@ -42,7 +43,7 @@ import {
|
|||||||
|
|
||||||
function External(): JSX.Element {
|
function External(): JSX.Element {
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
|
const [selectedData, setSelectedData] = useState<any>(undefined);
|
||||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||||
|
|
||||||
const servicename = decodeURIComponent(encodedServiceName);
|
const servicename = decodeURIComponent(encodedServiceName);
|
||||||
@ -223,17 +224,18 @@ function External(): JSX.Element {
|
|||||||
|
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
const onGraphClickHandler = useGraphClickHandler(setSelectedTimeStamp);
|
const onGraphClickHandler = useGraphClickHandler(
|
||||||
|
setSelectedTimeStamp,
|
||||||
|
setSelectedData,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<GraphControlsPanel
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
id="external_call_error_percentage_button"
|
id="external_call_error_percentage_button"
|
||||||
onClick={onViewTracePopupClick({
|
onViewTracesClick={onViewTracePopupClick({
|
||||||
servicename,
|
servicename,
|
||||||
selectedTraceTags,
|
selectedTraceTags,
|
||||||
timestamp: selectedTimeStamp,
|
timestamp: selectedTimeStamp,
|
||||||
@ -241,21 +243,28 @@ function External(): JSX.Element {
|
|||||||
stepInterval,
|
stepInterval,
|
||||||
safeNavigate,
|
safeNavigate,
|
||||||
})}
|
})}
|
||||||
>
|
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||||
View Traces
|
servicename,
|
||||||
</Button>
|
timestamp: selectedTimeStamp,
|
||||||
|
domainName: selectedData?.address || '',
|
||||||
|
isError: true,
|
||||||
|
stepInterval: 300,
|
||||||
|
safeNavigate,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Card data-testid="external_call_error_percentage">
|
<Card data-testid="external_call_error_percentage">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
widget={externalCallErrorWidget}
|
widget={externalCallErrorWidget}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||||
onGraphClickHandler(
|
onGraphClickHandler(
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
mouseX,
|
mouseX,
|
||||||
mouseY,
|
mouseY,
|
||||||
'external_call_error_percentage',
|
'external_call_error_percentage',
|
||||||
|
data,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
@ -266,11 +275,9 @@ function External(): JSX.Element {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<GraphControlsPanel
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
id="external_call_duration_button"
|
id="external_call_duration_button"
|
||||||
onClick={onViewTracePopupClick({
|
onViewTracesClick={onViewTracePopupClick({
|
||||||
servicename,
|
servicename,
|
||||||
selectedTraceTags,
|
selectedTraceTags,
|
||||||
timestamp: selectedTimeStamp,
|
timestamp: selectedTimeStamp,
|
||||||
@ -278,22 +285,29 @@ function External(): JSX.Element {
|
|||||||
stepInterval,
|
stepInterval,
|
||||||
safeNavigate,
|
safeNavigate,
|
||||||
})}
|
})}
|
||||||
>
|
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||||
View Traces
|
servicename,
|
||||||
</Button>
|
timestamp: selectedTimeStamp,
|
||||||
|
domainName: selectedData?.address,
|
||||||
|
isError: false,
|
||||||
|
stepInterval: 300,
|
||||||
|
safeNavigate,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card data-testid="external_call_duration">
|
<Card data-testid="external_call_duration">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
widget={externalCallDurationWidget}
|
widget={externalCallDurationWidget}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||||
onGraphClickHandler(
|
onGraphClickHandler(
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
mouseX,
|
mouseX,
|
||||||
mouseY,
|
mouseY,
|
||||||
'external_call_duration',
|
'external_call_duration',
|
||||||
|
data,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
@ -306,11 +320,9 @@ function External(): JSX.Element {
|
|||||||
|
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<GraphControlsPanel
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
id="external_call_rps_by_address_button"
|
id="external_call_rps_by_address_button"
|
||||||
onClick={onViewTracePopupClick({
|
onViewTracesClick={onViewTracePopupClick({
|
||||||
servicename,
|
servicename,
|
||||||
selectedTraceTags,
|
selectedTraceTags,
|
||||||
timestamp: selectedTimeStamp,
|
timestamp: selectedTimeStamp,
|
||||||
@ -318,21 +330,28 @@ function External(): JSX.Element {
|
|||||||
stepInterval,
|
stepInterval,
|
||||||
safeNavigate,
|
safeNavigate,
|
||||||
})}
|
})}
|
||||||
>
|
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||||
View Traces
|
servicename,
|
||||||
</Button>
|
timestamp: selectedTimeStamp,
|
||||||
|
domainName: selectedData?.address,
|
||||||
|
isError: false,
|
||||||
|
stepInterval: 300,
|
||||||
|
safeNavigate,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Card data-testid="external_call_rps_by_address">
|
<Card data-testid="external_call_rps_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
widget={externalCallRPSWidget}
|
widget={externalCallRPSWidget}
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
onClickHandler={(xValue, yValue, mouseX, mouseY, data): Promise<void> =>
|
||||||
onGraphClickHandler(
|
onGraphClickHandler(
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
mouseX,
|
mouseX,
|
||||||
mouseY,
|
mouseY,
|
||||||
'external_call_rps_by_address',
|
'external_call_rps_by_address',
|
||||||
|
data,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
@ -343,11 +362,9 @@ function External(): JSX.Element {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Button
|
<GraphControlsPanel
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
id="external_call_duration_by_address_button"
|
id="external_call_duration_by_address_button"
|
||||||
onClick={onViewTracePopupClick({
|
onViewTracesClick={onViewTracePopupClick({
|
||||||
servicename,
|
servicename,
|
||||||
selectedTraceTags,
|
selectedTraceTags,
|
||||||
timestamp: selectedTimeStamp,
|
timestamp: selectedTimeStamp,
|
||||||
@ -355,22 +372,29 @@ function External(): JSX.Element {
|
|||||||
stepInterval,
|
stepInterval,
|
||||||
safeNavigate,
|
safeNavigate,
|
||||||
})}
|
})}
|
||||||
>
|
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||||
View Traces
|
servicename,
|
||||||
</Button>
|
timestamp: selectedTimeStamp,
|
||||||
|
domainName: selectedData?.address,
|
||||||
|
isError: false,
|
||||||
|
stepInterval: 300,
|
||||||
|
safeNavigate,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card data-testid="external_call_duration_by_address">
|
<Card data-testid="external_call_duration_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
widget={externalCallDurationAddressWidget}
|
widget={externalCallDurationAddressWidget}
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||||
onGraphClickHandler(
|
onGraphClickHandler(
|
||||||
xValue,
|
xValue,
|
||||||
yValue,
|
yValue,
|
||||||
mouseX,
|
mouseX,
|
||||||
mouseY,
|
mouseY,
|
||||||
'external_call_duration_by_address',
|
'external_call_duration_by_address',
|
||||||
|
data,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
display: none;
|
display: none;
|
||||||
width: 110px;
|
width: 150px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--bg-slate-400);
|
background: var(--bg-slate-400);
|
||||||
|
@ -2,18 +2,20 @@ import './GraphControlsPanel.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { DraftingCompass, ScrollText } from 'lucide-react';
|
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
|
||||||
|
|
||||||
interface GraphControlsPanelProps {
|
interface GraphControlsPanelProps {
|
||||||
id: string;
|
id: string;
|
||||||
onViewLogsClick: () => void;
|
onViewLogsClick?: () => void;
|
||||||
onViewTracesClick: () => void;
|
onViewTracesClick: () => void;
|
||||||
|
onViewAPIMonitoringClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GraphControlsPanel({
|
function GraphControlsPanel({
|
||||||
id,
|
id,
|
||||||
onViewLogsClick,
|
onViewLogsClick,
|
||||||
onViewTracesClick,
|
onViewTracesClick,
|
||||||
|
onViewAPIMonitoringClick,
|
||||||
}: GraphControlsPanelProps): JSX.Element {
|
}: GraphControlsPanelProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div id={id} className="graph-controls-panel">
|
<div id={id} className="graph-controls-panel">
|
||||||
@ -26,6 +28,7 @@ function GraphControlsPanel({
|
|||||||
>
|
>
|
||||||
View traces
|
View traces
|
||||||
</Button>
|
</Button>
|
||||||
|
{onViewLogsClick && (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
icon={<ScrollText size={14} />}
|
icon={<ScrollText size={14} />}
|
||||||
@ -35,8 +38,25 @@ function GraphControlsPanel({
|
|||||||
>
|
>
|
||||||
View logs
|
View logs
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{onViewAPIMonitoringClick && (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<Binoculars size={14} />}
|
||||||
|
size="small"
|
||||||
|
onClick={onViewAPIMonitoringClick}
|
||||||
|
style={{ color: Color.BG_VANILLA_100 }}
|
||||||
|
>
|
||||||
|
View Third Party API
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GraphControlsPanel.defaultProps = {
|
||||||
|
onViewLogsClick: undefined,
|
||||||
|
onViewAPIMonitoringClick: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export default GraphControlsPanel;
|
export default GraphControlsPanel;
|
||||||
|
@ -7,6 +7,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import useClickOutside from 'hooks/useClickOutside';
|
import useClickOutside from 'hooks/useClickOutside';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
|
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
||||||
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
|
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
|
||||||
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||||
@ -14,7 +15,11 @@ import {
|
|||||||
BaseAutocompleteData,
|
BaseAutocompleteData,
|
||||||
DataTypes,
|
DataTypes,
|
||||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
Query,
|
||||||
|
TagFilterItem,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { Tags } from 'types/reducer/trace';
|
import { Tags } from 'types/reducer/trace';
|
||||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||||
@ -40,6 +45,16 @@ interface OnViewTracePopupClickProps {
|
|||||||
safeNavigate: (url: string) => void;
|
safeNavigate: (url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OnViewAPIMonitoringPopupClickProps {
|
||||||
|
servicename: string;
|
||||||
|
timestamp: number;
|
||||||
|
stepInterval?: number;
|
||||||
|
domainName: string;
|
||||||
|
isError: boolean;
|
||||||
|
|
||||||
|
safeNavigate: (url: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function generateExplorerPath(
|
export function generateExplorerPath(
|
||||||
isViewLogsClicked: boolean | undefined,
|
isViewLogsClicked: boolean | undefined,
|
||||||
urlParams: URLSearchParams,
|
urlParams: URLSearchParams,
|
||||||
@ -107,14 +122,92 @@ export function onViewTracePopupClick({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateAPIMonitoringPath = (
|
||||||
|
domainName: string,
|
||||||
|
startTime: number,
|
||||||
|
endTime: number,
|
||||||
|
filters: IBuilderQuery['filters'],
|
||||||
|
): string => {
|
||||||
|
const basePath = ROUTES.API_MONITORING;
|
||||||
|
return `${basePath}?${createQueryParams({
|
||||||
|
apiMonitoringParams: JSON.stringify({
|
||||||
|
selectedDomain: domainName,
|
||||||
|
selectedView: 'endpoint_stats',
|
||||||
|
modalTimeRange: {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
},
|
||||||
|
selectedInterval: 'custom',
|
||||||
|
endPointDetailsLocalFilters: filters,
|
||||||
|
}),
|
||||||
|
})}`;
|
||||||
|
};
|
||||||
|
export function onViewAPIMonitoringPopupClick({
|
||||||
|
servicename,
|
||||||
|
timestamp,
|
||||||
|
domainName,
|
||||||
|
isError,
|
||||||
|
stepInterval,
|
||||||
|
safeNavigate,
|
||||||
|
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
|
||||||
|
return (): void => {
|
||||||
|
const endTime = timestamp + (stepInterval || 60);
|
||||||
|
const startTime = timestamp - (stepInterval || 60);
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
...(isError
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
key: {
|
||||||
|
key: 'hasError',
|
||||||
|
dataType: DataTypes.bool,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'hasError--bool--tag--true',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['true'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
key: {
|
||||||
|
key: 'service.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: servicename,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
const newPath = generateAPIMonitoringPath(
|
||||||
|
domainName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
safeNavigate(newPath);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useGraphClickHandler(
|
export function useGraphClickHandler(
|
||||||
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
||||||
|
setSelectedData?: (data: any) => void | Dispatch<SetStateAction<any>>,
|
||||||
): (
|
): (
|
||||||
xValue: number,
|
xValue: number,
|
||||||
yValue: number,
|
yValue: number,
|
||||||
mouseX: number,
|
mouseX: number,
|
||||||
mouseY: number,
|
mouseY: number,
|
||||||
type: string,
|
type: string,
|
||||||
|
data?: any,
|
||||||
) => Promise<void> {
|
) => Promise<void> {
|
||||||
const buttonRef = useRef<HTMLElement | null>(null);
|
const buttonRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
@ -134,6 +227,7 @@ export function useGraphClickHandler(
|
|||||||
mouseX: number,
|
mouseX: number,
|
||||||
mouseY: number,
|
mouseY: number,
|
||||||
type: string,
|
type: string,
|
||||||
|
data?: any,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const id = `${type}_button`;
|
const id = `${type}_button`;
|
||||||
const buttonElement = document.getElementById(id);
|
const buttonElement = document.getElementById(id);
|
||||||
@ -145,6 +239,9 @@ export function useGraphClickHandler(
|
|||||||
buttonElement.style.left = `${mouseX}px`;
|
buttonElement.style.left = `${mouseX}px`;
|
||||||
buttonElement.style.top = `${mouseY}px`;
|
buttonElement.style.top = `${mouseY}px`;
|
||||||
setSelectedTimeStamp(Math.floor(xValue));
|
setSelectedTimeStamp(Math.floor(xValue));
|
||||||
|
if (setSelectedData && data) {
|
||||||
|
setSelectedData(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||||
buttonElement.style.display = 'none';
|
buttonElement.style.display = 'none';
|
||||||
|
@ -125,7 +125,7 @@ const menuItems: SidebarItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: ROUTES.API_MONITORING,
|
key: ROUTES.API_MONITORING,
|
||||||
label: 'API Monitoring',
|
label: 'Third Party API',
|
||||||
icon: <Binoculars size={16} />,
|
icon: <Binoculars size={16} />,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
},
|
},
|
||||||
|
@ -175,16 +175,32 @@ export function getWidgetQuery({
|
|||||||
export const convertToNanoseconds = (timestamp: number): bigint =>
|
export const convertToNanoseconds = (timestamp: number): bigint =>
|
||||||
BigInt((timestamp * 1e9).toFixed(0));
|
BigInt((timestamp * 1e9).toFixed(0));
|
||||||
|
|
||||||
|
export const getCustomTimeRangeWindowSweepInMS = (
|
||||||
|
customTimeRangeWindowForCoRelation: string | undefined,
|
||||||
|
): number => {
|
||||||
|
switch (customTimeRangeWindowForCoRelation) {
|
||||||
|
case '5m':
|
||||||
|
return 2.5 * 60 * 1000;
|
||||||
|
case '10m':
|
||||||
|
return 5 * 60 * 1000;
|
||||||
|
default:
|
||||||
|
return 5 * 60 * 1000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getStartAndEndTimesInMilliseconds = (
|
export const getStartAndEndTimesInMilliseconds = (
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
delta?: number,
|
||||||
): { start: number; end: number } => {
|
): { start: number; end: number } => {
|
||||||
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 300,000 milliseconds
|
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 300,000 milliseconds
|
||||||
|
|
||||||
const pointInTime = Math.floor(timestamp * 1000);
|
const pointInTime = Math.floor(timestamp * 1000);
|
||||||
|
|
||||||
// Convert timestamp to milliseconds and floor it
|
// Convert timestamp to milliseconds and floor it
|
||||||
const start = Math.floor(pointInTime - FIVE_MINUTES_IN_MILLISECONDS);
|
const start = Math.floor(
|
||||||
const end = Math.floor(pointInTime + FIVE_MINUTES_IN_MILLISECONDS);
|
pointInTime - (delta || FIVE_MINUTES_IN_MILLISECONDS),
|
||||||
|
);
|
||||||
|
const end = Math.floor(pointInTime + (delta || FIVE_MINUTES_IN_MILLISECONDS));
|
||||||
|
|
||||||
return { start, end };
|
return { start, end };
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user