Compare commits

...

22 Commits

Author SHA1 Message Date
Ishan Uniyal
60425bf807 chore: meter as default signalSource 2026-02-23 13:05:58 +05:30
Ishan Uniyal
7b3502b3e8 feat: testcase 2026-02-23 06:19:05 +05:30
Ishan Uniyal
2ec5dc4518 feat: test case 2026-02-22 18:07:31 +05:30
Ishan Uniyal
069af59d75 feat: query search optimise 2026-02-22 17:50:16 +05:30
Ishan Uniyal
c755999d6d feat: query search optimise 2026-02-22 17:44:42 +05:30
Ishan Uniyal
54d58b3958 feat: unchecked show more and tooltip for adaptive 2026-02-22 17:23:22 +05:30
Srikanth Chekuri
8c736a7b51 Merge branch 'main' into SIG_8931 2026-02-20 08:03:40 +05:30
Ishan Uniyal
ed77517150 feat: value suggestion optimised 2026-02-19 19:27:14 +05:30
Ishan Uniyal
761a78f59c feat: key suggestion made common 2026-02-18 06:43:35 +05:30
Ishan
94525c538e Merge branch 'main' into SIG_8931 2026-02-17 12:51:55 +05:30
Ishan Uniyal
0327af3be4 feat: relatedValues and all values together with unchecked filter handling 2026-02-17 12:50:18 +05:30
Ishan
2ed43caf79 Merge branch 'main' into SIG_8931 2026-02-13 12:48:38 +05:30
Ishan Uniyal
939dfa8fc6 feat: revert env.ts 2026-01-30 15:03:22 +05:30
Ishan Uniyal
3c86c02a58 feat: merge conflict 2026-01-30 15:02:13 +05:30
Ishan
b1045bb1e0 Merge branch 'main' into SIG_8931 2026-01-30 14:46:36 +05:30
Ishan Uniyal
2c0ab52806 feat: pr comments signal source 2026-01-30 11:01:28 +05:30
Ishan Uniyal
dc512ab14c feat: code cleanup 2026-01-30 11:01:28 +05:30
Ishan Uniyal
cf80979dfc feat: css updated 2026-01-29 15:57:30 +05:30
Ishan Uniyal
1aae409931 feat: checkbox jitter updated 2026-01-29 15:57:30 +05:30
Ishan Uniyal
c160cf9b4e feat: testcases updated 2026-01-29 15:57:30 +05:30
Ishan Uniyal
799cd4bbca chore: removed env.ts 2026-01-29 15:57:30 +05:30
Ishan Uniyal
8b5a881641 feat: api migration and related values feature 2026-01-29 15:57:30 +05:30
14 changed files with 601 additions and 139 deletions

View File

@@ -1,5 +1,6 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import store from 'store';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
@@ -17,6 +18,12 @@ export const getKeySuggestions = (
signalSource = '',
} = props;
const { globalTime } = store.getState();
const resolvedTimeRange = {
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
};
const encodedSignal = encodeURIComponent(signal);
const encodedSearchText = encodeURIComponent(searchText);
const encodedMetricName = encodeURIComponent(metricName);
@@ -24,7 +31,14 @@ export const getKeySuggestions = (
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
let url = `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`;
if (resolvedTimeRange.startUnixMilli !== undefined) {
url += `&startUnixMilli=${resolvedTimeRange.startUnixMilli}`;
}
if (resolvedTimeRange.endUnixMilli !== undefined) {
url += `&endUnixMilli=${resolvedTimeRange.endUnixMilli}`;
}
return axios.get(url);
};

View File

@@ -1,5 +1,6 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import store from 'store';
import {
QueryKeyValueRequestProps,
QueryKeyValueSuggestionsResponseProps,
@@ -8,7 +9,20 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText, signalSource, metricName } = props;
const {
signal,
key,
searchText,
signalSource,
metricName,
existingQuery,
} = props;
const { globalTime } = store.getState();
const resolvedTimeRange = {
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
};
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
@@ -16,7 +30,17 @@ export const getValueSuggestions = (
const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
);
let url = `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`;
if (resolvedTimeRange.startUnixMilli !== undefined) {
url += `&startUnixMilli=${resolvedTimeRange.startUnixMilli}`;
}
if (resolvedTimeRange.endUnixMilli !== undefined) {
url += `&endUnixMilli=${resolvedTimeRange.endUnixMilli}`;
}
if (existingQuery) {
url += `&existingQuery=${encodeURIComponent(existingQuery)}`;
}
return axios.get(url);
};

View File

@@ -34,7 +34,6 @@ function ListViewOrderBy({
>([]);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Fetch key suggestions based on debounced input
const { data, isLoading } = useQuery({
queryKey: ['orderByKeySuggestions', dataSource, debouncedInput],
queryFn: async () => {

View File

@@ -271,7 +271,6 @@ function QuerySearch({
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
@@ -431,6 +430,7 @@ function QuerySearch({
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
const existingQuery = queryData.filter?.expression || '';
try {
const response = await getValueSuggestions({
@@ -439,9 +439,9 @@ function QuerySearch({
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
existingQuery,
}); // Skip updates if component unmounted or key changed
// Skip updates if component unmounted or key changed
if (
!isMountedRef.current ||
lastKeyRef.current !== key ||
@@ -453,7 +453,9 @@ function QuerySearch({
// Process the response data
const responseData = response.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const relatedValues = values.relatedValues || [];
const stringValues =
relatedValues.length > 0 ? relatedValues : values.stringValues || [];
const numberValues = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
@@ -528,11 +530,12 @@ function QuerySearch({
},
[
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
queryData.filter?.expression,
toggleSuggestions,
dataSource,
signalSource,
debouncedMetricName,
],
);
@@ -1249,19 +1252,17 @@ function QuerySearch({
if (!queryContext) {
return;
}
// Trigger suggestions based on context
if (editorRef.current) {
// Only trigger suggestions and fetch if editor is focused (i.e., user is interacting)
if (isFocused && editorRef.current) {
toggleSuggestions(10);
}
// Handle value suggestions for value context
if (queryContext.isInValue) {
const { keyToken, currentToken } = queryContext;
const key = keyToken || currentToken;
// Only fetch if needed and if we have a valid key
if (key && key !== activeKey && !isLoadingSuggestions) {
fetchValueSuggestions({ key });
// Handle value suggestions for value context
if (queryContext.isInValue) {
const { keyToken, currentToken } = queryContext;
const key = keyToken || currentToken;
// Only fetch if needed and if we have a valid key
if (key && key !== activeKey && !isLoadingSuggestions) {
fetchValueSuggestions({ key });
}
}
}
}, [
@@ -1270,6 +1271,7 @@ function QuerySearch({
isLoadingSuggestions,
activeKey,
fetchValueSuggestions,
isFocused,
]);
const getTooltipContent = (): JSX.Element => (

View File

@@ -48,7 +48,12 @@
.filter-separator {
height: 1px;
background-color: var(--bg-slate-400);
margin: 4px 0;
margin: 7px 0;
&.related-separator {
opacity: 0.5;
margin: 0.5px 0;
}
}
.value {
@@ -138,6 +143,93 @@
cursor: pointer;
}
}
.search-prompt {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 12px;
margin-top: 4px;
border: 1px dashed var(--bg-amber-500);
border-radius: 10px;
color: var(--bg-amber-200);
background: linear-gradient(
90deg,
var(--bg-ink-500) 0%,
var(--bg-ink-400) 100%
);
cursor: pointer;
transition: all 0.16s ease, transform 0.12s ease;
text-align: left;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
&:hover {
background: linear-gradient(
90deg,
var(--bg-ink-400) 0%,
var(--bg-ink-300) 100%
);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
}
&:active {
transform: translateY(1px);
}
&__icon {
color: var(--bg-amber-400);
flex-shrink: 0;
}
&__text {
display: flex;
flex-direction: column;
gap: 2px;
}
&__title {
color: var(--bg-amber-200);
}
&__subtitle {
color: var(--bg-amber-300);
font-size: 12px;
}
}
.lightMode & {
.search-prompt {
border: 1px dashed var(--bg-amber-500);
color: var(--bg-amber-800);
background: linear-gradient(
90deg,
var(--bg-vanilla-200) 0%,
var(--bg-vanilla-100) 100%
);
box-shadow: 0 2px 12px rgba(184, 107, 0, 0.08);
&:hover {
background: linear-gradient(
90deg,
var(--bg-vanilla-100) 0%,
var(--bg-vanilla-50) 100%
);
box-shadow: 0 4px 16px rgba(184, 107, 0, 0.15);
}
&__icon {
color: var(--bg-amber-600);
}
&__title {
color: var(--bg-amber-800);
}
&__subtitle {
color: var(--bg-amber-800);
}
}
}
.go-to-docs {
display: flex;
flex-direction: column;

View File

@@ -152,7 +152,7 @@ describe('CheckboxFilter - User Flows', () => {
expect(screen.getByText('Service Name')).toBeInTheDocument();
await waitFor(() => {
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
});
// User should see visual separator between checked and unchecked items
@@ -186,7 +186,7 @@ describe('CheckboxFilter - User Flows', () => {
// Initially auto-opened due to active filters
await waitFor(() => {
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
});
// User manually closes the filter
@@ -194,7 +194,7 @@ describe('CheckboxFilter - User Flows', () => {
// User should see filter is now closed (respecting user preference)
expect(
screen.queryByPlaceholderText('Filter values'),
screen.queryByPlaceholderText('Search values'),
).not.toBeInTheDocument();
// User manually opens the filter again
@@ -202,7 +202,7 @@ describe('CheckboxFilter - User Flows', () => {
// User should see filter is now open (respecting user preference)
await waitFor(() => {
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
});
});

View File

@@ -2,8 +2,15 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { Fragment, useMemo, useState } from 'react';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import {
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Button, Checkbox, Input, InputRef, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
@@ -11,19 +18,14 @@ import {
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { OPERATORS } from 'constants/antlrQueryConstants';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
@@ -60,6 +62,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// null = no user action, true = user opened, false = user closed
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
const [visibleUncheckedCount, setVisibleUncheckedCount] = useState<number>(5);
const {
lastUsedQuery,
@@ -81,6 +84,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
// Extract current filter expression for the active query
const currentFilterExpression = useMemo(() => {
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
return queryData?.filter?.expression || '';
}, [currentQuery.builder.queryData, activeQueryIndex]);
// Check if this filter has active filters in the query
const isSomeFilterPresentForCurrentAttribute = useMemo(
() =>
@@ -112,54 +121,125 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
filter.defaultOpen,
]);
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: filter.aggregateOperator || 'noop',
dataSource: filter.dataSource || DataSource.LOGS,
aggregateAttribute: filter.aggregateAttribute || '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
searchText: searchText ?? '',
},
{
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
);
const {
data: keyValueSuggestions,
isLoading: isLoadingKeyValueSuggestions,
refetch: refetchKeyValueSuggestions,
} = useGetQueryKeyValueSuggestions({
key: filter.attributeKey.key,
signal: filter.dataSource || DataSource.LOGS,
signalSource: 'meter',
searchText: searchText || '',
existingQuery: currentFilterExpression,
options: {
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
enabled: isOpen,
keepPreviousData: true,
},
});
const attributeValues: string[] = useMemo(() => {
const dataType = filter.attributeKey.dataType || DataTypes.String;
const searchInputRef = useRef<InputRef | null>(null);
const searchContainerRef = useRef<HTMLDivElement | null>(null);
const previousFiltersItemsRef = useRef(
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items,
);
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
// Process the response data
// Refetch when other filters change (not this filter)
// Watch for when filters.items is different from previous value, indicating other filters changed
useEffect(() => {
const currentFiltersItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
const previousFiltersItems = previousFiltersItemsRef.current;
// Check if filters items have changed (not the same)
const filtersChanged = !isEqual(previousFiltersItems, currentFiltersItems);
if (isOpen && filtersChanged) {
// Check if OTHER filters (not this filter) have changed
const currentOtherFilters = currentFiltersItems?.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
const previousOtherFilters = previousFiltersItems?.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
// Refetch if other filters changed (not just this filter's values)
const otherFiltersChanged = !isEqual(
currentOtherFilters,
previousOtherFilters,
);
// Only update ref if we have valid API data or if filters actually changed
// Don't update if search returned 0 results to preserve unchecked values
const hasValidData = keyValueSuggestions && !isLoadingKeyValueSuggestions;
if (otherFiltersChanged || hasValidData) {
previousFiltersItemsRef.current = currentFiltersItems;
}
if (otherFiltersChanged) {
refetchKeyValueSuggestions();
}
} else {
previousFiltersItemsRef.current = currentFiltersItems;
}
}, [
activeQueryIndex,
isOpen,
refetchKeyValueSuggestions,
filter.attributeKey.key,
currentQuery.builder.queryData,
keyValueSuggestions,
isLoadingKeyValueSuggestions,
]);
const handleSearchPromptClick = useCallback((): void => {
if (searchContainerRef.current) {
searchContainerRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
if (searchInputRef.current) {
setTimeout(() => searchInputRef.current?.focus({ cursor: 'end' }), 120);
}
}, []);
const isDataComplete = useMemo(() => {
if (keyValueSuggestions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseData = keyValueSuggestions?.data as any;
return responseData.data?.complete || false;
}
return false;
}, [keyValueSuggestions]);
const previousUncheckedValuesRef = useRef<string[]>([]);
const { attributeValues, relatedValuesSet } = useMemo(() => {
if (keyValueSuggestions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseData = keyValueSuggestions?.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
const relatedValues: string[] = values.relatedValues || [];
const stringValues: string[] = values.stringValues || [];
const numberValues: number[] = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
const stringOptions = stringValues
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
const valuesToUse = [
...relatedValues,
...stringValues.filter(
(value: string | null | undefined) =>
value !== null &&
value !== undefined &&
value !== '' &&
!relatedValues.includes(value),
),
];
const stringOptions = valuesToUse.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
// Generate options from number values
const numberOptions = numberValues
.filter(
(value: number | null | undefined): value is number =>
@@ -167,15 +247,27 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)
.map((value: number) => value.toString());
// Combine all options and make sure we don't have duplicate labels
return [...stringOptions, ...numberOptions];
}
const filteredRelated = new Set(
relatedValues.filter(
(v): v is string => v !== null && v !== undefined && v !== '',
),
);
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return (data?.payload?.[key] || []).filter(
(val) => val !== undefined && val !== null,
);
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
const baseValues = [...stringOptions, ...numberOptions];
const previousUnchecked = previousUncheckedValuesRef.current || [];
const preservedUnchecked = previousUnchecked.filter(
(value) => !baseValues.includes(value),
);
return {
attributeValues: [...baseValues, ...preservedUnchecked],
relatedValuesSet: filteredRelated,
};
}
return {
attributeValues: [] as string[],
relatedValuesSet: new Set<string>(),
};
}, [keyValueSuggestions]);
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
@@ -249,22 +341,51 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
// Sort checked items to the top, then unchecked items
const currentAttributeKeys = useMemo(() => {
// Sort checked items to the top; always show unchecked items beneath, regardless of pagination
const {
visibleCheckedValues,
uncheckedValues,
visibleUncheckedValues,
visibleCheckedCount,
hasMoreChecked,
hasMoreUnchecked,
checkedSeparatorIndex,
} = useMemo(() => {
const checkedValues = attributeValues.filter(
(val) => currentFilterState[val],
);
const uncheckedValues = attributeValues.filter(
(val) => !currentFilterState[val],
);
return [...checkedValues, ...uncheckedValues].slice(0, visibleItemsCount);
}, [attributeValues, currentFilterState, visibleItemsCount]);
const unchecked = attributeValues.filter((val) => !currentFilterState[val]);
const visibleChecked = checkedValues.slice(0, visibleItemsCount);
const visibleUnchecked = unchecked.slice(0, visibleUncheckedCount);
// Count of checked values in the currently visible items
const checkedValuesCount = useMemo(
() => currentAttributeKeys.filter((val) => currentFilterState[val]).length,
[currentAttributeKeys, currentFilterState],
);
const findSeparatorIndex = (list: string[]): number => {
if (relatedValuesSet.size === 0) {
return -1;
}
const firstNonRelated = list.findIndex((v) => !relatedValuesSet.has(v));
return firstNonRelated > 0 ? firstNonRelated : -1;
};
return {
visibleCheckedValues: visibleChecked,
uncheckedValues: unchecked,
visibleUncheckedValues: visibleUnchecked,
visibleCheckedCount: visibleChecked.length,
hasMoreChecked: checkedValues.length > visibleChecked.length,
hasMoreUnchecked: unchecked.length > visibleUnchecked.length,
checkedSeparatorIndex: findSeparatorIndex(visibleChecked),
};
}, [
attributeValues,
currentFilterState,
visibleItemsCount,
visibleUncheckedCount,
relatedValuesSet,
]);
useEffect(() => {
previousUncheckedValuesRef.current = uncheckedValues;
}, [uncheckedValues]);
const handleClearFilterAttribute = (): void => {
const preparedQuery: Query = {
@@ -305,6 +426,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
isOnlyOrAllClicked: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
setVisibleUncheckedCount(5);
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
// if only or all are clicked we do not need to worry about anything just override whatever we have
@@ -565,6 +687,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (isOpen) {
setUserToggleState(false);
setVisibleItemsCount(10);
setVisibleUncheckedCount(5);
} else {
setUserToggleState(true);
}
@@ -593,35 +716,93 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
</section>
</section>
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
{isOpen && isLoadingKeyValueSuggestions && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoadingKeyValueSuggestions && (
<>
{!isEmptyStateWithDocsEnabled && (
<section className="search">
<section className="search" ref={searchContainerRef}>
<Input
placeholder="Filter values"
placeholder="Search values"
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
ref={searchInputRef}
/>
</section>
)}
{attributeValues.length > 0 ? (
<section className="values">
{currentAttributeKeys.map((value: string, index: number) => (
{visibleCheckedValues.map((value: string, index: number) => (
<Fragment key={value}>
{index === checkedValuesCount && checkedValuesCount > 0 && (
<div
key="separator"
className="filter-separator"
data-testid="filter-separator"
/>
{index === checkedSeparatorIndex && (
<div className="filter-separator related-separator" />
)}
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
rootClassName="check-box"
/>
<div
className={cx(
'checkbox-value-section',
isFilterDisabled ? 'filter-disabled' : '',
)}
onClick={(): void => {
if (isFilterDisabled) {
return;
}
onChange(value, currentFilterState[value], true);
}}
>
<div className={`${filter.title} label-${value}`} />
{filter.customRendererForValue ? (
filter.customRendererForValue(value)
) : (
<Typography.Text
className="value-string"
ellipsis={{ tooltip: { placement: 'top' } }}
>
{String(value)}
</Typography.Text>
)}
<Button type="text" className="only-btn">
{isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
</div>
</Fragment>
))}
{hasMoreChecked && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
{visibleCheckedCount > 0 && uncheckedValues.length > 0 && (
<div className="filter-separator" data-testid="filter-separator" />
)}
{visibleUncheckedValues.map((value: string) => (
<Fragment key={value}>
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
@@ -673,6 +854,17 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</div>
</Fragment>
))}
{hasMoreUnchecked && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleUncheckedCount((prev) => prev + 5)}
>
Show More...
</Typography.Text>
</section>
)}
</section>
) : isEmptyStateWithDocsEnabled ? (
<LogsQuickFilterEmptyState attributeKey={filter.attributeKey.key} />
@@ -681,16 +873,18 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<Typography.Text>No values found</Typography.Text>{' '}
</section>
)}
{visibleItemsCount < attributeValues?.length && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
{visibleItemsCount >= attributeValues?.length &&
attributeValues?.length > 0 &&
!isDataComplete && (
<section className="search-prompt" onClick={handleSearchPromptClick}>
<AlertTriangle size={16} className="search-prompt__icon" />
<span className="search-prompt__text">
<Typography.Text className="search-prompt__subtitle">
Tap to search and load more suggestions.
</Typography.Text>
</span>
</section>
)}
</>
)}
</div>

View File

@@ -127,6 +127,34 @@
align-items: center;
padding: 8px;
}
.filters-info {
display: flex;
align-items: center;
padding: 6px 10px 0 10px;
color: var(--bg-vanilla-400);
gap: 6px;
flex-wrap: wrap;
.filters-info-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
height: auto;
color: var(--bg-vanilla-400);
&:hover {
color: var(--bg-robin-500);
}
}
.filters-info-text {
color: var(--bg-vanilla-400);
font-size: 13px;
line-height: 16px;
}
}
}
.perilin-bg {
@@ -180,5 +208,30 @@
}
}
}
.filters-info {
color: var(--bg-ink-400);
.filters-info-toggle {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-300);
}
}
.filters-info-text {
color: var(--bg-ink-400);
}
}
}
}
.filters-info-tooltip-title {
font-weight: var(--font-weight-bold);
margin-bottom: 4px;
}
.filters-info-tooltip-detail {
margin-top: 4px;
}

View File

@@ -23,7 +23,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { isFunction, isNull } from 'lodash-es';
import { Frown, Settings2 as SettingsIcon } from 'lucide-react';
import { Frown, Lightbulb, Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
@@ -291,6 +291,27 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
</div>
)}
<section className="filters-info">
<Tooltip
title={
<div className="filters-info-tooltip">
<div className="filters-info-tooltip-title">Adaptive Filters</div>
<div>Values update automatically as you apply filters.</div>
<div className="filters-info-tooltip-detail">
The most relevant values are shown first, followed by all other
available options.
</div>
</div>
}
placement="right"
mouseEnterDelay={0.3}
>
<Typography.Text className="filters-info-toggle">
<Lightbulb size={15} />
Adaptive filters
</Typography.Text>
</Tooltip>
</section>
<section className="filters">
{filterConfig.map((filter) => {
switch (filter.type) {

View File

@@ -4,6 +4,7 @@ import {
useApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import {
otherFiltersResponse,
quickFiltersAttributeValuesResponse,
@@ -24,6 +25,8 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
}));
jest.mock('container/ApiMonitoring/queryParams');
jest.mock('hooks/querySuggestions/useGetQueryKeyValueSuggestions');
const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn();
const putHandler = jest.fn();
@@ -32,13 +35,15 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
>;
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const mockUseGetQueryKeyValueSuggestions = jest.mocked(
useGetQueryKeyValueSuggestions,
);
const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
const FILTER_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@@ -62,10 +67,7 @@ const setupServer = (): void => {
putHandler(await req.json());
return res(ctx.status(200), ctx.json({}));
}),
rest.get(quickFiltersAttributeValuesURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
rest.get(fieldsValuesURL, (_req, res, ctx) =>
rest.get('*/api/v1/fields/values*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
);
@@ -135,14 +137,24 @@ beforeEach(() => {
queryData: [
{
queryName: QUERY_NAME,
filters: { items: [{ key: 'test', value: 'value' }] },
filters: { items: [], op: 'AND' },
filter: { expression: '' },
},
],
},
},
lastUsedQuery: 0,
panelType: 'logs',
redirectWithQueryBuilderData,
});
// Mock the hook to return data with mq-kafka
mockUseGetQueryKeyValueSuggestions.mockReturnValue({
data: quickFiltersAttributeValuesResponse,
isLoading: false,
refetch: jest.fn(),
} as any);
mockUseApiMonitoringParams.mockReturnValue([
{ showIP: true } as ApiMonitoringParams,
mockSetApiMonitoringParams,
@@ -259,8 +271,9 @@ describe('Quick Filters', () => {
render(<TestQuickFilters />);
// Prefer role if possible; if label text isnt wired to input, clicking the label text is OK
const target = await screen.findByText('mq-kafka');
// Wait for the filter to load with data
const target = await screen.findByText('mq-kafka', {}, { timeout: 5000 });
await user.click(target);
await waitFor(() => {

View File

@@ -1,7 +1,10 @@
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { AxiosError, AxiosResponse } from 'axios';
import { AppState } from 'store/reducers';
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
import { GlobalReducer } from 'types/reducer/globalTime';
export const useGetQueryKeyValueSuggestions = ({
key,
@@ -9,6 +12,7 @@ export const useGetQueryKeyValueSuggestions = ({
searchText,
signalSource,
metricName,
existingQuery,
options,
}: {
key: string;
@@ -20,11 +24,19 @@ export const useGetQueryKeyValueSuggestions = ({
AxiosError
>;
metricName?: string;
existingQuery?: string;
}): UseQueryResult<
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
AxiosError
> =>
useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({
> => {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
return useQuery<
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
AxiosError
>({
queryKey: [
'queryKeyValueSuggestions',
key,
@@ -32,6 +44,8 @@ export const useGetQueryKeyValueSuggestions = ({
searchText,
signalSource,
metricName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
],
queryFn: () =>
getValueSuggestions({
@@ -40,6 +54,8 @@ export const useGetQueryKeyValueSuggestions = ({
searchText: searchText || '',
signalSource: signalSource as 'meter' | '',
metricName: metricName || '',
existingQuery,
}),
...options,
});
};

View File

@@ -118,13 +118,13 @@ export const otherFiltersResponse = {
export const quickFiltersAttributeValuesResponse = {
status: 'success',
data: {
stringAttributeValues: [
'mq-kafka',
'otel-demo',
'otlp-python',
'sample-flask',
],
numberAttributeValues: null,
boolAttributeValues: null,
data: {
values: {
relatedValues: ['mq-kafka', 'otel-demo', 'otlp-python', 'sample-flask'],
stringValues: ['mq-kafka', 'otel-demo', 'otlp-python', 'sample-flask'],
numberValues: [],
},
complete: true,
},
},
};

View File

@@ -592,6 +592,39 @@ body {
}
}
.ant-btn-text:hover,
.ant-btn-text:focus-visible {
background-color: var(--bg-vanilla-200) !important;
}
.ant-btn-link:hover,
.ant-btn-link:focus-visible {
background-color: transparent !important;
}
.ant-btn-default:hover,
.ant-btn-default:focus-visible,
.ant-btn-text:not(.ant-btn-primary):hover,
.ant-btn-text:not(.ant-btn-primary):focus-visible,
.ant-btn:not(.ant-btn-primary):not(.ant-btn-dangerous):hover,
.ant-btn:not(.ant-btn-primary):not(.ant-btn-dangerous):focus-visible {
background-color: var(--bg-vanilla-200) !important;
}
.ant-typography:hover,
.ant-typography:focus-visible {
background-color: transparent !important;
}
.ant-tooltip {
--antd-arrow-background-color: var(--bg-vanilla-300);
.ant-tooltip-inner {
background-color: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
// Enhanced legend light mode styles
.u-legend-enhanced {
// Light mode scrollbar styling

View File

@@ -47,6 +47,7 @@ export interface QueryKeyValueRequestProps {
searchText: string;
signalSource?: 'meter' | '';
metricName?: string;
existingQuery?: string;
}
export type SignalType = 'traces' | 'logs' | 'metrics';