mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 16:58:59 +08:00
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:
parent
62810428d8
commit
69e94cbd38
@ -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);
|
||||
}
|
||||
|
@ -504,6 +504,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
onChange(value, currentFilterState[value], true);
|
||||
}}
|
||||
>
|
||||
<div className={`${filter.title} label-${value}`} />
|
||||
{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)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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,13 +106,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
};
|
||||
|
||||
const lastQueryName =
|
||||
showQueryName &&
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
return (
|
||||
<div className="quick-filters-container">
|
||||
<div className="quick-filters">
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
||||
source !== QuickFiltersSource.API_MONITORING && (
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
@ -109,12 +120,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
{lastQueryName ? 'Filters for' : 'Filters'}
|
||||
</Typography.Text>
|
||||
{lastQueryName && (
|
||||
<Tooltip
|
||||
title={`Filter currently in sync with query ${lastQueryName}`}
|
||||
>
|
||||
<Typography.Text className="sync-tag">
|
||||
{lastQueryName}
|
||||
</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</section>
|
||||
@ -125,6 +132,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{showFilterCollapse && (
|
||||
<Tooltip title="Collapse Filters">
|
||||
<div className="right-action-icon-container">
|
||||
<VerticalAlignTopOutlined
|
||||
@ -133,7 +141,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isDynamicFilters && (
|
||||
)}
|
||||
{isDynamicFilters && isAdmin && (
|
||||
<Tooltip title="Settings">
|
||||
<div
|
||||
className={classNames('right-action-icon-container', {
|
||||
@ -179,6 +188,23 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{filterConfig.map((filter) => {
|
||||
switch (filter.type) {
|
||||
@ -190,6 +216,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.DURATION:
|
||||
return <Duration filter={filter} onFilterChange={onFilterChange} />;
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
@ -204,6 +232,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
)}
|
||||
</div>
|
||||
@ -235,4 +264,6 @@ QuickFilters.defaultProps = {
|
||||
onFilterChange: null,
|
||||
signal: '',
|
||||
config: [],
|
||||
showFilterCollapse: true,
|
||||
showQueryName: true,
|
||||
};
|
||||
|
@ -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<React.SetStateAction<FilterType[]>>;
|
||||
}): 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 <OtherFiltersSkeleton />;
|
||||
const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
|
||||
if (isLoading) return <OtherFiltersSkeleton />;
|
||||
if (!otherFilters?.length)
|
||||
return <div className="no-values-found">No values found</div>;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
|
@ -33,7 +33,7 @@ const useFilterConfig = ({
|
||||
const isDynamicFilters = useMemo(() => customFilters.length > 0, [
|
||||
customFilters,
|
||||
]);
|
||||
const { isLoading: isCustomFiltersLoading } = useQuery<
|
||||
const { isFetching: isCustomFiltersLoading } = useQuery<
|
||||
SuccessResponse<PayloadProps> | 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,
|
||||
|
@ -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(<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
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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<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
|
||||
// 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),
|
||||
);
|
||||
};
|
||||
|
@ -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 {
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<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
|
||||
className="qf-api-monitoring"
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={ApiMonitoringQuickFiltersConfig}
|
||||
signal={SignalType.API_MONITORING}
|
||||
showFilterCollapse={false}
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>
|
||||
</section>
|
||||
|
@ -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',
|
||||
|
@ -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 && (
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
className="qf-exceptions"
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={ExceptionsQuickFiltersConfig}
|
||||
signal={SignalType.EXCEPTIONS}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</section>
|
||||
|
@ -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(<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();
|
||||
@ -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(<TracesExplorer />);
|
||||
};
|
||||
|
||||
// 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(<TracesExplorer />);
|
||||
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 - ', () => {
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
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(<TracesExplorer />);
|
||||
const { getByText, getByTestId } = render(
|
||||
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
|
||||
<TracesExplorer />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// 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(<TracesExplorer />);
|
||||
|
||||
const { container } = render(
|
||||
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
|
||||
<TracesExplorer />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
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(<TracesExplorer />);
|
||||
const { getByText } = render(
|
||||
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
|
||||
<TracesExplorer />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
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(<TracesExplorer />);
|
||||
const { getByText } = render(
|
||||
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}>
|
||||
<TracesExplorer />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const createAlertBtn = getByText('Create an Alert');
|
||||
expect(createAlertBtn).toBeInTheDocument();
|
||||
|
@ -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 {
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="trace-explorer-page">
|
||||
<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
|
||||
className={cx('trace-explorer', {
|
||||
|
Loading…
x
Reference in New Issue
Block a user