Compare commits

...

1 Commits

Author SHA1 Message Date
Abhi Kumar
d2775a0055 fix: added fix for dashboard dropdown menus 2026-05-21 11:38:22 +05:30
4 changed files with 326 additions and 238 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
@@ -14,15 +14,18 @@ import {
LockKeyhole,
PenLine,
Plus,
Trash2,
X,
} from '@signozhq/icons';
import { Card, Input, Modal, Popover, Tag, Tooltip } from 'antd';
import { Card, Input, Modal, Tag, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
import { useDeleteDashboardDialog } from 'container/ListOfDashboard/TableComponents/DeleteButton';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
@@ -90,12 +93,16 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
const selectedData = dashboardData
? {
...dashboardData.data,
uuid: dashboardData.id,
}
: ({} as DashboardData);
const selectedData = useMemo(
() =>
dashboardData
? {
...dashboardData.data,
uuid: dashboardData.id,
}
: ({} as DashboardData),
[dashboardData],
);
const { dashboardVariables } = useDashboardVariables();
const {
@@ -113,8 +120,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { user } = useAppContext();
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] =
useState<boolean>(false);
const [isRenameDashboardOpen, setIsRenameDashboardOpen] =
useState<boolean>(false);
@@ -155,10 +160,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
const handleLockDashboardToggle = useCallback((): void => {
handleDashboardLockToggle(!isDashboardLocked);
};
}, [handleDashboardLockToggle, isDashboardLocked]);
const onNameChangeHandler = (): void => {
if (!dashboardData) {
@@ -192,6 +196,124 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { t } = useTranslation(['dashboard', 'common']);
const {
openConfirmation: openDeleteConfirmation,
isDisabled: isDeleteDisabled,
tooltipContent: deleteTooltipContent,
contextHolder: deleteContextHolder,
} = useDeleteDashboardDialog({
createdBy: dashboardData?.createdBy || '',
name: dashboardData?.data.title || '',
id: String(dashboardData?.id) || '',
isLocked: isDashboardLocked,
routeToListPage: true,
});
const isIntegrationDashboard = dashboardData?.createdBy === 'integration';
const dashboardMenuItems: MenuItem[] = useMemo(() => {
const items: MenuItem[] = [];
if (isAuthor || user.role === USER_ROLES.ADMIN) {
items.push({
key: 'lock-unlock-dashboard',
icon: <LockKeyhole size={14} />,
label: isIntegrationDashboard ? (
<Tooltip title="Dashboards created by integrations cannot be unlocked">
<span>{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}</span>
</Tooltip>
) : (
<span>{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}</span>
),
disabled: isIntegrationDashboard,
onClick: handleLockDashboardToggle,
});
}
if (!isDashboardLocked && editDashboard) {
items.push({
key: 'rename-dashboard',
icon: <PenLine size={14} />,
label: 'Rename',
onClick: () => setIsRenameDashboardOpen(true),
});
}
items.push({
key: 'fullscreen',
icon: <Fullscreen size={14} />,
label: 'Full screen',
onClick: () => {
handle.enter();
},
});
items.push({ type: 'divider', key: 'sep-1' });
if (!isDashboardLocked && addPanelPermission) {
items.push({
key: 'new-section',
icon: <FolderKanban size={14} />,
label: 'New section',
onClick: () => setIsPanelNameModalOpen(true),
});
}
items.push({
key: 'export-json',
icon: <FileJson size={14} />,
label: 'Export JSON',
onClick: () => {
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
},
});
items.push({
key: 'copy-json',
icon: <ClipboardCopy size={14} />,
label: 'Copy as JSON',
onClick: () => {
setCopy(JSON.stringify(sanitizeDashboardData(selectedData), null, 2));
},
});
items.push({ type: 'divider', key: 'sep-2' });
items.push({
key: 'delete-dashboard',
icon: <Trash2 size={14} />,
label: deleteTooltipContent ? (
<Tooltip placement="left" title={deleteTooltipContent}>
<span>Delete Dashboard</span>
</Tooltip>
) : (
<span>Delete Dashboard</span>
),
danger: true,
disabled: isDeleteDisabled,
onClick: openDeleteConfirmation,
});
return items;
}, [
isAuthor,
user.role,
isIntegrationDashboard,
isDashboardLocked,
editDashboard,
addPanelPermission,
handleLockDashboardToggle,
handle,
selectedData,
setCopy,
deleteTooltipContent,
isDeleteDisabled,
openDeleteConfirmation,
]);
// used to set the initial value for the updatedTitle
// the context value is sometimes not available during the initial render
// due to which the updatedTitle is set to some previous value
@@ -360,114 +482,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
</div>
<div className="right-section">
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover
open={isDashboardSettingsOpen}
arrow={false}
onOpenChange={(visible): void => setIsDashbordSettingsOpen(visible)}
rootClassName="dashboard-settings"
content={
<div className="menu-content">
<section className="section-1">
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
<Tooltip
title={
dashboardData?.createdBy === 'integration' &&
'Dashboards created by integrations cannot be unlocked'
}
>
<Button
disabled={dashboardData?.createdBy === 'integration'}
onClick={handleLockDashboardToggle}
data-testid="lock-unlock-dashboard"
variant="ghost"
color="secondary"
prefix={<LockKeyhole size={14} />}
>
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
</Button>
</Tooltip>
)}
{!isDashboardLocked && editDashboard && (
<Button
onClick={(): void => {
setIsRenameDashboardOpen(true);
setIsDashbordSettingsOpen(false);
}}
variant="ghost"
color="secondary"
prefix={<PenLine size={14} />}
>
Rename
</Button>
)}
<Button
onClick={handle.enter}
variant="ghost"
color="secondary"
prefix={<Fullscreen size={14} />}
>
Full screen
</Button>
</section>
<section className="section-2">
{!isDashboardLocked && addPanelPermission && (
<Button
onClick={(): void => {
setIsPanelNameModalOpen(true);
setIsDashbordSettingsOpen(false);
}}
variant="ghost"
color="secondary"
prefix={<FolderKanban size={14} />}
>
New section
</Button>
)}
<Button
onClick={(): void => {
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
setIsDashbordSettingsOpen(false);
}}
variant="ghost"
color="secondary"
prefix={<FileJson size={14} />}
>
Export JSON
</Button>
<Button
onClick={(): void => {
setCopy(
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
);
setIsDashbordSettingsOpen(false);
}}
variant="ghost"
color="secondary"
prefix={<ClipboardCopy size={14} />}
>
Copy as JSON
</Button>
</section>
<section className="delete-dashboard">
<DeleteButton
createdBy={dashboardData?.createdBy || ''}
name={dashboardData?.data.title || ''}
id={String(dashboardData?.id) || ''}
isLocked={isDashboardLocked}
routeToListPage
/>
</section>
</div>
}
trigger="click"
placement="bottomRight"
>
<Dropdown menu={{ items: dashboardMenuItems }} align="end">
<Button
className="icons"
data-testid="options"
@@ -476,7 +491,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
size="icon"
prefix={<Ellipsis size={14} />}
/>
</Popover>
</Dropdown>
{deleteContextHolder}
{!isDashboardLocked && editDashboard && (
<>
<Button

View File

@@ -37,10 +37,6 @@ import cx from 'classnames';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import {
downloadObjectAsJson,
sanitizeDashboardData,
} from 'container/DashboardContainer/DashboardDescription/utils';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
// see more: https://github.com/lucide-icons/lucide/issues/94
@@ -60,19 +56,14 @@ import {
Check,
Clock4,
Ellipsis,
EllipsisVertical,
Expand,
ExternalLink,
FileJson,
Github,
HdmiPort,
LayoutGrid,
Link2,
Plus,
Radius,
RotateCw,
Search,
SquareArrowOutUpRight,
} from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
@@ -85,8 +76,6 @@ import {
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
@@ -94,7 +83,7 @@ import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import ImportJSON from './ImportJSON';
import { RequestDashboardBtn } from './RequestDashboardBtn';
import { DeleteButton } from './TableComponents/DeleteButton';
import { DashboardRowActions } from './TableComponents/DashboardRowActions';
import {
DashboardDynamicColumns,
DynamicColumns,
@@ -377,15 +366,6 @@ function DashboardsList(): JSX.Element {
});
};
const handleJsonExport = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
event.preventDefault();
downloadObjectAsJson(
sanitizeDashboardData({ ...dashboard, title: dashboard.name }),
dashboard.name,
);
};
return (
<div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action">
@@ -430,80 +410,11 @@ function DashboardsList(): JSX.Element {
</div>
{action && (
<Popover
content={
<div className="dashboard-action-content">
<section className="section-1">
<Button
className="action-btn"
onClick={onClickHandler}
variant="ghost"
color="secondary"
prefix={<Expand size={12} />}
>
View
</Button>
<Button
className="action-btn"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
}}
variant="ghost"
color="secondary"
prefix={<SquareArrowOutUpRight size={12} />}
>
Open in New Tab
</Button>
<Button
className="action-btn"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
}}
variant="ghost"
color="secondary"
prefix={<Link2 size={12} />}
>
Copy Link
</Button>
<Button
className="action-btn"
onClick={handleJsonExport}
variant="ghost"
color="secondary"
prefix={<FileJson size={12} />}
>
Export JSON
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboard-actions"
trigger="click"
>
<EllipsisVertical
className="dashboard-action-icon"
size={14}
data-testid="dashboard-action-icon"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
/>
</Popover>
<DashboardRowActions
dashboard={dashboard}
setCopy={setCopy}
safeNavigate={(linkTo): void => safeNavigate(linkTo)}
/>
)}
</div>
<div className="dashboard-details">

View File

@@ -0,0 +1,133 @@
import { useMemo } from 'react';
import {
EllipsisVertical,
Expand,
FileJson,
Link2,
SquareArrowOutUpRight,
Trash2,
} from '@signozhq/icons';
import { Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import {
downloadObjectAsJson,
sanitizeDashboardData,
} from 'container/DashboardContainer/DashboardDescription/utils';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import { Data } from '../DashboardsList';
import { useDeleteDashboardDialog } from './DeleteButton';
interface DashboardRowActionsProps {
dashboard: Data;
setCopy: (s: string) => void;
safeNavigate: (link: string) => void;
}
export function DashboardRowActions({
dashboard,
setCopy,
safeNavigate,
}: DashboardRowActionsProps): JSX.Element {
const link = `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const {
openConfirmation,
isDisabled: isDeleteDisabled,
tooltipContent: deleteTooltipContent,
contextHolder,
} = useDeleteDashboardDialog({
createdBy: dashboard.createdBy,
name: dashboard.name,
id: dashboard.id,
isLocked: dashboard.isLocked,
});
const items: MenuItem[] = useMemo(
() => [
{
key: 'view',
icon: <Expand size={12} />,
label: 'View',
onClick: (): void => {
safeNavigate(link);
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,
});
},
},
{
key: 'open-new-tab',
icon: <SquareArrowOutUpRight size={12} />,
label: 'Open in New Tab',
onClick: (): void => openInNewTab(link),
},
{
key: 'copy-link',
icon: <Link2 size={12} />,
label: 'Copy Link',
onClick: (): void => setCopy(getAbsoluteUrl(link)),
},
{
key: 'export-json',
icon: <FileJson size={12} />,
label: 'Export JSON',
onClick: (): void =>
downloadObjectAsJson(
sanitizeDashboardData({ ...dashboard, title: dashboard.name }),
dashboard.name,
),
},
{ type: 'divider', key: 'sep' },
{
key: 'delete',
icon: <Trash2 size={12} />,
label: deleteTooltipContent ? (
<Tooltip placement="left" title={deleteTooltipContent}>
<span>Delete Dashboard</span>
</Tooltip>
) : (
<span>Delete Dashboard</span>
),
danger: true,
disabled: isDeleteDisabled,
onClick: openConfirmation,
},
],
[
dashboard,
link,
safeNavigate,
setCopy,
deleteTooltipContent,
isDeleteDisabled,
openConfirmation,
],
);
return (
<>
<DropdownMenuSimple menu={{ items }} align="end">
<Button
variant="ghost"
color="secondary"
size="icon"
className="dashboard-action-icon"
data-testid="dashboard-action-icon"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
prefix={<EllipsisVertical size={14} />}
/>
</DropdownMenuSimple>
{contextHolder}
</>
);
}

View File

@@ -15,7 +15,7 @@ import { USER_ROLES } from 'types/roles';
import styles from '../DashboardActions.module.scss';
import { Data } from '../DashboardsList';
interface DeleteButtonProps {
interface UseDeleteDashboardDialogArgs {
createdBy: string;
name: string;
id: string;
@@ -23,26 +23,30 @@ interface DeleteButtonProps {
routeToListPage?: boolean;
}
export function DeleteButton({
interface UseDeleteDashboardDialogResult {
openConfirmation: () => void;
isDisabled: boolean;
tooltipContent: string;
contextHolder: React.ReactElement;
}
export function useDeleteDashboardDialog({
createdBy,
name,
id,
isLocked,
routeToListPage,
}: DeleteButtonProps): JSX.Element {
routeToListPage = false,
}: UseDeleteDashboardDialogArgs): UseDeleteDashboardDialogResult {
const [modal, contextHolder] = Modal.useModal();
const { user } = useAppContext();
const isAuthor = user?.email === createdBy;
const queryClient = useQueryClient();
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']);
const deleteDashboardMutation = useDeleteDashboard(id);
const openConfirmationDialog = useCallback((): void => {
const openConfirmation = useCallback((): void => {
const { destroy } = modal.confirm({
title: (
<Typography.Title level={5}>
@@ -95,23 +99,47 @@ export function DeleteButton({
routeToListPage,
]);
const getDeleteTooltipContent = (): string => {
if (isLocked) {
if (user.role === USER_ROLES.ADMIN || isAuthor) {
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
}
return t('dashboard:locked_dashboard_delete_tooltip_editor');
let tooltipContent = '';
if (isLocked) {
if (user.role === USER_ROLES.ADMIN || isAuthor) {
tooltipContent = t('dashboard:locked_dashboard_delete_tooltip_admin_author');
} else {
tooltipContent = t('dashboard:locked_dashboard_delete_tooltip_editor');
}
return '';
};
}
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
return { openConfirmation, isDisabled, tooltipContent, contextHolder };
}
interface DeleteButtonProps {
createdBy: string;
name: string;
id: string;
isLocked: boolean;
routeToListPage?: boolean;
}
export function DeleteButton({
createdBy,
name,
id,
isLocked,
routeToListPage,
}: DeleteButtonProps): JSX.Element {
const { openConfirmation, isDisabled, tooltipContent, contextHolder } =
useDeleteDashboardDialog({
createdBy,
name,
id,
isLocked,
routeToListPage,
});
return (
<>
<Tooltip placement="left" title={getDeleteTooltipContent()}>
<Tooltip placement="left" title={tooltipContent}>
<Button
type="text"
className={styles.deleteBtn}
@@ -121,7 +149,7 @@ export function DeleteButton({
e.preventDefault();
e.stopPropagation();
if (!isLocked) {
openConfirmationDialog();
openConfirmation();
}
}}
>