feat: added new and cloned panel at the bottom of the page (#6993)

* feat: added new and cloned panel at the bottom of the page

* feat: added common util and took possible space available in last row in account

* feat: added changes for empty layout

* feat: added different test cases

* feat: remove console.log

* feat: added default value to widgetWidth
This commit is contained in:
SagarRajput-7 2025-02-11 17:01:17 +05:30 committed by GitHub
parent 9a75e27ec3
commit 02c2b55d5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 34 deletions

View File

@ -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(
{

View File

@ -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,
});
});
});

View File

@ -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,

View File

@ -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,
};
};

View File

@ -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 || []),
{