diff --git a/frontend/src/container/ApiMonitoring/APIMonitoringUtils.test.tsx b/frontend/src/container/ApiMonitoring/APIMonitoringUtils.test.tsx index 971502a282..eec8c2fbab 100644 --- a/frontend/src/container/ApiMonitoring/APIMonitoringUtils.test.tsx +++ b/frontend/src/container/ApiMonitoring/APIMonitoringUtils.test.tsx @@ -316,7 +316,7 @@ describe('API Monitoring Utils', () => { { metric: { [SPAN_ATTRIBUTES.URL_PATH]: '/api/test', - [SPAN_ATTRIBUTES.STATUS_CODE]: '500', + [SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]: '500', status_message: 'Internal Server Error', }, values: [[1000000100, '10']], diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx index 8264758854..37b3412df8 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx @@ -35,6 +35,11 @@ function AllEndPoints({ initialFilters: IBuilderQuery['filters']; setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void; }): JSX.Element { + const [groupBySearchValue, setGroupBySearchValue] = useState(''); + const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{ + [key: string]: any; + }>({}); + const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters, @@ -42,7 +47,7 @@ function AllEndPoints({ dataSource: DataSource.TRACES, aggregateAttribute: '', aggregateOperator: 'noop', - searchText: '', + searchText: groupBySearchValue, tagType: '', }); @@ -52,34 +57,66 @@ function AllEndPoints({ const handleGroupByChange = useCallback( (value: IBuilderQuery['groupBy']) => { - const groupBy = []; + const newGroupBy = []; for (let index = 0; index < value.length; index++) { const element = (value[index] as unknown) as string; - const key = groupByFiltersData?.payload?.attributeKeys?.find( - (key) => key.key === element, - ); + // 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) { - groupBy.push(key); + if (key) { + newGroupBy.push(key); + } } } - setGroupBy(groupBy); + + setGroupBy(newGroupBy); + setGroupBySearchValue(''); }, - [groupByFiltersData, setGroupBy], + [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]); + }, [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]; @@ -168,6 +205,7 @@ function AllEndPoints({ placeholder="Search for attribute" options={groupByOptions} onChange={handleGroupByChange} + onSearch={(value: string): void => setGroupBySearchValue(value)} />{' '}
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx index 2909525bbd..ffaba24cde 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx @@ -229,7 +229,7 @@ function DomainDetails({ )} diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx index d8d7c9e837..a8e6de8292 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx @@ -12,17 +12,14 @@ import { getTopErrorsQueryPayload, TopErrorsResponseRow, } from 'container/ApiMonitoring/utils'; -import { - CustomTimeType, - Time, -} from 'container/TopNav/DateTimeSelectionV2/config'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { Info } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useQueries } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import EndPointsDropDown from './components/EndPointsDropDown'; @@ -32,17 +29,14 @@ import { SPAN_ATTRIBUTES } from './constants'; function TopErrors({ domainName, timeRange, - handleTimeChange, + initialFilters, }: { domainName: string; timeRange: { startTime: number; endTime: number; }; - handleTimeChange: ( - interval: Time | CustomTimeType, - dateTimeRange?: [number, number], - ) => void; + initialFilters: IBuilderQuery['filters']; }): JSX.Element { const { startTime: minTime, endTime: maxTime } = timeRange; @@ -65,11 +59,12 @@ function TopErrors({ op: '=', value: endPointName, }, + ...initialFilters.items, ] - : [], + : [...initialFilters.items], op: 'AND', }), - [domainName, endPointName, minTime, maxTime], + [domainName, endPointName, minTime, maxTime, initialFilters], ); // Since only one query here @@ -135,10 +130,6 @@ function TopErrors({ [endPointDropDownDataQueries], ); - useEffect(() => { - handleTimeChange('6h'); - }, [handleTimeChange]); - const navigateToExplorer = useNavigateToExplorer(); if (isError) { diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx index ccb670dc2e..722ae3b67e 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx @@ -146,13 +146,13 @@ function DomainMetrics({ { const errorRatePercent = Number( - Number(formattedDomainMetricsData.errorRate).toFixed(1), + Number(formattedDomainMetricsData.errorRate).toFixed(2), ); if (errorRatePercent >= 90) return Color.BG_SAKURA_500; if (errorRatePercent >= 60) return Color.BG_AMBER_500; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx index 314bcd3f3e..07f7d268df 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx @@ -90,12 +90,12 @@ function EndPointMetrics({ { const errorRatePercent = Number( - Number(metricsData?.errorRate ?? 0).toFixed(1), + Number(metricsData?.errorRate ?? 0).toFixed(2), ); if (errorRatePercent >= 90) return Color.BG_SAKURA_500; if (errorRatePercent >= 60) return Color.BG_AMBER_500; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts index ce0fb4e120..8b5b433bed 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts @@ -14,6 +14,7 @@ export const VIEW_TYPES = { export const SPAN_ATTRIBUTES = { URL_PATH: 'http.url', STATUS_CODE: 'status_code', + RESPONSE_STATUS_CODE: 'response_status_code', SERVER_NAME: 'net.peer.name', SERVER_PORT: 'net.peer.port', } as const; diff --git a/frontend/src/container/ApiMonitoring/__tests__/AllEndPoints.test.tsx b/frontend/src/container/ApiMonitoring/__tests__/AllEndPoints.test.tsx index 511191e322..f7838d4383 100644 --- a/frontend/src/container/ApiMonitoring/__tests__/AllEndPoints.test.tsx +++ b/frontend/src/container/ApiMonitoring/__tests__/AllEndPoints.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { getAllEndpointsWidgetData, getGroupByFiltersFromGroupByValues, @@ -129,6 +129,11 @@ describe('AllEndPoints', () => { }); }); + // Add cleanup after each test + afterEach(() => { + cleanup(); + }); + it('renders component correctly', () => { // eslint-disable-next-line react/jsx-props-no-spreading render(); diff --git a/frontend/src/container/ApiMonitoring/__tests__/TopErrors.test.tsx b/frontend/src/container/ApiMonitoring/__tests__/TopErrors.test.tsx index 6110d3cf77..752cb07cde 100644 --- a/frontend/src/container/ApiMonitoring/__tests__/TopErrors.test.tsx +++ b/frontend/src/container/ApiMonitoring/__tests__/TopErrors.test.tsx @@ -61,7 +61,10 @@ describe('TopErrors', () => { startTime: 1000000000, endTime: 1000010000, }, - handleTimeChange: jest.fn(), + initialFilters: { + items: [], + op: 'AND', + }, }; // Setup basic mocks @@ -223,12 +226,6 @@ describe('TopErrors', () => { } }); - it('calls handleTimeChange with 6h on mount', () => { - // eslint-disable-next-line react/jsx-props-no-spreading - render(); - expect(mockProps.handleTimeChange).toHaveBeenCalledWith('6h'); - }); - it('renders error state when isError is true', () => { // Mock useQueries to return isError: true (useQueries as jest.Mock).mockImplementationOnce(() => [ diff --git a/frontend/src/container/ApiMonitoring/utils.tsx b/frontend/src/container/ApiMonitoring/utils.tsx index f8ba976481..632055ed81 100644 --- a/frontend/src/container/ApiMonitoring/utils.tsx +++ b/frontend/src/container/ApiMonitoring/utils.tsx @@ -208,17 +208,16 @@ export const columnsConfig: ColumnType[] = [ align: 'right', className: `column`, render: (errorRate: number | string): React.ReactNode => { - if (errorRate === 'n/a' || errorRate === '-') { - return '-'; - } + const errorRateValue = + errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate; return ( { - const errorRatePercent = Number((errorRate as number).toFixed(1)); + const errorRatePercent = Number((errorRateValue as number).toFixed(2)); if (errorRatePercent >= 90) return Color.BG_SAKURA_500; if (errorRatePercent >= 60) return Color.BG_AMBER_500; return Color.BG_FOREST_500; @@ -941,18 +940,6 @@ export const getTopErrorsQueryPayload = ( op: '=', value: 'Client', }, - { - id: '75d65388', - key: { - key: 'status_message', - dataType: DataTypes.String, - type: '', - isColumn: true, - isJSON: false, - }, - op: 'exists', - value: '', - }, { id: 'b1af6bdb', key: { @@ -965,6 +952,18 @@ export const getTopErrorsQueryPayload = ( op: 'exists', value: '', }, + { + id: '75d65388', + key: { + key: 'status_message', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + }, + op: 'exists', + value: '', + }, { id: '4872bf91', key: { @@ -977,6 +976,18 @@ export const getTopErrorsQueryPayload = ( op: '=', value: domainName, }, + { + id: 'ab4c885d', + key: { + key: 'has_error', + dataType: DataTypes.bool, + type: '', + isColumn: true, + isJSON: false, + }, + op: '=', + value: true, + }, ...filters.items, ], }, @@ -1000,11 +1011,12 @@ export const getTopErrorsQueryPayload = ( isJSON: false, }, { - key: 'status_code', - dataType: DataTypes.Float64, - type: '', + dataType: DataTypes.String, isColumn: true, isJSON: false, + key: 'response_status_code', + type: '', + id: 'response_status_code--string----true', }, { key: 'status_message', @@ -1327,7 +1339,7 @@ export const formatEndPointsDataForTable = ( export interface TopErrorsResponseRow { metric: { [SPAN_ATTRIBUTES.URL_PATH]: string; - [SPAN_ATTRIBUTES.STATUS_CODE]: string; + [SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]: string; status_message: string; }; values: [number, string][]; @@ -1356,10 +1368,10 @@ export const formatTopErrorsDataForTable = ( ? '-' : row.metric[SPAN_ATTRIBUTES.URL_PATH], statusCode: - row.metric[SPAN_ATTRIBUTES.STATUS_CODE] === 'n/a' || - row.metric[SPAN_ATTRIBUTES.STATUS_CODE] === undefined + row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === 'n/a' || + row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === undefined ? '-' - : row.metric[SPAN_ATTRIBUTES.STATUS_CODE], + : row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE], statusMessage: row.metric.status_message === 'n/a' || row.metric.status_message === undefined @@ -2005,7 +2017,7 @@ export const getEndPointDetailsQueryPayload = ( type: 'tag', }, op: '=', - value: 'api.github.com', + value: domainName, }, { id: '212678b9', @@ -3057,28 +3069,28 @@ export const dependentServicesColumns: ColumnType[] = [ render: ( errorPercentage: number | string, // eslint-disable-next-line sonarjs/no-identical-functions - ): React.ReactNode => ( - { - const errorPercentagePercent = Number( - (errorPercentage as number).toFixed(1), - ); - if (errorPercentagePercent >= 90) return Color.BG_SAKURA_500; - if (errorPercentagePercent >= 60) return Color.BG_AMBER_500; - return Color.BG_FOREST_500; - })()} - className="progress-bar error-rate" - /> - ), + ): React.ReactNode => { + const errorPercentageValue = + errorPercentage === 'n/a' || errorPercentage === '-' ? 0 : errorPercentage; + return ( + { + const errorPercentagePercent = Number( + (errorPercentageValue as number).toFixed(2), + ); + if (errorPercentagePercent >= 90) return Color.BG_SAKURA_500; + if (errorPercentagePercent >= 60) return Color.BG_AMBER_500; + return Color.BG_FOREST_500; + })()} + className="progress-bar error-rate" + /> + ); + }, sorter: (a: DependentServicesData, b: DependentServicesData): number => { const errorPercentageA = a.errorPercentage === '-' || a.errorPercentage === 'n/a' @@ -3658,12 +3670,9 @@ export const getAllEndpointsWidgetData = ( widget.renderColumnCell = { [SPAN_ATTRIBUTES.URL_PATH]: (url: any): ReactNode => { - const { endpoint, port } = extractPortAndEndpoint(url); + const { endpoint } = extractPortAndEndpoint(url); return ( - - {port !== '-' && port !== 'n/a' ? `:${port}` : ''} - {endpoint === 'n/a' || url === undefined ? '-' : endpoint} - + {endpoint === 'n/a' || url === undefined ? '-' : endpoint} ); }, A: (numOfCalls: any): ReactNode => ( @@ -3695,7 +3704,7 @@ export const getAllEndpointsWidgetData = ( percent={Number( ((errorRate === 'n/a' || errorRate === '-' ? 0 - : errorRate) as number).toFixed(1), + : errorRate) as number).toFixed(2), )} strokeLinecap="butt" size="small" @@ -3704,7 +3713,7 @@ export const getAllEndpointsWidgetData = ( const errorRatePercent = Number( ((errorRate === 'n/a' || errorRate === '-' ? 0 - : errorRate) as number).toFixed(1), + : errorRate) as number).toFixed(2), ); if (errorRatePercent >= 90) return Color.BG_SAKURA_500; if (errorRatePercent >= 60) return Color.BG_AMBER_500;