Compare commits

..

13 Commits

Author SHA1 Message Date
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 586 additions and 135 deletions

View File

@@ -15,6 +15,8 @@ export const getKeySuggestions = (
fieldContext = '',
fieldDataType = '',
signalSource = '',
startUnixMilli = '',
endUnixMilli = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -24,7 +26,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 (startUnixMilli !== undefined) {
url += `&startUnixMilli=${startUnixMilli}`;
}
if (endUnixMilli !== undefined) {
url += `&endUnixMilli=${endUnixMilli}`;
}
return axios.get(url);
};

View File

@@ -8,7 +8,16 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText, signalSource, metricName } = props;
const {
signal,
key,
searchText,
signalSource,
metricName,
startUnixMilli,
endUnixMilli,
existingQuery,
} = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
@@ -16,7 +25,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 (startUnixMilli !== undefined) {
url += `&startUnixMilli=${startUnixMilli}`;
}
if (endUnixMilli !== undefined) {
url += `&endUnixMilli=${endUnixMilli}`;
}
if (existingQuery) {
url += `&existingQuery=${encodeURIComponent(existingQuery)}`;
}
return axios.get(url);
};

View File

@@ -1,9 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { Select, Spin } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { AppState } from 'store/reducers';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import './ListViewOrderBy.styles.scss';
@@ -34,13 +37,35 @@ function ListViewOrderBy({
>([]);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Get time range from Redux global state
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Convert nanoseconds to milliseconds - use useMemo with both deps to avoid multiple updates
const timeRange = useMemo(
() => ({
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
}),
[globalTime.minTime, globalTime.maxTime],
);
// Fetch key suggestions based on debounced input
const { data, isLoading } = useQuery({
queryKey: ['orderByKeySuggestions', dataSource, debouncedInput],
queryKey: [
'orderByKeySuggestions',
dataSource,
debouncedInput,
timeRange.startUnixMilli,
timeRange.endUnixMilli,
],
queryFn: async () => {
const response = await getKeySuggestions({
signal: dataSource,
searchText: debouncedInput,
startUnixMilli: timeRange.startUnixMilli,
endUnixMilli: timeRange.endUnixMilli,
});
return response.data;
},

View File

@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { CheckCircleFilled } from '@ant-design/icons';
import {
autocompletion,
@@ -33,6 +34,8 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
import { Info, TriangleAlert } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { AppState } from 'store/reducers';
import {
IDetailedError,
IQueryContext,
@@ -41,6 +44,7 @@ import {
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
getCurrentValueIndexAtCursor,
getQueryContextAtCursor,
@@ -112,6 +116,20 @@ function QuerySearch({
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
// Get time range from Redux global state
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Convert nanoseconds to milliseconds - use useMemo with both deps to avoid multiple updates
const timeRange = useMemo(
() => ({
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
}),
[globalTime.minTime, globalTime.maxTime],
);
const handleQueryValidation = useCallback((newExpression: string): void => {
try {
const validationResponse = validateQuery(newExpression);
@@ -270,8 +288,9 @@ function QuerySearch({
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
startUnixMilli: timeRange.startUnixMilli,
endUnixMilli: timeRange.endUnixMilli,
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
@@ -301,6 +320,8 @@ function QuerySearch({
queryData.aggregateAttribute?.key,
signalSource,
hardcodedAttributeKeys,
timeRange.startUnixMilli,
timeRange.endUnixMilli,
],
);
@@ -313,7 +334,12 @@ function QuerySearch({
setKeySuggestions([]);
debouncedFetchKeySuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource, debouncedMetricName]);
}, [
dataSource,
debouncedMetricName,
timeRange.startUnixMilli,
timeRange.endUnixMilli,
]);
// Add a state for tracking editing mode
const [editingMode, setEditingMode] = useState<
@@ -431,6 +457,7 @@ function QuerySearch({
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
const existingQuery = queryData.filter?.expression || '';
try {
const response = await getValueSuggestions({
@@ -439,9 +466,10 @@ function QuerySearch({
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
startUnixMilli: timeRange.startUnixMilli,
endUnixMilli: timeRange.endUnixMilli,
existingQuery,
}); // Skip updates if component unmounted or key changed
if (
!isMountedRef.current ||
lastKeyRef.current !== key ||
@@ -453,10 +481,10 @@ function QuerySearch({
// Process the response data
const responseData = response.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
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
const stringOptions = stringValues
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
@@ -528,11 +556,14 @@ function QuerySearch({
},
[
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
queryData.filter?.expression,
toggleSuggestions,
dataSource,
signalSource,
debouncedMetricName,
timeRange.startUnixMilli,
timeRange.endUnixMilli,
],
);
@@ -1249,19 +1280,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 +1299,7 @@ function QuerySearch({
isLoadingSuggestions,
activeKey,
fetchValueSuggestions,
isFocused,
]);
const getTooltipContent = (): JSX.Element => (

View File

@@ -138,6 +138,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

@@ -2,8 +2,16 @@
/* 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 { useSelector } from 'react-redux';
import { Button, Checkbox, Input, InputRef, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
@@ -11,21 +19,18 @@ 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 { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
@@ -68,6 +73,15 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
panelType,
} = useQueryBuilder();
// Get time range from Redux global state
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Convert nanoseconds to milliseconds
const startUnixMilli = useMemo(() => Math.floor(minTime / 1000000), [minTime]);
const endUnixMilli = useMemo(() => Math.floor(maxTime / 1000000), [maxTime]);
// Determine if we're in ListView mode
const isListView = panelType === PANEL_TYPES.LIST;
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
@@ -81,6 +95,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,47 +132,128 @@ 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',
signalSource: source === QuickFiltersSource.METER_EXPLORER ? 'meter' : '',
startUnixMilli,
endUnixMilli,
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: string[] = useMemo(() => {
// const dataType = filter.attributeKey.dataType || DataTypes.String;
if (keyValueSuggestions) {
// Process the response data for all pages
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseData = keyValueSuggestions?.data as any;
const values = responseData.data?.values || {};
const relatedValues = values.relatedValues || [];
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
// Combine relatedValues first, then unique stringValues
const valuesToUse = [
...relatedValues,
...stringValues.filter(
(value: string | null | undefined) =>
value !== null &&
value !== undefined &&
value !== '' &&
!relatedValues.includes(value),
),
];
// Generate options from string values - explicitly handle empty strings
const stringOptions = stringValues
const stringOptions = valuesToUse
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
(value: string | null | undefined): value is string =>
@@ -167,15 +268,15 @@ 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 baseValues = [...stringOptions, ...numberOptions];
const previousUnchecked = previousUncheckedValuesRef.current || [];
const preservedUnchecked = previousUnchecked.filter(
(value) => !baseValues.includes(value),
);
return [...baseValues, ...preservedUnchecked];
}
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]);
return [];
}, [keyValueSuggestions]);
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
@@ -249,22 +350,30 @@ 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,
visibleCheckedCount,
hasMoreChecked,
} = useMemo(() => {
const checkedValues = attributeValues.filter(
(val) => currentFilterState[val],
);
const uncheckedValues = attributeValues.filter(
(val) => !currentFilterState[val],
);
return [...checkedValues, ...uncheckedValues].slice(0, visibleItemsCount);
const unchecked = attributeValues.filter((val) => !currentFilterState[val]);
const visibleChecked = checkedValues.slice(0, visibleItemsCount);
return {
visibleCheckedValues: visibleChecked,
uncheckedValues: unchecked,
visibleCheckedCount: visibleChecked.length,
hasMoreChecked: checkedValues.length > visibleChecked.length,
};
}, [attributeValues, currentFilterState, visibleItemsCount]);
// Count of checked values in the currently visible items
const checkedValuesCount = useMemo(
() => currentAttributeKeys.filter((val) => currentFilterState[val]).length,
[currentAttributeKeys, currentFilterState],
);
useEffect(() => {
previousUncheckedValuesRef.current = uncheckedValues;
}, [uncheckedValues]);
const handleClearFilterAttribute = (): void => {
const preparedQuery: Query = {
@@ -593,35 +702,90 @@ 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) => (
<Fragment key={value}>
{index === checkedValuesCount && checkedValuesCount > 0 && (
<div
key="separator"
className="filter-separator"
data-testid="filter-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" />
)}
{uncheckedValues.map((value: string) => (
<Fragment key={value}>
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
@@ -681,16 +845,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,21 @@
}
}
}
.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);
}
}
}
}

View File

@@ -12,7 +12,7 @@ import {
ComboboxList,
ComboboxTrigger,
} from '@signozhq/combobox';
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
import { Button, Skeleton, Switch, Tooltip, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
@@ -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';
@@ -51,6 +51,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
} = props;
const { user } = useAppContext();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isFiltersInfoOpen, setIsFiltersInfoOpen] = useState(false);
const isAdmin = user.role === USER_ROLES.ADMIN;
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
@@ -291,6 +292,21 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
</div>
)}
<section className="filters-info">
<Button
type="link"
className="filters-info-toggle"
onClick={(): void => setIsFiltersInfoOpen((prev) => !prev)}
>
<Lightbulb size={15} />
{isFiltersInfoOpen ? 'Hide info' : 'Adaptive filters'}
</Button>
{isFiltersInfoOpen && (
<Typography.Text className="filters-info-text">
Filter values update automatically based on other selected filters
</Typography.Text>
)}
</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

@@ -9,6 +9,9 @@ export const useGetQueryKeyValueSuggestions = ({
searchText,
signalSource,
metricName,
startUnixMilli,
endUnixMilli,
existingQuery,
options,
}: {
key: string;
@@ -20,6 +23,9 @@ export const useGetQueryKeyValueSuggestions = ({
AxiosError
>;
metricName?: string;
startUnixMilli?: number;
endUnixMilli?: number;
existingQuery?: string;
}): UseQueryResult<
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
AxiosError
@@ -40,6 +46,9 @@ export const useGetQueryKeyValueSuggestions = ({
searchText: searchText || '',
signalSource: signalSource as 'meter' | '',
metricName: metricName || '',
startUnixMilli,
endUnixMilli,
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

@@ -583,6 +583,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

@@ -29,6 +29,8 @@ export interface QueryKeyRequestProps {
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
metricName?: string;
signalSource?: 'meter' | '';
startUnixMilli?: number;
endUnixMilli?: number;
}
export interface QueryKeyValueSuggestionsProps {
@@ -47,6 +49,9 @@ export interface QueryKeyValueRequestProps {
searchText: string;
signalSource?: 'meter' | '';
metricName?: string;
startUnixMilli?: number;
endUnixMilli?: number;
existingQuery?: string;
}
export type SignalType = 'traces' | 'logs' | 'metrics';

View File

@@ -259,11 +259,6 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
return
}
if claims.UserID == id {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "users cannot delete themselves"))
return
}
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
render.Error(w, err)
return