mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-01 01:14:03 +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.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
|
||||||
[ROUTES.TRACE]: 'Try new Traces Explorer',
|
[ROUTES.TRACE]: 'Try new Traces Explorer',
|
||||||
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs 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.LOGS_EXPLORER ||
|
||||||
location.pathname === ROUTES.TRACE ||
|
location.pathname === ROUTES.TRACE ||
|
||||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER,
|
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||||
|
location.pathname === ROUTES.TRACES_EXPLORER,
|
||||||
[location.pathname],
|
[location.pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ function NewExplorerCTA(): JSX.Element | null {
|
|||||||
history.push(ROUTES.TRACES_EXPLORER);
|
history.push(ROUTES.TRACES_EXPLORER);
|
||||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||||
history.push(ROUTES.LOGS_EXPLORER);
|
history.push(ROUTES.LOGS_EXPLORER);
|
||||||
|
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||||
|
history.push(ROUTES.TRACE);
|
||||||
}
|
}
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
@ -47,6 +50,10 @@ function NewExplorerCTA(): JSX.Element | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ function QueryBuilderSearch({
|
|||||||
const { tagKey, tagOperator, tagValue } = getTagToken(tag);
|
const { tagKey, tagOperator, tagValue } = getTagToken(tag);
|
||||||
|
|
||||||
const filterAttribute = [...initialSourceKeys, ...sourceKeys].find(
|
const filterAttribute = [...initialSourceKeys, ...sourceKeys].find(
|
||||||
(key) => key.key === getRemovePrefixFromKey(tagKey),
|
(key) => key?.key === getRemovePrefixFromKey(tagKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
const computedTagValue =
|
const computedTagValue =
|
||||||
|
@ -72,7 +72,7 @@ const menuItems: SidebarItem[] = [
|
|||||||
icon: <BarChart2 size={16} />,
|
icon: <BarChart2 size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: ROUTES.TRACE,
|
key: ROUTES.TRACES_EXPLORER,
|
||||||
label: 'Traces',
|
label: 'Traces',
|
||||||
icon: <DraftingCompass size={16} />,
|
icon: <DraftingCompass size={16} />,
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
defaultLiveQueryDataConfig,
|
defaultLiveQueryDataConfig,
|
||||||
} from 'container/LiveLogs/constants';
|
} from 'container/LiveLogs/constants';
|
||||||
import { QueryHistoryState } from 'container/LiveLogs/types';
|
import { QueryHistoryState } from 'container/LiveLogs/types';
|
||||||
|
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
@ -63,6 +64,7 @@ function DateTimeSelection({
|
|||||||
location,
|
location,
|
||||||
updateTimeInterval,
|
updateTimeInterval,
|
||||||
globalTimeLoading,
|
globalTimeLoading,
|
||||||
|
showOldExplorerCTA = false,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [formSelector] = Form.useForm();
|
const [formSelector] = Form.useForm();
|
||||||
|
|
||||||
@ -561,6 +563,11 @@ function DateTimeSelection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="date-time-selector">
|
<div className="date-time-selector">
|
||||||
|
{showOldExplorerCTA && (
|
||||||
|
<div style={{ marginRight: 12 }}>
|
||||||
|
<NewExplorerCTA />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!hasSelectedTimeError && !refreshButtonHidden && (
|
{!hasSelectedTimeError && !refreshButtonHidden && (
|
||||||
<RefreshText
|
<RefreshText
|
||||||
{...{
|
{...{
|
||||||
@ -646,10 +653,12 @@ function DateTimeSelection({
|
|||||||
interface DateTimeSelectionV2Props {
|
interface DateTimeSelectionV2Props {
|
||||||
showAutoRefresh: boolean;
|
showAutoRefresh: boolean;
|
||||||
hideShareModal?: boolean;
|
hideShareModal?: boolean;
|
||||||
|
showOldExplorerCTA?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTimeSelection.defaultProps = {
|
DateTimeSelection.defaultProps = {
|
||||||
hideShareModal: false,
|
hideShareModal: false,
|
||||||
|
showOldExplorerCTA: false,
|
||||||
};
|
};
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
updateTimeInterval: (
|
updateTimeInterval: (
|
||||||
|
@ -41,6 +41,7 @@ export const useFetchKeysAndValues = (
|
|||||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||||
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
|
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
|
||||||
const [results, setResults] = useState<string[]>([]);
|
const [results, setResults] = useState<string[]>([]);
|
||||||
|
const [isAggregateFetching, setAggregateFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
const memoizedSearchParams = useMemo(
|
const memoizedSearchParams = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -106,22 +107,29 @@ export const useFetchKeysAndValues = (
|
|||||||
if (!tagKey || !tagOperator) {
|
if (!tagKey || !tagOperator) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setAggregateFetching(true);
|
||||||
|
|
||||||
const { payload } = await getAttributesValues({
|
try {
|
||||||
aggregateOperator: query.aggregateOperator,
|
const { payload } = await getAttributesValues({
|
||||||
dataSource: query.dataSource,
|
aggregateOperator: query.aggregateOperator,
|
||||||
aggregateAttribute: query.aggregateAttribute.key,
|
dataSource: query.dataSource,
|
||||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||||
tagType: filterAttributeKey?.type ?? '',
|
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||||
searchText: isInNInOperator(tagOperator)
|
tagType: filterAttributeKey?.type ?? '',
|
||||||
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
|
searchText: isInNInOperator(tagOperator)
|
||||||
: tagValue?.toString() ?? '',
|
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
|
||||||
});
|
: tagValue?.toString() ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
const values = Object.values(payload).find((el) => !!el) || [];
|
const values = Object.values(payload).find((el) => !!el) || [];
|
||||||
setResults(values);
|
setResults(values);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setAggregateFetching(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,7 +165,7 @@ export const useFetchKeysAndValues = (
|
|||||||
return {
|
return {
|
||||||
keys,
|
keys,
|
||||||
results,
|
results,
|
||||||
isFetching,
|
isFetching: isFetching || isAggregateFetching,
|
||||||
sourceKeys,
|
sourceKeys,
|
||||||
handleRemoveSourceKey,
|
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;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
margin: 8px 16px;
|
.trace-explorer-run-query {
|
||||||
gap: 8px;
|
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 {
|
.traces-explorer-views {
|
||||||
@ -11,3 +29,54 @@
|
|||||||
padding: 0 8px;
|
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 './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 axios from 'axios';
|
||||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
@ -19,13 +20,14 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
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 { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
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';
|
||||||
|
|
||||||
@ -180,42 +182,60 @@ function TracesExplorer(): JSX.Element {
|
|||||||
handleExplorerTabChange,
|
handleExplorerTabChange,
|
||||||
currentPanelType,
|
currentPanelType,
|
||||||
]);
|
]);
|
||||||
|
const [isOpen, setOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<>
|
<div className="trace-explorer-page">
|
||||||
<div className="trace-explorer-run-query">
|
<Card className="filter" hidden={!isOpen}>
|
||||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
<Filter setOpen={setOpen} />
|
||||||
<DateTimeSelector showAutoRefresh />
|
</Card>
|
||||||
</div>
|
<Card className="trace-explorer">
|
||||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
<div className={`trace-explorer-header ${isOpen ? 'single-child' : ''}`}>
|
||||||
<QuerySection />
|
{!isOpen && (
|
||||||
</ExplorerCard>
|
<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">
|
<Container className="traces-explorer-views">
|
||||||
<ActionsWrapper>
|
<ActionsWrapper>
|
||||||
<ExportPanel
|
<ExportPanel
|
||||||
query={exportDefaultQuery}
|
query={exportDefaultQuery}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
|
/>
|
||||||
|
</ActionsWrapper>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey={currentTab}
|
||||||
|
activeKey={currentTab}
|
||||||
|
items={tabsItems}
|
||||||
|
onChange={handleExplorerTabChange}
|
||||||
/>
|
/>
|
||||||
</ActionsWrapper>
|
</Container>
|
||||||
|
<ExplorerOptionWrapper
|
||||||
<Tabs
|
disabled={!stagedQuery}
|
||||||
defaultActiveKey={currentTab}
|
query={exportDefaultQuery}
|
||||||
activeKey={currentTab}
|
isLoading={isLoading}
|
||||||
items={tabsItems}
|
sourcepage={DataSource.TRACES}
|
||||||
onChange={handleExplorerTabChange}
|
onExport={handleExport}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Card>
|
||||||
<ExplorerOptionWrapper
|
</div>
|
||||||
disabled={!stagedQuery}
|
|
||||||
query={exportDefaultQuery}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sourcepage={DataSource.TRACES}
|
|
||||||
onExport={handleExport}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user