diff --git a/frontend/package.json b/frontend/package.json index 64ac911fc4..f0edc5c959 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,9 @@ "dependencies": { "@ant-design/colors": "6.0.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", "@mdx-js/loader": "2.3.0", "@mdx-js/react": "2.3.0", diff --git a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx index 89daacf094..a436f1126d 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx @@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element { diff --git a/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss new file mode 100644 index 0000000000..d53e8c4070 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss @@ -0,0 +1,5 @@ +.delete-variable-name { + font-weight: 700; + color: rgb(207, 19, 34); + font-style: italic; +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx index 76f0464bd2..a9b0aea09f 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -18,10 +18,10 @@ import { VariableQueryTypeArr, VariableSortTypeArr, } from 'types/api/dashboard/getAll'; -import { v4 } from 'uuid'; +import { v4 as generateUUID } from 'uuid'; import { variablePropsToPayloadVariables } from '../../../utils'; -import { TVariableViewMode } from '../types'; +import { TVariableMode } from '../types'; import { LabelContainer, VariableItemRow } from './styles'; const { Option } = Select; @@ -30,9 +30,9 @@ interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; onCancel: () => void; - onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; + onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void; validateName: (arg0: string) => boolean; - variableViewMode: TVariableViewMode; + mode: TVariableMode; } function VariableItem({ variableData, @@ -40,7 +40,7 @@ function VariableItem({ onCancel, onSave, validateName, - variableViewMode, + mode, }: VariableItemProps): JSX.Element { const [variableName, setVariableName] = useState( variableData.name || '', @@ -97,7 +97,7 @@ function VariableItem({ ]); const handleSave = (): void => { - const newVariableData: IDashboardVariable = { + const variable: IDashboardVariable = { name: variableName, description: variableDescription, type: queryType, @@ -111,16 +111,12 @@ function VariableItem({ selectedValue: (variableData.selectedValue || variableTextboxValue) as never, }), - modificationUUID: v4(), + modificationUUID: generateUUID(), + id: variableData.id || generateUUID(), + order: variableData.order, }; - onSave( - variableName, - newVariableData, - (variableViewMode === 'EDIT' && variableName !== variableData.name - ? variableData.name - : '') as string, - ); - onCancel(); + + onSave(mode, variable); }; // Fetches the preview values for the SQL variable query @@ -175,7 +171,6 @@ function VariableItem({ return (
- {/* Add Variable */} Name diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index de23e64068..4aa1bf9b22 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -1,20 +1,78 @@ +import '../DashboardSettings.styles.scss'; + import { blue, red } from '@ant-design/colors'; -import { PlusOutlined } from '@ant-design/icons'; -import { Button, Modal, Row, Space, Tag } from 'antd'; -import { ResizeTable } from 'components/ResizeTable'; +import { MenuOutlined, PlusOutlined } from '@ant-design/icons'; +import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; +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 { useNotifications } from 'hooks/useNotifications'; import { PencilIcon, TrashIcon } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; -import { TVariableViewMode } from './types'; +import { TVariableMode } from './types'; 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 + + {React.Children.map(children, (child) => { + if ((child as React.ReactElement).key === 'sort') { + return React.cloneElement(child as React.ReactElement, { + children: ( + + ), + }); + } + return child; + })} + + ); +} + function VariablesSetting(): JSX.Element { - const variableToDelete = useRef(null); + const variableToDelete = useRef(null); const [deleteVariableModal, setDeleteVariableModal] = useState(false); const { t } = useTranslation(['dashboard']); @@ -25,16 +83,15 @@ function VariablesSetting(): JSX.Element { const { variables = {} } = selectedDashboard?.data || {}; - const variablesTableData = Object.keys(variables).map((variableName) => ({ - key: variableName, - name: variableName, - ...variables[variableName], - })); + const [variablesTableData, setVariablesTableData] = useState([]); + const [variblesOrderArr, setVariablesOrderArr] = useState([]); + const [existingVariableNamesMap, setExistingVariableNamesMap] = useState< + Record + >({}); - const [ - variableViewMode, - setVariableViewMode, - ] = useState(null); + const [variableViewMode, setVariableViewMode] = useState( + null, + ); const [ variableEditData, @@ -47,7 +104,7 @@ function VariablesSetting(): JSX.Element { }; const onVariableViewModeEnter = ( - viewType: TVariableViewMode, + viewType: TVariableMode, varData: IDashboardVariable, ): void => { setVariableEditData(varData); @@ -56,6 +113,41 @@ function VariablesSetting(): JSX.Element { 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 = ( updatedVariablesData: Dashboard['data']['variables'], ): 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 = ( - name: string, + mode: TVariableMode, variableData: IDashboardVariable, - oldName: string, ): void => { - if (!variableData.name) { - return; + const updatedVariableData = { + ...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 }; - newVariables[name] = variableData; + const variables = convertVariablesToDbFormat(newVariablesArr); - if (oldName) { - delete newVariables[oldName]; - } - updateVariables(newVariables); + setVariablesTableData(newVariablesArr); + updateVariables(variables); onDoneVariableViewMode(); }; - const onVariableDeleteHandler = (variableName: string): void => { - variableToDelete.current = variableName; + const onVariableDeleteHandler = (variable: IDashboardVariable): void => { + variableToDelete.current = variable; setDeleteVariableModal(true); }; const handleDeleteConfirm = (): void => { - const newVariables = { ...variables }; - if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; - updateVariables(newVariables); + const newVariablesArr = variablesTableData.filter( + (variable: IDashboardVariable) => + variable.id !== variableToDelete?.current?.id, + ); + + const updatedVariables = convertVariablesToDbFormat(newVariablesArr); + + updateVariables(updatedVariables); variableToDelete.current = null; setDeleteVariableModal(false); }; @@ -125,31 +241,36 @@ function VariablesSetting(): JSX.Element { setDeleteVariableModal(false); }; - const validateVariableName = (name: string): boolean => !variables[name]; + const validateVariableName = (name: string): boolean => + !existingVariableNamesMap[name]; const columns = [ + { + key: 'sort', + width: '10%', + }, { title: 'Variable', dataIndex: 'name', - width: 100, + width: '40%', key: 'name', }, { title: 'Description', dataIndex: 'description', - width: 100, + width: '35%', key: 'description', }, { title: 'Actions', - width: 50, + width: '15%', key: 'action', - render: (_: IDashboardVariable): JSX.Element => ( + render: (variable: IDashboardVariable): JSX.Element => ( @@ -157,7 +278,9 @@ function VariablesSetting(): JSX.Element { type="text" style={{ padding: 8, color: red[6], cursor: 'pointer' }} onClick={(): void => { - if (_.name) onVariableDeleteHandler(_.name); + if (variable) { + onVariableDeleteHandler(variable); + } }} > @@ -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 ( <> {variableViewMode ? ( @@ -176,11 +344,17 @@ function VariablesSetting(): JSX.Element { onSave={onVariableSaveHandler} onCancel={onDoneVariableViewMode} validateName={validateVariableName} - variableViewMode={variableViewMode} + mode={variableViewMode} /> ) : ( <> - +