diff --git a/frontend/src/api/dynamicVariables/getFieldKeys.ts b/frontend/src/api/dynamicVariables/getFieldKeys.ts new file mode 100644 index 000000000000..eff101828624 --- /dev/null +++ b/frontend/src/api/dynamicVariables/getFieldKeys.ts @@ -0,0 +1,34 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys'; + +/** + * Get field keys for a given signal type + * @param signal Type of signal (traces, logs, metrics) + * @param name Optional search text + */ +export const getFieldKeys = async ( + signal?: 'traces' | 'logs' | 'metrics', + name?: string, +): Promise | ErrorResponse> => { + const params: Record = {}; + + if (signal) { + params.signal = signal; + } + + if (name) { + params.name = name; + } + + const response = await ApiBaseInstance.get('/fields/keys', { params }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; +}; + +export default getFieldKeys; diff --git a/frontend/src/api/dynamicVariables/getFieldValues.ts b/frontend/src/api/dynamicVariables/getFieldValues.ts new file mode 100644 index 000000000000..a79bd9029165 --- /dev/null +++ b/frontend/src/api/dynamicVariables/getFieldValues.ts @@ -0,0 +1,40 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues'; + +/** + * Get field values for a given signal type and field name + * @param signal Type of signal (traces, logs, metrics) + * @param name Name of the attribute for which values are being fetched + * @param value Optional search text + */ +export const getFieldValues = async ( + signal?: 'traces' | 'logs' | 'metrics', + name?: string, + value?: string, +): Promise | ErrorResponse> => { + const params: Record = {}; + + if (signal) { + params.signal = signal; + } + + if (name) { + params.name = name; + } + + if (value) { + params.value = value; + } + + const response = await ApiBaseInstance.get('/fields/values', { params }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; +}; + +export default getFieldValues; diff --git a/frontend/src/components/NewSelect/types.ts b/frontend/src/components/NewSelect/types.ts index 49369c89af78..27ebc6d3300d 100644 --- a/frontend/src/components/NewSelect/types.ts +++ b/frontend/src/components/NewSelect/types.ts @@ -24,7 +24,7 @@ export interface CustomSelectProps extends Omit { highlightSearch?: boolean; placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; popupMatchSelectWidth?: boolean; - errorMessage?: string; + errorMessage?: string | null; allowClear?: SelectProps['allowClear']; onRetry?: () => void; } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.styles.scss new file mode 100644 index 000000000000..6207e515e39d --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.styles.scss @@ -0,0 +1,42 @@ +.dynamic-variable-container { + display: grid; + grid-template-columns: 1fr 32px 200px; + gap: 32px; + align-items: center; + width: 100%; + margin: 24px 0; + + .ant-select { + .ant-select-selector { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + } + } + + .ant-input { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + max-width: 300px; + } + + .dynamic-variable-from-text { + font-family: 'Space Mono'; + font-size: 13px; + font-weight: 500; + white-space: nowrap; + } +} + +.lightMode { + .dynamic-variable-container { + .ant-select { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + } + } + + .ant-input { + border: 1px solid var(--bg-vanilla-300); + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.tsx new file mode 100644 index 000000000000..c0786c58d4af --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.tsx @@ -0,0 +1,175 @@ +import './DynamicVariable.styles.scss'; + +import { Select, Typography } from 'antd'; +import CustomSelect from 'components/NewSelect/CustomSelect'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; +import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys'; +import useDebounce from 'hooks/useDebounce'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { FieldKey } from 'types/api/dynamicVariables/getFieldKeys'; + +enum AttributeSource { + ALL_SOURCES = 'All Sources', + LOGS = 'Logs', + METRICS = 'Metrics', + TRACES = 'Traces', +} + +function DynamicVariable({ + setDynamicVariablesSelectedValue, + dynamicVariablesSelectedValue, +}: { + setDynamicVariablesSelectedValue: Dispatch< + SetStateAction< + | { + name: string; + value: string; + } + | undefined + > + >; + dynamicVariablesSelectedValue: + | { + name: string; + value: string; + } + | undefined; +}): JSX.Element { + const sources = [ + AttributeSource.ALL_SOURCES, + AttributeSource.LOGS, + AttributeSource.TRACES, + AttributeSource.METRICS, + ]; + + const [attributeSource, setAttributeSource] = useState(); + const [attributes, setAttributes] = useState>({}); + const [selectedAttribute, setSelectedAttribute] = useState(); + const [apiSearchText, setApiSearchText] = useState(''); + + const debouncedApiSearchText = useDebounce(apiSearchText, DEBOUNCE_DELAY); + + const [filteredAttributes, setFilteredAttributes] = useState< + Record + >({}); + + useEffect(() => { + if (dynamicVariablesSelectedValue?.name) { + setSelectedAttribute(dynamicVariablesSelectedValue.name); + } + + if (dynamicVariablesSelectedValue?.value) { + setAttributeSource(dynamicVariablesSelectedValue.value as AttributeSource); + } + }, [ + dynamicVariablesSelectedValue?.name, + dynamicVariablesSelectedValue?.value, + ]); + + const { data, error, isLoading, refetch } = useGetFieldKeys({ + signal: + attributeSource === AttributeSource.ALL_SOURCES + ? undefined + : (attributeSource?.toLowerCase() as 'traces' | 'logs' | 'metrics'), + name: debouncedApiSearchText, + }); + + const isComplete = useMemo(() => data?.payload?.complete === true, [data]); + + useEffect(() => { + if (data) { + const newAttributes = data.payload?.keys ?? {}; + setAttributes(newAttributes); + setFilteredAttributes(newAttributes); + } + }, [data]); + + // refetch when attributeSource changes + useEffect(() => { + if (attributeSource) { + refetch(); + } + }, [attributeSource, refetch, debouncedApiSearchText]); + + // Handle search based on whether we have complete data or not + const handleSearch = useCallback( + (text: string) => { + if (isComplete) { + // If complete is true, do client-side filtering + if (!text) { + setFilteredAttributes(attributes); + return; + } + + const filtered: Record = {}; + Object.keys(attributes).forEach((key) => { + if (key.toLowerCase().includes(text.toLowerCase())) { + filtered[key] = attributes[key]; + } + }); + setFilteredAttributes(filtered); + } else { + // If complete is false, debounce the API call + setApiSearchText(text); + } + }, + [attributes, isComplete], + ); + + // update setDynamicVariablesSelectedValue with debounce when attribute and source is selected + useEffect(() => { + if (selectedAttribute || attributeSource) { + setDynamicVariablesSelectedValue({ + name: selectedAttribute || dynamicVariablesSelectedValue?.name || '', + value: + attributeSource || + dynamicVariablesSelectedValue?.value || + AttributeSource.ALL_SOURCES, + }); + } + }, [ + selectedAttribute, + attributeSource, + setDynamicVariablesSelectedValue, + dynamicVariablesSelectedValue?.name, + dynamicVariablesSelectedValue?.value, + ]); + + return ( +
+ ({ + label: key, + value: key, + }))} + loading={isLoading} + status={error ? 'error' : undefined} + onChange={(value): void => { + setSelectedAttribute(value); + }} + showSearch + errorMessage={error as any} + value={selectedAttribute || dynamicVariablesSelectedValue?.name} + onSearch={handleSearch} + /> + from +