Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Feb 11, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
{
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/container/NewWidget/__test__/NewWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
17 changes: 6 additions & 11 deletions frontend/src/container/NewWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
getDefaultWidgetData,
getIsQueryModified,
handleQueryChange,
placeWidgetAtBottom,
} from './utils';

function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
Expand Down Expand Up @@ -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,
Expand Down
58 changes: 57 additions & 1 deletion frontend/src/container/NewWidget/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
};
};
17 changes: 7 additions & 10 deletions frontend/src/hooks/dashboard/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 || []),
{
Expand Down
Loading