import './GridCardLayout.styles.scss'; import { PlusOutlined } from '@ant-design/icons'; import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd'; import { useForm } from 'antd/es/form/Form'; import cx from 'classnames'; import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { themeColors } from 'constants/theme'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; import { defaultTo } from 'lodash-es'; import isEqual from 'lodash-es/isEqual'; import { FullscreenIcon, GripVertical, MoveDown, MoveUp, Settings, Trash2, } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { sortLayout } from 'providers/Dashboard/util'; import { useCallback, useEffect, useState } from 'react'; import { FullScreen, useFullScreenHandle } from 'react-full-screen'; import { ItemCallback, Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { ROLES, USER_ROLES } from 'types/roles'; import { ComponentTypes } from 'utils/permission'; import { v4 as uuid } from 'uuid'; import { EditMenuAction, ViewMenuAction } from './config'; import GridCard from './GridCard'; import { Button, ButtonContainer, Card, CardContainer, ReactGridLayout, } from './styles'; import { GraphLayoutProps } from './types'; import { removeUndefinedValuesFromLayout } from './utils'; function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { const { selectedDashboard, layouts, setLayouts, panelMap, setPanelMap, setSelectedDashboard, isDashboardLocked, } = useDashboard(); const { data } = selectedDashboard || {}; const handle = useFullScreenHandle(); const { pathname } = useLocation(); const dispatch = useDispatch(); const { widgets, variables } = data || {}; const { t } = useTranslation(['dashboard']); const { featureResponse, role, user } = useSelector( (state) => state.app, ); const isDarkMode = useIsDarkMode(); const [dashboardLayout, setDashboardLayout] = useState([]); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [currentSelectRowId, setCurrentSelectRowId] = useState( null, ); const [currentPanelMap, setCurrentPanelMap] = useState< Record >({}); useEffect(() => { setCurrentPanelMap(panelMap); }, [panelMap]); const [form] = useForm<{ title: string; }>(); const updateDashboardMutation = useUpdateDashboard(); const { notifications } = useNotifications(); const urlQuery = useUrlQuery(); let permissions: ComponentTypes[] = ['save_layout', 'add_panel']; if (isDashboardLocked) { permissions = ['edit_locked_dashboard', 'add_panel_locked_dashboard']; } const userRole: ROLES | null = selectedDashboard?.created_by === user?.email ? (USER_ROLES.AUTHOR as ROLES) : role; const [saveLayoutPermission, addPanelPermission] = useComponentPermission( permissions, userRole, ); useEffect(() => { setDashboardLayout(sortLayout(layouts)); }, [layouts]); const onSaveHandler = (): void => { if (!selectedDashboard) return; const updatedDashboard: Dashboard = { ...selectedDashboard, data: { ...selectedDashboard.data, panelMap: { ...currentPanelMap }, layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), }, uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutate(updatedDashboard, { onSuccess: (updatedDashboard) => { if (updatedDashboard.payload) { if (updatedDashboard.payload.data.layout) setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setSelectedDashboard(updatedDashboard.payload); setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); } featureResponse.refetch(); }, onError: () => { notifications.error({ message: SOMETHING_WENT_WRONG, }); }, }); }; const widgetActions = !isDashboardLocked ? [...ViewMenuAction, ...EditMenuAction] : [...ViewMenuAction]; const handleLayoutChange = (layout: Layout[]): void => { const filterLayout = removeUndefinedValuesFromLayout(layout); const filterDashboardLayout = removeUndefinedValuesFromLayout( dashboardLayout, ); if (!isEqual(filterLayout, filterDashboardLayout)) { const updatedLayout = sortLayout(layout); setDashboardLayout(updatedLayout); } }; const onDragSelect = useCallback( (start: number, end: number) => { const startTimestamp = Math.trunc(start); const endTimestamp = Math.trunc(end); urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; history.replace(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); } }, [dispatch, pathname, urlQuery], ); useEffect(() => { if ( dashboardLayout && Array.isArray(dashboardLayout) && dashboardLayout.length > 0 && !isEqual(layouts, dashboardLayout) && !isDashboardLocked && saveLayoutPermission && !updateDashboardMutation.isLoading ) { onSaveHandler(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dashboardLayout]); function handleAddRow(): void { if (!selectedDashboard) return; const id = uuid(); const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = { widgets: [], collapsed: false, }; const currentRowIdx = 0; for (let j = currentRowIdx; j < dashboardLayout.length; j++) { if (!currentPanelMap[dashboardLayout[j].i]) { newRowWidgetMap.widgets.push(dashboardLayout[j]); } else { break; } } const updatedDashboard: Dashboard = { ...selectedDashboard, data: { ...selectedDashboard.data, layout: [ { i: id, w: 12, minW: 12, minH: 1, maxH: 1, x: 0, h: 1, y: 0, }, ...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), ], panelMap: { ...currentPanelMap, [id]: newRowWidgetMap }, widgets: [ ...(selectedDashboard.data.widgets || []), { id, title: 'Sample Row', description: '', panelTypes: PANEL_GROUP_TYPES.ROW, }, ], }, uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutate(updatedDashboard, { // eslint-disable-next-line sonarjs/no-identical-functions onSuccess: (updatedDashboard) => { if (updatedDashboard.payload) { if (updatedDashboard.payload.data.layout) setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setSelectedDashboard(updatedDashboard.payload); setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); } featureResponse.refetch(); }, // eslint-disable-next-line sonarjs/no-identical-functions onError: () => { notifications.error({ message: SOMETHING_WENT_WRONG, }); }, }); } const handleRowSettingsClick = (id: string): void => { setIsSettingsModalOpen(true); setCurrentSelectRowId(id); }; const onSettingsModalSubmit = (): void => { const newTitle = form.getFieldValue('title'); if (!selectedDashboard) return; if (!currentSelectRowId) return; const currentWidget = selectedDashboard?.data?.widgets?.find( (e) => e.id === currentSelectRowId, ); if (!currentWidget) return; currentWidget.title = newTitle; const updatedWidgets = selectedDashboard?.data?.widgets?.filter( (e) => e.id !== currentSelectRowId, ); updatedWidgets?.push(currentWidget); const updatedSelectedDashboard: Dashboard = { ...selectedDashboard, data: { ...selectedDashboard.data, widgets: updatedWidgets, }, uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { onSuccess: (updatedDashboard) => { if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setSelectedDashboard && updatedDashboard.payload) { setSelectedDashboard(updatedDashboard.payload); } if (setPanelMap) setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); form.setFieldValue('title', ''); setIsSettingsModalOpen(false); setCurrentSelectRowId(null); featureResponse.refetch(); }, // eslint-disable-next-line sonarjs/no-identical-functions onError: () => { notifications.error({ message: SOMETHING_WENT_WRONG, }); }, }); }; // eslint-disable-next-line sonarjs/cognitive-complexity const handleRowCollapse = (id: string): void => { if (!selectedDashboard) return; const rowProperties = { ...currentPanelMap[id] }; const updatedPanelMap = { ...currentPanelMap }; let updatedDashboardLayout = [...dashboardLayout]; if (rowProperties.collapsed === true) { rowProperties.collapsed = false; const widgetsInsideTheRow = rowProperties.widgets; let maxY = 0; widgetsInsideTheRow.forEach((w) => { maxY = Math.max(maxY, w.y + w.h); }); const currentRowWidget = dashboardLayout.find((w) => w.i === id); if (currentRowWidget && widgetsInsideTheRow.length) { maxY -= currentRowWidget.h + currentRowWidget.y; } const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id); for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) { updatedDashboardLayout[j].y += maxY; if (updatedPanelMap[updatedDashboardLayout[j].i]) { updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[ updatedDashboardLayout[j].i // eslint-disable-next-line @typescript-eslint/no-loop-func ].widgets.map((w) => ({ ...w, y: w.y + maxY, })); } } updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow]; } else { rowProperties.collapsed = true; const currentIdx = dashboardLayout.findIndex((w) => w.i === id); let widgetsInsideTheRow: Layout[] = []; let isPanelMapUpdated = false; for (let j = currentIdx + 1; j < dashboardLayout.length; j++) { if (currentPanelMap[dashboardLayout[j].i]) { rowProperties.widgets = widgetsInsideTheRow; widgetsInsideTheRow = []; isPanelMapUpdated = true; break; } else { widgetsInsideTheRow.push(dashboardLayout[j]); } } if (!isPanelMapUpdated) { rowProperties.widgets = widgetsInsideTheRow; } let maxY = 0; widgetsInsideTheRow.forEach((w) => { maxY = Math.max(maxY, w.y + w.h); }); const currentRowWidget = dashboardLayout[currentIdx]; if (currentRowWidget && widgetsInsideTheRow.length) { maxY -= currentRowWidget.h + currentRowWidget.y; } for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) { updatedDashboardLayout[j].y += maxY; if (updatedPanelMap[updatedDashboardLayout[j].i]) { updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[ updatedDashboardLayout[j].i // eslint-disable-next-line @typescript-eslint/no-loop-func ].widgets.map((w) => ({ ...w, y: w.y + maxY, })); } } updatedDashboardLayout = updatedDashboardLayout.filter( (widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i), ); } setCurrentPanelMap((prev) => ({ ...prev, ...updatedPanelMap, [id]: { ...rowProperties, }, })); setDashboardLayout(sortLayout(updatedDashboardLayout)); }; const handleDragStop: ItemCallback = (_, oldItem, newItem): void => { if (currentPanelMap[oldItem.i]) { const differenceY = newItem.y - oldItem.y; const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({ ...w, y: w.y + differenceY, })); setCurrentPanelMap((prev) => ({ ...prev, [oldItem.i]: { ...prev[oldItem.i], widgets: widgetsInsideRow, }, })); } }; const handleRowDelete = (): void => { if (!selectedDashboard) return; if (!currentSelectRowId) return; const updatedWidgets = selectedDashboard?.data?.widgets?.filter( (e) => e.id !== currentSelectRowId, ); const updatedLayout = selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) || []; const updatedPanelMap = { ...currentPanelMap }; delete updatedPanelMap[currentSelectRowId]; const updatedSelectedDashboard: Dashboard = { ...selectedDashboard, data: { ...selectedDashboard.data, widgets: updatedWidgets, layout: updatedLayout, panelMap: updatedPanelMap, }, uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { onSuccess: (updatedDashboard) => { if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setSelectedDashboard && updatedDashboard.payload) { setSelectedDashboard(updatedDashboard.payload); } if (setPanelMap) setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); setIsDeleteModalOpen(false); setCurrentSelectRowId(null); featureResponse.refetch(); }, // eslint-disable-next-line sonarjs/no-identical-functions onError: () => { notifications.error({ message: SOMETHING_WENT_WRONG, }); }, }); }; return ( <> )} {!isDashboardLocked && addPanelPermission && ( )} {dashboardLayout.map((layout) => { const { i: id } = layout; const currentWidget = (widgets || [])?.find((e) => e.id === id); if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) { const rowWidgetProperties = currentPanelMap[id] || {}; return (
{rowWidgetProperties.collapsed && (
); } return ( ); })}
{ setIsSettingsModalOpen(false); setCurrentSelectRowId(null); }} >
widget.id === currentSelectRowId) ?.title as string, 'Sample Title', )} />
{ setIsDeleteModalOpen(false); setCurrentSelectRowId(null); }} onOk={(): void => handleRowDelete()} > Are you sure you want to delete this row
); } export default GraphLayout;