feat: added feat to add new panel in a section (#6999)

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

* feat: added different test cases

* feat: remove console.log

* feat: added default value to widgetWidth

* feat: added feat to add new panel in a section

* feat: added different test cases
This commit is contained in:
SagarRajput-7 2025-02-11 22:55:42 +05:30 committed by GitHub
parent d22ecb9f7c
commit 42fad23cb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 266 additions and 8 deletions

View File

@ -17,6 +17,7 @@ export default function DashboardEmptyState(): JSX.Element {
selectedDashboard, selectedDashboard,
isDashboardLocked, isDashboardLocked,
handleToggleDashboardSlider, handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard(); } = useDashboard();
const { user } = useAppContext(); const { user } = useAppContext();
@ -34,6 +35,7 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole); const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => { const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true); handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', { logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.uuid,

View File

@ -133,7 +133,8 @@
.menu-content { .menu-content {
.section-1 { .section-1 {
.rename-btn { .rename-btn,
.new-panel-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
@ -150,6 +151,10 @@
margin-inline-end: 0px; margin-inline-end: 0px;
} }
} }
.rename-btn {
padding-bottom: 10px;
}
} }
.section-2 { .section-2 {

View File

@ -65,6 +65,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked, isDashboardLocked,
dashboardQueryRangeCalled, dashboardQueryRangeCalled,
setDashboardQueryRangeCalled, setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
} = useDashboard(); } = useDashboard();
const { data } = selectedDashboard || {}; const { data } = selectedDashboard || {};
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -174,6 +175,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, { updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.payload) { if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout) if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setLayouts(sortLayout(updatedDashboard.payload.data.layout));

View File

@ -1,7 +1,12 @@
import { Button, Popover } from 'antd'; import { Button, Popover } from 'antd';
import { EllipsisIcon, PenLine, X } from 'lucide-react'; import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useState } from 'react'; import { useState } from 'react';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
interface WidgetRowHeaderProps { interface WidgetRowHeaderProps {
rowWidgetProperties: { rowWidgetProperties: {
@ -27,6 +32,23 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
id, id,
} = props; } = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false); const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);
return ( return (
<Popover <Popover
open={isRowSettingsOpen} open={isRowSettingsOpen}
@ -52,6 +74,20 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
Rename Rename
</Button> </Button>
</section> </section>
<section className="section-1">
<Button
className="new-panel-btn"
type="text"
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
}}
>
New Panel
</Button>
</section>
{!rowWidgetProperties.collapsed && ( {!rowWidgetProperties.collapsed && (
<section className="section-2"> <section className="section-2">
<Button <Button

View File

@ -100,6 +100,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
listSortOrder, listSortOrder,
setSelectedDashboard, setSelectedDashboard,
handleToggleDashboardSlider, handleToggleDashboardSlider,
setSelectedRowWidgetId,
handleDashboardLockToggle, handleDashboardLockToggle,
} = useDashboard(); } = useDashboard();
@ -157,6 +158,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole); const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => { const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true); handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', { logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.uuid,

View File

@ -6,7 +6,7 @@
// - Handling multiple rows correctly // - Handling multiple rows correctly
// - Handling widgets with different heights // - Handling widgets with different heights
import { placeWidgetAtBottom } from '../utils'; import { placeWidgetAtBottom, placeWidgetBetweenRows } from '../utils';
describe('placeWidgetAtBottom', () => { describe('placeWidgetAtBottom', () => {
it('should place widget at (0,0) when layout is empty', () => { it('should place widget at (0,0) when layout is empty', () => {
@ -90,3 +90,129 @@ describe('placeWidgetAtBottom', () => {
}); });
}); });
}); });
describe('placeWidgetBetweenRows', () => {
it('should return single widget layout when layout is empty', () => {
const result = placeWidgetBetweenRows('widget1', [], 'currentRow');
expect(result).toEqual([
{
i: 'widget1',
x: 0,
y: 0,
w: 6,
h: 6,
},
]);
});
it('should place widget at the end of the layout when no nextRowId is provided', () => {
const existingLayout = [
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
];
const result = placeWidgetBetweenRows('widget3', existingLayout, 'widget2');
expect(result).toEqual([
{ 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 },
]);
});
it('should place widget between current and next row', () => {
const existingLayout = [
{
h: 1,
i: "'widget1'",
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 0,
},
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
{
h: 1,
i: 'widget3',
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 7,
},
];
const result = placeWidgetBetweenRows(
'widget4',
existingLayout,
'widget1',
'widget3',
);
expect(result).toEqual([
{
h: 1,
i: "'widget1'",
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 0,
},
{
h: 6,
i: 'widget2',
w: 6,
x: 6,
y: 0,
},
{
h: 6,
i: 'widget4',
w: 6,
x: 0,
y: 6,
},
{
h: 1,
i: 'widget3',
maxH: 1,
minH: 1,
minW: 12,
moved: false,
static: false,
w: 12,
x: 0,
y: 7,
},
]);
});
it('should respect custom widget dimensions', () => {
const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 }];
const result = placeWidgetBetweenRows(
'widget2',
existingLayout,
'widget1',
null,
8,
3,
);
expect(result).toEqual([
{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 },
{ i: 'widget2', x: 0, y: 4, w: 8, h: 3 },
]);
});
});

View File

@ -7,7 +7,11 @@ import logEvent from 'api/common/logEvent';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import {
initialQueriesMap,
PANEL_GROUP_TYPES,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts'; import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants'; import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
@ -20,7 +24,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history'; import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es'; import { defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
@ -59,6 +63,7 @@ import {
getIsQueryModified, getIsQueryModified,
handleQueryChange, handleQueryChange,
placeWidgetAtBottom, placeWidgetAtBottom,
placeWidgetBetweenRows,
} from './utils'; } from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
@ -66,6 +71,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedDashboard, selectedDashboard,
setSelectedDashboard, setSelectedDashboard,
setToScrollWidgetId, setToScrollWidgetId,
selectedRowWidgetId,
setSelectedRowWidgetId,
} = useDashboard(); } = useDashboard();
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
@ -367,11 +374,33 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const widgetId = query.get('widgetId') || ''; const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || []; let updatedLayout = selectedDashboard.data.layout || [];
if (isNewDashboard) { if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout); const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem]; updatedLayout = [...updatedLayout, newLayoutItem];
} }
if (isNewDashboard && selectedRowWidgetId) {
// Find the next row by looking through remaining layout items
const currentIndex = updatedLayout.findIndex(
(e) => e.i === selectedRowWidgetId,
);
const nextRowIndex = updatedLayout.findIndex(
(item, index) =>
index > currentIndex &&
widgets?.find((w) => w.id === item.i)?.panelTypes ===
PANEL_GROUP_TYPES.ROW,
);
const nextRowId = nextRowIndex !== -1 ? updatedLayout[nextRowIndex].i : null;
const newLayoutItem = placeWidgetBetweenRows(
widgetId,
updatedLayout,
selectedRowWidgetId,
nextRowId,
);
updatedLayout = newLayoutItem;
}
const dashboard: Dashboard = { const dashboard: Dashboard = {
...selectedDashboard, ...selectedDashboard,
uuid: selectedDashboard.uuid, uuid: selectedDashboard.uuid,
@ -437,6 +466,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
updateDashboardMutation.mutateAsync(dashboard, { updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => { onSuccess: () => {
setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard); setSelectedDashboard(dashboard);
setToScrollWidgetId(selectedWidget?.id || ''); setToScrollWidgetId(selectedWidget?.id || '');
history.push({ history.push({
@ -449,16 +479,19 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedDashboard, selectedDashboard,
query, query,
isNewDashboard, isNewDashboard,
preWidgets, selectedRowWidgetId,
afterWidgets,
selectedWidget, selectedWidget,
selectedTime.enum, selectedTime.enum,
graphType, graphType,
currentQuery, currentQuery,
afterWidgets, preWidgets,
updateDashboardMutation, updateDashboardMutation,
handleError, handleError,
widgets,
setSelectedDashboard, setSelectedDashboard,
setToScrollWidgetId, setToScrollWidgetId,
setSelectedRowWidgetId,
dashboardId, dashboardId,
]); ]);

View File

@ -631,3 +631,43 @@ export const placeWidgetAtBottom = (
h: widgetHeight || 6, h: widgetHeight || 6,
}; };
}; };
export const placeWidgetBetweenRows = (
widgetId: string,
layout: Layout[],
_currentRowId: string,
nextRowId?: string | null,
widgetWidth?: number,
widgetHeight?: number,
): Layout[] => {
if (layout.length === 0) {
return [
{
i: widgetId,
x: 0,
y: 0,
w: widgetWidth || 6,
h: widgetHeight || 6,
},
];
}
const nextRowIndex = nextRowId
? layout.findIndex((item) => item.i === nextRowId)
: -1;
// slice the layout from current row to next row
const sectionWidgets =
nextRowIndex === -1 ? layout : layout.slice(0, nextRowIndex);
const newWidgetLayout = placeWidgetAtBottom(
widgetId,
sectionWidgets,
widgetWidth,
widgetHeight,
);
const remainingWidgets = nextRowIndex === -1 ? [] : layout.slice(nextRowIndex);
// add new layout in between the sectionWidgets and the rest of the layout
return [...sectionWidgets, newWidgetLayout, ...remainingWidgets];
};

View File

@ -71,6 +71,8 @@ const DashboardContext = createContext<IDashboardContext>({
setVariablesToGetUpdated: () => {}, setVariablesToGetUpdated: () => {},
dashboardQueryRangeCalled: false, dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {}, setDashboardQueryRangeCalled: () => {},
selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {},
}); });
interface Props { interface Props {
@ -87,6 +89,10 @@ export function DashboardProvider({
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false); const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [selectedRowWidgetId, setSelectedRowWidgetId] = useState<string | null>(
null,
);
const [ const [
dashboardQueryRangeCalled, dashboardQueryRangeCalled,
setDashboardQueryRangeCalled, setDashboardQueryRangeCalled,
@ -416,6 +422,8 @@ export function DashboardProvider({
setVariablesToGetUpdated, setVariablesToGetUpdated,
dashboardQueryRangeCalled, dashboardQueryRangeCalled,
setDashboardQueryRangeCalled, setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
}), }),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[ [
@ -435,6 +443,8 @@ export function DashboardProvider({
setVariablesToGetUpdated, setVariablesToGetUpdated,
dashboardQueryRangeCalled, dashboardQueryRangeCalled,
setDashboardQueryRangeCalled, setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
], ],
); );

View File

@ -45,4 +45,6 @@ export interface IDashboardContext {
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>; setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
dashboardQueryRangeCalled: boolean; dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void; setDashboardQueryRangeCalled: (value: boolean) => void;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
} }