mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-31 04:01:59 +08:00
feat: added trace-filter in new trace-explorer (#5081)
* feat: added trace-filter in new trace-explorer * feat: added trace-filter in new trace-explorer * feat: style improvement * feat: query builder sync and filter section refactor * feat: added duration and code refactor * feat: added default open case * feat: removed API calls and used keys from const * feat: added sync and prepare data logic for querybuilder * feat: added styles for lightmode * feat: code refactor and sync issue fixed * feat: code refactor and sync issue fixed * feat: code refactor and feedback issue fixed * feat: checkbox label and other feedback fix * feat: filter open and close btn style and handling * feat: added filter reset and clear all * feat: fixed query modification issue when filtering * feat: code refactor * feat: search text via BE API * feat: added CTA btn for old explorer page * feat: make trace-explorer default page * feat: removed new ribbon on CTA for old trace explorer * feat: fixed qb and filter panel sync via url state * feat: fixed duration section issues
This commit is contained in:
parent
1ce36c8344
commit
9733612be8
@ -8,4 +8,5 @@ export const buttonText: Record<string, string> = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
|
||||
[ROUTES.TRACE]: 'Try new Traces Explorer',
|
||||
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer',
|
||||
[ROUTES.TRACES_EXPLORER]: 'Switch to Old Trace Explorer',
|
||||
};
|
||||
|
@ -14,7 +14,8 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
() =>
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACE ||
|
||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER,
|
||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
@ -25,6 +26,8 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
history.push(ROUTES.TRACE);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
@ -47,6 +50,10 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
return button;
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ function QueryBuilderSearch({
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(tag);
|
||||
|
||||
const filterAttribute = [...initialSourceKeys, ...sourceKeys].find(
|
||||
(key) => key.key === getRemovePrefixFromKey(tagKey),
|
||||
(key) => key?.key === getRemovePrefixFromKey(tagKey),
|
||||
);
|
||||
|
||||
const computedTagValue =
|
||||
|
@ -72,7 +72,7 @@ const menuItems: SidebarItem[] = [
|
||||
icon: <BarChart2 size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.TRACE,
|
||||
key: ROUTES.TRACES_EXPLORER,
|
||||
label: 'Traces',
|
||||
icon: <DraftingCompass size={16} />,
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
defaultLiveQueryDataConfig,
|
||||
} from 'container/LiveLogs/constants';
|
||||
import { QueryHistoryState } from 'container/LiveLogs/types';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
@ -63,6 +64,7 @@ function DateTimeSelection({
|
||||
location,
|
||||
updateTimeInterval,
|
||||
globalTimeLoading,
|
||||
showOldExplorerCTA = false,
|
||||
}: Props): JSX.Element {
|
||||
const [formSelector] = Form.useForm();
|
||||
|
||||
@ -561,6 +563,11 @@ function DateTimeSelection({
|
||||
|
||||
return (
|
||||
<div className="date-time-selector">
|
||||
{showOldExplorerCTA && (
|
||||
<div style={{ marginRight: 12 }}>
|
||||
<NewExplorerCTA />
|
||||
</div>
|
||||
)}
|
||||
{!hasSelectedTimeError && !refreshButtonHidden && (
|
||||
<RefreshText
|
||||
{...{
|
||||
@ -646,10 +653,12 @@ function DateTimeSelection({
|
||||
interface DateTimeSelectionV2Props {
|
||||
showAutoRefresh: boolean;
|
||||
hideShareModal?: boolean;
|
||||
showOldExplorerCTA?: boolean;
|
||||
}
|
||||
|
||||
DateTimeSelection.defaultProps = {
|
||||
hideShareModal: false,
|
||||
showOldExplorerCTA: false,
|
||||
};
|
||||
interface DispatchProps {
|
||||
updateTimeInterval: (
|
||||
|
@ -41,6 +41,7 @@ export const useFetchKeysAndValues = (
|
||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [isAggregateFetching, setAggregateFetching] = useState<boolean>(false);
|
||||
|
||||
const memoizedSearchParams = useMemo(
|
||||
() => [
|
||||
@ -106,22 +107,29 @@ export const useFetchKeysAndValues = (
|
||||
if (!tagKey || !tagOperator) {
|
||||
return;
|
||||
}
|
||||
setAggregateFetching(true);
|
||||
|
||||
const { payload } = await getAttributesValues({
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: filterAttributeKey?.type ?? '',
|
||||
searchText: isInNInOperator(tagOperator)
|
||||
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
|
||||
: tagValue?.toString() ?? '',
|
||||
});
|
||||
try {
|
||||
const { payload } = await getAttributesValues({
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: filterAttributeKey?.type ?? '',
|
||||
searchText: isInNInOperator(tagOperator)
|
||||
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
|
||||
: tagValue?.toString() ?? '',
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
const values = Object.values(payload).find((el) => !!el) || [];
|
||||
setResults(values);
|
||||
if (payload) {
|
||||
const values = Object.values(payload).find((el) => !!el) || [];
|
||||
setResults(values);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setAggregateFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -157,7 +165,7 @@ export const useFetchKeysAndValues = (
|
||||
return {
|
||||
keys,
|
||||
results,
|
||||
isFetching,
|
||||
isFetching: isFetching || isAggregateFetching,
|
||||
sourceKeys,
|
||||
handleRemoveSourceKey,
|
||||
};
|
||||
|
143
frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx
Normal file
143
frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { Input, Slider } from 'antd';
|
||||
import { SliderRangeProps } from 'antd/es/slider';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { addFilter, FilterType, traceFilterKeys } from './filterUtils';
|
||||
|
||||
interface DurationProps {
|
||||
selectedFilters: FilterType | undefined;
|
||||
setSelectedFilters: Dispatch<SetStateAction<FilterType | undefined>>;
|
||||
}
|
||||
|
||||
export function DurationSection(props: DurationProps): JSX.Element {
|
||||
const { setSelectedFilters, selectedFilters } = props;
|
||||
|
||||
const getDuration = useMemo(() => {
|
||||
if (selectedFilters?.durationNanoMin || selectedFilters?.durationNanoMax) {
|
||||
return {
|
||||
minDuration: selectedFilters?.durationNanoMin?.values || '',
|
||||
maxDuration: selectedFilters?.durationNanoMax?.values || '',
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedFilters?.durationNano) {
|
||||
return {
|
||||
minDuration: getMs(selectedFilters?.durationNano?.values?.[0] || ''),
|
||||
maxDuration: getMs(selectedFilters?.durationNano?.values?.[1] || ''),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
maxDuration: '',
|
||||
minDuration: '',
|
||||
};
|
||||
}, [selectedFilters]);
|
||||
|
||||
const [preMax, setPreMax] = useState<string>('');
|
||||
const [preMin, setPreMin] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
setPreMax(getDuration.maxDuration as string);
|
||||
setPreMin(getDuration.minDuration as string);
|
||||
}, [getDuration]);
|
||||
|
||||
const updateDurationFilter = (min: string, max: string): void => {
|
||||
const durationMin = 'durationNanoMin';
|
||||
const durationMax = 'durationNanoMax';
|
||||
|
||||
addFilter(durationMin, min, setSelectedFilters, traceFilterKeys.durationNano);
|
||||
addFilter(durationMax, max, setSelectedFilters, traceFilterKeys.durationNano);
|
||||
};
|
||||
|
||||
const onRangeSliderHandler = (number: [string, string]): void => {
|
||||
const [min, max] = number;
|
||||
|
||||
setPreMin(min);
|
||||
setPreMax(max);
|
||||
};
|
||||
|
||||
const debouncedFunction = useDebouncedFn(
|
||||
(min, max) => {
|
||||
updateDurationFilter(min as string, max as string);
|
||||
},
|
||||
1500,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const onChangeMaxHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target;
|
||||
const min = preMin;
|
||||
const max = value;
|
||||
|
||||
onRangeSliderHandler([min, max]);
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onChangeMinHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target;
|
||||
const min = value;
|
||||
const max = preMax;
|
||||
|
||||
onRangeSliderHandler([min, max]);
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onRangeHandler: SliderRangeProps['onChange'] = ([min, max]) => {
|
||||
updateDurationFilter(min.toString(), max.toString());
|
||||
};
|
||||
|
||||
const TipComponent = useCallback((value: undefined | number) => {
|
||||
if (value === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
return <div>{`${value?.toString()}ms`}</div>;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="duration-inputs">
|
||||
<Input
|
||||
type="number"
|
||||
addonBefore="MIN"
|
||||
placeholder="0"
|
||||
className="min-max-input"
|
||||
onChange={onChangeMinHandler}
|
||||
value={preMin}
|
||||
addonAfter="ms"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
addonBefore="MAX"
|
||||
placeholder="100000000"
|
||||
className="min-max-input"
|
||||
onChange={onChangeMaxHandler}
|
||||
value={preMax}
|
||||
addonAfter="ms"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100000}
|
||||
range
|
||||
tooltip={{ formatter: TipComponent }}
|
||||
onChange={([min, max]): void => {
|
||||
onRangeSliderHandler([String(min), String(max)]);
|
||||
}}
|
||||
onAfterChange={onRangeHandler}
|
||||
value={[Number(preMin), Number(preMax)]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
190
frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss
Normal file
190
frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss
Normal file
@ -0,0 +1,190 @@
|
||||
.collapseContainer {
|
||||
background-color: var(--bg-ink-500);
|
||||
.ant-collapse-header {
|
||||
padding: 12px !important;
|
||||
align-items: center !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;
|
||||
|
||||
.submenu-checkbox {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.hasError-Error {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-cherry-500);
|
||||
}
|
||||
|
||||
.hasError-Ok {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-forest-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lightMode {
|
||||
.collapseContainer {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
247
frontend/src/pages/TracesExplorer/Filter/Filter.tsx
Normal file
247
frontend/src/pages/TracesExplorer/Filter/Filter.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import './Filter.styles.scss';
|
||||
|
||||
import {
|
||||
FilterOutlined,
|
||||
SyncOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Flex, Tooltip, Typography } from 'antd';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isArray, isEqual } from 'lodash-es';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
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';
|
||||
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
AllTraceFilterKeyValue,
|
||||
AllTraceFilterOptions,
|
||||
FilterType,
|
||||
HandleRunProps,
|
||||
unionTagFilterItems,
|
||||
} from './filterUtils';
|
||||
import { Section } from './Section';
|
||||
|
||||
interface FilterProps {
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export function Filter(props: FilterProps): JSX.Element {
|
||||
const { setOpen } = props;
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>
|
||||
>();
|
||||
|
||||
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 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[]),
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData, selectedFilters],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleRun();
|
||||
}, [selectedFilters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" className="filter-header">
|
||||
<Flex gap={8} align="center">
|
||||
<div className="filter-title">
|
||||
<FilterOutlined />
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
</div>
|
||||
<Tooltip title="Reset" placement="right">
|
||||
<Button
|
||||
onClick={(): void => handleRun({ resetAll: true })}
|
||||
className="sync-icon"
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tooltip title="Collapse" placement="right">
|
||||
<Button onClick={(): void => setOpen(false)} className="arrow-icon">
|
||||
<VerticalAlignTopOutlined rotate={270} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<>
|
||||
{AllTraceFilterOptions.filter(
|
||||
(i) => i !== 'durationNanoMax' && i !== 'durationNanoMin',
|
||||
).map((panelName) => (
|
||||
<Section
|
||||
key={panelName}
|
||||
panelName={panelName}
|
||||
selectedFilters={selectedFilters}
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
81
frontend/src/pages/TracesExplorer/Filter/Section.tsx
Normal file
81
frontend/src/pages/TracesExplorer/Filter/Section.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import './Filter.styles.scss';
|
||||
|
||||
import { Button, Collapse, Divider } from 'antd';
|
||||
import { Dispatch, MouseEvent, SetStateAction } from 'react';
|
||||
|
||||
import { DurationSection } from './DurationSection';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
AllTraceFilterKeyValue,
|
||||
FilterType,
|
||||
HandleRunProps,
|
||||
} from './filterUtils';
|
||||
import { SectionBody } from './SectionContent';
|
||||
|
||||
interface SectionProps {
|
||||
panelName: AllTraceFilterKeys;
|
||||
selectedFilters: FilterType | undefined;
|
||||
setSelectedFilters: Dispatch<SetStateAction<FilterType | undefined>>;
|
||||
handleRun: (props?: HandleRunProps) => void;
|
||||
}
|
||||
export function Section(props: SectionProps): JSX.Element {
|
||||
const { panelName, setSelectedFilters, selectedFilters, handleRun } = props;
|
||||
|
||||
const onClearHandler = (e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
selectedFilters?.[panelName] ||
|
||||
selectedFilters?.durationNanoMin ||
|
||||
selectedFilters?.durationNanoMax
|
||||
) {
|
||||
handleRun({ clearByType: panelName });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Divider plain className="divider" />
|
||||
<div className="section-body-header">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
className="collapseContainer"
|
||||
defaultActiveKey={
|
||||
['hasError', 'durationNano', 'serviceName'].includes(panelName)
|
||||
? panelName
|
||||
: undefined
|
||||
}
|
||||
items={[
|
||||
panelName === 'durationNano'
|
||||
? {
|
||||
key: panelName,
|
||||
children: (
|
||||
<DurationSection
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
selectedFilters={selectedFilters}
|
||||
/>
|
||||
),
|
||||
label: AllTraceFilterKeyValue[panelName],
|
||||
}
|
||||
: {
|
||||
key: panelName,
|
||||
children: (
|
||||
<SectionBody
|
||||
type={panelName}
|
||||
selectedFilters={selectedFilters}
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
),
|
||||
label: AllTraceFilterKeyValue[panelName],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button type="link" onClick={onClearHandler}>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
161
frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx
Normal file
161
frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import './Filter.styles.scss';
|
||||
|
||||
import { Button, Card, Checkbox, Input, Tooltip } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { ParaGraph } from 'container/Trace/Filters/Panel/PanelBody/Common/styles';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { defaultTo, isEmpty } from 'lodash-es';
|
||||
import {
|
||||
ChangeEvent,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
addFilter,
|
||||
AllTraceFilterKeys,
|
||||
FilterType,
|
||||
HandleRunProps,
|
||||
removeFilter,
|
||||
statusFilterOption,
|
||||
useGetAggregateValues,
|
||||
} from './filterUtils';
|
||||
|
||||
interface SectionBodyProps {
|
||||
type: AllTraceFilterKeys;
|
||||
selectedFilters: FilterType | undefined;
|
||||
setSelectedFilters: Dispatch<SetStateAction<FilterType | undefined>>;
|
||||
handleRun: (props?: HandleRunProps) => void;
|
||||
}
|
||||
|
||||
export function SectionBody(props: SectionBodyProps): JSX.Element {
|
||||
const { type, setSelectedFilters, selectedFilters, handleRun } = props;
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState(10);
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
const [checkedItems, setCheckedItems] = useState<string[]>(
|
||||
defaultTo(selectedFilters?.[type]?.values as string[], []),
|
||||
);
|
||||
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [isFetching, setFetching] = useState<boolean>(false);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setCheckedItems(defaultTo(selectedFilters?.[type]?.values as string[], [])),
|
||||
[selectedFilters, type],
|
||||
);
|
||||
const debouncedSearchText = useDebounce(searchFilter, DEBOUNCE_DELAY);
|
||||
|
||||
const { isFetching: fetching, keys, results: res } = useGetAggregateValues({
|
||||
value: type,
|
||||
searchText: debouncedSearchText,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setResults(res);
|
||||
setFetching(fetching);
|
||||
}, [fetching, res]);
|
||||
|
||||
const handleShowMore = (): void => {
|
||||
setVisibleItemsCount((prevCount) => prevCount + 10);
|
||||
};
|
||||
|
||||
const listData = useMemo(
|
||||
() =>
|
||||
(type === 'hasError' ? statusFilterOption : results)
|
||||
.filter((i) => i.length)
|
||||
.filter((filter) => {
|
||||
if (searchFilter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return filter
|
||||
.toLocaleLowerCase()
|
||||
.includes(searchFilter.toLocaleLowerCase());
|
||||
})
|
||||
.slice(0, visibleItemsCount),
|
||||
[results, searchFilter, type, visibleItemsCount],
|
||||
);
|
||||
|
||||
const onCheckHandler = (event: CheckboxChangeEvent, value: string): void => {
|
||||
const { checked } = event.target;
|
||||
let newValue = value;
|
||||
if (type === 'hasError') {
|
||||
newValue = String(value === 'Error');
|
||||
}
|
||||
if (checked) {
|
||||
addFilter(type, newValue, setSelectedFilters, keys);
|
||||
setCheckedItems((prev) => {
|
||||
if (!prev.includes(newValue)) {
|
||||
prev.push(newValue);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
} else if (checkedItems.length === 1) {
|
||||
handleRun({ clearByType: type });
|
||||
setCheckedItems([]);
|
||||
} else {
|
||||
removeFilter(type, newValue, setSelectedFilters, keys);
|
||||
setCheckedItems((prev) => prev.filter((item) => item !== newValue));
|
||||
}
|
||||
};
|
||||
|
||||
const checkboxMatcher = (item: string): boolean =>
|
||||
checkedItems?.includes(type === 'hasError' ? String(item === 'Error') : item);
|
||||
|
||||
const labelClassname = (item: string): string => `${type}-${item}`;
|
||||
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = e.target.value;
|
||||
setSearchFilter(inputValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
className="section-card"
|
||||
loading={type === 'hasError' ? false : isFetching}
|
||||
key={type}
|
||||
>
|
||||
<>
|
||||
<Input.Search
|
||||
value={searchFilter}
|
||||
onChange={handleSearch}
|
||||
placeholder="Filter Values"
|
||||
className="search-input"
|
||||
/>
|
||||
{listData.length === 0 && isEmpty(searchFilter) ? (
|
||||
<div style={{ padding: '8px 18px' }}>No data found</div>
|
||||
) : (
|
||||
<>
|
||||
{listData.map((item) => (
|
||||
<Checkbox
|
||||
className="submenu-checkbox"
|
||||
key={`${type}-${item}`}
|
||||
onChange={(e): void => onCheckHandler(e, item)}
|
||||
checked={checkboxMatcher(item)}
|
||||
>
|
||||
<div className="checkbox-label">
|
||||
<div className={labelClassname(item)} />
|
||||
<Tooltip overlay={<div>{item}</div>} placement="rightTop">
|
||||
<ParaGraph ellipsis style={{ maxWidth: 200 }}>
|
||||
{item}
|
||||
</ParaGraph>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Checkbox>
|
||||
))}
|
||||
{visibleItemsCount < results.length && (
|
||||
<Button onClick={handleShowMore} type="link">
|
||||
Show More
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Card>
|
||||
);
|
||||
}
|
347
frontend/src/pages/TracesExplorer/Filter/filterUtils.ts
Normal file
347
frontend/src/pages/TracesExplorer/Filter/filterUtils.ts
Normal file
@ -0,0 +1,347 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { isArray } from 'lodash-es';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const AllTraceFilterKeyValue = {
|
||||
durationNanoMin: 'Duration',
|
||||
durationNano: 'Duration',
|
||||
durationNanoMax: 'Duration',
|
||||
hasError: 'Status',
|
||||
serviceName: 'Service Name',
|
||||
name: 'Operation / Name',
|
||||
rpcMethod: 'RPC Method',
|
||||
responseStatusCode: 'Status Code',
|
||||
httpHost: 'HTTP Host',
|
||||
httpMethod: 'HTTP Method',
|
||||
httpRoute: 'HTTP Route',
|
||||
httpUrl: 'HTTP URL',
|
||||
traceID: 'Trace ID',
|
||||
};
|
||||
|
||||
export type AllTraceFilterKeys = keyof typeof AllTraceFilterKeyValue;
|
||||
|
||||
// Type for the values of AllTraceFilterKeyValue
|
||||
export type AllTraceFilterValues = typeof AllTraceFilterKeyValue[AllTraceFilterKeys];
|
||||
|
||||
export const AllTraceFilterOptions = Object.keys(
|
||||
AllTraceFilterKeyValue,
|
||||
) as (keyof typeof AllTraceFilterKeyValue)[];
|
||||
|
||||
export const statusFilterOption = ['Error', 'Ok'];
|
||||
|
||||
export type FilterType = Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>;
|
||||
|
||||
export const addFilter = (
|
||||
filterType: AllTraceFilterKeys,
|
||||
value: string,
|
||||
setSelectedFilters: Dispatch<
|
||||
SetStateAction<
|
||||
| Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>
|
||||
| undefined
|
||||
>
|
||||
>,
|
||||
keys?: BaseAutocompleteData,
|
||||
): void => {
|
||||
setSelectedFilters((prevFilters) => {
|
||||
const isDuration = [
|
||||
'durationNanoMax',
|
||||
'durationNanoMin',
|
||||
'durationNano',
|
||||
].includes(filterType);
|
||||
|
||||
// If previous filters are undefined, initialize them
|
||||
if (!prevFilters) {
|
||||
return ({
|
||||
[filterType]: { values: isDuration ? value : [value], keys },
|
||||
} as unknown) as FilterType;
|
||||
}
|
||||
// If the filter type doesn't exist, initialize it
|
||||
if (!prevFilters[filterType]?.values.length) {
|
||||
return {
|
||||
...prevFilters,
|
||||
[filterType]: { values: isDuration ? value : [value], keys },
|
||||
};
|
||||
}
|
||||
// If the value already exists, don't add it again
|
||||
if (prevFilters[filterType].values.includes(value)) {
|
||||
return prevFilters;
|
||||
}
|
||||
// Otherwise, add the value to the existing array
|
||||
return {
|
||||
...prevFilters,
|
||||
[filterType]: {
|
||||
values: isDuration ? value : [...prevFilters[filterType].values, value],
|
||||
keys,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Function to remove a filter
|
||||
export const removeFilter = (
|
||||
filterType: AllTraceFilterKeys,
|
||||
value: string,
|
||||
setSelectedFilters: Dispatch<
|
||||
SetStateAction<
|
||||
| Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>
|
||||
| undefined
|
||||
>
|
||||
>,
|
||||
keys?: BaseAutocompleteData,
|
||||
): void => {
|
||||
setSelectedFilters((prevFilters) => {
|
||||
if (!prevFilters || !prevFilters[filterType]?.values.length) {
|
||||
return prevFilters;
|
||||
}
|
||||
|
||||
const prevValue = prevFilters[filterType]?.values;
|
||||
const updatedValues = !isArray(prevValue)
|
||||
? prevValue
|
||||
: prevValue?.filter((item: any) => item !== value);
|
||||
|
||||
if (updatedValues.length === 0) {
|
||||
const { [filterType]: item, ...remainingFilters } = prevFilters;
|
||||
return Object.keys(remainingFilters).length > 0
|
||||
? (remainingFilters as FilterType)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...prevFilters,
|
||||
[filterType]: { values: updatedValues, keys },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const removeAllFilters = (
|
||||
filterType: AllTraceFilterKeys,
|
||||
setSelectedFilters: Dispatch<
|
||||
SetStateAction<
|
||||
| Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[]; keys: BaseAutocompleteData }
|
||||
>
|
||||
| undefined
|
||||
>
|
||||
>,
|
||||
): void => {
|
||||
setSelectedFilters((prevFilters) => {
|
||||
if (!prevFilters || !prevFilters[filterType]) {
|
||||
return prevFilters;
|
||||
}
|
||||
|
||||
const { [filterType]: item, ...remainingFilters } = prevFilters;
|
||||
|
||||
return Object.keys(remainingFilters).length > 0
|
||||
? (remainingFilters as Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[]; keys: BaseAutocompleteData }
|
||||
>)
|
||||
: undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export const traceFilterKeys: Record<
|
||||
AllTraceFilterKeys,
|
||||
BaseAutocompleteData
|
||||
> = {
|
||||
durationNano: {
|
||||
key: 'durationNano',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
},
|
||||
hasError: {
|
||||
key: 'hasError',
|
||||
dataType: DataTypes.bool,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'hasError--bool--tag--true',
|
||||
},
|
||||
serviceName: {
|
||||
key: 'serviceName',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
},
|
||||
name: {
|
||||
key: 'name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
},
|
||||
rpcMethod: {
|
||||
key: 'rpcMethod',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'rpcMethod--string--tag--true',
|
||||
},
|
||||
responseStatusCode: {
|
||||
key: 'responseStatusCode',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
},
|
||||
httpHost: {
|
||||
key: 'httpHost',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpHost--string--tag--true',
|
||||
},
|
||||
httpMethod: {
|
||||
key: 'httpMethod',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
},
|
||||
httpRoute: {
|
||||
key: 'httpRoute',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpRoute--string--tag--true',
|
||||
},
|
||||
httpUrl: {
|
||||
key: 'httpUrl',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpUrl--string--tag--true',
|
||||
},
|
||||
traceID: {
|
||||
key: 'traceID',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'traceID--string--tag--true',
|
||||
},
|
||||
durationNanoMin: {
|
||||
key: 'durationNanoMin',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNanoMin--float64--tag--true',
|
||||
},
|
||||
durationNanoMax: {
|
||||
key: 'durationNanoMax',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNanoMax--float64--tag--true',
|
||||
},
|
||||
};
|
||||
|
||||
interface AggregateValuesProps {
|
||||
value: AllTraceFilterKeys;
|
||||
searchText?: string;
|
||||
}
|
||||
|
||||
type IuseGetAggregateValue = {
|
||||
keys: BaseAutocompleteData;
|
||||
results: string[];
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
export function useGetAggregateValues(
|
||||
props: AggregateValuesProps,
|
||||
): IuseGetAggregateValue {
|
||||
const { value, searchText } = props;
|
||||
const keyData = traceFilterKeys[value];
|
||||
const [isFetching, setFetching] = useState<boolean>(true);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
|
||||
const getValues = async (): Promise<void> => {
|
||||
try {
|
||||
setResults([]);
|
||||
const { payload } = await getAttributesValues({
|
||||
aggregateOperator: 'noop',
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateAttribute: '',
|
||||
attributeKey: value,
|
||||
filterAttributeKeyDataType: keyData ? keyData.dataType : DataTypes.EMPTY,
|
||||
tagType: keyData.type ?? '',
|
||||
searchText: searchText ?? '',
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
const values = Object.values(payload).find((el) => !!el) || [];
|
||||
setResults(values);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getValues();
|
||||
}, [searchText]);
|
||||
|
||||
if (!value) {
|
||||
setFetching(false);
|
||||
return { keys: keyData, results, isFetching };
|
||||
}
|
||||
|
||||
return { keys: keyData, results, isFetching };
|
||||
}
|
||||
|
||||
export function unionTagFilterItems(
|
||||
items1: TagFilterItem[],
|
||||
items2: TagFilterItem[],
|
||||
): TagFilterItem[] {
|
||||
const unionMap = new Map<string, TagFilterItem>();
|
||||
|
||||
items1.forEach((item) => {
|
||||
const keyOp = `${item?.key?.key}_${item.op}`;
|
||||
unionMap.set(keyOp, item);
|
||||
});
|
||||
|
||||
items2.forEach((item) => {
|
||||
const keyOp = `${item?.key?.key}_${item.op}`;
|
||||
unionMap.set(keyOp, item);
|
||||
});
|
||||
|
||||
return Array.from(unionMap.values());
|
||||
}
|
||||
|
||||
export interface HandleRunProps {
|
||||
resetAll?: boolean;
|
||||
clearByType?: AllTraceFilterKeys;
|
||||
}
|
@ -1,9 +1,27 @@
|
||||
.trace-explorer-run-query {
|
||||
.trace-explorer-header {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin: 8px 16px;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
|
||||
.trace-explorer-run-query {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin: 8px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-outlined-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border-top: 1px solid var(--bg-slate-400);
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer-header.single-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.traces-explorer-views {
|
||||
@ -11,3 +29,54 @@
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer-page {
|
||||
display: flex;
|
||||
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
|
||||
border-right: 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background-color: var(--bg-ink-500);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-500);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
border-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.trace-explorer-page {
|
||||
.filter {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background-color: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
border-color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import './TracesExplorer.styles.scss';
|
||||
|
||||
import { Tabs } from 'antd';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Tabs, Tooltip } from 'antd';
|
||||
import axios from 'axios';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
@ -19,13 +20,14 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
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';
|
||||
|
||||
@ -180,42 +182,60 @@ function TracesExplorer(): JSX.Element {
|
||||
handleExplorerTabChange,
|
||||
currentPanelType,
|
||||
]);
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<>
|
||||
<div className="trace-explorer-run-query">
|
||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<QuerySection />
|
||||
</ExplorerCard>
|
||||
<div className="trace-explorer-page">
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<Filter setOpen={setOpen} />
|
||||
</Card>
|
||||
<Card className="trace-explorer">
|
||||
<div className={`trace-explorer-header ${isOpen ? 'single-child' : ''}`}>
|
||||
{!isOpen && (
|
||||
<Tooltip title="Expand filters" placement="right">
|
||||
<Button
|
||||
onClick={(): void => setOpen(!isOpen)}
|
||||
className="filter-outlined-btn"
|
||||
>
|
||||
<FilterOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="trace-explorer-run-query">
|
||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||
<DateTimeSelector showAutoRefresh showOldExplorerCTA />
|
||||
</div>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<QuerySection />
|
||||
</ExplorerCard>
|
||||
|
||||
<Container className="traces-explorer-views">
|
||||
<ActionsWrapper>
|
||||
<ExportPanel
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
onExport={handleExport}
|
||||
<Container className="traces-explorer-views">
|
||||
<ActionsWrapper>
|
||||
<ExportPanel
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</ActionsWrapper>
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
items={tabsItems}
|
||||
onChange={handleExplorerTabChange}
|
||||
/>
|
||||
</ActionsWrapper>
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
items={tabsItems}
|
||||
onChange={handleExplorerTabChange}
|
||||
</Container>
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
sourcepage={DataSource.TRACES}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</Container>
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
sourcepage={DataSource.TRACES}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</>
|
||||
</Card>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user