diff --git a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss index 73849b010a..6524f55b16 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss @@ -30,6 +30,7 @@ .right-action { display: flex; align-items: center; + min-width: 48px; .clear-all { font-size: 12px; @@ -52,10 +53,14 @@ .checkbox-value-section { display: flex; align-items: center; - justify-content: space-between; + gap: 4px; width: calc(100% - 24px); cursor: pointer; + .value-string { + width: 100%; + } + &.filter-disabled { cursor: not-allowed; @@ -74,9 +79,6 @@ } } - .value-string { - } - .only-btn { display: none; } @@ -177,3 +179,17 @@ } } } + +.label-false { + width: 2px; + height: 11px; + border-radius: 2px; + background: var(--bg-cherry-500); +} + +.label-true { + width: 2px; + height: 11px; + border-radius: 2px; + background: var(--bg-forest-500); +} diff --git a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx index a9d96adf32..784c7cdd24 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx @@ -504,6 +504,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { onChange(value, currentFilterState[value], true); }} > +
{filter.customRendererForValue ? ( filter.customRendererForValue(value) ) : ( @@ -511,7 +512,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { className="value-string" ellipsis={{ tooltip: { placement: 'right' } }} > - {value} + {String(value)} )} + )} +
+ ); +} + +Duration.defaultProps = { + onFilterChange: (): void => {}, +}; + +export default Duration; diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index ed443e8e4f..af989aea13 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -5,19 +5,24 @@ import { SyncOutlined, VerticalAlignTopOutlined, } from '@ant-design/icons'; -import { Skeleton, Tooltip, Typography } from 'antd'; +import { Skeleton, Switch, Tooltip, Typography } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; +import logEvent from 'api/common/logEvent'; import classNames from 'classnames'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import { LOCALSTORAGE } from 'constants/localStorage'; +import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { cloneDeep, isFunction, isNull } from 'lodash-es'; import { Settings2 as SettingsIcon } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { useMemo, useState } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { USER_ROLES } from 'types/roles'; import Checkbox from './FilterRenderers/Checkbox/Checkbox'; +import Duration from './FilterRenderers/Duration/Duration'; import Slider from './FilterRenderers/Slider/Slider'; import useFilterConfig from './hooks/useFilterConfig'; import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip'; @@ -32,8 +37,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { source, onFilterChange, signal, + showFilterCollapse = true, + showQueryName = true, } = props; + const { user } = useAppContext(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const isAdmin = user.role === USER_ROLES.ADMIN; + const [params, setParams] = useApiMonitoringParams(); + const showIP = params.showIP ?? true; const { filterConfig, @@ -95,36 +106,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { }; const lastQueryName = + showQueryName && currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; return (
- {source !== QuickFiltersSource.INFRA_MONITORING && - source !== QuickFiltersSource.API_MONITORING && ( -
-
- - - {lastQueryName ? 'Filters for' : 'Filters'} - - {lastQueryName && ( - - - {lastQueryName} - - - )} -
- -
- -
- -
+ {source !== QuickFiltersSource.INFRA_MONITORING && ( +
+
+ + + {lastQueryName ? 'Filters for' : 'Filters'} + + {lastQueryName && ( + + {lastQueryName} + )} +
+ +
+ +
+ +
+
+ {showFilterCollapse && (
- {isDynamicFilters && ( - -
- setIsSettingsOpen(true)} - /> - { - setLocalStorageKey( - LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT, - 'false', - ); - }} - /> -
-
- )} -
+ )} + {isDynamicFilters && isAdmin && ( + +
+ setIsSettingsOpen(true)} + /> + { + setLocalStorageKey( + LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT, + 'false', + ); + }} + /> +
+
+ )}
- )} +
+ )} {isCustomFiltersLoading ? (
@@ -179,31 +188,51 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
) : ( -
- {filterConfig.map((filter) => { - switch (filter.type) { - case FiltersType.CHECKBOX: - return ( - - ); - case FiltersType.SLIDER: - return ; - // eslint-disable-next-line sonarjs/no-duplicated-branches - default: - return ( - - ); - } - })} -
+ <> + {source === QuickFiltersSource.API_MONITORING && ( +
+ Show IP addresses + { + logEvent('API Monitoring: Show IP addresses clicked', { + showIP: !(showIP ?? true), + }); + setParams({ showIP }); + }} + /> +
+ )} +
+ {filterConfig.map((filter) => { + switch (filter.type) { + case FiltersType.CHECKBOX: + return ( + + ); + case FiltersType.DURATION: + return ; + case FiltersType.SLIDER: + return ; + // eslint-disable-next-line sonarjs/no-duplicated-branches + default: + return ( + + ); + } + })} +
+
)}
@@ -235,4 +264,6 @@ QuickFilters.defaultProps = { onFilterChange: null, signal: '', config: [], + showFilterCollapse: true, + showQueryName: true, }; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx index 05c9954295..635338ef5e 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx @@ -3,10 +3,12 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants'; import { SignalType } from 'components/QuickFilters/types'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions'; import { useMemo } from 'react'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; +import { DataSource } from 'types/common/queryBuilder'; function OtherFiltersSkeleton(): JSX.Element { return ( @@ -34,6 +36,11 @@ function OtherFilters({ addedFilters: FilterType[]; setAddedFilters: React.Dispatch>; }): JSX.Element { + const isLogDataSource = useMemo( + () => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS, + [signal], + ); + const { data: suggestionsData, isFetching: isFetchingSuggestions, @@ -45,18 +52,39 @@ function OtherFilters({ }, { queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue], - enabled: !!signal, + enabled: !!signal && isLogDataSource, }, ); - const otherFilters = useMemo( - () => - suggestionsData?.payload?.attributes?.filter( - (attr) => !addedFilters.some((filter) => filter.key === attr.key), - ), - [suggestionsData, addedFilters], + const { + data: aggregateKeysData, + isFetching: isFetchingAggregateKeys, + } = useGetAggregateKeys( + { + searchText: inputValue, + dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType], + aggregateOperator: 'noop', + aggregateAttribute: '', + tagType: '', + }, + { + queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue], + enabled: !!signal && !isLogDataSource, + }, ); + const otherFilters = useMemo(() => { + let filterAttributes; + if (isLogDataSource) { + filterAttributes = suggestionsData?.payload?.attributes || []; + } else { + filterAttributes = aggregateKeysData?.payload?.attributeKeys || []; + } + return filterAttributes?.filter( + (attr) => !addedFilters.some((filter) => filter.key === attr.key), + ); + }, [suggestionsData, aggregateKeysData, addedFilters, isLogDataSource]); + const handleAddFilter = (filter: FilterType): void => { setAddedFilters((prev) => [ ...prev, @@ -71,7 +99,8 @@ function OtherFilters({ }; const renderFilters = (): React.ReactNode => { - if (isFetchingSuggestions) return ; + const isLoading = isFetchingSuggestions || isFetchingAggregateKeys; + if (isLoading) return ; if (!otherFilters?.length) return
No values found
; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss index 6fa7054344..e6cbfc642e 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss @@ -7,6 +7,7 @@ background: var(--bg-slate-500); transition: width 0.05s ease-in-out; overflow: hidden; + color: var(--bg-vanilla-100); &.qf-logs-explorer { height: calc(100vh - 45px); @@ -16,6 +17,14 @@ height: 100vh; } + &.qf-api-monitoring { + height: calc(100vh - 45px); + } + + &.qf-traces-explorer { + height: calc(100vh - 45px); + } + &.hidden { width: 0; } @@ -172,6 +181,7 @@ .lightMode { .quick-filters-settings { background: var(--bg-vanilla-100); + color: var(--bg-slate-500); .search { .ant-input { background-color: var(--bg-vanilla-100); diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx index f365ddba06..bf4406c304 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx @@ -1,3 +1,4 @@ +import logEvent from 'api/common/logEvent'; import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters'; import axios, { AxiosError } from 'axios'; import { SignalType } from 'components/QuickFilters/types'; @@ -46,6 +47,9 @@ const useQuickFilterSettings = ({ onSuccess: () => { setIsSettingsOpen(false); setIsStale(true); + logEvent('Quick Filters Settings: changes saved', { + addedFilters, + }); notifications.success({ message: 'Quick filters updated successfully', placement: 'bottomRight', diff --git a/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx index 2a095d6685..fb2659a681 100644 --- a/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx +++ b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx @@ -33,7 +33,7 @@ const useFilterConfig = ({ const isDynamicFilters = useMemo(() => customFilters.length > 0, [ customFilters, ]); - const { isLoading: isCustomFiltersLoading } = useQuery< + const { isFetching: isCustomFiltersLoading } = useQuery< SuccessResponse | ErrorResponse, Error >( @@ -49,10 +49,10 @@ const useFilterConfig = ({ enabled: !!signal && isStale, }, ); - const filterConfig = useMemo(() => getFilterConfig(customFilters, config), [ - config, - customFilters, - ]); + const filterConfig = useMemo( + () => getFilterConfig(signal, customFilters, config), + [config, customFilters, signal], + ); return { filterConfig, diff --git a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx index 069d52d6eb..f998e587ee 100644 --- a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx +++ b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx @@ -1,6 +1,7 @@ import '@testing-library/jest-dom'; import { + act, cleanup, fireEvent, render, @@ -8,6 +9,7 @@ import { waitFor, } from '@testing-library/react'; import { ENVIRONMENT } from 'constants/env'; +import ROUTES from 'constants/routes'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { otherFiltersResponse, @@ -17,6 +19,7 @@ import { import { server } from 'mocks-server/server'; import { rest } from 'msw'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; +import { USER_ROLES } from 'types/roles'; import QuickFilters from '../QuickFilters'; import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types'; @@ -26,6 +29,21 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({ useQueryBuilder: jest.fn(), })); +// eslint-disable-next-line sonarjs/no-duplicate-string +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`, + }), +})); + +const userRole = USER_ROLES.ADMIN; + +// mock useAppContext +jest.mock('providers/App/App', () => ({ + useAppContext: jest.fn(() => ({ user: { role: userRole } })), +})); + const handleFilterVisibilityChange = jest.fn(); const redirectWithQueryBuilderData = jest.fn(); const putHandler = jest.fn(); @@ -163,7 +181,9 @@ describe('Quick Filters with custom filters', () => { expect(screen.getByText('Filters for')).toBeInTheDocument(); expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); await screen.findByText(FILTER_SERVICE_NAME); - await screen.findByText('otel-demo'); + const allByText = await screen.findAllByText('otel-demo'); + // since 2 filter collapse are open, there are 2 filter items visible + expect(allByText).toHaveLength(2); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); fireEvent.click(icon); @@ -285,4 +305,59 @@ describe('Quick Filters with custom filters', () => { ); expect(requestBody.signal).toBe(SIGNAL); }); + + // render duration filter + it('should render duration slider for duration_nono filter', async () => { + // Set up fake timers **before rendering** + jest.useFakeTimers(); + + const { getByTestId } = render(); + await screen.findByText(FILTER_SERVICE_NAME); + expect(screen.getByText('Duration')).toBeInTheDocument(); + + // click to open the duration filter + fireEvent.click(screen.getByText('Duration')); + + const minDuration = getByTestId('min-input') as HTMLInputElement; + const maxDuration = getByTestId('max-input') as HTMLInputElement; + expect(minDuration).toHaveValue(null); + expect(minDuration).toHaveProperty('placeholder', '0'); + expect(maxDuration).toHaveValue(null); + expect(maxDuration).toHaveProperty('placeholder', '100000000'); + + await act(async () => { + // set values + fireEvent.change(minDuration, { target: { value: '10000' } }); + fireEvent.change(maxDuration, { target: { value: '20000' } }); + jest.advanceTimersByTime(2000); + }); + await waitFor(() => { + expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( + expect.objectContaining({ + builder: { + queryData: expect.arrayContaining([ + expect.objectContaining({ + filters: expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + key: expect.objectContaining({ key: 'durationNano' }), + op: '>=', + value: 10000000000, + }), + expect.objectContaining({ + key: expect.objectContaining({ key: 'durationNano' }), + op: '<=', + value: 20000000000, + }), + ]), + }), + }), + ]), + }, + }), + ); + }); + + jest.useRealTimers(); // Clean up + }); }); diff --git a/frontend/src/components/QuickFilters/types.ts b/frontend/src/components/QuickFilters/types.ts index e39daf232d..45c671e77e 100644 --- a/frontend/src/components/QuickFilters/types.ts +++ b/frontend/src/components/QuickFilters/types.ts @@ -5,6 +5,7 @@ import { DataSource } from 'types/common/queryBuilder'; export enum FiltersType { SLIDER = 'SLIDER', CHECKBOX = 'CHECKBOX', + DURATION = 'DURATION', // ALIAS FOR DURATION_NANO } export enum MinMax { @@ -42,6 +43,8 @@ export interface IQuickFiltersProps { onFilterChange?: (query: Query) => void; signal?: SignalType; className?: string; + showFilterCollapse?: boolean; + showQueryName?: boolean; } export enum QuickFiltersSource { diff --git a/frontend/src/components/QuickFilters/utils.tsx b/frontend/src/components/QuickFilters/utils.tsx index 4d1ed5ffa5..4df900e835 100644 --- a/frontend/src/components/QuickFilters/utils.tsx +++ b/frontend/src/components/QuickFilters/utils.tsx @@ -1,30 +1,53 @@ +import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants'; import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; -import { FiltersType, IQuickFiltersConfig } from './types'; +import { FiltersType, IQuickFiltersConfig, SignalType } from './types'; -const getFilterName = (str: string): string => +const FILTER_TITLE_MAP: Record = { + duration_nano: 'Duration', + hasError: 'Has Error (Status)', +}; + +const FILTER_TYPE_MAP: Record = { + duration_nano: FiltersType.DURATION, +}; + +const getFilterName = (str: string): string => { + if (FILTER_TITLE_MAP[str]) { + return FILTER_TITLE_MAP[str]; + } // replace . and _ with space // capitalize the first letter of each word - str + return str .replace(/\./g, ' ') .replace(/_/g, ' ') .split(' ') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); +}; + +const getFilterType = (att: FilterType): FiltersType => { + if (FILTER_TYPE_MAP[att.key]) { + return FILTER_TYPE_MAP[att.key]; + } + return FiltersType.CHECKBOX; +}; export const getFilterConfig = ( + signal?: SignalType, customFilters?: FilterType[], config?: IQuickFiltersConfig[], ): IQuickFiltersConfig[] => { - if (!customFilters?.length) { + if (!customFilters?.length || !signal) { return config || []; } return customFilters.map( (att, index) => ({ - type: FiltersType.CHECKBOX, + type: getFilterType(att), title: getFilterName(att.key), + dataSource: SIGNAL_DATA_SOURCE_MAP[signal], attributeKey: { id: att.key, key: att.key, @@ -33,7 +56,7 @@ export const getFilterConfig = ( isColumn: att.isColumn, isJSON: att.isJSON, }, - defaultOpen: index === 0, + defaultOpen: index < 2, } as IQuickFiltersConfig), ); }; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx b/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx index 14c57a6742..c255cab7c0 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx @@ -1,23 +1,16 @@ import './Explorer.styles.scss'; -import { FilterOutlined } from '@ant-design/icons'; import * as Sentry from '@sentry/react'; -import { Switch, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; import cx from 'classnames'; import QuickFilters from 'components/QuickFilters/QuickFilters'; -import { QuickFiltersSource } from 'components/QuickFilters/types'; +import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import { useEffect } from 'react'; -import { useApiMonitoringParams } from '../queryParams'; -import { ApiMonitoringQuickFiltersConfig } from '../utils'; import DomainList from './Domains/DomainList'; function Explorer(): JSX.Element { - const [params, setParams] = useApiMonitoringParams(); - const showIP = params.showIP ?? true; - useEffect(() => { logEvent('API Monitoring: Landing page visited', {}); }, []); @@ -26,29 +19,12 @@ function Explorer(): JSX.Element { }>
-
- - Filters -
- -
- Show IP addresses - { - logEvent('API Monitoring: Show IP addresses clicked', { - showIP: !(showIP ?? true), - }); - setParams({ showIP }); - }} - /> -
- {}} />
diff --git a/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts b/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts index 3bc5a15e53..bcb69e0db7 100644 --- a/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts +++ b/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts @@ -17,6 +17,13 @@ export const quickFiltersListResponse = { isColumn: false, isJSON: false, }, + { + key: 'duration_nano', + dataType: 'float64', + type: 'tag', + isColumn: false, + isJSON: false, + }, { key: 'quantity', dataType: 'float64', diff --git a/frontend/src/pages/AllErrors/index.tsx b/frontend/src/pages/AllErrors/index.tsx index 38d7bc44b3..d2f048c4a6 100644 --- a/frontend/src/pages/AllErrors/index.tsx +++ b/frontend/src/pages/AllErrors/index.tsx @@ -6,7 +6,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; import cx from 'classnames'; import QuickFilters from 'components/QuickFilters/QuickFilters'; -import { QuickFiltersSource } from 'components/QuickFilters/types'; +import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import RouteTab from 'components/RouteTab'; import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar'; import { LOCALSTORAGE } from 'constants/localStorage'; @@ -20,7 +20,6 @@ import { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { routes } from './config'; -import { ExceptionsQuickFiltersConfig } from './utils'; function AllErrors(): JSX.Element { const { pathname } = useLocation(); @@ -49,8 +48,9 @@ function AllErrors(): JSX.Element { {showFilters && (
diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index 91e3bd1c21..acce20df6a 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -1,5 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ import userEvent from '@testing-library/user-event'; +import { ENVIRONMENT } from 'constants/env'; import { initialQueriesMap, initialQueryBuilderFormValues, @@ -7,10 +8,10 @@ import { } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; +import { quickFiltersListResponse } from 'mocks-server/__mockdata__/customQuickFilters'; import { queryRangeForListView, queryRangeForTableView, - queryRangeForTimeSeries, queryRangeForTraceView, } from 'mocks-server/__mockdata__/query_range'; import { server } from 'mocks-server/server'; @@ -18,6 +19,7 @@ import { rest } from 'msw'; import { QueryBuilderContext } from 'providers/QueryBuilder'; import { act, + cleanup, fireEvent, render, screen, @@ -42,6 +44,9 @@ import { const historyPush = jest.fn(); +const BASE_URL = ENVIRONMENT.baseURL; +const FILTER_SERVICE_NAME = 'Service Name'; + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: (): { pathname: string } => ({ @@ -435,24 +440,6 @@ describe('TracesExplorer - Filters', () => { ][0].builder.queryData[0].filters.items, ).toEqual([]); }); - - it('filter panel should collapse & uncollapsed', async () => { - const { getByText, getByTestId } = render(); - - Object.values(AllTraceFilterKeyValue).forEach((filter) => { - expect(getByText(filter)).toBeInTheDocument(); - }); - - // Filter panel should collapse - const collapseButton = getByTestId('toggle-filter-panel'); - expect(collapseButton).toBeInTheDocument(); - fireEvent.click(collapseButton); - - // uncollapse btn should be present - expect( - await screen.findByTestId('filter-uncollapse-btn'), - ).toBeInTheDocument(); - }); }); const handleExplorerTabChangeTest = jest.fn(); @@ -463,57 +450,32 @@ jest.mock('hooks/useHandleExplorerTabChange', () => ({ })); describe('TracesExplorer - ', () => { - it('should render the traces explorer page', async () => { + const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/traces`; + + const setupServer = (): void => { server.use( - rest.post('http://localhost/api/v4/query_range', (req, res, ctx) => - res(ctx.status(200), ctx.json(queryRangeForTimeSeries)), + rest.get(quickFiltersListURL, (_, res, ctx) => + res(ctx.status(200), ctx.json(quickFiltersListResponse)), ), ); - const { findByText, getByText } = render(); + }; - // assert mocked date time selection - expect(await findByText('MockDateTimeSelection')).toBeInTheDocument(); - - // assert stage&Btn - expect(getByText('Stage & Run Query')).toBeInTheDocument(); - - // assert QB - will not write tests for QB as that would be covererd in QB tests separately - expect( - getByText( - 'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options', - ), - ).toBeInTheDocument(); - expect(getByText('AGGREGATION INTERVAL')).toBeInTheDocument(); - // why is this present here?? - // expect(getByText('Metrics name')).toBeInTheDocument(); - // expect(getByText('WHERE')).toBeInTheDocument(); - // expect(getByText('Legend Format')).toBeInTheDocument(); - - // assert timeseries chart mock - // expect(await screen.findByText('MockUplot')).toBeInTheDocument(); + beforeEach(() => { + setupServer(); }); - it('check tab navigation', async () => { - const { getByTestId, getByText } = render(); + afterEach(() => { + server.resetHandlers(); + }); - // switch to Table view - const TableBtn = getByText('Table View'); - expect(TableBtn).toBeInTheDocument(); - fireEvent.click(TableBtn); - - expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TABLE); - - // switch to traces view - const tracesBtn = getByTestId('Traces'); - expect(tracesBtn).toBeInTheDocument(); - fireEvent.click(tracesBtn); - - expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TRACE); + afterAll(() => { + server.close(); + cleanup(); }); it('trace explorer - list view', async () => { server.use( - rest.post('http://localhost/api/v4/query_range', (req, res, ctx) => + rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => res(ctx.status(200), ctx.json(queryRangeForListView)), ), ); @@ -524,6 +486,7 @@ describe('TracesExplorer - ', () => { , ); + await screen.findByText(FILTER_SERVICE_NAME); expect(await screen.findByText('Timestamp')).toBeInTheDocument(); expect(getByText('options_menu.options')).toBeInTheDocument(); @@ -536,7 +499,7 @@ describe('TracesExplorer - ', () => { it('trace explorer - table view', async () => { server.use( - rest.post('http://localhost/api/v4/query_range', (req, res, ctx) => + rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => res(ctx.status(200), ctx.json(queryRangeForTableView)), ), ); @@ -554,7 +517,7 @@ describe('TracesExplorer - ', () => { it('trace explorer - trace view', async () => { server.use( - rest.post('http://localhost/api/v4/query_range', (req, res, ctx) => + rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => res(ctx.status(200), ctx.json(queryRangeForTraceView)), ), ); @@ -591,7 +554,11 @@ describe('TracesExplorer - ', () => { }); it('test for explorer options', async () => { - const { getByText, getByTestId } = render(); + const { getByText, getByTestId } = render( + + + , + ); // assert explorer options - action btns [ @@ -619,8 +586,12 @@ describe('TracesExplorer - ', () => { }); it('select a view options - assert and save this view', async () => { - const { container } = render(); - + const { container } = render( + + + , + ); + await screen.findByText(FILTER_SERVICE_NAME); await act(async () => { fireEvent.mouseDown( container.querySelector( @@ -664,7 +635,12 @@ describe('TracesExplorer - ', () => { }); it('create a dashboard btn assert', async () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); + await screen.findByText(FILTER_SERVICE_NAME); const createDashboardBtn = getByText('Add to Dashboard'); expect(createDashboardBtn).toBeInTheDocument(); @@ -687,7 +663,12 @@ describe('TracesExplorer - ', () => { }); it('create an alert btn assert', async () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); + await screen.findByText(FILTER_SERVICE_NAME); const createAlertBtn = getByText('Create an Alert'); expect(createAlertBtn).toBeInTheDocument(); diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 448e41bf33..c1e82a82d0 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -7,6 +7,8 @@ import logEvent from 'api/common/logEvent'; import axios from 'axios'; import cx from 'classnames'; import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; +import QuickFilters from 'components/QuickFilters/QuickFilters'; +import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import { LOCALSTORAGE } from 'constants/localStorage'; import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; @@ -34,7 +36,6 @@ import { DataSource } from 'types/common/queryBuilder'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { v4 } from 'uuid'; -import { Filter } from './Filter/Filter'; import { ActionsWrapper, Container } from './styles'; import { getTabsItems } from './utils'; @@ -244,7 +245,14 @@ function TracesExplorer(): JSX.Element { }>