Custom Quick FIlters: Integration across other tabs (#8001)

* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* chore: quick filters - added filters init (#7867)

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: no results state

* fix: minor fix

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

* fix: minor refactor

* test: added test cases

* feat: integrate custom quick filters in logs

* Custom quick filter: Other Filters | Search integration (#7939)

* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* feat: no results state

* fix: minor fix

* Custom Quick FIlters: UI fixes and Announcement Tooltip (#7950)

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: code refactor

* feat: debounce search

* feat: refactor

* feat: exceptions integrate

* feat: api monitoring qf integrate

* feat: handle query name show

* Custom quick filters: Tests and pr review comments (#7967)

* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* feat: no results state

* fix: minor fix

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

* fix: minor refactor

* test: added test cases

* feat: integrate custom quick filters in logs

* feat: code refactor

* feat: debounce search

* feat: refactor

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: integrate traces data source to settings

* feat: duration nano traces filter in qf

* fix: allow only admins to change qf  settings

* feat: has error handling

* feat: fix existing tests

* feat: update test cases

* feat: update test cases

* feat: minor refactor

* feat: minor refactor

* feat: log quick filter settings changes

* feat: log quick filter settings changes

* feat: log quick filter settings changes

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
This commit is contained in:
Aditya Singh 2025-05-27 20:04:57 +05:30 committed by GitHub
parent 62810428d8
commit 69e94cbd38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 825 additions and 206 deletions

View File

@ -30,6 +30,7 @@
.right-action { .right-action {
display: flex; display: flex;
align-items: center; align-items: center;
min-width: 48px;
.clear-all { .clear-all {
font-size: 12px; font-size: 12px;
@ -52,10 +53,14 @@
.checkbox-value-section { .checkbox-value-section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 4px;
width: calc(100% - 24px); width: calc(100% - 24px);
cursor: pointer; cursor: pointer;
.value-string {
width: 100%;
}
&.filter-disabled { &.filter-disabled {
cursor: not-allowed; cursor: not-allowed;
@ -74,9 +79,6 @@
} }
} }
.value-string {
}
.only-btn { .only-btn {
display: none; 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);
}

View File

@ -504,6 +504,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
onChange(value, currentFilterState[value], true); onChange(value, currentFilterState[value], true);
}} }}
> >
<div className={`${filter.title} label-${value}`} />
{filter.customRendererForValue ? ( {filter.customRendererForValue ? (
filter.customRendererForValue(value) filter.customRendererForValue(value)
) : ( ) : (
@ -511,7 +512,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
className="value-string" className="value-string"
ellipsis={{ tooltip: { placement: 'right' } }} ellipsis={{ tooltip: { placement: 'right' } }}
> >
{value} {String(value)}
</Typography.Text> </Typography.Text>
)} )}
<Button type="text" className="only-btn"> <Button type="text" className="only-btn">

View File

@ -0,0 +1,174 @@
.collapseContainer {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
.ant-collapse-header {
padding: 12px !important;
align-items: center !important;
}
.ant-collapse-expand-icon {
padding-right: 9px !important;
}
.ant-collapse-header-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
.duration-inputs {
display: grid;
gap: 12px;
.min-max-input {
.ant-input-group-addon {
color: var(--bg-vanilla-400);
font-family: 'Space Mono', monospace;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
padding: 0 6px;
}
.ant-input {
padding: 4px 6px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono', monospace;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
}
}
}
}
.divider {
background-color: var(--bg-slate-400);
margin: 0;
border-color: var(--bg-slate-400);
}
.filter-header {
padding: 16px 8px 16px 12px;
.filter-title {
display: flex;
gap: 6px;
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
}
.sync-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
padding: 8px;
}
.arrow-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
padding-top: 8px;
.anticon-vertical-align-top {
svg {
width: 16px;
height: 16px;
}
}
}
}
.section-body-header {
display: flex;
> button {
position: absolute;
right: 4px;
padding-top: 13px;
}
.ant-collapse {
width: 100%;
}
}
.section-card {
background-color: var(--bg-ink-500);
.ant-card-body {
padding: 0;
display: flex;
flex-direction: column;
max-height: 500px;
overflow-x: hidden;
overflow-y: auto;
}
width: 240px;
}
.lightMode {
.collapseContainer {
background-color: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-collapse-header-text {
color: var(--bg-slate-100);
}
.duration-inputs {
.min-max-input {
.ant-input-group-addon {
color: var(--bg-slate-100);
}
.ant-input {
color: var(--bg-slate-100);
}
}
}
}
.divider {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.filter-header {
.filter-title {
.ant-typography {
color: var(--bg-slate-100);
}
}
.arrow-icon {
background-color: var(--bg-vanilla-100);
}
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
.section-card {
background-color: var(--bg-vanilla-100);
box-shadow: none;
}
}

View File

@ -0,0 +1,281 @@
/* eslint-disable react-hooks/exhaustive-deps */
import './Duration.styles.scss';
import { Button, Collapse } from 'antd';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
HandleRunProps,
unionTagFilterItems,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
function Duration({
filter,
onFilterChange,
}: {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
}): JSX.Element {
const [selectedFilters, setSelectedFilters] = useState<
Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>
>();
const [activeKeys, setActiveKeys] = useState<string[]>([
filter.defaultOpen ? 'durationNano' : '',
]);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {
const filters = compositeQuery?.builder.queryData?.[0].filters;
if (!filters) {
return {} as FilterType;
}
return (filters.items || [])
.filter((item) =>
Object.keys(AllTraceFilterKeyValue).includes(item.key?.key as string),
)
.filter(
(item) =>
(item.op === 'in' && item.key?.key !== 'durationNano') ||
(item.key?.key === 'durationNano' && ['>=', '<='].includes(item.op)),
)
.reduce((acc, item) => {
const keys = item.key as BaseAutocompleteData;
const attributeName = item.key?.key || '';
const values = item.value as string[];
if ((attributeName as AllTraceFilterKeys) === 'durationNano') {
if (item.op === '>=') {
acc.durationNanoMin = {
values: getMs(String(values)),
keys,
};
} else {
acc.durationNanoMax = {
values: getMs(String(values)),
keys,
};
}
return acc;
}
if (attributeName) {
if (acc[attributeName as AllTraceFilterKeys]) {
const existingValue = acc[attributeName as AllTraceFilterKeys];
acc[attributeName as AllTraceFilterKeys] = {
values: [...existingValue.values, ...values],
keys,
};
} else {
acc[attributeName as AllTraceFilterKeys] = { values, keys };
}
}
return acc;
}, {} as FilterType);
}, [compositeQuery]);
useEffect(() => {
if (!isEqual(syncSelectedFilters, selectedFilters)) {
setSelectedFilters(syncSelectedFilters);
}
}, [syncSelectedFilters]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const preparePostData = (): TagFilterItem[] => {
if (!selectedFilters) {
return [];
}
const items = Object.keys(selectedFilters)?.flatMap((attribute) => {
const { keys, values } = selectedFilters[attribute as AllTraceFilterKeys];
if (
['durationNanoMax', 'durationNanoMin', 'durationNano'].includes(
attribute as AllTraceFilterKeys,
)
) {
if (!values || !values.length) {
return [];
}
let minValue = '';
let maxValue = '';
const durationItems: TagFilterItem[] = [];
if (isArray(values)) {
minValue = values?.[0];
maxValue = values?.[1];
const minItems: TagFilterItem = {
id: uuid().slice(0, 8),
op: '>=',
key: keys,
value: Number(minValue) * 1000000,
};
const maxItems: TagFilterItem = {
id: uuid().slice(0, 8),
op: '<=',
key: keys,
value: Number(maxValue) * 1000000,
};
return maxValue ? [minItems, maxItems] : [minItems];
}
if (attribute === 'durationNanoMin') {
durationItems.push({
id: uuid().slice(0, 8),
op: '>=',
key: keys,
value: Number(values) * 1000000,
});
} else {
durationItems.push({
id: uuid().slice(0, 8),
op: '<=',
key: keys,
value: Number(values) * 1000000,
});
}
return durationItems;
}
return {
id: uuid().slice(0, 8),
key: keys,
op: 'in',
value: values,
};
});
return items as TagFilterItem[];
};
const removeFilterItemIds = (query: Query): Query => {
const clonedQuery = cloneDeep(query);
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
...data,
filters: {
...data.filters,
items: data.filters?.items?.map((item) => ({
...item,
id: '',
})),
},
}));
return clonedQuery;
};
const handleRun = useCallback(
(props?: HandleRunProps): void => {
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: props?.resetAll
? []
: (unionTagFilterItems(item.filters?.items, preparePostData())
.map((item) =>
item.key?.key === props?.clearByType ? undefined : item,
)
.filter((i) => i) as TagFilterItem[]),
},
})),
},
};
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
if (
isEqual(currentQueryWithoutIds, preparedQueryWithoutIds) &&
!props?.resetAll
) {
return;
}
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
},
[currentQuery, redirectWithQueryBuilderData, selectedFilters],
);
useEffect(() => {
handleRun();
}, [selectedFilters]);
const onClearHandler = (e: React.MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
if (selectedFilters?.durationNanoMin || selectedFilters?.durationNanoMax) {
handleRun({ clearByType: 'durationNano' });
}
};
return (
<div className="section-body-header" data-testid="collapse-duration">
<Collapse
bordered={false}
className="collapseContainer"
activeKey={activeKeys}
onChange={(keys): void => setActiveKeys(keys as string[])}
items={[
{
key: 'durationNano',
children: (
<DurationSection
setSelectedFilters={setSelectedFilters}
selectedFilters={selectedFilters}
/>
),
label: 'Duration',
},
]}
/>
{activeKeys.includes('durationNano') && (
<Button
type="link"
onClick={onClearHandler}
data-testid="collapse-duration-clearBtn"
>
Clear All
</Button>
)}
</div>
);
}
Duration.defaultProps = {
onFilterChange: (): void => {},
};
export default Duration;

View File

@ -5,19 +5,24 @@ import {
SyncOutlined, SyncOutlined,
VerticalAlignTopOutlined, VerticalAlignTopOutlined,
} from '@ant-design/icons'; } 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 getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set'; import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames'; import classNames from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction, isNull } from 'lodash-es'; import { cloneDeep, isFunction, isNull } from 'lodash-es';
import { Settings2 as SettingsIcon } from 'lucide-react'; import { Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
import Checkbox from './FilterRenderers/Checkbox/Checkbox'; import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Duration from './FilterRenderers/Duration/Duration';
import Slider from './FilterRenderers/Slider/Slider'; import Slider from './FilterRenderers/Slider/Slider';
import useFilterConfig from './hooks/useFilterConfig'; import useFilterConfig from './hooks/useFilterConfig';
import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip'; import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip';
@ -32,8 +37,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source, source,
onFilterChange, onFilterChange,
signal, signal,
showFilterCollapse = true,
showQueryName = true,
} = props; } = props;
const { user } = useAppContext();
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const isAdmin = user.role === USER_ROLES.ADMIN;
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
const { const {
filterConfig, filterConfig,
@ -95,13 +106,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
}; };
const lastQueryName = const lastQueryName =
showQueryName &&
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
return ( return (
<div className="quick-filters-container"> <div className="quick-filters-container">
<div className="quick-filters"> <div className="quick-filters">
{source !== QuickFiltersSource.INFRA_MONITORING && {source !== QuickFiltersSource.INFRA_MONITORING && (
source !== QuickFiltersSource.API_MONITORING && (
<section className="header"> <section className="header">
<section className="left-actions"> <section className="left-actions">
<FilterOutlined /> <FilterOutlined />
@ -109,12 +120,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
{lastQueryName ? 'Filters for' : 'Filters'} {lastQueryName ? 'Filters for' : 'Filters'}
</Typography.Text> </Typography.Text>
{lastQueryName && ( {lastQueryName && (
<Tooltip <Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
title={`Filter currently in sync with query ${lastQueryName}`} <Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
>
<Typography.Text className="sync-tag">
{lastQueryName}
</Typography.Text>
</Tooltip> </Tooltip>
)} )}
</section> </section>
@ -125,6 +132,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<SyncOutlined className="sync-icon" onClick={handleReset} /> <SyncOutlined className="sync-icon" onClick={handleReset} />
</div> </div>
</Tooltip> </Tooltip>
{showFilterCollapse && (
<Tooltip title="Collapse Filters"> <Tooltip title="Collapse Filters">
<div className="right-action-icon-container"> <div className="right-action-icon-container">
<VerticalAlignTopOutlined <VerticalAlignTopOutlined
@ -133,7 +141,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/> />
</div> </div>
</Tooltip> </Tooltip>
{isDynamicFilters && ( )}
{isDynamicFilters && isAdmin && (
<Tooltip title="Settings"> <Tooltip title="Settings">
<div <div
className={classNames('right-action-icon-container', { className={classNames('right-action-icon-container', {
@ -179,6 +188,23 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
</div> </div>
) : ( ) : (
<OverlayScrollbar> <OverlayScrollbar>
<>
{source === QuickFiltersSource.API_MONITORING && (
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP ?? true}
onClick={(): void => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !(showIP ?? true),
});
setParams({ showIP });
}}
/>
</div>
)}
<section className="filters"> <section className="filters">
{filterConfig.map((filter) => { {filterConfig.map((filter) => {
switch (filter.type) { switch (filter.type) {
@ -190,6 +216,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
onFilterChange={onFilterChange} onFilterChange={onFilterChange}
/> />
); );
case FiltersType.DURATION:
return <Duration filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER: case FiltersType.SLIDER:
return <Slider filter={filter} />; return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches // eslint-disable-next-line sonarjs/no-duplicated-branches
@ -204,6 +232,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
} }
})} })}
</section> </section>
</>
</OverlayScrollbar> </OverlayScrollbar>
)} )}
</div> </div>
@ -235,4 +264,6 @@ QuickFilters.defaultProps = {
onFilterChange: null, onFilterChange: null,
signal: '', signal: '',
config: [], config: [],
showFilterCollapse: true,
showQueryName: true,
}; };

View File

@ -3,10 +3,12 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants'; import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types'; import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions'; import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
function OtherFiltersSkeleton(): JSX.Element { function OtherFiltersSkeleton(): JSX.Element {
return ( return (
@ -34,6 +36,11 @@ function OtherFilters({
addedFilters: FilterType[]; addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>; setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
}): JSX.Element { }): JSX.Element {
const isLogDataSource = useMemo(
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const { const {
data: suggestionsData, data: suggestionsData,
isFetching: isFetchingSuggestions, isFetching: isFetchingSuggestions,
@ -45,18 +52,39 @@ function OtherFilters({
}, },
{ {
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue], queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal, enabled: !!signal && isLogDataSource,
}, },
); );
const otherFilters = useMemo( const {
() => data: aggregateKeysData,
suggestionsData?.payload?.attributes?.filter( isFetching: isFetchingAggregateKeys,
(attr) => !addedFilters.some((filter) => filter.key === attr.key), } = useGetAggregateKeys(
), {
[suggestionsData, addedFilters], 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 => { const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [ setAddedFilters((prev) => [
...prev, ...prev,
@ -71,7 +99,8 @@ function OtherFilters({
}; };
const renderFilters = (): React.ReactNode => { const renderFilters = (): React.ReactNode => {
if (isFetchingSuggestions) return <OtherFiltersSkeleton />; const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
if (isLoading) return <OtherFiltersSkeleton />;
if (!otherFilters?.length) if (!otherFilters?.length)
return <div className="no-values-found">No values found</div>; return <div className="no-values-found">No values found</div>;

View File

@ -7,6 +7,7 @@
background: var(--bg-slate-500); background: var(--bg-slate-500);
transition: width 0.05s ease-in-out; transition: width 0.05s ease-in-out;
overflow: hidden; overflow: hidden;
color: var(--bg-vanilla-100);
&.qf-logs-explorer { &.qf-logs-explorer {
height: calc(100vh - 45px); height: calc(100vh - 45px);
@ -16,6 +17,14 @@
height: 100vh; height: 100vh;
} }
&.qf-api-monitoring {
height: calc(100vh - 45px);
}
&.qf-traces-explorer {
height: calc(100vh - 45px);
}
&.hidden { &.hidden {
width: 0; width: 0;
} }
@ -172,6 +181,7 @@
.lightMode { .lightMode {
.quick-filters-settings { .quick-filters-settings {
background: var(--bg-vanilla-100); background: var(--bg-vanilla-100);
color: var(--bg-slate-500);
.search { .search {
.ant-input { .ant-input {
background-color: var(--bg-vanilla-100); background-color: var(--bg-vanilla-100);

View File

@ -1,3 +1,4 @@
import logEvent from 'api/common/logEvent';
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters'; import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
import { SignalType } from 'components/QuickFilters/types'; import { SignalType } from 'components/QuickFilters/types';
@ -46,6 +47,9 @@ const useQuickFilterSettings = ({
onSuccess: () => { onSuccess: () => {
setIsSettingsOpen(false); setIsSettingsOpen(false);
setIsStale(true); setIsStale(true);
logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({ notifications.success({
message: 'Quick filters updated successfully', message: 'Quick filters updated successfully',
placement: 'bottomRight', placement: 'bottomRight',

View File

@ -33,7 +33,7 @@ const useFilterConfig = ({
const isDynamicFilters = useMemo(() => customFilters.length > 0, [ const isDynamicFilters = useMemo(() => customFilters.length > 0, [
customFilters, customFilters,
]); ]);
const { isLoading: isCustomFiltersLoading } = useQuery< const { isFetching: isCustomFiltersLoading } = useQuery<
SuccessResponse<PayloadProps> | ErrorResponse, SuccessResponse<PayloadProps> | ErrorResponse,
Error Error
>( >(
@ -49,10 +49,10 @@ const useFilterConfig = ({
enabled: !!signal && isStale, enabled: !!signal && isStale,
}, },
); );
const filterConfig = useMemo(() => getFilterConfig(customFilters, config), [ const filterConfig = useMemo(
config, () => getFilterConfig(signal, customFilters, config),
customFilters, [config, customFilters, signal],
]); );
return { return {
filterConfig, filterConfig,

View File

@ -1,6 +1,7 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { import {
act,
cleanup, cleanup,
fireEvent, fireEvent,
render, render,
@ -8,6 +9,7 @@ import {
waitFor, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env'; import { ENVIRONMENT } from 'constants/env';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { import {
otherFiltersResponse, otherFiltersResponse,
@ -17,6 +19,7 @@ import {
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
import { rest } from 'msw'; import { rest } from 'msw';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { USER_ROLES } from 'types/roles';
import QuickFilters from '../QuickFilters'; import QuickFilters from '../QuickFilters';
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types'; import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
@ -26,6 +29,21 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(), 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 handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn(); const redirectWithQueryBuilderData = jest.fn();
const putHandler = 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('Filters for')).toBeInTheDocument();
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
await screen.findByText(FILTER_SERVICE_NAME); 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); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); fireEvent.click(icon);
@ -285,4 +305,59 @@ describe('Quick Filters with custom filters', () => {
); );
expect(requestBody.signal).toBe(SIGNAL); 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(<TestQuickFilters signal={SIGNAL} />);
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
});
}); });

View File

@ -5,6 +5,7 @@ import { DataSource } from 'types/common/queryBuilder';
export enum FiltersType { export enum FiltersType {
SLIDER = 'SLIDER', SLIDER = 'SLIDER',
CHECKBOX = 'CHECKBOX', CHECKBOX = 'CHECKBOX',
DURATION = 'DURATION', // ALIAS FOR DURATION_NANO
} }
export enum MinMax { export enum MinMax {
@ -42,6 +43,8 @@ export interface IQuickFiltersProps {
onFilterChange?: (query: Query) => void; onFilterChange?: (query: Query) => void;
signal?: SignalType; signal?: SignalType;
className?: string; className?: string;
showFilterCollapse?: boolean;
showQueryName?: boolean;
} }
export enum QuickFiltersSource { export enum QuickFiltersSource {

View File

@ -1,30 +1,53 @@
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; 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<string, string> = {
duration_nano: 'Duration',
hasError: 'Has Error (Status)',
};
const FILTER_TYPE_MAP: Record<string, FiltersType> = {
duration_nano: FiltersType.DURATION,
};
const getFilterName = (str: string): string => {
if (FILTER_TITLE_MAP[str]) {
return FILTER_TITLE_MAP[str];
}
// replace . and _ with space // replace . and _ with space
// capitalize the first letter of each word // capitalize the first letter of each word
str return str
.replace(/\./g, ' ') .replace(/\./g, ' ')
.replace(/_/g, ' ') .replace(/_/g, ' ')
.split(' ') .split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ');
};
const getFilterType = (att: FilterType): FiltersType => {
if (FILTER_TYPE_MAP[att.key]) {
return FILTER_TYPE_MAP[att.key];
}
return FiltersType.CHECKBOX;
};
export const getFilterConfig = ( export const getFilterConfig = (
signal?: SignalType,
customFilters?: FilterType[], customFilters?: FilterType[],
config?: IQuickFiltersConfig[], config?: IQuickFiltersConfig[],
): IQuickFiltersConfig[] => { ): IQuickFiltersConfig[] => {
if (!customFilters?.length) { if (!customFilters?.length || !signal) {
return config || []; return config || [];
} }
return customFilters.map( return customFilters.map(
(att, index) => (att, index) =>
({ ({
type: FiltersType.CHECKBOX, type: getFilterType(att),
title: getFilterName(att.key), title: getFilterName(att.key),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
attributeKey: { attributeKey: {
id: att.key, id: att.key,
key: att.key, key: att.key,
@ -33,7 +56,7 @@ export const getFilterConfig = (
isColumn: att.isColumn, isColumn: att.isColumn,
isJSON: att.isJSON, isJSON: att.isJSON,
}, },
defaultOpen: index === 0, defaultOpen: index < 2,
} as IQuickFiltersConfig), } as IQuickFiltersConfig),
); );
}; };

View File

@ -1,23 +1,16 @@
import './Explorer.styles.scss'; import './Explorer.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters'; 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 ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useApiMonitoringParams } from '../queryParams';
import { ApiMonitoringQuickFiltersConfig } from '../utils';
import DomainList from './Domains/DomainList'; import DomainList from './Domains/DomainList';
function Explorer(): JSX.Element { function Explorer(): JSX.Element {
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
useEffect(() => { useEffect(() => {
logEvent('API Monitoring: Landing page visited', {}); logEvent('API Monitoring: Landing page visited', {});
}, []); }, []);
@ -26,29 +19,12 @@ function Explorer(): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}> <div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section"> <section className="api-quick-filter-left-section">
<div className="api-quick-filters-header">
<FilterOutlined />
<Typography.Text>Filters</Typography.Text>
</div>
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP ?? true}
onClick={(): void => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !(showIP ?? true),
});
setParams({ showIP });
}}
/>
</div>
<QuickFilters <QuickFilters
className="qf-api-monitoring"
source={QuickFiltersSource.API_MONITORING} source={QuickFiltersSource.API_MONITORING}
config={ApiMonitoringQuickFiltersConfig} signal={SignalType.API_MONITORING}
showFilterCollapse={false}
showQueryName={false}
handleFilterVisibilityChange={(): void => {}} handleFilterVisibilityChange={(): void => {}}
/> />
</section> </section>

View File

@ -17,6 +17,13 @@ export const quickFiltersListResponse = {
isColumn: false, isColumn: false,
isJSON: false, isJSON: false,
}, },
{
key: 'duration_nano',
dataType: 'float64',
type: 'tag',
isColumn: false,
isJSON: false,
},
{ {
key: 'quantity', key: 'quantity',
dataType: 'float64', dataType: 'float64',

View File

@ -6,7 +6,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames'; import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters'; 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 RouteTab from 'components/RouteTab';
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar'; import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
@ -20,7 +20,6 @@ import { useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { routes } from './config'; import { routes } from './config';
import { ExceptionsQuickFiltersConfig } from './utils';
function AllErrors(): JSX.Element { function AllErrors(): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -49,8 +48,9 @@ function AllErrors(): JSX.Element {
{showFilters && ( {showFilters && (
<section className={cx('all-errors-quick-filter-section')}> <section className={cx('all-errors-quick-filter-section')}>
<QuickFilters <QuickFilters
className="qf-exceptions"
source={QuickFiltersSource.EXCEPTIONS} source={QuickFiltersSource.EXCEPTIONS}
config={ExceptionsQuickFiltersConfig} signal={SignalType.EXCEPTIONS}
handleFilterVisibilityChange={handleFilterVisibilityChange} handleFilterVisibilityChange={handleFilterVisibilityChange}
/> />
</section> </section>

View File

@ -1,5 +1,6 @@
/* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable sonarjs/no-duplicate-string */
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { ENVIRONMENT } from 'constants/env';
import { import {
initialQueriesMap, initialQueriesMap,
initialQueryBuilderFormValues, initialQueryBuilderFormValues,
@ -7,10 +8,10 @@ import {
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { quickFiltersListResponse } from 'mocks-server/__mockdata__/customQuickFilters';
import { import {
queryRangeForListView, queryRangeForListView,
queryRangeForTableView, queryRangeForTableView,
queryRangeForTimeSeries,
queryRangeForTraceView, queryRangeForTraceView,
} from 'mocks-server/__mockdata__/query_range'; } from 'mocks-server/__mockdata__/query_range';
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
@ -18,6 +19,7 @@ import { rest } from 'msw';
import { QueryBuilderContext } from 'providers/QueryBuilder'; import { QueryBuilderContext } from 'providers/QueryBuilder';
import { import {
act, act,
cleanup,
fireEvent, fireEvent,
render, render,
screen, screen,
@ -42,6 +44,9 @@ import {
const historyPush = jest.fn(); const historyPush = jest.fn();
const BASE_URL = ENVIRONMENT.baseURL;
const FILTER_SERVICE_NAME = 'Service Name';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({ useLocation: (): { pathname: string } => ({
@ -435,24 +440,6 @@ describe('TracesExplorer - Filters', () => {
][0].builder.queryData[0].filters.items, ][0].builder.queryData[0].filters.items,
).toEqual([]); ).toEqual([]);
}); });
it('filter panel should collapse & uncollapsed', async () => {
const { getByText, getByTestId } = render(<TracesExplorer />);
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(); const handleExplorerTabChangeTest = jest.fn();
@ -463,57 +450,32 @@ jest.mock('hooks/useHandleExplorerTabChange', () => ({
})); }));
describe('TracesExplorer - ', () => { 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( server.use(
rest.post('http://localhost/api/v4/query_range', (req, res, ctx) => rest.get(quickFiltersListURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(queryRangeForTimeSeries)), res(ctx.status(200), ctx.json(quickFiltersListResponse)),
), ),
); );
const { findByText, getByText } = render(<TracesExplorer />); };
// assert mocked date time selection beforeEach(() => {
expect(await findByText('MockDateTimeSelection')).toBeInTheDocument(); setupServer();
// 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();
}); });
it('check tab navigation', async () => { afterEach(() => {
const { getByTestId, getByText } = render(<TracesExplorer />); server.resetHandlers();
});
// switch to Table view afterAll(() => {
const TableBtn = getByText('Table View'); server.close();
expect(TableBtn).toBeInTheDocument(); cleanup();
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);
}); });
it('trace explorer - list view', async () => { it('trace explorer - list view', async () => {
server.use( 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)), res(ctx.status(200), ctx.json(queryRangeForListView)),
), ),
); );
@ -524,6 +486,7 @@ describe('TracesExplorer - ', () => {
</QueryBuilderContext.Provider>, </QueryBuilderContext.Provider>,
); );
await screen.findByText(FILTER_SERVICE_NAME);
expect(await screen.findByText('Timestamp')).toBeInTheDocument(); expect(await screen.findByText('Timestamp')).toBeInTheDocument();
expect(getByText('options_menu.options')).toBeInTheDocument(); expect(getByText('options_menu.options')).toBeInTheDocument();
@ -536,7 +499,7 @@ describe('TracesExplorer - ', () => {
it('trace explorer - table view', async () => { it('trace explorer - table view', async () => {
server.use( 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)), res(ctx.status(200), ctx.json(queryRangeForTableView)),
), ),
); );
@ -554,7 +517,7 @@ describe('TracesExplorer - ', () => {
it('trace explorer - trace view', async () => { it('trace explorer - trace view', async () => {
server.use( 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)), res(ctx.status(200), ctx.json(queryRangeForTraceView)),
), ),
); );
@ -591,7 +554,11 @@ describe('TracesExplorer - ', () => {
}); });
it('test for explorer options', async () => { it('test for explorer options', async () => {
const { getByText, getByTestId } = render(<TracesExplorer />); const { getByText, getByTestId } = render(
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
<TracesExplorer />
</QueryBuilderContext.Provider>,
);
// assert explorer options - action btns // assert explorer options - action btns
[ [
@ -619,8 +586,12 @@ describe('TracesExplorer - ', () => {
}); });
it('select a view options - assert and save this view', async () => { it('select a view options - assert and save this view', async () => {
const { container } = render(<TracesExplorer />); const { container } = render(
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
<TracesExplorer />
</QueryBuilderContext.Provider>,
);
await screen.findByText(FILTER_SERVICE_NAME);
await act(async () => { await act(async () => {
fireEvent.mouseDown( fireEvent.mouseDown(
container.querySelector( container.querySelector(
@ -664,7 +635,12 @@ describe('TracesExplorer - ', () => {
}); });
it('create a dashboard btn assert', async () => { it('create a dashboard btn assert', async () => {
const { getByText } = render(<TracesExplorer />); const { getByText } = render(
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
<TracesExplorer />
</QueryBuilderContext.Provider>,
);
await screen.findByText(FILTER_SERVICE_NAME);
const createDashboardBtn = getByText('Add to Dashboard'); const createDashboardBtn = getByText('Add to Dashboard');
expect(createDashboardBtn).toBeInTheDocument(); expect(createDashboardBtn).toBeInTheDocument();
@ -687,7 +663,12 @@ describe('TracesExplorer - ', () => {
}); });
it('create an alert btn assert', async () => { it('create an alert btn assert', async () => {
const { getByText } = render(<TracesExplorer />); const { getByText } = render(
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
<TracesExplorer />
</QueryBuilderContext.Provider>,
);
await screen.findByText(FILTER_SERVICE_NAME);
const createAlertBtn = getByText('Create an Alert'); const createAlertBtn = getByText('Create an Alert');
expect(createAlertBtn).toBeInTheDocument(); expect(createAlertBtn).toBeInTheDocument();

View File

@ -7,6 +7,8 @@ import logEvent from 'api/common/logEvent';
import axios from 'axios'; import axios from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; 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 { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes'; import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@ -34,7 +36,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { Filter } from './Filter/Filter';
import { ActionsWrapper, Container } from './styles'; import { ActionsWrapper, Container } from './styles';
import { getTabsItems } from './utils'; import { getTabsItems } from './utils';
@ -244,7 +245,14 @@ function TracesExplorer(): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="trace-explorer-page"> <div className="trace-explorer-page">
<Card className="filter" hidden={!isOpen}> <Card className="filter" hidden={!isOpen}>
<Filter setOpen={setOpen} /> <QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.TRACES_EXPLORER}
signal={SignalType.TRACES}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}
/>
</Card> </Card>
<Card <Card
className={cx('trace-explorer', { className={cx('trace-explorer', {