diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 030c4f707e0..097463f946d 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -6,6 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { placeWidgetAtBottom } from 'container/NewWidget/utils'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; @@ -133,18 +134,14 @@ function WidgetGraphComponent({ (l) => l.i === widget.id, ); - // added the cloned panel on the top as it is given most priority when arranging - // in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel - const layout = [ - { - i: uuid, - w: originalPanelLayout?.w || 6, - x: 0, - h: originalPanelLayout?.h || 6, - y: 0, - }, - ...(selectedDashboard.data.layout || []), - ]; + const newLayoutItem = placeWidgetAtBottom( + uuid, + selectedDashboard?.data.layout || [], + originalPanelLayout?.w || 6, + originalPanelLayout?.h || 6, + ); + + const layout = [...(selectedDashboard.data.layout || []), newLayoutItem]; updateDashboardMutation.mutateAsync( { diff --git a/frontend/src/container/NewWidget/__test__/NewWidget.test.tsx b/frontend/src/container/NewWidget/__test__/NewWidget.test.tsx new file mode 100644 index 00000000000..bab32530d9d --- /dev/null +++ b/frontend/src/container/NewWidget/__test__/NewWidget.test.tsx @@ -0,0 +1,92 @@ +// This test suite covers several important scenarios: +// - Empty layout - widget should be placed at origin (0,0) +// - Empty layout with custom dimensions +// - Placing widget next to an existing widget when there's space in the last row +// - Placing widget at bottom when the last row is full +// - Handling multiple rows correctly +// - Handling widgets with different heights + +import { placeWidgetAtBottom } from '../utils'; + +describe('placeWidgetAtBottom', () => { + it('should place widget at (0,0) when layout is empty', () => { + const result = placeWidgetAtBottom('widget1', []); + expect(result).toEqual({ + i: 'widget1', + x: 0, + y: 0, + w: 6, + h: 6, + }); + }); + + it('should place widget at (0,0) with custom dimensions when layout is empty', () => { + const result = placeWidgetAtBottom('widget1', [], 4, 8); + expect(result).toEqual({ + i: 'widget1', + x: 0, + y: 0, + w: 4, + h: 8, + }); + }); + + it('should place widget next to existing widget in last row if space available', () => { + const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 }]; + const result = placeWidgetAtBottom('widget2', existingLayout); + expect(result).toEqual({ + i: 'widget2', + x: 6, + y: 0, + w: 6, + h: 6, + }); + }); + + it('should place widget at bottom when last row is full', () => { + const existingLayout = [ + { i: 'widget1', x: 0, y: 0, w: 6, h: 6 }, + { i: 'widget2', x: 6, y: 0, w: 6, h: 6 }, + ]; + const result = placeWidgetAtBottom('widget3', existingLayout); + expect(result).toEqual({ + i: 'widget3', + x: 0, + y: 6, + w: 6, + h: 6, + }); + }); + + it('should handle multiple rows correctly', () => { + const existingLayout = [ + { i: 'widget1', x: 0, y: 0, w: 6, h: 6 }, + { i: 'widget2', x: 6, y: 0, w: 6, h: 6 }, + { i: 'widget3', x: 0, y: 6, w: 6, h: 6 }, + ]; + const result = placeWidgetAtBottom('widget4', existingLayout); + expect(result).toEqual({ + i: 'widget4', + x: 6, + y: 6, + w: 6, + h: 6, + }); + }); + + it('should handle widgets with different heights', () => { + const existingLayout = [ + { i: 'widget1', x: 0, y: 0, w: 6, h: 8 }, + { i: 'widget2', x: 6, y: 0, w: 6, h: 4 }, + ]; + const result = placeWidgetAtBottom('widget3', existingLayout); + // y = 2 here as later the react-grid-layout will add 2px to the y value while adjusting the layout + expect(result).toEqual({ + i: 'widget3', + x: 6, + y: 2, + w: 6, + h: 6, + }); + }); +}); diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 7b04ed74f78..e45c73b18b9 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -58,6 +58,7 @@ import { getDefaultWidgetData, getIsQueryModified, handleQueryChange, + placeWidgetAtBottom, } from './utils'; function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { @@ -363,20 +364,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { return; } - const widgetId = query.get('widgetId'); + const widgetId = query.get('widgetId') || ''; let updatedLayout = selectedDashboard.data.layout || []; + if (isNewDashboard) { - updatedLayout = [ - { - i: widgetId || '', - w: 6, - x: 0, - h: 6, - y: 0, - }, - ...updatedLayout, - ]; + const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout); + updatedLayout = [...updatedLayout, newLayoutItem]; } + const dashboard: Dashboard = { ...selectedDashboard, uuid: selectedDashboard.uuid, diff --git a/frontend/src/container/NewWidget/utils.ts b/frontend/src/container/NewWidget/utils.ts index cb498ef932a..8527b90ac5d 100644 --- a/frontend/src/container/NewWidget/utils.ts +++ b/frontend/src/container/NewWidget/utils.ts @@ -10,7 +10,8 @@ import { PANEL_TYPES_INITIAL_QUERY, } from 'container/NewDashboard/ComponentsSlider/constants'; import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config'; -import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es'; +import { cloneDeep, defaultTo, isEmpty, isEqual, set, unset } from 'lodash-es'; +import { Layout } from 'react-grid-layout'; import { Widgets } from 'types/api/dashboard/getAll'; import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; @@ -575,3 +576,58 @@ export const unitOptions = (columnUnit: string): DefaultOptionType[] => { options: getCategorySelectOptionByName(filteredCategory), })); }; + +export const placeWidgetAtBottom = ( + widgetId: string, + layout: Layout[], + widgetWidth?: number, + widgetHeight?: number, +): Layout => { + if (layout.length === 0) { + return { i: widgetId, x: 0, y: 0, w: widgetWidth || 6, h: widgetHeight || 6 }; + } + + // Find the maximum Y coordinate and height + const { maxY } = layout.reduce( + (acc, curr) => ({ + maxY: Math.max(acc.maxY, curr.y + curr.h), + }), + { maxY: 0 }, + ); + + // Check for available space in the last row + const lastRowWidgets = layout.filter((item) => item.y + item.h === maxY); + const occupiedXInLastRow = lastRowWidgets.reduce( + (acc, widget) => acc + widget.w, + 0, + ); + + // If there's space in the last row (total width < 12) + if (occupiedXInLastRow < 12) { + // Find the rightmost X coordinate in the last row + const maxXInLastRow = lastRowWidgets.reduce( + (acc, widget) => Math.max(acc, widget.x + widget.w), + 0, + ); + + // If there's enough space for a 6-width widget + if (maxXInLastRow + defaultTo(widgetWidth, 6) <= 12) { + return { + i: widgetId, + x: maxXInLastRow, + y: maxY - (widgetHeight || 6), // Align with the last row + w: widgetWidth || 6, + h: widgetHeight || 6, + }; + } + } + + // If no space in last row, place at the bottom + return { + i: widgetId, + x: 0, + y: maxY, + w: widgetWidth || 6, + h: widgetHeight || 6, + }; +}; diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index ed61e255819..1f602d95d5c 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -1,5 +1,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils'; +import { placeWidgetAtBottom } from 'container/NewWidget/utils'; import { Dashboard } from 'types/api/dashboard/getAll'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -22,20 +23,16 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( ...convertKeysToColumnFields(selectedColumns || []), ]; + const newLayoutItem = placeWidgetAtBottom( + widgetId, + dashboard?.data?.layout || [], + ); + return { ...dashboard, data: { ...dashboard.data, - layout: [ - { - i: widgetId, - w: 6, - x: 0, - h: 6, - y: 0, - }, - ...(dashboard?.data?.layout || []), - ], + layout: [...(dashboard?.data?.layout || []), newLayoutItem], widgets: [ ...(dashboard?.data?.widgets || []), {