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:
SagarRajput-7 2024-06-04 14:03:49 +05:30 committed by GitHub
parent 1ce36c8344
commit 9733612be8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1338 additions and 55 deletions

View File

@ -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',
};

View File

@ -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;
}

View File

@ -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 =

View File

@ -72,7 +72,7 @@ const menuItems: SidebarItem[] = [
icon: <BarChart2 size={16} />,
},
{
key: ROUTES.TRACE,
key: ROUTES.TRACES_EXPLORER,
label: 'Traces',
icon: <DraftingCompass size={16} />,
},

View File

@ -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: (

View File

@ -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,
};

View 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>
);
}

View 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);
}
}

View 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}
/>
))}
</>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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;
}

View File

@ -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);
}
}
}

View File

@ -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>
);
}