Compare commits

..

1 Commits

Author SHA1 Message Date
SagarRajput-7
8b448e9df8 feat: added user-friendly format to dashboard variable url
"
2025-05-20 10:52:30 +05:30
29 changed files with 69 additions and 1308 deletions

View File

@@ -1,114 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldKeys } from '../getFieldKeys';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldKeys API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockSuccessResponse = {
data: {
status: 'success',
data: {
keys: {
'service.name': [],
'http.status_code': [],
},
complete: true,
},
},
};
it('should call API with correct parameters when no args provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with no parameters
await getFieldKeys();
// Verify API was called correctly with empty params object
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: {},
});
});
it('should call API with signal parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with signal parameter
await getFieldKeys('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'traces' },
});
});
it('should call API with name parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with name parameter
await getFieldKeys(undefined, 'service');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { name: 'service' },
});
});
it('should call API with both signal and name when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with both parameters
await getFieldKeys('logs', 'service');
// Verify API was called with both parameters
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'logs', name: 'service' },
});
});
it('should return properly formatted response', async () => {
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call the function
const result = await getFieldKeys('traces');
// Verify the returned structure matches our expected format
expect(result).toEqual({
statusCode: 200,
error: null,
message: 'success',
payload: mockSuccessResponse.data.data,
});
});
});

View File

@@ -1,209 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldValues } from '../getFieldValues';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldValues API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call the API with correct parameters (no options)', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function without parameters
await getFieldValues();
// Verify API was called correctly with empty params
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {},
});
});
it('should call the API with signal parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with signal parameter
await getFieldValues('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { signal: 'traces' },
});
});
it('should call the API with name parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with name parameter
await getFieldValues(undefined, 'service.name');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name' },
});
});
it('should call the API with value parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend'],
},
complete: false,
},
},
});
// Call function with value parameter
await getFieldValues(undefined, 'service.name', 'front');
// Verify API was called with value parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name', value: 'front' },
});
});
it('should call the API with time range parameters', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with time range parameters
const startUnixMilli = 1625097600000000; // Note: nanoseconds
const endUnixMilli = 1625184000000000;
await getFieldValues(
'logs',
'service.name',
undefined,
startUnixMilli,
endUnixMilli,
);
// Verify API was called with time range parameters (converted to milliseconds)
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {
signal: 'logs',
name: 'service.name',
startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
},
});
});
it('should normalize the response values', async () => {
// Mock API response with multiple value types
const mockResponse = {
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
numberValues: [200, 404],
boolValues: [true, false],
},
complete: true,
},
},
};
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
// Call the function
const result = await getFieldValues('traces', 'mixed.values');
// Verify the response has normalized values array
expect(result.payload?.normalizedValues).toContain('frontend');
expect(result.payload?.normalizedValues).toContain('backend');
expect(result.payload?.normalizedValues).toContain('200');
expect(result.payload?.normalizedValues).toContain('404');
expect(result.payload?.normalizedValues).toContain('true');
expect(result.payload?.normalizedValues).toContain('false');
expect(result.payload?.normalizedValues?.length).toBe(6);
});
it('should return a properly formatted success response', async () => {
// Create mock response
const mockApiResponse = {
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
};
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
// Call the function
const result = await getFieldValues('traces', 'service.name');
// Verify the returned structure
expect(result).toEqual({
statusCode: 200,
error: null,
message: 'success',
payload: expect.objectContaining({
values: expect.any(Object),
normalizedValues: expect.any(Array),
complete: true,
}),
});
});
});

View File

@@ -27,6 +27,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
import {
ALL_SELECTED_VALUE,
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForMultiSelect,
@@ -38,8 +39,6 @@ enum ToggleTagValue {
All = 'All',
}
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
placeholder = 'Search...',
className,
@@ -65,7 +64,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
maxTagTextLength,
onDropdownVisibleChange,
showIncompleteDataMessage = false,
showLabels = false,
...rest
}) => {
// ===== State & Refs =====
@@ -1710,11 +1708,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Custom Tag Render (needs significant updates)
const tagRender = useCallback(
(props: CustomTagProps): React.ReactElement => {
const { label: labelProp, value, closable, onClose } = props;
const label = showLabels
? options.find((option) => option.value === value)?.label || labelProp
: labelProp;
const { label, value, closable, onClose } = props;
// If the display value is the special ALL value, render the ALL tag
if (allOptionShown) {
@@ -1838,6 +1832,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
console.log('newValue', newValue);
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}

View File

@@ -60,5 +60,4 @@ export interface CustomMultiSelectProps
onRetry?: () => void;
maxTagTextLength?: number;
showIncompleteDataMessage?: boolean;
showLabels?: boolean;
}

View File

@@ -3,6 +3,8 @@ import { OptionData } from './types';
export const SPACEKEY = ' ';
export const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
export const prioritizeOrAddOptionForSingleSelect = (
options: OptionData[],
value: string,

View File

@@ -46,5 +46,5 @@ export enum QueryParams {
msgSystem = 'msgSystem',
destination = 'destination',
kindString = 'kindString',
variableConfigs = 'variableConfigs',
variables = 'variables',
}

View File

@@ -3,8 +3,6 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@@ -13,7 +11,7 @@ import getTimeString from 'lib/getTimeString';
import { isEqual } from 'lodash-es';
import isEmpty from 'lodash-es/isEmpty';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
@@ -68,12 +66,6 @@ function GridCardGraph({
GlobalReducer
>((state) => state.globalTime);
const { dynamicVariables } = useGetDynamicVariables();
const dynamicVariableToWidgetsMap = useMemo(
() => createDynamicVariableToWidgetsMap(dynamicVariables, [widget]),
[dynamicVariables, widget],
);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
@@ -205,22 +197,20 @@ function GridCardGraph({
maxTime,
minTime,
globalSelectedInterval,
variables,
widget?.query,
widget?.panelTypes,
widget.timePreferance,
widget.fillSpans,
requestData,
variables
? Object.entries(variables).reduce((acc, [id, variable]) => {
if (
variable.type !== 'DYNAMIC' ||
(dynamicVariableToWidgetsMap?.[id] &&
dynamicVariableToWidgetsMap?.[id].includes(widget.id))
) {
return { ...acc, [id]: variable.selectedValue };
}
return acc;
}, {})
? Object.entries(variables).reduce(
(acc, [id, variable]) => ({
...acc,
[id]: variable.selectedValue,
}),
{},
)
: {},
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
? [customTimeRange.startTime, customTimeRange.endTime]

View File

@@ -70,17 +70,6 @@
gap: 3px;
color: red;
}
.apply-to-all-button {
width: min-content;
height: 22px;
border-radius: 2px;
display: flex;
padding: 0px 6px;
align-items: center;
gap: 3px;
background: var(--bg-slate-400);
}
}
}
@@ -123,10 +112,6 @@
.edit-variable-button {
background: var(--bg-vanilla-300);
}
.apply-to-all-button {
background: var(--bg-vanilla-300);
}
}
}

View File

@@ -213,22 +213,6 @@
}
}
.dynamic-variable-section {
justify-content: space-between;
margin-bottom: 0;
.typography-variables {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 339px;
}
}
.variable-textbox-section {
justify-content: space-between;
margin-bottom: 0;
@@ -476,18 +460,6 @@
}
}
.default-value-section {
.default-value-description {
color: var(--bg-ink-400);
}
}
.dynamic-variable-section {
.typography-variables {
color: var(--bg-ink-400);
}
}
.variable-textbox-section {
.typography-variables {
color: var(--bg-ink-400);

View File

@@ -38,7 +38,6 @@ import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableMode } from '../types';
import DynamicVariable from './DynamicVariable/DynamicVariable';
import { LabelContainer, VariableItemRow } from './styles';
import { WidgetSelector } from './WidgetSelector';
const { Option } = Select;
@@ -136,14 +135,6 @@ function VariableItem({
endUnixMilli: maxTime,
});
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
useEffect(() => {
if (queryType === 'DYNAMIC') {
setSelectedWidgets(variableData?.dynamicVariablesWidgetIds || []);
}
}, [queryType, variableData?.dynamicVariablesWidgetIds]);
useEffect(() => {
if (queryType === 'CUSTOM') {
setPreviewValues(
@@ -212,10 +203,6 @@ function VariableItem({
dynamicVariablesAttribute: dynamicVariablesSelectedValue?.name,
dynamicVariablesSource: dynamicVariablesSelectedValue?.value,
}),
...(queryType === 'DYNAMIC' && {
dynamicVariablesWidgetIds:
selectedWidgets?.length > 0 ? selectedWidgets : [],
}),
};
onSave(mode, variable);
@@ -576,19 +563,6 @@ function VariableItem({
</VariableItemRow>
</>
)}
{queryType === 'DYNAMIC' && (
<VariableItemRow className="dynamic-variable-section">
<LabelContainer>
<Typography className="typography-variables">
Select Panels to apply this variable
</Typography>
</LabelContainer>
<WidgetSelector
selectedWidgets={selectedWidgets}
setSelectedWidgets={setSelectedWidgets}
/>
</VariableItemRow>
)}
</div>
</div>
<div className="variable-item-footer">

View File

@@ -1,44 +0,0 @@
import { CustomMultiSelect } from 'components/NewSelect';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
export function WidgetSelector({
selectedWidgets,
setSelectedWidgets,
}: {
selectedWidgets: string[];
setSelectedWidgets: (widgets: string[]) => void;
}): JSX.Element {
const { selectedDashboard } = useDashboard();
// Get layout IDs for cross-referencing
const layoutIds = new Set(
(selectedDashboard?.data?.layout || []).map((item) => item.i),
);
// Filter and deduplicate widgets by ID, keeping only those with layout entries
const widgets = Object.values(
(selectedDashboard?.data?.widgets || []).reduce(
(acc: Record<string, any>, widget) => {
if (widget.id && layoutIds.has(widget.id)) {
acc[widget.id] = widget;
}
return acc;
},
{},
),
);
return (
<CustomMultiSelect
placeholder="Select Panels"
options={widgets.map((widget) => ({
label: generateGridTitle(widget.title),
value: widget.id,
}))}
value={selectedWidgets}
onChange={(value): void => setSelectedWidgets(value as string[])}
showLabels
/>
);
}

View File

@@ -1,181 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { cloneDeep, isArray, isEmpty } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
/**
* Updates the query filters in a builder query by appending new tag filters
*/
const updateQueryFilters = (
queryData: IBuilderQuery,
filter: TagFilterItem,
): IBuilderQuery => {
const existingFilters = queryData.filters?.items || [];
// addition | update
const currentFilterKey = filter.key?.key;
const valueToAdd = filter.value.toString();
const newItems: TagFilterItem[] = [];
existingFilters.forEach((existingFilter) => {
const newFilter = cloneDeep(existingFilter);
if (
newFilter.key?.key === currentFilterKey &&
!(isArray(newFilter.value) && newFilter.value.includes(valueToAdd)) &&
newFilter.value !== valueToAdd
) {
if (isEmpty(newFilter.value)) {
newFilter.value = valueToAdd;
newFilter.op = 'IN';
} else {
newFilter.value = (isArray(newFilter.value)
? [...newFilter.value, valueToAdd]
: [newFilter.value, valueToAdd]) as string[] | string;
newFilter.op = 'IN';
}
}
newItems.push(newFilter);
});
// if yet the filter key doesn't get added then add it
if (!newItems.find((item) => item.key?.key === currentFilterKey)) {
newItems.push(filter);
}
return {
...queryData,
filters: {
...queryData.filters,
items: newItems,
},
};
};
/**
* Updates a single widget by adding filters to its query
*/
const updateSingleWidget = (
widget: Widgets,
filter: TagFilterItem,
): Widgets => {
if (!widget.query?.builder?.queryData || isEmpty(filter)) {
return widget;
}
return {
...widget,
query: {
...widget.query,
builder: {
...widget.query.builder,
queryData: widget.query.builder.queryData.map(
(queryData) => updateQueryFilters(queryData, filter), // todo - Sagar: check for multiple query or not
),
},
},
};
};
const removeIfPresent = (
queryData: IBuilderQuery,
filter: TagFilterItem,
): IBuilderQuery => {
const existingFilters = queryData.filters?.items || [];
// addition | update
const currentFilterKey = filter.key?.key;
const valueToAdd = filter.value.toString();
const newItems: TagFilterItem[] = [];
existingFilters.forEach((existingFilter) => {
const newFilter = cloneDeep(existingFilter);
if (newFilter.key?.key === currentFilterKey) {
if (isArray(newFilter.value) && newFilter.value.includes(valueToAdd)) {
newFilter.value = newFilter.value.filter((value) => value !== valueToAdd);
} else if (newFilter.value === valueToAdd) {
return;
}
}
newItems.push(newFilter);
});
return {
...queryData,
filters: {
...queryData.filters,
items: newItems,
},
};
};
const updateAfterRemoval = (
widget: Widgets,
filter: TagFilterItem,
): Widgets => {
if (!widget.query?.builder?.queryData || isEmpty(filter)) {
return widget;
}
// remove the filters where the current filter is available as value as this widget is not selected anymore, hence removal
return {
...widget,
query: {
...widget.query,
builder: {
...widget.query.builder,
queryData: widget.query.builder.queryData.map(
(queryData) => removeIfPresent(queryData, filter), // todo - Sagar: check for multiple query or not
),
},
},
};
};
/**
* A function that takes a dashboard configuration and a list of tag filters
* and returns an updated dashboard with the filters appended to widget queries.
*
* @param dashboard The dashboard configuration
* @param filters Array of tag filters to apply to widgets
* @param widgetIds Optional array of widget IDs to filter which widgets get updated
* @returns Updated dashboard configuration with filters applied
*/
export const addTagFiltersToDashboard = (
dashboard: Dashboard | undefined,
filter: TagFilterItem,
widgetIds?: string[],
applyToAll?: boolean,
): Dashboard | undefined => {
if (!dashboard || isEmpty(filter)) {
return dashboard;
}
// Create a deep copy to avoid mutating the original dashboard
const updatedDashboard = cloneDeep(dashboard);
// Process each widget to add filters
if (updatedDashboard.data.widgets) {
updatedDashboard.data.widgets = updatedDashboard.data.widgets.map(
(widget) => {
// Only apply to widgets with 'query' property
if ('query' in widget) {
// If widgetIds is provided, only update widgets with matching IDs
if (!applyToAll && widgetIds && !widgetIds.includes(widget.id)) {
// removal if needed
return updateAfterRemoval(widget as Widgets, filter);
}
return updateSingleWidget(widget as Widgets, filter);
}
return widget;
},
);
}
return updatedDashboard;
};

View File

@@ -15,20 +15,13 @@ import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
import { RowProps } from 'antd/lib';
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
import { useNotifications } from 'hooks/useNotifications';
import { PenLine, Trash2 } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dashboard,
IDashboardVariable,
Widgets,
} from 'types/api/dashboard/getAll';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableMode } from './types';
import VariableItem from './VariableItem/VariableItem';
@@ -95,7 +88,7 @@ function VariablesSetting({
const { notifications } = useNotifications();
const { variables = {}, widgets = [] } = selectedDashboard?.data || {};
const { variables = {} } = selectedDashboard?.data || {};
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
@@ -169,60 +162,18 @@ function VariablesSetting({
setExistingVariableNamesMap(variableNamesMap);
}, [variables]);
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
const { dynamicVariables } = useGetDynamicVariables();
const dynamicVariableToWidgetsMap = useMemo(
() =>
createDynamicVariableToWidgetsMap(
dynamicVariables,
(widgets as Widgets[]) || [],
),
[dynamicVariables, widgets],
);
// initialize and adjust dynamicVariablesWidgetIds values for all variables
useEffect(() => {
const newVariablesArr = Object.values(variables).map(
(variable: IDashboardVariable) => {
if (variable.type === 'DYNAMIC') {
return {
...variable,
dynamicVariablesWidgetIds: dynamicVariableToWidgetsMap[variable.id] || [],
};
}
return variable;
},
);
setVariablesTableData(newVariablesArr);
}, [variables, dynamicVariableToWidgetsMap]);
const updateVariables = (
updatedVariablesData: Dashboard['data']['variables'],
currentRequestedId?: string,
applyToAll?: boolean,
): void => {
if (!selectedDashboard) {
return;
}
const newDashboard =
(currentRequestedId &&
addDynamicVariableToPanels(
selectedDashboard,
updatedVariablesData[currentRequestedId || ''],
applyToAll,
)) ||
selectedDashboard;
updateMutation.mutateAsync(
{
...newDashboard,
...selectedDashboard,
data: {
...newDashboard.data,
...selectedDashboard.data,
variables: updatedVariablesData,
},
},
@@ -255,7 +206,6 @@ function VariablesSetting({
const onVariableSaveHandler = (
mode: TVariableMode,
variableData: IDashboardVariable,
applyToAll?: boolean,
): void => {
const updatedVariableData = {
...variableData,
@@ -279,7 +229,7 @@ function VariablesSetting({
const variables = convertVariablesToDbFormat(newVariablesArr);
setVariablesTableData(newVariablesArr);
updateVariables(variables, variableData?.id, applyToAll);
updateVariables(variables);
onDoneVariableViewMode();
};
@@ -325,18 +275,6 @@ function VariablesSetting({
{variable.description}
</Typography.Text>
<Space className="actions-btns">
{variable.type === 'DYNAMIC' && (
<Button
type="text"
onClick={(): void =>
onVariableSaveHandler(variableViewMode || 'EDIT', variable, true)
}
className="apply-to-all-button"
loading={updateMutation.isLoading}
>
<Typography.Text>Apply to all</Typography.Text>
</Button>
)}
<Button
type="text"
onClick={(): void => onVariableViewModeEnter('EDIT', variable)}

View File

@@ -1,4 +1,5 @@
import { Row } from 'antd';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -114,7 +115,11 @@ function DashboardVariableSelection(): JSX.Element | null {
if (id) {
updateLocalStorageDashboardVariables(name, value, allSelected);
updateUrlVariable(id, value, allSelected);
if (allSelected) {
updateUrlVariable(name || id, ALL_SELECTED_VALUE);
} else {
updateUrlVariable(name || id, value);
}
if (selectedDashboard) {
setSelectedDashboard((prev) => {

View File

@@ -2,7 +2,6 @@
/* eslint-disable no-nested-ternary */
import './DashboardVariableSelection.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { getFieldValues } from 'api/dynamicVariables/getFieldValues';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
@@ -297,11 +296,6 @@ function DynamicVariableSelection({
<div className="variable-item">
<Typography.Text className="variable-name" ellipsis>
${variableData.name}
{variableData.description && (
<Tooltip title={variableData.description}>
<InfoCircleOutlined className="info-icon" />
</Tooltip>
)}
</Typography.Text>
<div className="variable-value">
{variableData.multiSelect ? (

View File

@@ -162,7 +162,7 @@ describe('VariableItem', () => {
</MockQueryClientProvider>,
);
expect(screen.getByText('ALL')).toBeInTheDocument();
expect(screen.getByTitle('ALL')).toBeInTheDocument();
});
test('calls useEffect when the component mounts', () => {

View File

@@ -12,7 +12,6 @@ import {
} from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
@@ -242,8 +241,6 @@ function QueryBuilderSearchV2(
return false;
}, [currentState, query.aggregateAttribute.dataType, query.dataSource]);
const { dynamicVariables } = useGetDynamicVariables();
const { data, isFetching } = useGetAggregateKeys(
{
searchText: searchValue?.split(' ')[0],
@@ -798,19 +795,6 @@ function QueryBuilderSearchV2(
const dataType = currentFilterItem?.key?.dataType || DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
values.push(...(attributeValues?.payload?.[key] || []));
// here we want to suggest the variable name matching with the key here, we will go over the dynamic variables for the keys
const variableName = dynamicVariables.find(
(variable) =>
variable?.dynamicVariablesAttribute === currentFilterItem?.key?.key,
)?.name;
if (variableName) {
const variableValue = `$${variableName}`;
if (!values.includes(variableValue)) {
values.unshift(variableValue);
}
}
}
setDropdownOptions(
@@ -830,8 +814,6 @@ function QueryBuilderSearchV2(
searchValue,
suggestionsData?.payload?.attributes,
operatorConfigKey,
currentFilterItem?.key?.key,
dynamicVariables,
]);
// keep the query in sync with the selected tags in logs explorer page

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable react/jsx-props-no-spreading */
import {
act,
@@ -91,11 +90,6 @@ const renderWithContext = (props = {}): RenderResult => {
);
};
// Constants to fix linter errors
const TYPE_TAG = 'tag';
const IS_COLUMN_FALSE = false;
const IS_JSON_FALSE = false;
const mockAggregateKeysData = {
payload: {
attributeKeys: [
@@ -103,19 +97,11 @@ const mockAggregateKeysData = {
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'http.status',
dataType: DataTypes.String,
type: TYPE_TAG,
isColumn: IS_COLUMN_FALSE,
isJSON: IS_JSON_FALSE,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'http.status--string--tag--false',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: TYPE_TAG,
isColumn: IS_COLUMN_FALSE,
isJSON: IS_JSON_FALSE,
id: 'service.name--string--tag--false',
},
],
},
};
@@ -141,34 +127,6 @@ jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
})),
}));
// Mock the dynamic variables hook to test variable suggestion feature
const mockDynamicVariables = [
{
id: 'var1',
name: 'service',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'Traces',
selectedValue: 'frontend',
multiSelect: false,
showALLOption: false,
allSelected: false,
description: '',
sort: 'DISABLED',
dashboardName: 'Test Dashboard',
dashboardId: 'dashboard-123',
},
];
jest.mock('hooks/dashboard/useGetDynamicVariables', () => ({
useGetDynamicVariables: jest.fn(() => ({
dynamicVariables: mockDynamicVariables,
isLoading: false,
isError: false,
refetch: jest.fn(),
})),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
@@ -237,66 +195,3 @@ describe('Suggestion Key -> Operator -> Value Flow', () => {
);
});
});
describe('Dynamic Variable Suggestions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should suggest dynamic variable when key matches a variable attribute', async () => {
const { container } = renderWithContext();
// Get the combobox input
const combobox = container.querySelector(
'.query-builder-search-v2 .ant-select-selection-search-input',
) as HTMLInputElement;
// Focus and type to trigger key suggestions for service.name
await act(async () => {
fireEvent.focus(combobox);
fireEvent.change(combobox, { target: { value: 'service.' } });
});
// Wait for dropdown to appear
await screen.findByRole('listbox');
// Select service.name key from suggestions
const serviceNameOption = await screen.findByText('service.name');
await act(async () => {
fireEvent.click(serviceNameOption);
});
// Select equals operator
await act(async () => {
const equalsOption = screen.getByText('=');
fireEvent.click(equalsOption);
});
// Should show value suggestions including the dynamic variable
// For 'service.name', we expect to see '$service' as the first suggestion
const variableSuggestion = await screen.findByText('$service');
expect(variableSuggestion).toBeInTheDocument();
// Regular values should still be shown
expect(screen.getByText('200')).toBeInTheDocument();
expect(screen.getByText('404')).toBeInTheDocument();
// Select the variable suggestion
await act(async () => {
fireEvent.click(variableSuggestion);
});
// Verify the query was updated with the variable as value
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'service.name' }),
op: '=',
value: '$service',
}),
]),
}),
);
});
});

View File

@@ -1,224 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { useGetDynamicVariables } from '../useGetDynamicVariables';
// Mock the dependencies
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQuery: jest.fn(),
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: jest.fn(),
}));
// Sample dashboard data with variables
const mockDashboardData = {
data: {
title: 'Test Dashboard',
variables: {
var1: {
id: 'var1',
name: 'service',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'Traces',
selectedValue: 'frontend',
multiSelect: false,
showALLOption: false,
allSelected: false,
description: '',
sort: 'DISABLED',
},
var2: {
id: 'var2',
name: 'status',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'http.status_code',
dynamicVariablesSource: 'Traces',
selectedValue: '200',
multiSelect: false,
showALLOption: false,
allSelected: false,
description: '',
sort: 'DISABLED',
},
var3: {
id: 'var3',
name: 'interval',
type: 'CUSTOM', // Not DYNAMIC - should be filtered out
customValue: '5m',
multiSelect: false,
showALLOption: false,
allSelected: false,
description: '',
sort: 'DISABLED',
},
},
},
uuid: 'dashboard-123',
loading: false,
error: null,
};
// Mock refetch function
const mockRefetch = jest.fn();
// Constants
const DASHBOARD_ID = 'dashboard-123';
// Create a wrapper for the renderHook function with the QueryClientProvider
const createWrapper = (): React.FC<{ children: ReactNode }> => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// Define as function declaration to fix linter error
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
return Wrapper;
};
describe('useGetDynamicVariables', () => {
beforeEach(() => {
jest.clearAllMocks();
// Mock the useDashboard hook
(useDashboard as jest.Mock).mockReturnValue({
dashboardId: DASHBOARD_ID,
});
// Mock the useQuery hook for successful response
(useQuery as jest.Mock).mockReturnValue({
data: mockDashboardData,
isLoading: false,
isError: false,
refetch: mockRefetch,
});
});
it('should return dynamic variables from the dashboard', async () => {
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.dynamicVariables).toHaveLength(2); // Only DYNAMIC type variables
expect(result.current.dynamicVariables[0].name).toBe('service');
expect(result.current.dynamicVariables[1].name).toBe('status');
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
});
// Verify each dynamic variable has dashboard info
expect(result.current.dynamicVariables[0].dashboardName).toBe(
'Test Dashboard',
);
expect(result.current.dynamicVariables[0].dashboardId).toBe(DASHBOARD_ID);
});
it('should use dashboardId from props if provided', async () => {
const customDashboardId = 'custom-dashboard-id';
renderHook(() => useGetDynamicVariables({ dashboardId: customDashboardId }), {
wrapper: createWrapper(),
});
// Check that useQuery was called with the custom dashboardId
expect(useQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: expect.arrayContaining(['DASHBOARD_BY_ID', customDashboardId]),
}),
);
});
it('should return empty array when dashboard has no variables', async () => {
// Mock no variables in dashboard
(useQuery as jest.Mock).mockReturnValue({
data: {
data: { title: 'Empty Dashboard' },
uuid: 'dashboard-empty',
loading: false,
error: null,
},
isLoading: false,
isError: false,
refetch: mockRefetch,
});
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
expect(result.current.dynamicVariables).toHaveLength(0);
});
it('should return empty array when dashboard is null', async () => {
// Mock null dashboard data
(useQuery as jest.Mock).mockReturnValue({
data: null,
isLoading: false,
isError: false,
refetch: mockRefetch,
});
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
expect(result.current.dynamicVariables).toHaveLength(0);
});
it('should handle loading state', async () => {
// Mock loading state
(useQuery as jest.Mock).mockReturnValue({
data: null,
isLoading: true,
isError: false,
refetch: mockRefetch,
});
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
expect(result.current.isLoading).toBe(true);
expect(result.current.dynamicVariables).toHaveLength(0);
});
it('should handle error state', async () => {
// Mock error state
(useQuery as jest.Mock).mockReturnValue({
data: null,
isLoading: false,
isError: true,
refetch: mockRefetch,
});
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
expect(result.current.isError).toBe(true);
expect(result.current.dynamicVariables).toHaveLength(0);
});
it('should call refetch when returned function is called', async () => {
const { result } = renderHook(() => useGetDynamicVariables(), {
wrapper: createWrapper(),
});
result.current.refetch();
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,49 +0,0 @@
import { addTagFiltersToDashboard } from 'container/NewDashboard/DashboardSettings/Variables/addTagFiltersToDashboard';
import { useCallback } from 'react';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { getFiltersFromKeyValue } from './utils';
/**
* A hook that returns a function to add dynamic variables to dashboard panels as tag filters.
*
* @returns A function that, when given a dashboard and variable config, returns the updated dashboard.
*/
export const useAddDynamicVariableToPanels = (): ((
dashboard: Dashboard | undefined,
variableConfig: IDashboardVariable,
applyToAll?: boolean,
) => Dashboard | undefined) =>
useCallback(
(
dashboard: Dashboard | undefined,
variableConfig: IDashboardVariable,
applyToAll?: boolean,
): Dashboard | undefined => {
if (!variableConfig) return dashboard;
const {
dynamicVariablesAttribute,
name,
dynamicVariablesWidgetIds,
} = variableConfig;
const tagFilters: TagFilterItem = getFiltersFromKeyValue(
dynamicVariablesAttribute || '',
`$${name}`,
'',
'IN',
); // todo - Sagar: make a logic to have correct type and other details
return addTagFiltersToDashboard(
dashboard,
tagFilters,
dynamicVariablesWidgetIds,
applyToAll,
);
},
[],
);
export default useAddDynamicVariableToPanels;

View File

@@ -1,55 +0,0 @@
import getDashboard from 'api/dashboard/get';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
export interface DynamicVariable extends IDashboardVariable {
dashboardName: string;
dashboardId: string;
}
interface UseGetDynamicVariablesProps {
dashboardId?: string;
}
export const useGetDynamicVariables = (
props?: UseGetDynamicVariablesProps,
): {
dynamicVariables: DynamicVariable[];
isLoading: boolean;
isError: boolean;
refetch: () => void;
} => {
const { dashboardId: dashboardIdFromProps } = props || {};
const { dashboardId: dashboardIdFromDashboard } = useDashboard();
const dashboardId = dashboardIdFromProps || dashboardIdFromDashboard;
const { data: dashboard, isLoading, isError, refetch } = useQuery({
queryFn: () => getDashboard({ uuid: dashboardId }),
queryKey: [REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId],
});
const dynamicVariables = useMemo(() => {
if (!dashboard?.data?.variables) return [];
const variables: DynamicVariable[] = [];
Object.entries(dashboard.data.variables).forEach(([, variable]) => {
if (variable.type === 'DYNAMIC') {
variables.push({
...variable,
dashboardName: dashboard.data.title,
dashboardId: dashboard.uuid,
});
}
});
return variables;
}, [dashboard]);
return { dynamicVariables, isLoading, isError, refetch };
};

View File

@@ -85,7 +85,7 @@ function useGetResolvedText({
const variablePatterns = [
`\\{\\{\\s*?\\.([^\\s}]+)\\s*?\\}\\}`, // {{.var}}
`\\{\\{\\s*([^\\s}]+)\\s*\\}\\}`, // {{var}}
`${escapedMatcher}([^\\s]+)`, // matcher + var.name
`${escapedMatcher}([\\w.]+)`, // matcher + var.name
`\\[\\[\\s*([^\\s\\]]+)\\s*\\]\\]`, // [[var]]
];
return new RegExp(variablePatterns.join('|'), 'g');

View File

@@ -5,19 +5,17 @@ import { useHistory } from 'react-router-dom';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
interface LocalStoreDashboardVariables {
[id: string]: {
selectedValue: IDashboardVariable['selectedValue'];
allSelected: boolean;
};
[name: string]:
| IDashboardVariable['selectedValue'][]
| IDashboardVariable['selectedValue'];
}
interface UseVariablesFromUrlReturn {
getUrlVariables: () => LocalStoreDashboardVariables;
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
updateUrlVariable: (
id: string,
name: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
clearUrlVariables: () => void;
}
@@ -27,14 +25,14 @@ const useVariablesFromUrl = (): UseVariablesFromUrlReturn => {
const history = useHistory();
const getUrlVariables = useCallback((): LocalStoreDashboardVariables => {
const variableConfigsParam = urlQuery.get(QueryParams.variableConfigs);
const variablesParam = urlQuery.get(QueryParams.variables);
if (!variableConfigsParam) {
if (!variablesParam) {
return {};
}
try {
return JSON.parse(decodeURIComponent(variableConfigsParam));
return JSON.parse(decodeURIComponent(variablesParam));
} catch (error) {
console.error('Failed to parse variables from URL:', error);
return {};
@@ -46,11 +44,11 @@ const useVariablesFromUrl = (): UseVariablesFromUrlReturn => {
const params = new URLSearchParams(urlQuery.toString());
if (Object.keys(variables).length === 0) {
params.delete(QueryParams.variableConfigs);
params.delete(QueryParams.variables);
} else {
try {
const encodedVariables = encodeURIComponent(JSON.stringify(variables));
params.set(QueryParams.variableConfigs, encodedVariables);
params.set(QueryParams.variables, encodedVariables);
} catch (error) {
console.error('Failed to serialize variables for URL:', error);
}
@@ -65,7 +63,7 @@ const useVariablesFromUrl = (): UseVariablesFromUrlReturn => {
const clearUrlVariables = useCallback((): void => {
const params = new URLSearchParams(urlQuery.toString());
params.delete(QueryParams.variableConfigs);
params.delete(QueryParams.variables);
params.delete('options');
history.replace({
@@ -74,19 +72,15 @@ const useVariablesFromUrl = (): UseVariablesFromUrlReturn => {
}, [history, urlQuery]);
const updateUrlVariable = useCallback(
(
id: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
): void => {
(name: string, selectedValue: IDashboardVariable['selectedValue']): void => {
const currentVariables = getUrlVariables();
const updatedVariables = {
...currentVariables,
[id]: { selectedValue, allSelected },
[name]: selectedValue,
};
setUrlVariables(updatedVariables);
setUrlVariables(updatedVariables as LocalStoreDashboardVariables);
},
[getUrlVariables, setUrlVariables],
);

View File

@@ -1,20 +1,9 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import { isArray } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
Query,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuidv4 } from 'uuid';
import { DynamicVariable } from './useGetDynamicVariables';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
const baseLogsSelectedColumns = {
dataType: 'string',
@@ -67,65 +56,3 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
},
};
};
export const getFiltersFromKeyValue = (
key: string,
value: string | number,
type?: string,
op?: string,
dataType?: DataTypes,
): TagFilterItem => ({
id: uuidv4(),
key: {
key,
dataType: dataType || DataTypes.String,
type: type || '',
isColumn: false,
isJSON: false,
id: `${key}--${dataType || DataTypes.String}--${type || ''}--false`,
},
op: op || '=',
value: value.toString(),
});
export const createDynamicVariableToWidgetsMap = (
dynamicVariables: DynamicVariable[],
widgets: Widgets[],
// eslint-disable-next-line sonarjs/cognitive-complexity
): Record<string, string[]> => {
const dynamicVariableToWidgetsMap: Record<string, string[]> = {};
// Initialize map with empty arrays for each variable
dynamicVariables.forEach((variable) => {
if (variable.id) {
dynamicVariableToWidgetsMap[variable.id] = [];
}
});
// Check each widget for usage of dynamic variables
if (Array.isArray(widgets)) {
widgets.forEach((widget) => {
if (widget.query?.builder?.queryData) {
widget.query.builder.queryData.forEach((queryData: IBuilderQuery) => {
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
// For each filter, check if it uses any dynamic variable
dynamicVariables.forEach((variable) => {
if (
variable.dynamicVariablesAttribute &&
filter.key?.key === variable.dynamicVariablesAttribute &&
((isArray(filter.value) &&
filter.value.includes(`$${variable.name}`)) ||
filter.value === `$${variable.name}`) &&
!dynamicVariableToWidgetsMap[variable.id].includes(widget.id)
) {
dynamicVariableToWidgetsMap[variable.id].push(widget.id);
}
});
});
});
}
});
}
return dynamicVariableToWidgetsMap;
};

View File

@@ -3,7 +3,6 @@ import {
getTagToken,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { Option } from 'container/QueryBuilder/type';
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { isEmpty } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -26,19 +25,10 @@ export const useOptions = (
result: string[],
isFetching: boolean,
whereClauseConfig?: WhereClauseConfig,
// eslint-disable-next-line sonarjs/cognitive-complexity
): Option[] => {
const [options, setOptions] = useState<Option[]>([]);
const operators = useOperators(key, keys);
// get matching dynamic variables to suggest
const { dynamicVariables } = useGetDynamicVariables();
const variableName = dynamicVariables.find(
(variable) => variable?.dynamicVariablesAttribute === key,
)?.name;
const variableAsValue = variableName ? `$${variableName}` : '';
const getLabel = useCallback(
(data: BaseAutocompleteData): Option['label'] =>
transformStringWithPrefix({
@@ -73,13 +63,7 @@ export const useOptions = (
const getOptionsWithValidOperator = useCallback(
(key: string, results: string[], searchValue: string) => {
const hasAllResults = results.every((value) => result.includes(value));
let newResults = results;
if (!isEmpty(variableAsValue)) {
newResults = [variableAsValue, ...newResults];
}
const values = getKeyOpValue(newResults);
const values = getKeyOpValue(results);
return hasAllResults
? [
@@ -96,7 +80,7 @@ export const useOptions = (
...values,
];
},
[getKeyOpValue, result, variableAsValue],
[getKeyOpValue, result],
);
const getKeyOperatorOptions = useCallback(
@@ -144,10 +128,7 @@ export const useOptions = (
newOptions = getKeyOperatorOptions(key);
} else if (key && operator) {
if (isMulti) {
const resultsWithVariable = isEmpty(variableAsValue)
? results
: [variableAsValue, ...results];
newOptions = resultsWithVariable.map((item) => ({
newOptions = results.map((item) => ({
label: checkCommaInValue(String(item)),
value: String(item),
}));
@@ -180,7 +161,6 @@ export const useOptions = (
getKeyOperatorOptions,
getOptionsWithValidOperator,
isFetching,
variableAsValue,
]);
return useMemo(

View File

@@ -26,8 +26,6 @@ export const getDashboardVariables = (
variablesTuple[value.name] =
value?.type === 'DYNAMIC' &&
value?.allSelected &&
value?.showALLOption &&
value?.multiSelect &&
!value?.haveCustomValuesSelected
? '__all__'
: value?.selectedValue;

View File

@@ -3,6 +3,7 @@ import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get';
import lockDashboardApi from 'api/dashboard/lockDashboard';
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
@@ -228,8 +229,11 @@ export function DashboardProvider({
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
const variablesFromUrl = getUrlVariables();
// values from url
const urlVariable = getUrlVariables()[variableData.id];
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
@@ -240,7 +244,9 @@ export function DashboardProvider({
if (urlVariable) {
updatedVariable = {
...updatedVariable,
...urlVariable,
...(urlVariable !== ALL_SELECTED_VALUE &&
updatedVariable?.showALLOption && { selectedValue: urlVariable }),
...(urlVariable === ALL_SELECTED_VALUE && { allSelected: true }),
};
}

View File

@@ -1,3 +1,4 @@
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { commaValuesParser } from '../../lib/dashbaordVariables/customCommaValuesParser';
@@ -18,28 +19,29 @@ export const initializeDefaultVariables = (
variables: Record<string, IDashboardVariable>,
getUrlVariables: () => UrlVariables | undefined,
updateUrlVariable: (
id: string,
name: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void,
): void => {
if (!variables) return;
Object.values(variables).forEach((variable) => {
const { id, name } = variable;
const { id, name, allSelected, showALLOption } = variable;
const urlVariables = getUrlVariables();
// Check if either id or name is available in URL variables
const existsInUrl =
(id && urlVariables?.[id]) || (name && urlVariables?.[name]);
const value =
variable.type === 'CUSTOM'
? commaValuesParser(variable?.customValue || '')
: variable?.selectedValue || variable?.defaultValue;
if (!existsInUrl) {
updateUrlVariable(
id,
variable.type === 'CUSTOM'
? commaValuesParser(variable?.customValue || '')
: variable?.selectedValue || variable?.defaultValue,
variable.allSelected || false,
name || id,
allSelected && showALLOption ? ALL_SELECTED_VALUE : value,
);
}
});

View File

@@ -52,7 +52,6 @@ export interface IDashboardVariable {
dynamicVariablesAttribute?: string;
dynamicVariablesSource?: string;
haveCustomValuesSelected?: boolean;
dynamicVariablesWidgetIds?: string[];
}
export interface Dashboard {
id: number;