Rearrange variables (#4187)

* feat: variable re-arrange

* feat: update variable update from dashboard description

* feat: update variable update from dashboard description

* feat: update custom variable dropdown values on change

* feat: handle dependent value updates to dashboard description

* feat: handle dependent 0th order variable update

* feat: update variable item test

* feat: transform variables data to support rearraging

* feat: update modal import

* feat: remove console logs

* feat: ts-ignore

* feat: show variable name in delete modal
This commit is contained in:
Yunus M 2023-12-15 13:10:02 +05:30 committed by GitHub
parent 418ab67d50
commit 1d014ab4f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 544 additions and 176 deletions

View File

@ -29,6 +29,9 @@
"dependencies": { "dependencies": {
"@ant-design/colors": "6.0.0", "@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0", "@ant-design/icons": "4.8.0",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@grafana/data": "^9.5.2", "@grafana/data": "^9.5.2",
"@mdx-js/loader": "2.3.0", "@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0", "@mdx-js/react": "2.3.0",

View File

@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
<DrawerContainer <DrawerContainer
title={drawerTitle} title={drawerTitle}
placement="right" placement="right"
width="50%" width="60%"
onClose={onClose} onClose={onClose}
open={visible} open={visible}
> >

View File

@ -0,0 +1,5 @@
.delete-variable-name {
font-weight: 700;
color: rgb(207, 19, 34);
font-style: italic;
}

View File

@ -18,10 +18,10 @@ import {
VariableQueryTypeArr, VariableQueryTypeArr,
VariableSortTypeArr, VariableSortTypeArr,
} from 'types/api/dashboard/getAll'; } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { variablePropsToPayloadVariables } from '../../../utils'; import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableViewMode } from '../types'; import { TVariableMode } from '../types';
import { LabelContainer, VariableItemRow } from './styles'; import { LabelContainer, VariableItemRow } from './styles';
const { Option } = Select; const { Option } = Select;
@ -30,9 +30,9 @@ interface VariableItemProps {
variableData: IDashboardVariable; variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>; existingVariables: Record<string, IDashboardVariable>;
onCancel: () => void; onCancel: () => void;
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void;
validateName: (arg0: string) => boolean; validateName: (arg0: string) => boolean;
variableViewMode: TVariableViewMode; mode: TVariableMode;
} }
function VariableItem({ function VariableItem({
variableData, variableData,
@ -40,7 +40,7 @@ function VariableItem({
onCancel, onCancel,
onSave, onSave,
validateName, validateName,
variableViewMode, mode,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const [variableName, setVariableName] = useState<string>( const [variableName, setVariableName] = useState<string>(
variableData.name || '', variableData.name || '',
@ -97,7 +97,7 @@ function VariableItem({
]); ]);
const handleSave = (): void => { const handleSave = (): void => {
const newVariableData: IDashboardVariable = { const variable: IDashboardVariable = {
name: variableName, name: variableName,
description: variableDescription, description: variableDescription,
type: queryType, type: queryType,
@ -111,16 +111,12 @@ function VariableItem({
selectedValue: (variableData.selectedValue || selectedValue: (variableData.selectedValue ||
variableTextboxValue) as never, variableTextboxValue) as never,
}), }),
modificationUUID: v4(), modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,
}; };
onSave(
variableName, onSave(mode, variable);
newVariableData,
(variableViewMode === 'EDIT' && variableName !== variableData.name
? variableData.name
: '') as string,
);
onCancel();
}; };
// Fetches the preview values for the SQL variable query // Fetches the preview values for the SQL variable query
@ -175,7 +171,6 @@ function VariableItem({
return ( return (
<div className="variable-item-container"> <div className="variable-item-container">
<div className="variable-item-content"> <div className="variable-item-content">
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
<VariableItemRow> <VariableItemRow>
<LabelContainer> <LabelContainer>
<Typography>Name</Typography> <Typography>Name</Typography>

View File

@ -1,20 +1,78 @@
import '../DashboardSettings.styles.scss';
import { blue, red } from '@ant-design/colors'; import { blue, red } from '@ant-design/colors';
import { PlusOutlined } from '@ant-design/icons'; import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Modal, Row, Space, Tag } from 'antd'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { ResizeTable } from 'components/ResizeTable'; import {
DndContext,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
// eslint-disable-next-line import/no-extraneous-dependencies
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 { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon, TrashIcon } from 'lucide-react'; import { PencilIcon, TrashIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableViewMode } from './types'; import { TVariableMode } from './types';
import VariableItem from './VariableItem/VariableItem'; import VariableItem from './VariableItem/VariableItem';
function TableRow({ children, ...props }: RowProps): JSX.Element {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id: props['data-row-key'],
});
const style: React.CSSProperties = {
...props.style,
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
transition,
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
{React.Children.map(children, (child) => {
if ((child as React.ReactElement).key === 'sort') {
return React.cloneElement(child as React.ReactElement, {
children: (
<MenuOutlined
ref={setActivatorNodeRef}
style={{ touchAction: 'none', cursor: 'move' }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...listeners}
/>
),
});
}
return child;
})}
</tr>
);
}
function VariablesSetting(): JSX.Element { function VariablesSetting(): JSX.Element {
const variableToDelete = useRef<string | null>(null); const variableToDelete = useRef<IDashboardVariable | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false); const [deleteVariableModal, setDeleteVariableModal] = useState(false);
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
@ -25,16 +83,15 @@ function VariablesSetting(): JSX.Element {
const { variables = {} } = selectedDashboard?.data || {}; const { variables = {} } = selectedDashboard?.data || {};
const variablesTableData = Object.keys(variables).map((variableName) => ({ const [variablesTableData, setVariablesTableData] = useState<any>([]);
key: variableName, const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
name: variableName, const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
...variables[variableName], Record<string, string>
})); >({});
const [ const [variableViewMode, setVariableViewMode] = useState<null | TVariableMode>(
variableViewMode, null,
setVariableViewMode, );
] = useState<null | TVariableViewMode>(null);
const [ const [
variableEditData, variableEditData,
@ -47,7 +104,7 @@ function VariablesSetting(): JSX.Element {
}; };
const onVariableViewModeEnter = ( const onVariableViewModeEnter = (
viewType: TVariableViewMode, viewType: TVariableMode,
varData: IDashboardVariable, varData: IDashboardVariable,
): void => { ): void => {
setVariableEditData(varData); setVariableEditData(varData);
@ -56,6 +113,41 @@ function VariablesSetting(): JSX.Element {
const updateMutation = useUpdateDashboard(); const updateMutation = useUpdateDashboard();
useEffect(() => {
const tableRowData = [];
const variableOrderArr = [];
const variableNamesMap = {};
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { order, id, name } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
if (name) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variableNamesMap[name] = name;
}
if (order) {
variableOrderArr.push(order);
}
}
tableRowData.sort((a, b) => a.order - b.order);
variableOrderArr.sort((a, b) => a - b);
setVariablesTableData(tableRowData);
setVariablesOrderArr(variableOrderArr);
setExistingVariableNamesMap(variableNamesMap);
}, [variables]);
const updateVariables = ( const updateVariables = (
updatedVariablesData: Dashboard['data']['variables'], updatedVariablesData: Dashboard['data']['variables'],
): void => { ): void => {
@ -89,34 +181,58 @@ function VariablesSetting(): JSX.Element {
); );
}; };
const getVariableOrder = (): number => {
if (variblesOrderArr && variblesOrderArr.length > 0) {
return variblesOrderArr[variblesOrderArr.length - 1] + 1;
}
return 0;
};
const onVariableSaveHandler = ( const onVariableSaveHandler = (
name: string, mode: TVariableMode,
variableData: IDashboardVariable, variableData: IDashboardVariable,
oldName: string,
): void => { ): void => {
if (!variableData.name) { const updatedVariableData = {
return; ...variableData,
order: variableData?.order >= 0 ? variableData.order : getVariableOrder(),
};
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
if (variable.id === updatedVariableData.id) {
return updatedVariableData;
}
return variable;
},
);
if (mode === 'ADD') {
newVariablesArr.push(updatedVariableData);
} }
const newVariables = { ...variables }; const variables = convertVariablesToDbFormat(newVariablesArr);
newVariables[name] = variableData;
if (oldName) { setVariablesTableData(newVariablesArr);
delete newVariables[oldName]; updateVariables(variables);
}
updateVariables(newVariables);
onDoneVariableViewMode(); onDoneVariableViewMode();
}; };
const onVariableDeleteHandler = (variableName: string): void => { const onVariableDeleteHandler = (variable: IDashboardVariable): void => {
variableToDelete.current = variableName; variableToDelete.current = variable;
setDeleteVariableModal(true); setDeleteVariableModal(true);
}; };
const handleDeleteConfirm = (): void => { const handleDeleteConfirm = (): void => {
const newVariables = { ...variables }; const newVariablesArr = variablesTableData.filter(
if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; (variable: IDashboardVariable) =>
updateVariables(newVariables); variable.id !== variableToDelete?.current?.id,
);
const updatedVariables = convertVariablesToDbFormat(newVariablesArr);
updateVariables(updatedVariables);
variableToDelete.current = null; variableToDelete.current = null;
setDeleteVariableModal(false); setDeleteVariableModal(false);
}; };
@ -125,31 +241,36 @@ function VariablesSetting(): JSX.Element {
setDeleteVariableModal(false); setDeleteVariableModal(false);
}; };
const validateVariableName = (name: string): boolean => !variables[name]; const validateVariableName = (name: string): boolean =>
!existingVariableNamesMap[name];
const columns = [ const columns = [
{
key: 'sort',
width: '10%',
},
{ {
title: 'Variable', title: 'Variable',
dataIndex: 'name', dataIndex: 'name',
width: 100, width: '40%',
key: 'name', key: 'name',
}, },
{ {
title: 'Description', title: 'Description',
dataIndex: 'description', dataIndex: 'description',
width: 100, width: '35%',
key: 'description', key: 'description',
}, },
{ {
title: 'Actions', title: 'Actions',
width: 50, width: '15%',
key: 'action', key: 'action',
render: (_: IDashboardVariable): JSX.Element => ( render: (variable: IDashboardVariable): JSX.Element => (
<Space> <Space>
<Button <Button
type="text" type="text"
style={{ padding: 8, cursor: 'pointer', color: blue[5] }} style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', _)} onClick={(): void => onVariableViewModeEnter('EDIT', variable)}
> >
<PencilIcon size={14} /> <PencilIcon size={14} />
</Button> </Button>
@ -157,7 +278,9 @@ function VariablesSetting(): JSX.Element {
type="text" type="text"
style={{ padding: 8, color: red[6], cursor: 'pointer' }} style={{ padding: 8, color: red[6], cursor: 'pointer' }}
onClick={(): void => { onClick={(): void => {
if (_.name) onVariableDeleteHandler(_.name); if (variable) {
onVariableDeleteHandler(variable);
}
}} }}
> >
<TrashIcon size={14} /> <TrashIcon size={14} />
@ -167,6 +290,51 @@ function VariablesSetting(): JSX.Element {
}, },
]; ];
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = ({ active, over }: DragEndEvent): void => {
if (active.id !== over?.id) {
const activeIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier }) => i.key === active.id,
);
const overIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier | undefined }) => i.key === over?.id,
);
const updatedVariables: IDashboardVariable[] = arrayMove(
variablesTableData,
activeIndex,
overIndex,
);
const reArrangedVariables = {};
for (let index = 0; index < updatedVariables.length; index += 1) {
const variableName = updatedVariables[index].name;
if (variableName) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reArrangedVariables[variableName] = {
...updatedVariables[index],
order: index,
};
}
}
updateVariables(reArrangedVariables);
setVariablesTableData(updatedVariables);
}
};
return ( return (
<> <>
{variableViewMode ? ( {variableViewMode ? (
@ -176,11 +344,17 @@ function VariablesSetting(): JSX.Element {
onSave={onVariableSaveHandler} onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode} onCancel={onDoneVariableViewMode}
validateName={validateVariableName} validateName={validateVariableName}
variableViewMode={variableViewMode} mode={variableViewMode}
/> />
) : ( ) : (
<> <>
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}> <Row
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
padding: '0.5rem 0',
}}
>
<Button <Button
data-testid="add-new-variable" data-testid="add-new-variable"
type="primary" type="primary"
@ -192,7 +366,28 @@ function VariablesSetting(): JSX.Element {
</Button> </Button>
</Row> </Row>
<ResizeTable columns={columns} dataSource={variablesTableData} /> <DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={onDragEnd}
>
<SortableContext
// rowKey array
items={variablesTableData.map((variable: { key: any }) => variable.key)}
>
<Table
components={{
body: {
row: TableRow,
},
}}
rowKey="key"
columns={columns}
pagination={false}
dataSource={variablesTableData}
/>
</SortableContext>
</DndContext>
</> </>
)} )}
<Modal <Modal
@ -202,8 +397,13 @@ function VariablesSetting(): JSX.Element {
onOk={handleDeleteConfirm} onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel} onCancel={handleDeleteCancel}
> >
Are you sure you want to delete variable{' '} <Typography.Text>
<Tag>{variableToDelete.current}</Tag>? Are you sure you want to delete variable{' '}
<span className="delete-variable-name">
{variableToDelete?.current?.name}
</span>
?
</Typography.Text>
</Modal> </Modal>
</> </>
); );

View File

@ -1 +1,7 @@
export type TVariableViewMode = 'EDIT' | 'ADD'; export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD';
export const VariableModes = {
VIEW: 'VIEW',
EDIT: 'EDIT',
ADD: 'ADD',
};

View File

@ -1,14 +1,14 @@
import { Row } from 'antd'; import { Row } from 'antd';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { map, sortBy } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { convertVariablesToDbFormat } from './util';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null { function DashboardVariableSelection(): JSX.Element | null {
@ -21,8 +21,32 @@ function DashboardVariableSelection(): JSX.Element | null {
const [update, setUpdate] = useState<boolean>(false); const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>(''); const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
useEffect(() => {
if (variables) {
const tableRowData = [];
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { id } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
}
}, [variables]);
const onVarChanged = (name: string): void => { const onVarChanged = (name: string): void => {
setLastUpdatedVar(name); setLastUpdatedVar(name);
setUpdate(!update); setUpdate(!update);
@ -64,40 +88,56 @@ function DashboardVariableSelection(): JSX.Element | null {
const onValueUpdate = ( const onValueUpdate = (
name: string, name: string,
id: string,
value: IDashboardVariable['selectedValue'], value: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
): void => { ): void => {
const updatedVariablesData = { ...variables }; if (id) {
updatedVariablesData[name].selectedValue = value; const newVariablesArr = variablesTableData.map(
updatedVariablesData[name].allSelected = allSelected; (variable: IDashboardVariable) => {
const variableCopy = { ...variable };
console.log('onValue Update', name); if (variableCopy.id === id) {
variableCopy.selectedValue = value;
variableCopy.allSelected = allSelected;
}
if (role !== 'VIEWER' && selectedDashboard) { return variableCopy;
updateVariables(name, updatedVariablesData); },
);
const variables = convertVariablesToDbFormat(newVariablesArr);
if (role !== 'VIEWER' && selectedDashboard) {
updateVariables(name, variables);
}
onVarChanged(name);
setUpdate(!update);
} }
onVarChanged(name);
setUpdate(!update);
}; };
if (!variables) { if (!variables) {
return null; return null;
} }
const variablesKeys = sortBy(Object.keys(variables)); const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return ( return (
<Row> <Row>
{variablesKeys && {orderBasedSortedVariables &&
map(variablesKeys, (variableName) => ( Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) => (
<VariableItem <VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`} key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables} existingVariables={variables}
lastUpdatedVar={lastUpdatedVar} lastUpdatedVar={lastUpdatedVar}
variableData={{ variableData={{
name: variableName, name: variable.name,
...variables[variableName], ...variable,
change: update, change: update,
}} }}
onValueUpdate={onValueUpdate} onValueUpdate={onValueUpdate}

View File

@ -14,6 +14,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
const mockVariableData: IDashboardVariable = { const mockVariableData: IDashboardVariable = {
id: 'test_variable',
description: 'Test Variable', description: 'Test Variable',
type: 'TEXTBOX', type: 'TEXTBOX',
textboxValue: 'defaultValue', textboxValue: 'defaultValue',
@ -95,6 +96,7 @@ describe('VariableItem', () => {
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); // expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
expect(mockOnValueUpdate).toHaveBeenCalledWith( expect(mockOnValueUpdate).toHaveBeenCalledWith(
'testVariable', 'testVariable',
'test_variable',
'newValue', 'newValue',
false, false,
); );

View File

@ -2,13 +2,14 @@ import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors'; import { orange } from '@ant-design/colors';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Select, Typography } from 'antd'; import { Input, Popover, Select, Tooltip, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import map from 'lodash-es/map'; import map from 'lodash-es/map';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { IDashboardVariable } from 'types/api/dashboard/getAll';
@ -27,6 +28,7 @@ interface VariableItemProps {
existingVariables: Record<string, IDashboardVariable>; existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: ( onValueUpdate: (
name: string, name: string,
id: string,
arg1: IDashboardVariable['selectedValue'], arg1: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
) => void; ) => void;
@ -48,6 +50,7 @@ function VariableItem({
onValueUpdate, onValueUpdate,
lastUpdatedVar, lastUpdatedVar,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const { isDashboardLocked } = useDashboard();
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[], [],
); );
@ -137,8 +140,9 @@ function VariableItem({
} else { } else {
[value] = newOptionsData; [value] = newOptionsData;
} }
if (variableData.name) {
onValueUpdate(variableData.name, value, allSelected); if (variableData && variableData?.name && variableData?.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} }
} }
@ -149,14 +153,13 @@ function VariableItem({
console.error(e); console.error(e);
} }
} else if (variableData.type === 'CUSTOM') { } else if (variableData.type === 'CUSTOM') {
setOptionsData( const optionsData = sortValues(
sortValues( commaValuesParser(variableData.customValue || ''),
commaValuesParser(variableData.customValue || ''), variableData.sort,
variableData.sort, ) as never;
) as never,
); setOptionsData(optionsData);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}; };
const { isLoading } = useQuery(getQueryKey(variableData), { const { isLoading } = useQuery(getQueryKey(variableData), {
@ -195,9 +198,9 @@ function VariableItem({
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0) (Array.isArray(value) && value.length === 0)
) { ) {
onValueUpdate(variableData.name, optionsData, true); onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else { } else {
onValueUpdate(variableData.name, value, false); onValueUpdate(variableData.name, variableData.id, value, false);
} }
}; };
@ -230,72 +233,79 @@ function VariableItem({
getOptions(null); getOptions(null);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [variableData.type, variableData.customValue]);
return ( return (
<VariableContainer> <Tooltip
<Typography.Text className="variable-name" ellipsis> placement="top"
${variableData.name} title={isDashboardLocked ? 'Dashboard is locked' : ''}
</Typography.Text> >
<VariableValue> <VariableContainer>
{variableData.type === 'TEXTBOX' ? ( <Typography.Text className="variable-name" ellipsis>
<Input ${variableData.name}
placeholder="Enter value" </Typography.Text>
bordered={false} <VariableValue>
value={variableValue} {variableData.type === 'TEXTBOX' ? (
onChange={(e): void => { <Input
setVaribleValue(e.target.value || ''); placeholder="Enter value"
}} disabled={isDashboardLocked}
style={{
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
}}
/>
) : (
!errorMessage &&
optionsData && (
<Select
value={selectValue}
onChange={handleChange}
bordered={false} bordered={false}
placeholder="Select value" value={variableValue}
mode={mode} onChange={(e): void => {
dropdownMatchSelectWidth={false} setVaribleValue(e.target.value || '');
style={SelectItemStyle} }}
loading={isLoading} style={{
showArrow width:
showSearch 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
data-testid="variable-select" }}
> />
{enableSelectAll && ( ) : (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}> !errorMessage &&
ALL optionsData && (
</Select.Option> <Select
)} value={selectValue}
{map(optionsData, (option) => ( onChange={handleChange}
<Select.Option bordered={false}
data-testid={`option-${option}`} placeholder="Select value"
key={option.toString()} mode={mode}
value={option} dropdownMatchSelectWidth={false}
> style={SelectItemStyle}
{option.toString()} loading={isLoading}
</Select.Option> showArrow
))} showSearch
</Select> data-testid="variable-select"
) disabled={isDashboardLocked}
)} >
{errorMessage && ( {enableSelectAll && (
<span style={{ margin: '0 0.5rem' }}> <Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
<Popover ALL
placement="top" </Select.Option>
content={<Typography>{errorMessage}</Typography>} )}
> {map(optionsData, (option) => (
<WarningOutlined style={{ color: orange[5] }} /> <Select.Option
</Popover> data-testid={`option-${option}`}
</span> key={option.toString()}
)} value={option}
</VariableValue> >
</VariableContainer> {option.toString()}
</Select.Option>
))}
</Select>
)
)}
{variableData.type !== 'TEXTBOX' && errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover
placement="top"
content={<Typography>{errorMessage}</Typography>}
>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</VariableValue>
</VariableContainer>
</Tooltip>
); );
} }

View File

@ -1,3 +1,5 @@
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual( export function areArraysEqual(
a: (string | number | boolean)[], a: (string | number | boolean)[],
b: (string | number | boolean)[], b: (string | number | boolean)[],
@ -14,3 +16,16 @@ export function areArraysEqual(
return true; return true;
} }
export const convertVariablesToDbFormat = (
variblesArr: IDashboardVariable[],
): Dashboard['data']['variables'] =>
variblesArr.reduce((result, obj: IDashboardVariable) => {
const { id } = obj;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line no-param-reassign
result[id] = obj;
return result;
}, {});

View File

@ -6,8 +6,10 @@ export function variablePropsToPayloadVariables(
): PayloadVariables { ): PayloadVariables {
const payloadVariables: PayloadVariables = {}; const payloadVariables: PayloadVariables = {};
Object.entries(variables).forEach(([key, value]) => { Object.entries(variables).forEach(([, value]) => {
payloadVariables[key] = value?.selectedValue; if (value?.name) {
payloadVariables[value.name] = value?.selectedValue;
}
}); });
return payloadVariables; return payloadVariables;

View File

@ -71,8 +71,8 @@ export default function ModuleStepsContainer({
} = useOnboardingContext(); } = useOnboardingContext();
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
const lastStepIndex = selectedModuleSteps.length - 1; const lastStepIndex = selectedModuleSteps.length - 1;
const isValidForm = (): boolean => { const isValidForm = (): boolean => {

View File

@ -20,9 +20,13 @@ export const getDashboardVariables = (
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,
}; };
Object.keys(variables).forEach((key) => {
variablesTuple[key] = variables[key].selectedValue; Object.entries(variables).forEach(([, value]) => {
if (value?.name) {
variablesTuple[value.name] = value?.selectedValue;
}
}); });
return variablesTuple; return variablesTuple;
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -1,4 +1,4 @@
import Modal from 'antd/es/modal'; import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get'; import getDashboard from 'api/dashboard/get';
import lockDashboardApi from 'api/dashboard/lockDashboard'; import lockDashboardApi from 'api/dashboard/lockDashboard';
import unlockDashboardApi from 'api/dashboard/unlockDashboard'; import unlockDashboardApi from 'api/dashboard/unlockDashboard';
@ -30,9 +30,10 @@ import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import { IDashboardContext } from './types'; import { IDashboardContext } from './types';
@ -102,6 +103,44 @@ export function DashboardProvider({
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
const dashboardRef = useRef<Dashboard>(); const dashboardRef = useRef<Dashboard>();
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = JSON.parse(JSON.stringify(data));
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
// eslint-disable-next-line no-restricted-syntax
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
const dashboardResponse = useQuery( const dashboardResponse = useQuery(
[REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params], [REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params],
{ {
@ -112,26 +151,27 @@ export function DashboardProvider({
}), }),
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onSuccess: (data) => { onSuccess: (data) => {
const updatedDate = dayjs(data.updated_at); const updatedDashboardData = transformDashboardVariables(data);
const updatedDate = dayjs(updatedDashboardData.updated_at);
setIsDashboardLocked(data?.isLocked || false); setIsDashboardLocked(updatedDashboardData?.isLocked || false);
// on first render // on first render
if (updatedTimeRef.current === null) { if (updatedTimeRef.current === null) {
setSelectedDashboard(data); setSelectedDashboard(updatedDashboardData);
updatedTimeRef.current = updatedDate; updatedTimeRef.current = updatedDate;
dashboardRef.current = data; dashboardRef.current = updatedDashboardData;
setLayouts(getUpdatedLayout(data.data.layout)); setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
} }
if ( if (
updatedTimeRef.current !== null && updatedTimeRef.current !== null &&
updatedDate.isAfter(updatedTimeRef.current) && updatedDate.isAfter(updatedTimeRef.current) &&
isVisible && isVisible &&
dashboardRef.current?.id === data.id dashboardRef.current?.id === updatedDashboardData.id
) { ) {
// show modal when state is out of sync // show modal when state is out of sync
const modal = onModal.confirm({ const modal = onModal.confirm({
@ -139,7 +179,7 @@ export function DashboardProvider({
title: t('dashboard_has_been_updated'), title: t('dashboard_has_been_updated'),
content: t('do_you_want_to_refresh_the_dashboard'), content: t('do_you_want_to_refresh_the_dashboard'),
onOk() { onOk() {
setSelectedDashboard(data); setSelectedDashboard(updatedDashboardData);
const { maxTime, minTime } = getMinMax( const { maxTime, minTime } = getMinMax(
globalTime.selectedTime, globalTime.selectedTime,
@ -156,32 +196,32 @@ export function DashboardProvider({
}, },
}); });
dashboardRef.current = data; dashboardRef.current = updatedDashboardData;
updatedTimeRef.current = dayjs(data.updated_at); updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
setLayouts(getUpdatedLayout(data.data.layout)); setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
}, },
}); });
modalRef.current = modal; modalRef.current = modal;
} else { } else {
// normal flow // normal flow
updatedTimeRef.current = dayjs(data.updated_at); updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
dashboardRef.current = data; dashboardRef.current = updatedDashboardData;
if (!isEqual(selectedDashboard, data)) { if (!isEqual(selectedDashboard, updatedDashboardData)) {
setSelectedDashboard(data); setSelectedDashboard(updatedDashboardData);
} }
if ( if (
!isEqual( !isEqual(
[omitBy(layouts, (value): boolean => isUndefined(value))[0]], [omitBy(layouts, (value): boolean => isUndefined(value))[0]],
data.data.layout, updatedDashboardData.data.layout,
) )
) { ) {
setLayouts(getUpdatedLayout(data.data.layout)); setLayouts(getUpdatedLayout(updatedDashboardData.data.layout));
} }
} }
}, },

View File

@ -14,6 +14,8 @@ export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const;
export type TSortVariableValuesType = typeof VariableSortTypeArr[number]; export type TSortVariableValuesType = typeof VariableSortTypeArr[number];
export interface IDashboardVariable { export interface IDashboardVariable {
id: string;
order?: any;
name?: string; // key will be the source of truth name?: string; // key will be the source of truth
description: string; description: string;
type: TVariableQueryType; type: TVariableQueryType;

View File

@ -2346,6 +2346,45 @@
resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@dnd-kit/accessibility@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz#1054e19be276b5f1154ced7947fc0cb5d99192e0"
integrity sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.1.0.tgz#e81a3d10d9eca5d3b01cbf054171273a3fe01def"
integrity sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==
dependencies:
"@dnd-kit/accessibility" "^3.1.0"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/modifiers@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz#229666dd4e8b9487f348035117f993af755b3db9"
integrity sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-8.0.0.tgz#086b7ac6723d4618a4ccb6f0227406d8a8862a96"
integrity sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
dependencies:
tslib "^2.0.0"
"@emotion/hash@^0.8.0": "@emotion/hash@^0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
@ -14821,6 +14860,11 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tsutils@^3.21.0: tsutils@^3.21.0:
version "3.21.0" version "3.21.0"
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"