feat: added apply to all and variable removal logical

This commit is contained in:
SagarRajput-7 2025-05-19 03:15:22 +05:30
parent f9f463cce6
commit b743a5a844
9 changed files with 304 additions and 34 deletions

View File

@ -70,6 +70,17 @@
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);
}
}
}

View File

@ -213,6 +213,22 @@
}
}
.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;

View File

@ -599,7 +599,7 @@ function VariableItem({
<VariableItemRow className="dynamic-variable-section">
<LabelContainer>
<Typography className="typography-variables">
Select Widgets to apply
Select Panels to apply this variable
</Typography>
</LabelContainer>
<WidgetSelector

View File

@ -11,17 +11,32 @@ export function WidgetSelector({
}): JSX.Element {
const { selectedDashboard } = useDashboard();
const widgets = selectedDashboard?.data?.widgets || [];
// 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 Widgets"
placeholder="Select Panels"
options={widgets.map((widget) => ({
label: generateGridTitle(widget.title),
value: widget.id,
}))}
value={selectedWidgets}
labelInValue
onChange={(value): void => setSelectedWidgets(value as string[])}
showLabels
/>

View File

@ -1,4 +1,5 @@
import { cloneDeep } from 'lodash-es';
/* eslint-disable sonarjs/cognitive-complexity */
import { cloneDeep, isArray, isEmpty } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import {
IBuilderQuery,
@ -10,24 +11,60 @@ import {
*/
const updateQueryFilters = (
queryData: IBuilderQuery,
filters: TagFilterItem[],
): IBuilderQuery => ({
...queryData,
filters: {
...queryData.filters,
items: [...(queryData.filters?.items || []), ...filters],
op: queryData.filters?.op || 'AND',
},
});
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,
op: queryData.filters?.op || 'AND',
},
};
};
/**
* Updates a single widget by adding filters to its query
*/
const updateSingleWidget = (
widget: Widgets,
filters: TagFilterItem[],
filter: TagFilterItem,
): Widgets => {
if (!widget.query?.builder?.queryData) {
if (!widget.query?.builder?.queryData || isEmpty(filter)) {
return widget;
}
@ -37,8 +74,65 @@ const updateSingleWidget = (
...widget.query,
builder: {
...widget.query.builder,
queryData: widget.query.builder.queryData.map((queryData) =>
updateQueryFilters(queryData, filters),
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,
op: queryData.filters?.op || 'AND',
},
};
};
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
),
},
},
@ -56,10 +150,11 @@ const updateSingleWidget = (
*/
export const addTagFiltersToDashboard = (
dashboard: Dashboard | undefined,
filters: TagFilterItem[],
filter: TagFilterItem,
widgetIds?: string[],
applyToAll?: boolean,
): Dashboard | undefined => {
if (!dashboard || !filters.length) {
if (!dashboard || isEmpty(filter)) {
return dashboard;
}
@ -73,10 +168,11 @@ export const addTagFiltersToDashboard = (
// Only apply to widgets with 'query' property
if ('query' in widget) {
// If widgetIds is provided, only update widgets with matching IDs
if (widgetIds && !widgetIds.includes(widget.id)) {
return widget;
if (!applyToAll && widgetIds && !widgetIds.includes(widget.id)) {
// removal if needed
return updateAfterRemoval(widget as Widgets, filter);
}
return updateSingleWidget(widget as Widgets, filters);
return updateSingleWidget(widget as Widgets, filter);
}
return widget;
},

View File

@ -16,13 +16,19 @@ 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, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import {
Dashboard,
IDashboardVariable,
Widgets,
} from 'types/api/dashboard/getAll';
import { TVariableMode } from './types';
import VariableItem from './VariableItem/VariableItem';
@ -89,7 +95,7 @@ function VariablesSetting({
const { notifications } = useNotifications();
const { variables = {} } = selectedDashboard?.data || {};
const { variables = {}, widgets = [] } = selectedDashboard?.data || {};
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
@ -165,9 +171,39 @@ function VariablesSetting({
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;
@ -178,6 +214,7 @@ function VariablesSetting({
addDynamicVariableToPanels(
selectedDashboard,
updatedVariablesData[currentRequestedId || ''],
applyToAll,
)) ||
selectedDashboard;
@ -214,6 +251,7 @@ function VariablesSetting({
const onVariableSaveHandler = (
mode: TVariableMode,
variableData: IDashboardVariable,
applyToAll?: boolean,
): void => {
const updatedVariableData = {
...variableData,
@ -237,7 +275,7 @@ function VariablesSetting({
const variables = convertVariablesToDbFormat(newVariablesArr);
setVariablesTableData(newVariablesArr);
updateVariables(variables, variableData?.id);
updateVariables(variables, variableData?.id, applyToAll);
onDoneVariableViewMode();
};
@ -283,6 +321,17 @@ 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"
>
<Typography.Text>Apply to all</Typography.Text>
</Button>
)}
<Button
type="text"
onClick={(): void => onVariableViewModeEnter('EDIT', variable)}

View File

@ -2,6 +2,7 @@
/* 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';
@ -296,6 +297,11 @@ 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

@ -1,9 +1,9 @@
import { getFiltersFromKeyValue } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
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 { addTagFiltersToDashboard } from '../../container/NewDashboard/DashboardSettings/Variables/addTagFiltersToDashboard';
import { getFiltersFromKeyValue } from './utils';
/**
* A hook that returns a function to add dynamic variables to dashboard panels as tag filters.
@ -13,11 +13,13 @@ import { addTagFiltersToDashboard } from '../../container/NewDashboard/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;
@ -27,14 +29,18 @@ export const useAddDynamicVariableToPanels = (): ((
dynamicVariablesWidgetIds,
} = variableConfig;
const tagFilters: TagFilterItem[] = [
getFiltersFromKeyValue(dynamicVariablesAttribute || '', `$${name}`),
];
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,
);
},
[],

View File

@ -2,8 +2,17 @@ import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { isArray } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { 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';
const baseLogsSelectedColumns = {
dataType: 'string',
@ -56,3 +65,65 @@ 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;
};