mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 04:05:56 +08:00
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:
parent
9a75e27ec3
commit
02c2b55d5e
@ -6,6 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -133,18 +134,14 @@ function WidgetGraphComponent({
|
|||||||
(l) => l.i === widget.id,
|
(l) => l.i === widget.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// added the cloned panel on the top as it is given most priority when arranging
|
const newLayoutItem = placeWidgetAtBottom(
|
||||||
// in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel
|
uuid,
|
||||||
const layout = [
|
selectedDashboard?.data.layout || [],
|
||||||
{
|
originalPanelLayout?.w || 6,
|
||||||
i: uuid,
|
originalPanelLayout?.h || 6,
|
||||||
w: originalPanelLayout?.w || 6,
|
);
|
||||||
x: 0,
|
|
||||||
h: originalPanelLayout?.h || 6,
|
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
...(selectedDashboard.data.layout || []),
|
|
||||||
];
|
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(
|
updateDashboardMutation.mutateAsync(
|
||||||
{
|
{
|
||||||
|
92
frontend/src/container/NewWidget/__test__/NewWidget.test.tsx
Normal file
92
frontend/src/container/NewWidget/__test__/NewWidget.test.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -58,6 +58,7 @@ import {
|
|||||||
getDefaultWidgetData,
|
getDefaultWidgetData,
|
||||||
getIsQueryModified,
|
getIsQueryModified,
|
||||||
handleQueryChange,
|
handleQueryChange,
|
||||||
|
placeWidgetAtBottom,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||||
@ -363,20 +364,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetId = query.get('widgetId');
|
const widgetId = query.get('widgetId') || '';
|
||||||
let updatedLayout = selectedDashboard.data.layout || [];
|
let updatedLayout = selectedDashboard.data.layout || [];
|
||||||
|
|
||||||
if (isNewDashboard) {
|
if (isNewDashboard) {
|
||||||
updatedLayout = [
|
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
|
||||||
{
|
updatedLayout = [...updatedLayout, newLayoutItem];
|
||||||
i: widgetId || '',
|
|
||||||
w: 6,
|
|
||||||
x: 0,
|
|
||||||
h: 6,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
...updatedLayout,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboard: Dashboard = {
|
const dashboard: Dashboard = {
|
||||||
...selectedDashboard,
|
...selectedDashboard,
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
|
@ -10,7 +10,8 @@ import {
|
|||||||
PANEL_TYPES_INITIAL_QUERY,
|
PANEL_TYPES_INITIAL_QUERY,
|
||||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||||
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
|
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 { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
@ -575,3 +576,58 @@ export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
|
|||||||
options: getCategorySelectOptionByName(filteredCategory),
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||||
|
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@ -22,20 +23,16 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
|
|||||||
...convertKeysToColumnFields(selectedColumns || []),
|
...convertKeysToColumnFields(selectedColumns || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const newLayoutItem = placeWidgetAtBottom(
|
||||||
|
widgetId,
|
||||||
|
dashboard?.data?.layout || [],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
data: {
|
data: {
|
||||||
...dashboard.data,
|
...dashboard.data,
|
||||||
layout: [
|
layout: [...(dashboard?.data?.layout || []), newLayoutItem],
|
||||||
{
|
|
||||||
i: widgetId,
|
|
||||||
w: 6,
|
|
||||||
x: 0,
|
|
||||||
h: 6,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
...(dashboard?.data?.layout || []),
|
|
||||||
],
|
|
||||||
widgets: [
|
widgets: [
|
||||||
...(dashboard?.data?.widgets || []),
|
...(dashboard?.data?.widgets || []),
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user