mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 11:29:03 +08:00
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:
parent
d22ecb9f7c
commit
42fad23cb0
@ -17,6 +17,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
handleToggleDashboardSlider,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const { user } = useAppContext();
|
||||
@ -34,6 +35,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
|
@ -133,7 +133,8 @@
|
||||
|
||||
.menu-content {
|
||||
.section-1 {
|
||||
.rename-btn {
|
||||
.rename-btn,
|
||||
.new-panel-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@ -150,6 +151,10 @@
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.rename-btn {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-2 {
|
||||
|
@ -65,6 +65,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
isDashboardLocked,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
const { pathname } = useLocation();
|
||||
@ -174,6 +175,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
setSelectedRowWidgetId(null);
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
|
@ -1,7 +1,12 @@
|
||||
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 { Layout } from 'react-grid-layout';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
interface WidgetRowHeaderProps {
|
||||
rowWidgetProperties: {
|
||||
@ -27,6 +32,23 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
id,
|
||||
} = props;
|
||||
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 (
|
||||
<Popover
|
||||
open={isRowSettingsOpen}
|
||||
@ -52,6 +74,20 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
Rename
|
||||
</Button>
|
||||
</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 && (
|
||||
<section className="section-2">
|
||||
<Button
|
||||
|
@ -100,6 +100,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
listSortOrder,
|
||||
setSelectedDashboard,
|
||||
handleToggleDashboardSlider,
|
||||
setSelectedRowWidgetId,
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
@ -157,6 +158,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
|
@ -6,7 +6,7 @@
|
||||
// - Handling multiple rows correctly
|
||||
// - Handling widgets with different heights
|
||||
|
||||
import { placeWidgetAtBottom } from '../utils';
|
||||
import { placeWidgetAtBottom, placeWidgetBetweenRows } from '../utils';
|
||||
|
||||
describe('placeWidgetAtBottom', () => {
|
||||
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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,11 @@ import logEvent from 'api/common/logEvent';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
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 { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
@ -20,7 +24,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
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 { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@ -59,6 +63,7 @@ import {
|
||||
getIsQueryModified,
|
||||
handleQueryChange,
|
||||
placeWidgetAtBottom,
|
||||
placeWidgetBetweenRows,
|
||||
} from './utils';
|
||||
|
||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
@ -66,6 +71,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
@ -367,11 +374,33 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const widgetId = query.get('widgetId') || '';
|
||||
let updatedLayout = selectedDashboard.data.layout || [];
|
||||
|
||||
if (isNewDashboard) {
|
||||
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
|
||||
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
|
||||
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 = {
|
||||
...selectedDashboard,
|
||||
uuid: selectedDashboard.uuid,
|
||||
@ -437,6 +466,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
updateDashboardMutation.mutateAsync(dashboard, {
|
||||
onSuccess: () => {
|
||||
setSelectedRowWidgetId(null);
|
||||
setSelectedDashboard(dashboard);
|
||||
setToScrollWidgetId(selectedWidget?.id || '');
|
||||
history.push({
|
||||
@ -449,16 +479,19 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedDashboard,
|
||||
query,
|
||||
isNewDashboard,
|
||||
preWidgets,
|
||||
selectedRowWidgetId,
|
||||
afterWidgets,
|
||||
selectedWidget,
|
||||
selectedTime.enum,
|
||||
graphType,
|
||||
currentQuery,
|
||||
afterWidgets,
|
||||
preWidgets,
|
||||
updateDashboardMutation,
|
||||
handleError,
|
||||
widgets,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
dashboardId,
|
||||
]);
|
||||
|
||||
|
@ -631,3 +631,43 @@ export const placeWidgetAtBottom = (
|
||||
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];
|
||||
};
|
||||
|
@ -71,6 +71,8 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
setVariablesToGetUpdated: () => {},
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: () => {},
|
||||
selectedRowWidgetId: '',
|
||||
setSelectedRowWidgetId: () => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@ -87,6 +89,10 @@ export function DashboardProvider({
|
||||
|
||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||
|
||||
const [selectedRowWidgetId, setSelectedRowWidgetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
@ -416,6 +422,8 @@ export function DashboardProvider({
|
||||
setVariablesToGetUpdated,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@ -435,6 +443,8 @@ export function DashboardProvider({
|
||||
setVariablesToGetUpdated,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -45,4 +45,6 @@ export interface IDashboardContext {
|
||||
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
dashboardQueryRangeCalled: boolean;
|
||||
setDashboardQueryRangeCalled: (value: boolean) => void;
|
||||
selectedRowWidgetId: string | null;
|
||||
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user