From 972fadf79a83bf30ebf1ae1894e814a912dab0be Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Thu, 15 May 2025 00:08:06 +0530 Subject: [PATCH] feat: added dynamic variables creation flow (#7541) * feat: added dynamic variables creation flow * feat: added keys and value apis and hooks * feat: added api and select component changes * feat: added keys fetching and preview values * feat: added dynamic variable to variable items * feat: handled value persistence and tab switches * feat: added default value and formed a schema for dyn-variables * feat: added client and server side searches * feat: corrected the initial load getfieldKey api * feat: removed fetch on mount restriction --- .../src/api/dynamicVariables/getFieldKeys.ts | 34 ++++ .../api/dynamicVariables/getFieldValues.ts | 40 ++++ frontend/src/components/NewSelect/types.ts | 2 +- .../DynamicVariable.styles.scss | 42 +++++ .../DynamicVariable/DynamicVariable.tsx | 175 ++++++++++++++++++ .../VariableItem/VariableItem.styles.scss | 6 +- .../Variables/VariableItem/VariableItem.tsx | 116 +++++++++++- .../hooks/dynamicVariables/useGetFieldKeys.ts | 35 ++++ .../dynamicVariables/useGetFieldValues.ts | 38 ++++ frontend/src/types/api/dashboard/getAll.ts | 10 +- .../api/dynamicVariables/getFieldKeys.ts | 23 +++ .../api/dynamicVariables/getFieldValues.ts | 9 + 12 files changed, 522 insertions(+), 8 deletions(-) create mode 100644 frontend/src/api/dynamicVariables/getFieldKeys.ts create mode 100644 frontend/src/api/dynamicVariables/getFieldValues.ts create mode 100644 frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.styles.scss create mode 100644 frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/DynamicVariable/DynamicVariable.tsx create mode 100644 frontend/src/hooks/dynamicVariables/useGetFieldKeys.ts create mode 100644 frontend/src/hooks/dynamicVariables/useGetFieldValues.ts create mode 100644 frontend/src/types/api/dynamicVariables/getFieldKeys.ts create mode 100644 frontend/src/types/api/dynamicVariables/getFieldValues.ts 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 +