From 191d9b0648bc084cf0d4adbfc00ca1238721cf90 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 30 Apr 2024 14:36:47 +0530 Subject: [PATCH] feat: introducing collapsable rows for dashboards (#4806) * feat: dashboard panel grouping initial setup * feat: added panel map to the dashboard response and subsequent types for the same * feat: added panel map to the dashboard response and subsequent types for the same * feat: added settings modal * feat: handle panel collapse and open changes * feat: handle creating panel map when dashboard layout changes * feat: handle creating panel map when dashboard layout changes * feat: refactor code * feat: handle multiple collapsable rows * fix: type issues * feat: handle row collapse button and scroll * feat: handle y axis movement for rows * feat: handle delete row * feat: handle settings name change * feat: disable collapse/uncollapse when dashboard loading to avoid async states * feat: decrease the height of the grouping row * fix: row height management * fix: handle empty row case * feat: remove resize handle from the row * feat: handle re-arrangement of panels * feat: increase height of default new widget * feat: added safety checks --- frontend/public/locales/en-GB/dashboard.json | 1 + frontend/public/locales/en/dashboard.json | 1 + frontend/src/constants/queryBuilder.ts | 5 + .../GridCard/WidgetGraphComponent.tsx | 2 +- .../GridCardLayout/GridCardLayout.tsx | 435 +++++++++++++++++- .../src/container/GridCardLayout/config.ts | 2 +- .../src/container/GridCardLayout/styles.ts | 11 + frontend/src/container/NewWidget/index.tsx | 4 +- frontend/src/hooks/dashboard/utils.ts | 2 +- frontend/src/pages/DashboardWidget/index.tsx | 3 +- .../src/providers/Dashboard/Dashboard.tsx | 27 +- frontend/src/providers/Dashboard/types.ts | 2 + frontend/src/providers/Dashboard/util.ts | 18 +- frontend/src/types/api/dashboard/getAll.ts | 12 +- 14 files changed, 502 insertions(+), 23 deletions(-) diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index bc7969d053..49a0ff39dd 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -16,6 +16,7 @@ "new_dashboard_title": "Sample Title", "layout_saved_successfully": "Layout saved successfully", "add_panel": "Add Panel", + "add_row": "Add Row", "save_layout": "Save Layout", "variable_updated_successfully": "Variable updated successfully", "error_while_updating_variable": "Error while updating variable", diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index 9c0529cd73..c214c027c2 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -16,6 +16,7 @@ "new_dashboard_title": "Sample Title", "layout_saved_successfully": "Layout saved successfully", "add_panel": "Add Panel", + "add_row": "Add Row", "save_layout": "Save Layout", "full_view": "Full Screen View", "variable_updated_successfully": "Variable updated successfully", diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index c1603e02e5..b5feadc9f2 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -289,6 +289,11 @@ export enum PANEL_TYPES { EMPTY_WIDGET = 'EMPTY_WIDGET', } +// eslint-disable-next-line @typescript-eslint/naming-convention +export enum PANEL_GROUP_TYPES { + ROW = 'row', +} + // eslint-disable-next-line @typescript-eslint/naming-convention export enum ATTRIBUTE_TYPES { SUM = 'Sum', diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 505a1864bc..35fb0e878a 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -135,7 +135,7 @@ function WidgetGraphComponent({ i: uuid, w: 6, x: 0, - h: 3, + h: 6, y: 0, }, ]; diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 24f13ec9e2..f4c8b34743 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -1,11 +1,13 @@ import './GridCardLayout.styles.scss'; import { PlusOutlined } from '@ant-design/icons'; -import { Flex, Tooltip } from 'antd'; +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 { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; -import { PANEL_TYPES } from 'constants/queryBuilder'; +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'; @@ -13,12 +15,21 @@ 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 } from 'lucide-react'; +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 { Layout } from 'react-grid-layout'; +import { ItemCallback, Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -28,6 +39,7 @@ 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'; @@ -46,6 +58,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { selectedDashboard, layouts, setLayouts, + panelMap, + setPanelMap, setSelectedDashboard, isDashboardLocked, } = useDashboard(); @@ -66,6 +80,26 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { 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(); @@ -88,7 +122,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { ); useEffect(() => { - setDashboardLayout(layouts); + setDashboardLayout(sortLayout(layouts)); }, [layouts]); const onSaveHandler = (): void => { @@ -98,6 +132,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { ...selectedDashboard, data: { ...selectedDashboard.data, + panelMap: { ...currentPanelMap }, layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), }, uuid: selectedDashboard.uuid, @@ -107,8 +142,9 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { onSuccess: (updatedDashboard) => { if (updatedDashboard.payload) { if (updatedDashboard.payload.data.layout) - setLayouts(updatedDashboard.payload.data.layout); + setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setSelectedDashboard(updatedDashboard.payload); + setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); } featureResponse.refetch(); @@ -131,7 +167,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { dashboardLayout, ); if (!isEqual(filterLayout, filterDashboardLayout)) { - setDashboardLayout(layout); + const updatedLayout = sortLayout(layout); + setDashboardLayout(updatedLayout); } }; @@ -168,6 +205,283 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { // 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 ( <> @@ -209,13 +523,23 @@ Thanks`} {t('dashboard:add_panel')} )} + {!isDashboardLocked && addPanelPermission && ( + + )} 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 +
); diff --git a/frontend/src/container/GridCardLayout/config.ts b/frontend/src/container/GridCardLayout/config.ts index 2801913df8..67c731e07b 100644 --- a/frontend/src/container/GridCardLayout/config.ts +++ b/frontend/src/container/GridCardLayout/config.ts @@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = { i: PANEL_TYPES.EMPTY_WIDGET, w: 6, x: 0, - h: 3, + h: 6, y: 0, }; diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index 224bb8d4de..ee4144b9c5 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -29,6 +29,17 @@ interface Props { export const CardContainer = styled.div` overflow: auto; + &.row-card { + .row-panel { + height: 100%; + display: flex; + justify-content: space-between; + background: var(--bg-ink-400); + align-items: center; + overflow: hidden; + } + } + &.enable-resize { :hover { .react-resizable-handle { diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index cbdcd36097..ca7ec82ff2 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -104,7 +104,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { return defaultTo( selectedWidget, getDefaultWidgetData(widgetId || '', selectedGraph), - ); + ) as Widgets; }, [query, selectedGraph, widgets]); const [selectedWidget, setSelectedWidget] = useState(getWidget()); @@ -257,7 +257,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { i: widgetId || '', w: 6, x: 0, - h: 3, + h: 6, y: 0, }, ...updatedLayout, diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index a66204ae62..ad3fadd8b9 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -16,7 +16,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( i: widgetId, w: 6, x: 0, - h: 3, + h: 6, y: 0, }, ...(dashboard?.data?.layout || []), diff --git a/frontend/src/pages/DashboardWidget/index.tsx b/frontend/src/pages/DashboardWidget/index.tsx index d32ded450b..2e161497a2 100644 --- a/frontend/src/pages/DashboardWidget/index.tsx +++ b/frontend/src/pages/DashboardWidget/index.tsx @@ -9,6 +9,7 @@ import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useEffect, useState } from 'react'; import { generatePath, useLocation, useParams } from 'react-router-dom'; +import { Widgets } from 'types/api/dashboard/getAll'; function DashboardWidget(): JSX.Element | null { const { search } = useLocation(); @@ -24,7 +25,7 @@ function DashboardWidget(): JSX.Element | null { const { data } = selectedDashboard || {}; const { widgets } = data || {}; - const selectedWidget = widgets?.find((e) => e.id === widgetId); + const selectedWidget = widgets?.find((e) => e.id === widgetId) as Widgets; useEffect(() => { const params = new URLSearchParams(search); diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx index 1428e00e21..0f8307aaa1 100644 --- a/frontend/src/providers/Dashboard/Dashboard.tsx +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -10,6 +10,7 @@ import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashbo import useAxiosError from 'hooks/useAxiosError'; import useTabVisibility from 'hooks/useTabFocus'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; +import { defaultTo } from 'lodash-es'; import isEqual from 'lodash-es/isEqual'; import isUndefined from 'lodash-es/isUndefined'; import omitBy from 'lodash-es/omitBy'; @@ -37,6 +38,7 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { v4 as generateUUID } from 'uuid'; import { IDashboardContext } from './types'; +import { sortLayout } from './util'; const DashboardContext = createContext({ isDashboardSliderOpen: false, @@ -47,6 +49,8 @@ const DashboardContext = createContext({ selectedDashboard: {} as Dashboard, dashboardId: '', layouts: [], + panelMap: {}, + setPanelMap: () => {}, setLayouts: () => {}, setSelectedDashboard: () => {}, updatedTimeRef: {} as React.MutableRefObject, @@ -94,6 +98,10 @@ export function DashboardProvider({ const [layouts, setLayouts] = useState([]); + const [panelMap, setPanelMap] = useState< + Record + >({}); + const { isLoggedIn } = useSelector((state) => state.app); const dashboardId = @@ -199,7 +207,9 @@ export function DashboardProvider({ dashboardRef.current = updatedDashboardData; - setLayouts(getUpdatedLayout(updatedDashboardData.data.layout)); + setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData.data.layout))); + + setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {})); } if ( @@ -235,7 +245,11 @@ export function DashboardProvider({ updatedTimeRef.current = dayjs(updatedDashboardData.updated_at); - setLayouts(getUpdatedLayout(updatedDashboardData.data.layout)); + setLayouts( + sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)), + ); + + setPanelMap(defaultTo(updatedDashboardData.data.panelMap, {})); }, }); @@ -256,7 +270,11 @@ export function DashboardProvider({ updatedDashboardData.data.layout, ) ) { - setLayouts(getUpdatedLayout(updatedDashboardData.data.layout)); + setLayouts( + sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)), + ); + + setPanelMap(defaultTo(updatedDashboardData.data.panelMap, {})); } } }, @@ -323,7 +341,9 @@ export function DashboardProvider({ selectedDashboard, dashboardId, layouts, + panelMap, setLayouts, + setPanelMap, setSelectedDashboard, updatedTimeRef, setToScrollWidgetId, @@ -339,6 +359,7 @@ export function DashboardProvider({ selectedDashboard, dashboardId, layouts, + panelMap, toScrollWidgetId, updateLocalStorageDashboardVariables, currentDashboard, diff --git a/frontend/src/providers/Dashboard/types.ts b/frontend/src/providers/Dashboard/types.ts index f822fd39a6..6f78d804b1 100644 --- a/frontend/src/providers/Dashboard/types.ts +++ b/frontend/src/providers/Dashboard/types.ts @@ -12,6 +12,8 @@ export interface IDashboardContext { selectedDashboard: Dashboard | undefined; dashboardId: string; layouts: Layout[]; + panelMap: Record; + setPanelMap: React.Dispatch>>; setLayouts: React.Dispatch>; setSelectedDashboard: React.Dispatch< React.SetStateAction diff --git a/frontend/src/providers/Dashboard/util.ts b/frontend/src/providers/Dashboard/util.ts index 40ec15f75e..e5805f4dc1 100644 --- a/frontend/src/providers/Dashboard/util.ts +++ b/frontend/src/providers/Dashboard/util.ts @@ -1,22 +1,34 @@ +import { Layout } from 'react-grid-layout'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; export const getPreviousWidgets = ( selectedDashboard: Dashboard, selectedWidgetIndex: number, ): Widgets[] => - selectedDashboard.data.widgets?.slice(0, selectedWidgetIndex || 0) || []; + (selectedDashboard.data.widgets?.slice( + 0, + selectedWidgetIndex || 0, + ) as Widgets[]) || []; export const getNextWidgets = ( selectedDashboard: Dashboard, selectedWidgetIndex: number, ): Widgets[] => - selectedDashboard.data.widgets?.slice( + (selectedDashboard.data.widgets?.slice( (selectedWidgetIndex || 0) + 1, // this is never undefined selectedDashboard.data.widgets?.length, - ) || []; + ) as Widgets[]) || []; export const getSelectedWidgetIndex = ( selectedDashboard: Dashboard, widgetId: string | null, ): number => selectedDashboard.data.widgets?.findIndex((e) => e.id === widgetId) || 0; + +export const sortLayout = (layout: Layout[]): Layout[] => + [...layout].sort((a, b) => { + if (a.y === b.y) { + return a.x - b.x; + } + return a.y - b.y; + }); diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index ba23e55186..e7faf83023 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -1,4 +1,4 @@ -import { PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { ReactNode } from 'react'; @@ -59,13 +59,21 @@ export interface DashboardData { description?: string; tags?: string[]; name?: string; - widgets?: Widgets[]; + widgets?: Array; title: string; layout?: Layout[]; + panelMap?: Record; variables: Record; version?: string; } +export interface WidgetRow { + id: string; + panelTypes: PANEL_GROUP_TYPES; + title: ReactNode; + description: string; +} + export interface IBaseWidget { isStacked: boolean; id: string;