mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 00: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 {
|
.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);
|
||||||
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
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,
|
||||||
};
|
};
|
||||||
|
@ -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>;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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', {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user