mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 21:48:58 +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_EXPLORER": "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 QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useApiMonitoringParams } from '../../../queryParams';
|
||||
import { SPAN_ATTRIBUTES, VIEWS } from './constants';
|
||||
|
||||
function AllEndPoints({
|
||||
@ -35,6 +37,7 @@ function AllEndPoints({
|
||||
initialFilters: IBuilderQuery['filters'];
|
||||
setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void;
|
||||
}): JSX.Element {
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const [groupBySearchValue, setGroupBySearchValue] = useState<string>('');
|
||||
const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{
|
||||
[key: string]: any;
|
||||
@ -55,87 +58,38 @@ function AllEndPoints({
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
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('');
|
||||
},
|
||||
[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
|
||||
// --- FILTERS STATE SYNC ---
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
if (params.allEndpointsLocalFilters) {
|
||||
return params.allEndpointsLocalFilters;
|
||||
}
|
||||
// Initialize filters based on the initial endPointName prop
|
||||
const initialItems = [...initialFilters.items];
|
||||
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
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: IBuilderQuery['filters']): void => {
|
||||
// 1. Update local filters state immediately
|
||||
setFilters(newFilters);
|
||||
setParams({ allEndpointsLocalFilters: newFilters });
|
||||
},
|
||||
[], // Dependencies for the callback
|
||||
[setParams],
|
||||
);
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
@ -160,6 +114,91 @@ function AllEndPoints({
|
||||
[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(
|
||||
(props: any): void => {
|
||||
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
||||
@ -172,6 +211,14 @@ function AllEndPoints({
|
||||
items: initialItems,
|
||||
op: 'AND',
|
||||
});
|
||||
setParams({
|
||||
selectedEndPointName: props[SPAN_ATTRIBUTES.URL_PATH] as string,
|
||||
selectedView: VIEWS.ENDPOINT_STATS,
|
||||
endPointDetailsLocalFilters: {
|
||||
items: initialItems,
|
||||
op: 'AND',
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
filters,
|
||||
@ -179,6 +226,7 @@ function AllEndPoints({
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
groupBy,
|
||||
setParams,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -11,12 +11,13 @@ import {
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
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 { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { useApiMonitoringParams } from '../../../queryParams';
|
||||
import AllEndPoints from './AllEndPoints';
|
||||
import DomainMetrics from './components/DomainMetrics';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
@ -40,8 +41,13 @@ function DomainDetails({
|
||||
domainListLength: number;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
|
||||
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||
(params.selectedView as VIEWS) || VIEWS.ALL_ENDPOINTS,
|
||||
);
|
||||
const [selectedEndPointName, setSelectedEndPointName] = useState<string>(
|
||||
params.selectedEndPointName || '',
|
||||
);
|
||||
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
||||
IBuilderQuery['groupBy']
|
||||
>([]);
|
||||
@ -52,8 +58,27 @@ function DomainDetails({
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
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<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
@ -67,34 +92,62 @@ function DomainDetails({
|
||||
]);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
(params.selectedInterval as Time) || (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
// 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,
|
||||
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(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
setSelectedInterval(interval as Time);
|
||||
setParams({ selectedInterval: interval as string });
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
const newRange = {
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
};
|
||||
setModalTimeRange(newRange);
|
||||
setParams({ modalTimeRange: newRange });
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
const newRange = {
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
};
|
||||
setModalTimeRange(newRange);
|
||||
setParams({ modalTimeRange: newRange });
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[setParams],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -174,7 +227,6 @@ function DomainDetails({
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ALL_ENDPOINTS}
|
||||
@ -204,7 +256,7 @@ function DomainDetails({
|
||||
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
||||
<AllEndPoints
|
||||
domainName={domainData.domainName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedEndPointName={handleEndPointChange}
|
||||
setSelectedView={setSelectedView}
|
||||
groupBy={endPointsGroupBy}
|
||||
setGroupBy={setEndPointsGroupBy}
|
||||
@ -218,7 +270,7 @@ function DomainDetails({
|
||||
<EndPointDetails
|
||||
domainName={domainData.domainName}
|
||||
endPointName={selectedEndPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedEndPointName={handleEndPointChange}
|
||||
initialFilters={initialFiltersEndPointStats}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
|
||||
import {
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||
extractPortAndEndpoint,
|
||||
@ -59,13 +60,16 @@ function EndPointDetails({
|
||||
) => void;
|
||||
}): JSX.Element {
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
|
||||
// Local state for filters, combining endpoint filter and search filters
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
// Initialize filters based on the initial endPointName prop
|
||||
const initialItems = [...initialFilters.items];
|
||||
const initialItems = params.endPointDetailsLocalFilters
|
||||
? [...params.endPointDetailsLocalFilters.items]
|
||||
: [...initialFilters.items];
|
||||
if (endPointName) {
|
||||
initialItems.push({
|
||||
id: '92b8a1c1',
|
||||
@ -107,11 +111,26 @@ function EndPointDetails({
|
||||
});
|
||||
}, [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
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: IBuilderQuery['filters']): void => {
|
||||
// 1. Update local filters state immediately
|
||||
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
|
||||
const httpUrlFilter = newFilters.items.find(
|
||||
@ -126,7 +145,7 @@ function EndPointDetails({
|
||||
setSelectedEndPointName(derivedEndPointName);
|
||||
}
|
||||
},
|
||||
[endPointName, setSelectedEndPointName], // Dependencies for the callback
|
||||
[endPointName, setSelectedEndPointName, setParams], // Dependencies for the callback
|
||||
);
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
|
@ -215,7 +215,9 @@ function TopErrors({
|
||||
/>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@ function MetricOverTimeGraph({
|
||||
onDragSelect={onDragSelect}
|
||||
customOnDragSelect={(): void => {}}
|
||||
customTimeRange={timeRange}
|
||||
customTimeRangeWindowForCoRelation="5m"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -19,6 +19,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
@ -150,7 +151,12 @@ function StatusCodeBarCharts({
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
const TWO_AND_HALF_MINUTES_IN_MILLISECONDS = 2.5 * 60 * 1000; // 150,000 milliseconds
|
||||
const customFilters = getCustomFiltersForBarChart(metric);
|
||||
const { start, end } = getStartAndEndTimesInMilliseconds(
|
||||
xValue,
|
||||
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
|
||||
);
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
@ -164,6 +170,10 @@ function StatusCodeBarCharts({
|
||||
notifications,
|
||||
graphClick,
|
||||
customFilters,
|
||||
customTracesTimeRange: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
|
@ -16,7 +16,7 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
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 { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -25,6 +25,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { DEFAULT_PARAMS, useApiMonitoringParams } from '../../queryParams';
|
||||
import {
|
||||
columnsConfig,
|
||||
formatDataForTable,
|
||||
@ -32,7 +33,9 @@ import {
|
||||
} from '../../utils';
|
||||
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 { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@ -129,6 +132,16 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
[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 (
|
||||
<section className={cx('api-module-right-section')}>
|
||||
<Toolbar
|
||||
@ -179,6 +192,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
(item) => item.key === record.key,
|
||||
);
|
||||
setSelectedDomainIndex(dataIndex);
|
||||
setParams({ selectedDomain: record.domainName });
|
||||
logEvent('API Monitoring: Domain name row clicked', {});
|
||||
}
|
||||
},
|
||||
@ -196,6 +210,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
domainListLength={formattedDataForTable.length}
|
||||
handleClose={(): void => {
|
||||
setSelectedDomainIndex(-1);
|
||||
setParams(DEFAULT_PARAMS);
|
||||
}}
|
||||
domainListFilters={query?.filters}
|
||||
/>
|
||||
|
@ -8,13 +8,15 @@ import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useApiMonitoringParams } from '../queryParams';
|
||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||
import DomainList from './Domains/DomainList';
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
const [showIP, setShowIP] = useState<boolean>(true);
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const showIP = params.showIP ?? true;
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('API Monitoring: Landing page visited', {});
|
||||
@ -34,14 +36,12 @@ function Explorer(): JSX.Element {
|
||||
<Switch
|
||||
size="small"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={showIP}
|
||||
checked={showIP ?? true}
|
||||
onClick={(): void => {
|
||||
setShowIP((showIP): boolean => {
|
||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||
showIP: !showIP,
|
||||
});
|
||||
return !showIP;
|
||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||
showIP: !(showIP ?? true),
|
||||
});
|
||||
setParams({ showIP });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -52,7 +52,7 @@ function Explorer(): JSX.Element {
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>
|
||||
</section>
|
||||
<DomainList showIP={showIP} />
|
||||
<DomainList />
|
||||
</div>
|
||||
</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', () => {
|
||||
const mockProps = {
|
||||
domainName: 'test-domain',
|
||||
|
@ -25,6 +25,24 @@ jest.mock('react-query', () => ({
|
||||
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', () => ({
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
|
||||
'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 => {
|
||||
let legend = domainName;
|
||||
if (endPointName) {
|
||||
const { endpoint, port } = extractPortAndEndpoint(endPointName);
|
||||
const { endpoint } = extractPortAndEndpoint(endPointName);
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
|
||||
legend = `${endpoint}`;
|
||||
}
|
||||
|
||||
return getWidgetQueryBuilder(
|
||||
@ -3861,9 +3861,9 @@ export const getLatencyOverTimeWidgetData = (
|
||||
): Widgets => {
|
||||
let legend = domainName;
|
||||
if (endPointName) {
|
||||
const { endpoint, port } = extractPortAndEndpoint(endPointName);
|
||||
const { endpoint } = extractPortAndEndpoint(endPointName);
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
|
||||
legend = `${endpoint}`;
|
||||
}
|
||||
|
||||
return getWidgetQueryBuilder(
|
||||
|
@ -15,6 +15,10 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import {
|
||||
getCustomTimeRangeWindowSweepInMS,
|
||||
getStartAndEndTimesInMilliseconds,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
Dispatch,
|
||||
@ -57,6 +61,7 @@ function WidgetGraphComponent({
|
||||
customSeries,
|
||||
customErrorMessage,
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@ -263,6 +268,13 @@ function WidgetGraphComponent({
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
const customTracesTimeRange = getCustomTimeRangeWindowSweepInMS(
|
||||
customTimeRangeWindowForCoRelation,
|
||||
);
|
||||
const { start, end } = getStartAndEndTimesInMilliseconds(
|
||||
xValue,
|
||||
customTracesTimeRange,
|
||||
);
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
@ -275,6 +287,9 @@ function WidgetGraphComponent({
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
...(customTimeRangeWindowForCoRelation
|
||||
? { customTracesTimeRange: { start, end } }
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
|
||||
@ -393,6 +408,7 @@ WidgetGraphComponent.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
setLayout: undefined,
|
||||
onClickHandler: undefined,
|
||||
customTimeRangeWindowForCoRelation: undefined,
|
||||
};
|
||||
|
||||
export default WidgetGraphComponent;
|
||||
|
@ -49,6 +49,7 @@ function GridCardGraph({
|
||||
analyticsEvent,
|
||||
customTimeRange,
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
@ -289,6 +290,7 @@ function GridCardGraph({
|
||||
customSeries={customSeries}
|
||||
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
|
||||
customOnRowClick={customOnRowClick}
|
||||
customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -303,6 +305,7 @@ GridCardGraph.defaultProps = {
|
||||
headerMenuList: [MenuItemKeys.View],
|
||||
version: 'v3',
|
||||
analyticsEvent: undefined,
|
||||
customTimeRangeWindowForCoRelation: undefined,
|
||||
};
|
||||
|
||||
export default memo(GridCardGraph);
|
||||
|
@ -40,6 +40,7 @@ export interface WidgetGraphComponentProps {
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
customErrorMessage?: string;
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
@ -67,6 +68,7 @@ export interface GridCardGraphProps {
|
||||
endTime: number;
|
||||
};
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
@ -179,6 +179,7 @@ interface HandleGraphClickParams {
|
||||
notifications: NotificationInstance;
|
||||
graphClick: (props: GraphClickProps) => void;
|
||||
customFilters?: TagFilterItem[];
|
||||
customTracesTimeRange?: { start: number; end: number };
|
||||
}
|
||||
|
||||
export const handleGraphClick = async ({
|
||||
@ -194,6 +195,7 @@ export const handleGraphClick = async ({
|
||||
notifications,
|
||||
graphClick,
|
||||
customFilters,
|
||||
customTracesTimeRange,
|
||||
}: HandleGraphClickParams): Promise<void> => {
|
||||
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
||||
|
||||
@ -225,8 +227,10 @@ export const handleGraphClick = async ({
|
||||
navigateToExplorer({
|
||||
filters: [...result[key].filters, ...(customFilters || [])],
|
||||
dataSource: result[key].dataSource as DataSource,
|
||||
startTime: xValue,
|
||||
endTime: xValue + (stepInterval ?? 60),
|
||||
startTime: customTracesTimeRange ? customTracesTimeRange?.start : xValue,
|
||||
endTime: customTracesTimeRange
|
||||
? customTracesTimeRange?.end
|
||||
: xValue + (stepInterval ?? 60),
|
||||
shouldResolveQuery: true,
|
||||
}),
|
||||
}));
|
||||
|
@ -31,10 +31,11 @@ import { v4 as uuid } from 'uuid';
|
||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
|
||||
import { IServiceName } from './types';
|
||||
import {
|
||||
handleNonInQueryRange,
|
||||
onViewAPIMonitoringPopupClick,
|
||||
onViewTracePopupClick,
|
||||
useGetAPMToTracesQueries,
|
||||
useGraphClickHandler,
|
||||
@ -42,7 +43,7 @@ import {
|
||||
|
||||
function External(): JSX.Element {
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
|
||||
const [selectedData, setSelectedData] = useState<any>(undefined);
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
@ -223,17 +224,18 @@ function External(): JSX.Element {
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const onGraphClickHandler = useGraphClickHandler(setSelectedTimeStamp);
|
||||
const onGraphClickHandler = useGraphClickHandler(
|
||||
setSelectedTimeStamp,
|
||||
setSelectedData,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
<GraphControlsPanel
|
||||
id="external_call_error_percentage_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
@ -241,21 +243,28 @@ function External(): JSX.Element {
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address || '',
|
||||
isError: true,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
<Card data-testid="external_call_error_percentage">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
headerMenuList={MENU_ITEMS}
|
||||
widget={externalCallErrorWidget}
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||
onGraphClickHandler(
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_error_percentage',
|
||||
data,
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
@ -266,11 +275,9 @@ function External(): JSX.Element {
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
<GraphControlsPanel
|
||||
id="external_call_duration_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
@ -278,22 +285,29 @@ function External(): JSX.Element {
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
|
||||
<Card data-testid="external_call_duration">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
headerMenuList={MENU_ITEMS}
|
||||
widget={externalCallDurationWidget}
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||
onGraphClickHandler(
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_duration',
|
||||
data,
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
@ -306,11 +320,9 @@ function External(): JSX.Element {
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
<GraphControlsPanel
|
||||
id="external_call_rps_by_address_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
@ -318,21 +330,28 @@ function External(): JSX.Element {
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
<Card data-testid="external_call_rps_by_address">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
widget={externalCallRPSWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): Promise<void> =>
|
||||
onGraphClickHandler(
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_rps_by_address',
|
||||
data,
|
||||
)
|
||||
}
|
||||
onDragSelect={onDragSelect}
|
||||
@ -343,11 +362,9 @@ function External(): JSX.Element {
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
<GraphControlsPanel
|
||||
id="external_call_duration_by_address_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
@ -355,22 +372,29 @@ function External(): JSX.Element {
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
|
||||
<Card data-testid="external_call_duration_by_address">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
widget={externalCallDurationAddressWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
|
||||
onGraphClickHandler(
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_duration_by_address',
|
||||
data,
|
||||
);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
|
@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
display: none;
|
||||
width: 110px;
|
||||
width: 150px;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
background: var(--bg-slate-400);
|
||||
|
@ -2,18 +2,20 @@ import './GraphControlsPanel.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { DraftingCompass, ScrollText } from 'lucide-react';
|
||||
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
|
||||
|
||||
interface GraphControlsPanelProps {
|
||||
id: string;
|
||||
onViewLogsClick: () => void;
|
||||
onViewLogsClick?: () => void;
|
||||
onViewTracesClick: () => void;
|
||||
onViewAPIMonitoringClick?: () => void;
|
||||
}
|
||||
|
||||
function GraphControlsPanel({
|
||||
id,
|
||||
onViewLogsClick,
|
||||
onViewTracesClick,
|
||||
onViewAPIMonitoringClick,
|
||||
}: GraphControlsPanelProps): JSX.Element {
|
||||
return (
|
||||
<div id={id} className="graph-controls-panel">
|
||||
@ -26,17 +28,35 @@ function GraphControlsPanel({
|
||||
>
|
||||
View traces
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ScrollText size={14} />}
|
||||
size="small"
|
||||
onClick={onViewLogsClick}
|
||||
style={{ color: Color.BG_VANILLA_100 }}
|
||||
>
|
||||
View logs
|
||||
</Button>
|
||||
{onViewLogsClick && (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ScrollText size={14} />}
|
||||
size="small"
|
||||
onClick={onViewLogsClick}
|
||||
style={{ color: Color.BG_VANILLA_100 }}
|
||||
>
|
||||
View logs
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
GraphControlsPanel.defaultProps = {
|
||||
onViewLogsClick: undefined,
|
||||
onViewAPIMonitoringClick: undefined,
|
||||
};
|
||||
|
||||
export default GraphControlsPanel;
|
||||
|
@ -7,6 +7,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
||||
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
@ -14,7 +15,11 @@ import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} 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 { Tags } from 'types/reducer/trace';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
@ -40,6 +45,16 @@ interface OnViewTracePopupClickProps {
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
interface OnViewAPIMonitoringPopupClickProps {
|
||||
servicename: string;
|
||||
timestamp: number;
|
||||
stepInterval?: number;
|
||||
domainName: string;
|
||||
isError: boolean;
|
||||
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
isViewLogsClicked: boolean | undefined,
|
||||
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(
|
||||
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
||||
setSelectedData?: (data: any) => void | Dispatch<SetStateAction<any>>,
|
||||
): (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
type: string,
|
||||
data?: any,
|
||||
) => Promise<void> {
|
||||
const buttonRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
@ -134,6 +227,7 @@ export function useGraphClickHandler(
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
type: string,
|
||||
data?: any,
|
||||
): Promise<void> => {
|
||||
const id = `${type}_button`;
|
||||
const buttonElement = document.getElementById(id);
|
||||
@ -145,6 +239,9 @@ export function useGraphClickHandler(
|
||||
buttonElement.style.left = `${mouseX}px`;
|
||||
buttonElement.style.top = `${mouseY}px`;
|
||||
setSelectedTimeStamp(Math.floor(xValue));
|
||||
if (setSelectedData && data) {
|
||||
setSelectedData(data);
|
||||
}
|
||||
}
|
||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||
buttonElement.style.display = 'none';
|
||||
|
@ -125,7 +125,7 @@ const menuItems: SidebarItem[] = [
|
||||
},
|
||||
{
|
||||
key: ROUTES.API_MONITORING,
|
||||
label: 'API Monitoring',
|
||||
label: 'Third Party API',
|
||||
icon: <Binoculars size={16} />,
|
||||
isNew: true,
|
||||
},
|
||||
|
@ -175,16 +175,32 @@ export function getWidgetQuery({
|
||||
export const convertToNanoseconds = (timestamp: number): bigint =>
|
||||
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 = (
|
||||
timestamp: number,
|
||||
delta?: number,
|
||||
): { start: number; end: number } => {
|
||||
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 300,000 milliseconds
|
||||
|
||||
const pointInTime = Math.floor(timestamp * 1000);
|
||||
|
||||
// Convert timestamp to milliseconds and floor it
|
||||
const start = Math.floor(pointInTime - FIVE_MINUTES_IN_MILLISECONDS);
|
||||
const end = Math.floor(pointInTime + FIVE_MINUTES_IN_MILLISECONDS);
|
||||
const start = Math.floor(
|
||||
pointInTime - (delta || FIVE_MINUTES_IN_MILLISECONDS),
|
||||
);
|
||||
const end = Math.floor(pointInTime + (delta || FIVE_MINUTES_IN_MILLISECONDS));
|
||||
|
||||
return { start, end };
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user