mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-25 11:20:34 +01:00
Compare commits
16 Commits
refactor/i
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e81a57a3 | ||
|
|
c30994f966 | ||
|
|
9aef69a186 | ||
|
|
66bc4b4398 | ||
|
|
04c58a1572 | ||
|
|
9bbdd00858 | ||
|
|
316e9c7361 | ||
|
|
634166860b | ||
|
|
af8f2fa95a | ||
|
|
f81fd78ff6 | ||
|
|
b589a7b2e9 | ||
|
|
716dbc7847 | ||
|
|
3a92c7577f | ||
|
|
ba043a5741 | ||
|
|
6d2b99eb8d | ||
|
|
3765ca3d42 |
@@ -17,6 +17,8 @@ const BANNED_COMPONENTS = {
|
|||||||
Typography:
|
Typography:
|
||||||
'Use @signozhq/ui/typography Typography instead of antd Typography.',
|
'Use @signozhq/ui/typography Typography instead of antd Typography.',
|
||||||
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
|
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
|
||||||
|
Dropdown:
|
||||||
|
'Use @signozhq/ui DropdownMenuSimple (or the composable DropdownMenu primitives) from @signozhq/ui/dropdown-menu instead of antd Dropdown.',
|
||||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
.dropdown-button {
|
|
||||||
color: var(--l1-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Ellipsis } from '@signozhq/icons';
|
|
||||||
import { Button, Dropdown, MenuProps } from 'antd';
|
|
||||||
|
|
||||||
import './DropDown.styles.scss';
|
|
||||||
|
|
||||||
function DropDown({
|
|
||||||
element,
|
|
||||||
onDropDownItemClick,
|
|
||||||
}: {
|
|
||||||
element: JSX.Element[];
|
|
||||||
onDropDownItemClick?: MenuProps['onClick'];
|
|
||||||
}): JSX.Element {
|
|
||||||
const items: MenuProps['items'] = element.map(
|
|
||||||
(e: JSX.Element, index: number) => ({
|
|
||||||
label: e,
|
|
||||||
key: index,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isDdOpen, setDdOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items,
|
|
||||||
onMouseEnter: (): void => setDdOpen(true),
|
|
||||||
onMouseLeave: (): void => setDdOpen(false),
|
|
||||||
onClick: (item): void => onDropDownItemClick?.(item),
|
|
||||||
}}
|
|
||||||
open={isDdOpen}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
className={`dropdown-button`}
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDdOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ellipsis className="dropdown-icon" size={16} />
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDown.defaultProps = {
|
|
||||||
onDropDownItemClick: (): void => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropDown;
|
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import {
|
import { Button, Col, Popover, Row, Select, Space } from 'antd';
|
||||||
Button,
|
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
|
||||||
Col,
|
|
||||||
Dropdown,
|
|
||||||
MenuProps,
|
|
||||||
Popover,
|
|
||||||
Row,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
} from 'antd';
|
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
@@ -241,9 +233,9 @@ function ExplorerCard({
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Share2 onClick={onCopyUrlHandler} size="md" />
|
<Share2 onClick={onCopyUrlHandler} size="md" />
|
||||||
{viewKey && (
|
{viewKey && (
|
||||||
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
<DropdownMenuSimple menu={moreOptionMenu}>
|
||||||
<Ellipsis size="md" />
|
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</OffSetCol>
|
</OffSetCol>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Dropdown } from 'antd';
|
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isMultiQueryAllowed && (
|
{isMultiQueryAllowed && (
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
className="query-actions-dropdown"
|
className="query-actions-dropdown"
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
|
|||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
placement="bottomRight"
|
align="end"
|
||||||
>
|
>
|
||||||
<Ellipsis size={16} />
|
<Ellipsis size={16} />
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import type {
|
|||||||
TableColumnsType as ColumnsType,
|
TableColumnsType as ColumnsType,
|
||||||
TableColumnType as ColumnType,
|
TableColumnType as ColumnType,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { Button, Dropdown, Flex, MenuProps } from 'antd';
|
import { Button, Flex } from 'antd';
|
||||||
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
import { Switch } from '@signozhq/ui/switch';
|
import { Switch } from '@signozhq/ui/switch';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { SlidersHorizontal } from '@signozhq/icons';
|
import { SlidersHorizontal } from '@signozhq/icons';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import ResizeTable from './ResizeTable';
|
import ResizeTable from './ResizeTable';
|
||||||
import { DynamicColumnTableProps } from './types';
|
import { DynamicColumnTableProps } from './types';
|
||||||
@@ -84,8 +84,9 @@ function DynamicColumnTable({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: MenuProps['items'] =
|
const items: MenuItem[] =
|
||||||
dynamicColumns?.map((column, index) => ({
|
dynamicColumns?.map((column, index) => ({
|
||||||
|
key: String(index),
|
||||||
label: (
|
label: (
|
||||||
<div
|
<div
|
||||||
className="dynamicColumnsTable-items"
|
className="dynamicColumnsTable-items"
|
||||||
@@ -99,8 +100,6 @@ function DynamicColumnTable({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
key: index,
|
|
||||||
type: 'checkbox',
|
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// Get current page from URL or default to 1
|
// Get current page from URL or default to 1
|
||||||
@@ -129,18 +128,14 @@ function DynamicColumnTable({
|
|||||||
<Flex justify="flex-end" align="center" gap={8}>
|
<Flex justify="flex-end" align="center" gap={8}>
|
||||||
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
|
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
|
||||||
{dynamicColumns && (
|
{dynamicColumns && (
|
||||||
<Dropdown
|
<DropdownMenuSimple menu={{ items }}>
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
menu={{ items }}
|
|
||||||
trigger={['click']}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
className="dynamicColumnTable-button filter-btn"
|
className="dynamicColumnTable-button filter-btn"
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<SlidersHorizontal size={14} />}
|
icon={<SlidersHorizontal size={14} />}
|
||||||
data-testid="additional-filters-button"
|
data-testid="additional-filters-button"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
|
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
|
||||||
import { ChevronDown, Globe } from '@signozhq/icons';
|
import { ChevronDown, Globe } from '@signozhq/icons';
|
||||||
import { Button, Dropdown } from 'antd';
|
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||||
|
import { Button } from 'antd';
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import TimeItems, {
|
import TimeItems, {
|
||||||
timePreferance,
|
timePreferance,
|
||||||
@@ -27,20 +28,17 @@ function TimePreference({
|
|||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
items: menuItems,
|
items: menuItems.map((item) => ({
|
||||||
onClick: timeMenuItemOnChangeHandler,
|
...item,
|
||||||
|
onClick: timeMenuItemOnChangeHandler,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
[timeMenuItemOnChangeHandler],
|
[timeMenuItemOnChangeHandler],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<DropdownMenuSimple menu={menu} className="time-selection-menu">
|
||||||
menu={menu}
|
<Button className="time-selection-target">
|
||||||
rootClassName="time-selection-menu"
|
|
||||||
className="time-selection-target"
|
|
||||||
trigger={['click']}
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
<div className="button-selected-text">
|
<div className="button-selected-text">
|
||||||
<Globe size={14} />
|
<Globe size={14} />
|
||||||
<Typography.Text className="selected-value">
|
<Typography.Text className="selected-value">
|
||||||
@@ -49,7 +47,7 @@ function TimePreference({
|
|||||||
</div>
|
</div>
|
||||||
<ChevronDown size="md" />
|
<ChevronDown size="md" />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ import {
|
|||||||
} from '@signozhq/icons';
|
} from '@signozhq/icons';
|
||||||
import { Button } from '@signozhq/ui/button';
|
import { Button } from '@signozhq/ui/button';
|
||||||
import { Callout } from '@signozhq/ui/callout';
|
import { Callout } from '@signozhq/ui/callout';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@signozhq/ui/dropdown-menu';
|
||||||
import { toast } from '@signozhq/ui/sonner';
|
import { toast } from '@signozhq/ui/sonner';
|
||||||
import { Dropdown, Skeleton } from 'antd';
|
import { Skeleton } from 'antd';
|
||||||
import {
|
import {
|
||||||
RenderErrorResponseDTO,
|
RenderErrorResponseDTO,
|
||||||
ZeustypesHostDTO,
|
ZeustypesHostDTO,
|
||||||
@@ -200,10 +205,15 @@ export default function CustomDomainSettings(): JSX.Element {
|
|||||||
!workspaceName ? 'workspace-name-hidden' : ''
|
!workspaceName ? 'workspace-name-hidden' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<DropdownMenu>
|
||||||
trigger={['click']}
|
<DropdownMenuTrigger asChild>
|
||||||
disabled={isFetchingHosts}
|
<Button variant="link" color="none" disabled={isFetchingHosts}>
|
||||||
dropdownRender={(): JSX.Element => (
|
<Link2 size={12} />
|
||||||
|
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||||
|
<ChevronDown size={12} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
<div className="workspace-url-dropdown">
|
<div className="workspace-url-dropdown">
|
||||||
<span className="workspace-url-dropdown-header">
|
<span className="workspace-url-dropdown-header">
|
||||||
All Workspace URLs
|
All Workspace URLs
|
||||||
@@ -236,14 +246,8 @@ export default function CustomDomainSettings(): JSX.Element {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</DropdownMenuContent>
|
||||||
>
|
</DropdownMenu>
|
||||||
<Button variant="link" color="none">
|
|
||||||
<Link2 size={12} />
|
|
||||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
|
||||||
<ChevronDown size={12} />
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
<span className="custom-domain-card-meta-timezone">
|
<span className="custom-domain-card-meta-timezone">
|
||||||
<Clock size={11} />
|
<Clock size={11} />
|
||||||
{timezone.offset}
|
{timezone.offset}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
|
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { rest, server } from 'mocks-server/server';
|
import { rest, server } from 'mocks-server/server';
|
||||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
@@ -142,12 +143,13 @@ describe('CustomDomainSettings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows all workspace URLs as links in the dropdown', async () => {
|
it('shows all workspace URLs as links in the dropdown', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
render(<CustomDomainSettings />);
|
render(<CustomDomainSettings />);
|
||||||
|
|
||||||
await screen.findByText(/custom-host\.test\.cloud/i);
|
await screen.findByText(/custom-host\.test\.cloud/i);
|
||||||
|
|
||||||
// Open the URL dropdown
|
// Open the URL dropdown
|
||||||
fireEvent.click(
|
await user.click(
|
||||||
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
|
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CloudDownload } from '@signozhq/icons';
|
import { CloudDownload } from '@signozhq/icons';
|
||||||
import { Button, Dropdown, MenuProps, Flex } from 'antd';
|
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
|
||||||
|
import { Button, Flex } from 'antd';
|
||||||
import { unparse } from 'papaparse';
|
import { unparse } from 'papaparse';
|
||||||
|
|
||||||
import { DownloadProps } from './Download.types';
|
import { DownloadProps } from './Download.types';
|
||||||
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu} trigger={['click']}>
|
<DropdownMenuSimple menu={menu}>
|
||||||
<Button
|
<Button
|
||||||
className="download-button"
|
className="download-button"
|
||||||
loading={isLoading || isDownloading}
|
loading={isLoading || isDownloading}
|
||||||
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
|||||||
Download
|
Download
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { Col, Input as InputComponent } from 'antd';
|
||||||
Col,
|
|
||||||
Dropdown as DropDownComponent,
|
|
||||||
Input as InputComponent,
|
|
||||||
} from 'antd';
|
|
||||||
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
|
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -34,16 +30,6 @@ export const ButtonContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Dropdown = styled(DropDownComponent)`
|
|
||||||
&&& {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 150px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TextContainer = styled.div`
|
export const TextContainer = styled.div`
|
||||||
&&& {
|
&&& {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { AppProvider } from 'providers/App/App';
|
import { AppProvider } from 'providers/App/App';
|
||||||
@@ -176,6 +177,7 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
|||||||
|
|
||||||
describe('WidgetGraphComponent', () => {
|
describe('WidgetGraphComponent', () => {
|
||||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
const { getByTestId, findByRole, getByText, container } = render(
|
const { getByTestId, findByRole, getByText, container } = render(
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<ErrorModalProvider>
|
<ErrorModalProvider>
|
||||||
@@ -208,7 +210,7 @@ describe('WidgetGraphComponent', () => {
|
|||||||
expect(skeleton).toBeInTheDocument();
|
expect(skeleton).toBeInTheDocument();
|
||||||
|
|
||||||
const moreOptionsButton = getByTestId('widget-header-options');
|
const moreOptionsButton = getByTestId('widget-header-options');
|
||||||
fireEvent.mouseEnter(moreOptionsButton);
|
await user.click(moreOptionsButton);
|
||||||
|
|
||||||
const menu = await findByRole('menu');
|
const menu = await findByRole('menu');
|
||||||
expect(menu).toBeInTheDocument();
|
expect(menu).toBeInTheDocument();
|
||||||
|
|||||||
@@ -54,6 +54,17 @@
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// currently the width of the dropdown menu is set to 100% of the parent container,
|
||||||
|
// which is not desired. This is a workaround to unset that width and allow the dropdown menu to size based on its content.
|
||||||
|
// This is necessary because the dropdown menu can contain items with varying widths, and setting it to 100% can cause layout issues and make the menu look unbalanced.
|
||||||
|
// we should idealy fix this in the dropdown menu component itself, but for now this is a quick fix to ensure the dropdown menu looks correct in the widget header.
|
||||||
|
|
||||||
|
[data-radix-popper-content-wrapper]
|
||||||
|
[data-slot='dropdown-menu-content'].widget-header-dropdown
|
||||||
|
[data-slot='dropdown-menu-item'] {
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
.widget-api-actions {
|
.widget-api-actions {
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ describe('WidgetHeader', () => {
|
|||||||
|
|
||||||
describe('Create Alerts Menu Item', () => {
|
describe('Create Alerts Menu Item', () => {
|
||||||
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
|
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
render(
|
render(
|
||||||
<WidgetHeader
|
<WidgetHeader
|
||||||
title={TEST_WIDGET_TITLE}
|
title={TEST_WIDGET_TITLE}
|
||||||
@@ -483,7 +484,7 @@ describe('WidgetHeader', () => {
|
|||||||
|
|
||||||
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
||||||
expect(moreOptionsIcon).toBeInTheDocument();
|
expect(moreOptionsIcon).toBeInTheDocument();
|
||||||
await userEvent.hover(moreOptionsIcon);
|
await user.click(moreOptionsIcon);
|
||||||
|
|
||||||
await screen.findByText(CREATE_ALERTS_TEXT);
|
await screen.findByText(CREATE_ALERTS_TEXT);
|
||||||
|
|
||||||
@@ -494,6 +495,7 @@ describe('WidgetHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Create Alerts menu item is enabled and clickable', async () => {
|
it('Create Alerts menu item is enabled and clickable', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
const mockCreateAlertsHandler = jest.fn();
|
const mockCreateAlertsHandler = jest.fn();
|
||||||
const useCreateAlerts = jest.requireMock(
|
const useCreateAlerts = jest.requireMock(
|
||||||
'hooks/queryBuilder/useCreateAlerts',
|
'hooks/queryBuilder/useCreateAlerts',
|
||||||
@@ -517,12 +519,12 @@ describe('WidgetHeader', () => {
|
|||||||
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
|
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
|
||||||
|
|
||||||
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
||||||
await userEvent.hover(moreOptionsIcon);
|
await user.click(moreOptionsIcon);
|
||||||
|
|
||||||
const createAlertsMenuItem = await screen.findByText(CREATE_ALERTS_TEXT);
|
const createAlertsMenuItem = await screen.findByText(CREATE_ALERTS_TEXT);
|
||||||
|
|
||||||
// Verify the menu item is clickable by actually clicking it
|
// Verify the menu item is clickable by actually clicking it
|
||||||
await userEvent.click(createAlertsMenuItem);
|
await user.click(createAlertsMenuItem);
|
||||||
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
|
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from '@signozhq/icons';
|
} from '@signozhq/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
import { Button, Input, Tooltip } from 'antd';
|
||||||
|
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||||
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
|
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
|
||||||
@@ -128,7 +129,7 @@ function WidgetHeader({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
const onMenuItemSelectHandler = useCallback(
|
||||||
({ key }: { key: string }): void => {
|
({ key }: { key: string }): void => {
|
||||||
if (isTWidgetOptions(key)) {
|
if (isTWidgetOptions(key)) {
|
||||||
const functionToCall = keyMethodMapping[key];
|
const functionToCall = keyMethodMapping[key];
|
||||||
@@ -188,18 +189,8 @@ function WidgetHeader({
|
|||||||
{
|
{
|
||||||
key: MenuItemKeys.CreateAlerts,
|
key: MenuItemKeys.CreateAlerts,
|
||||||
icon: <Bell size="md" />,
|
icon: <Bell size="md" />,
|
||||||
label: (
|
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
|
||||||
<span
|
rightIcon: <SquareArrowOutUpRight size="lg" />,
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts]}
|
|
||||||
<SquareArrowOutUpRight size={10} />
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
|
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@@ -221,8 +212,10 @@ function WidgetHeader({
|
|||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
items: updatedMenuList,
|
items: updatedMenuList.map((item) => ({
|
||||||
onClick: onMenuItemSelectHandler,
|
...item,
|
||||||
|
onClick: onMenuItemSelectHandler,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
[updatedMenuList, onMenuItemSelectHandler],
|
[updatedMenuList, onMenuItemSelectHandler],
|
||||||
);
|
);
|
||||||
@@ -321,7 +314,12 @@ function WidgetHeader({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
|
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
|
||||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
<DropdownMenuSimple
|
||||||
|
menu={menu}
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
className="widget-header-dropdown"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
data-testid="widget-header-options"
|
data-testid="widget-header-options"
|
||||||
className={`widget-header-more-options ${
|
className={`widget-header-more-options ${
|
||||||
@@ -329,7 +327,7 @@ function WidgetHeader({
|
|||||||
}`}
|
}`}
|
||||||
icon={<EllipsisVertical size="md" />}
|
icon={<EllipsisVertical size="md" />}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface MenuItem {
|
|||||||
key: MenuItemKeys;
|
key: MenuItemKeys;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
|
rightIcon?: ReactNode;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
import type { MenuItem as DropdownMenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
|
|
||||||
import { MenuItemKeys } from './contants';
|
import { MenuItemKeys } from './contants';
|
||||||
import { MenuItem } from './types';
|
import { MenuItem } from './types';
|
||||||
|
|
||||||
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
|
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
|
||||||
actions
|
actions
|
||||||
.filter((action: MenuItem) => action.isVisible)
|
.filter((action: MenuItem) => action.isVisible)
|
||||||
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({
|
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { Button, Flex, Input } from 'antd';
|
import { Button, Flex, Input } from 'antd';
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import { Plus } from '@signozhq/icons';
|
import { Ellipsis, Plus } from '@signozhq/icons';
|
||||||
|
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||||
import type { ColumnsType } from 'antd/es/table/interface';
|
import type { ColumnsType } from 'antd/es/table/interface';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||||
@@ -15,7 +16,6 @@ import type {
|
|||||||
} from 'api/generated/services/sigNoz.schemas';
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import DropDown from 'components/DropDown/DropDown';
|
|
||||||
import {
|
import {
|
||||||
DynamicColumnsKey,
|
DynamicColumnsKey,
|
||||||
TableDataSource,
|
TableDataSource,
|
||||||
@@ -323,55 +323,67 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 10,
|
width: 10,
|
||||||
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
|
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => {
|
||||||
<div data-testid="alert-actions">
|
const actionItems = [
|
||||||
<DropDown
|
<ToggleAlertState
|
||||||
onDropDownItemClick={(item): void =>
|
key="1"
|
||||||
alertActionLogEvent(item.key, record)
|
disabled={record.disabled ?? false}
|
||||||
|
setData={setData}
|
||||||
|
id={id ?? ''}
|
||||||
|
/>,
|
||||||
|
<ColumnButton
|
||||||
|
key="2"
|
||||||
|
onClick={(e: React.MouseEvent): void =>
|
||||||
|
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
|
||||||
}
|
}
|
||||||
element={[
|
type="link"
|
||||||
<ToggleAlertState
|
loading={editLoader}
|
||||||
key="1"
|
>
|
||||||
disabled={record.disabled ?? false}
|
Edit
|
||||||
setData={setData}
|
</ColumnButton>,
|
||||||
id={id ?? ''}
|
<ColumnButton
|
||||||
/>,
|
key="3-new-tab"
|
||||||
<ColumnButton
|
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||||
key="2"
|
type="link"
|
||||||
onClick={(e: React.MouseEvent): void =>
|
loading={editLoader}
|
||||||
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
|
>
|
||||||
}
|
Edit in New Tab
|
||||||
|
</ColumnButton>,
|
||||||
|
<ColumnButton
|
||||||
|
key="3-clone"
|
||||||
|
onClick={onCloneHandler(record)}
|
||||||
|
type="link"
|
||||||
|
loading={cloneLoader}
|
||||||
|
>
|
||||||
|
Clone
|
||||||
|
</ColumnButton>,
|
||||||
|
<DeleteAlert
|
||||||
|
key="4"
|
||||||
|
notifications={notificationsApi}
|
||||||
|
setData={setData}
|
||||||
|
id={id ?? ''}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div data-testid="alert-actions">
|
||||||
|
<DropdownMenuSimple
|
||||||
|
menu={{
|
||||||
|
items: actionItems.map((element, index) => ({
|
||||||
|
key: String(index),
|
||||||
|
label: element,
|
||||||
|
onClick: ({ key }): void => alertActionLogEvent(key, record),
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
loading={editLoader}
|
style={{ color: 'var(--l1-foreground)' }}
|
||||||
>
|
icon={<Ellipsis size={16} />}
|
||||||
Edit
|
/>
|
||||||
</ColumnButton>,
|
</DropdownMenuSimple>
|
||||||
<ColumnButton
|
</div>
|
||||||
key="3"
|
);
|
||||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
},
|
||||||
type="link"
|
|
||||||
loading={editLoader}
|
|
||||||
>
|
|
||||||
Edit in New Tab
|
|
||||||
</ColumnButton>,
|
|
||||||
<ColumnButton
|
|
||||||
key="3"
|
|
||||||
onClick={onCloneHandler(record)}
|
|
||||||
type="link"
|
|
||||||
loading={cloneLoader}
|
|
||||||
>
|
|
||||||
Clone
|
|
||||||
</ColumnButton>,
|
|
||||||
<DeleteAlert
|
|
||||||
key="4"
|
|
||||||
notifications={notificationsApi}
|
|
||||||
setData={setData}
|
|
||||||
id={id ?? ''}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
|
||||||
Flex,
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
MenuProps,
|
|
||||||
Modal,
|
Modal,
|
||||||
Popover,
|
Popover,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const getCreateDashboardItems = useMemo(() => {
|
const getCreateDashboardItems = useMemo(() => {
|
||||||
const menuItems: MenuProps['items'] = [
|
const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div
|
<div
|
||||||
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
|
|||||||
|
|
||||||
{createNewDashboard && (
|
{createNewDashboard && (
|
||||||
<section className="actions">
|
<section className="actions">
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
overlayClassName="new-dashboard-menu"
|
className="new-dashboard-menu"
|
||||||
menu={{ items: getCreateDashboardItems }}
|
menu={{ items: getCreateDashboardItems }}
|
||||||
placement="bottomRight"
|
side="bottom"
|
||||||
trigger={['click']}
|
align="end"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
>
|
>
|
||||||
New Dashboard
|
New Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
className="learn-more"
|
className="learn-more"
|
||||||
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
|
|||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
{createNewDashboard && (
|
{createNewDashboard && (
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
overlayClassName="new-dashboard-menu"
|
className="new-dashboard-menu"
|
||||||
menu={{ items: getCreateDashboardItems }}
|
menu={{ items: getCreateDashboardItems }}
|
||||||
placement="bottomRight"
|
side="bottom"
|
||||||
trigger={['click']}
|
align="end"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
>
|
>
|
||||||
New dashboard
|
New dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { useCallback } from 'react';
|
|||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { Settings } from '@signozhq/icons';
|
import { Settings } from '@signozhq/icons';
|
||||||
import { Dropdown, MenuProps } from 'antd';
|
import {
|
||||||
|
type BaseMenuItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@signozhq/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
negateOperator,
|
negateOperator,
|
||||||
OPERATORS,
|
OPERATORS,
|
||||||
@@ -135,41 +141,38 @@ function BodyTitleRenderer({
|
|||||||
viewName,
|
viewName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onClickHandler: MenuProps['onClick'] = (props): void => {
|
const onClickHandler = (key: string): void => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
|
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
|
||||||
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
|
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
|
||||||
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
|
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = mapper[props.key];
|
const handler = mapper[key];
|
||||||
|
|
||||||
if (handler) {
|
if (handler) {
|
||||||
handler();
|
handler();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu: MenuProps = {
|
const menuItems: BaseMenuItem[] = [
|
||||||
items: [
|
{
|
||||||
{
|
key: DROPDOWN_KEY.FILTER_IN,
|
||||||
key: DROPDOWN_KEY.FILTER_IN,
|
label: `Filter for ${value}`,
|
||||||
label: `Filter for ${value}`,
|
},
|
||||||
},
|
{
|
||||||
{
|
key: DROPDOWN_KEY.FILTER_OUT,
|
||||||
key: DROPDOWN_KEY.FILTER_OUT,
|
label: `Filter out ${value}`,
|
||||||
label: `Filter out ${value}`,
|
},
|
||||||
},
|
...(isGroupBySupported
|
||||||
...(isGroupBySupported
|
? [
|
||||||
? [
|
{
|
||||||
{
|
key: DROPDOWN_KEY.GROUP_BY,
|
||||||
key: DROPDOWN_KEY.GROUP_BY,
|
label: `Group by ${nodeKey}`,
|
||||||
label: `Group by ${nodeKey}`,
|
},
|
||||||
},
|
]
|
||||||
]
|
: []),
|
||||||
: []),
|
];
|
||||||
],
|
|
||||||
onClick: onClickHandler,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNodeClick = useCallback(
|
const handleNodeClick = useCallback(
|
||||||
(e: React.MouseEvent): void => {
|
(e: React.MouseEvent): void => {
|
||||||
@@ -218,15 +221,23 @@ function BodyTitleRenderer({
|
|||||||
}}
|
}}
|
||||||
onMouseDown={(e): void => e.preventDefault()}
|
onMouseDown={(e): void => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<DropdownMenu>
|
||||||
menu={menu}
|
<DropdownMenuTrigger asChild>
|
||||||
trigger={['click']}
|
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
|
||||||
dropdownRender={(originNode): React.ReactNode => (
|
</DropdownMenuTrigger>
|
||||||
<div data-log-detail-ignore="true">{originNode}</div>
|
<DropdownMenuContent>
|
||||||
)}
|
<div data-log-detail-ignore="true">
|
||||||
>
|
{menuItems.map((item) => (
|
||||||
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
|
<DropdownMenuItem
|
||||||
</Dropdown>
|
key={item.key}
|
||||||
|
onSelect={(): void => onClickHandler(item.key as string)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{title.toString()}{' '}
|
{title.toString()}{' '}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||||
import { Button } from '@signozhq/ui/button';
|
import { Button } from '@signozhq/ui/button';
|
||||||
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
import { Input } from '@signozhq/ui/input';
|
import { Input } from '@signozhq/ui/input';
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import { useListUsers } from 'api/generated/services/users';
|
import { useListUsers } from 'api/generated/services/users';
|
||||||
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
||||||
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
||||||
@@ -95,7 +94,7 @@ function MembersSettings(): JSX.Element {
|
|||||||
).length;
|
).length;
|
||||||
const totalCount = allMembers.length;
|
const totalCount = allMembers.length;
|
||||||
|
|
||||||
const filterMenuItems: MenuProps['items'] = [
|
const filterMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
key: FilterMode.All,
|
key: FilterMode.All,
|
||||||
label: (
|
label: (
|
||||||
@@ -171,10 +170,9 @@ function MembersSettings(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="members-settings__controls">
|
<div className="members-settings__controls">
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
menu={{ items: filterMenuItems }}
|
menu={{ items: filterMenuItems }}
|
||||||
trigger={['click']}
|
className="members-filter-dropdown"
|
||||||
overlayClassName="members-filter-dropdown"
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -184,7 +182,7 @@ function MembersSettings(): JSX.Element {
|
|||||||
<span>{filterLabel}</span>
|
<span>{filterLabel}</span>
|
||||||
<ChevronDown size={12} className="members-filter-trigger__chevron" />
|
<ChevronDown size={12} className="members-filter-trigger__chevron" />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
|
|
||||||
<div className="members-settings__search">
|
<div className="members-settings__search">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
|
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { rest, server } from 'mocks-server/server';
|
import { rest, server } from 'mocks-server/server';
|
||||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
@@ -76,14 +77,15 @@ describe('MembersSettings (integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters to pending invites via the filter dropdown', async () => {
|
it('filters to pending invites via the filter dropdown', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
render(<MembersSettings />);
|
render(<MembersSettings />);
|
||||||
|
|
||||||
await screen.findByText('Alice Smith');
|
await screen.findByText('Alice Smith');
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
|
await user.click(screen.getByRole('button', { name: /all members/i }));
|
||||||
|
|
||||||
const pendingOption = await screen.findByText(/pending invites/i);
|
const pendingOption = await screen.findByText(/pending invites/i);
|
||||||
fireEvent.click(pendingOption);
|
await user.click(pendingOption);
|
||||||
|
|
||||||
await screen.findByText('charlie@signoz.io');
|
await screen.findByText('charlie@signoz.io');
|
||||||
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
|
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Dropdown, Skeleton } from 'antd';
|
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||||
|
import { Skeleton } from 'antd';
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import {
|
import {
|
||||||
useGetMetricAlerts,
|
useGetMetricAlerts,
|
||||||
@@ -126,12 +127,11 @@ function DashboardsAndAlertsPopover({
|
|||||||
return (
|
return (
|
||||||
<div className="dashboards-and-alerts-popover-container">
|
<div className="dashboards-and-alerts-popover-container">
|
||||||
{dashboardsPopoverContent && (
|
{dashboardsPopoverContent && (
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
menu={{
|
menu={{
|
||||||
items: dashboardsPopoverContent,
|
items: dashboardsPopoverContent,
|
||||||
}}
|
}}
|
||||||
placement="bottomLeft"
|
align="start"
|
||||||
trigger={['click']}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="dashboards-and-alerts-popover dashboards-popover"
|
className="dashboards-and-alerts-popover dashboards-popover"
|
||||||
@@ -142,15 +142,14 @@ function DashboardsAndAlertsPopover({
|
|||||||
{pluralize(dashboards.length, 'dashboard')}
|
{pluralize(dashboards.length, 'dashboard')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
{alertsPopoverContent && (
|
{alertsPopoverContent && (
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
menu={{
|
menu={{
|
||||||
items: alertsPopoverContent,
|
items: alertsPopoverContent,
|
||||||
}}
|
}}
|
||||||
placement="bottomLeft"
|
align="start"
|
||||||
trigger={['click']}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="dashboards-and-alerts-popover alerts-popover"
|
className="dashboards-and-alerts-popover alerts-popover"
|
||||||
@@ -161,7 +160,7 @@ function DashboardsAndAlertsPopover({
|
|||||||
{pluralize(alerts.length, 'alert rule')}
|
{pluralize(alerts.length, 'alert rule')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import {
|
|||||||
DropResult,
|
DropResult,
|
||||||
} from 'react-beautiful-dnd';
|
} from 'react-beautiful-dnd';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
import { Button, Divider, Input, Tooltip } from 'antd';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@signozhq/ui/dropdown-menu';
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
import { FieldDataType } from 'api/v5/v5';
|
import { FieldDataType } from 'api/v5/v5';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
@@ -159,34 +164,12 @@ function ExplorerColumnsRenderer({
|
|||||||
debouncedSetQuerySearchText(e.target.value);
|
debouncedSetQuerySearchText(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: MenuProps['items'] = [
|
const handleOpenChange = (nextOpen: boolean): void => {
|
||||||
{
|
setOpen(nextOpen);
|
||||||
key: 'search',
|
if (nextOpen) {
|
||||||
label: (
|
setSearchText('');
|
||||||
<Input
|
}
|
||||||
type="text"
|
};
|
||||||
placeholder="Search"
|
|
||||||
className="explorer-columns-search"
|
|
||||||
value={searchText}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'columns',
|
|
||||||
label: (
|
|
||||||
<ExplorerAttributeColumns
|
|
||||||
isLoading={isLoading}
|
|
||||||
data={data}
|
|
||||||
searchText={searchText}
|
|
||||||
isAttributeKeySelected={isAttributeKeySelected}
|
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
|
||||||
dataSource={initialDataSource}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const removeSelectedLogField = (name: string): void => {
|
const removeSelectedLogField = (name: string): void => {
|
||||||
if (
|
if (
|
||||||
@@ -238,13 +221,6 @@ function ExplorerColumnsRenderer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = (): void => {
|
|
||||||
setOpen(!open);
|
|
||||||
if (!open) {
|
|
||||||
setSearchText('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -327,25 +303,38 @@ function ExplorerColumnsRenderer({
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||||
menu={{ items }}
|
<DropdownMenuTrigger asChild>
|
||||||
arrow
|
<Button
|
||||||
placement="top"
|
className="action-btn"
|
||||||
open={open}
|
data-testid="add-columns-button"
|
||||||
overlayClassName="explorer-columns-dropdown"
|
icon={
|
||||||
>
|
<CirclePlus
|
||||||
<Button
|
size={16}
|
||||||
className="action-btn"
|
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||||
data-testid="add-columns-button"
|
/>
|
||||||
icon={
|
}
|
||||||
<CirclePlus
|
/>
|
||||||
size={16}
|
</DropdownMenuTrigger>
|
||||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
|
||||||
/>
|
<Input
|
||||||
}
|
type="text"
|
||||||
onClick={toggleDropdown}
|
placeholder="Search"
|
||||||
/>
|
className="explorer-columns-search"
|
||||||
</Dropdown>
|
value={searchText}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||||
|
/>
|
||||||
|
<ExplorerAttributeColumns
|
||||||
|
isLoading={isLoading}
|
||||||
|
data={data}
|
||||||
|
searchText={searchText}
|
||||||
|
isAttributeKeySelected={isAttributeKeySelected}
|
||||||
|
handleCheckboxChange={handleCheckboxChange}
|
||||||
|
dataSource={initialDataSource}
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ describe('ExplorerColumnsRenderer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('opens and closes the dropdown', async () => {
|
it('opens and closes the dropdown', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
render(
|
render(
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ExplorerColumnsRenderer
|
<ExplorerColumnsRenderer
|
||||||
@@ -158,12 +159,12 @@ describe('ExplorerColumnsRenderer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addButton = screen.getByTestId('add-columns-button');
|
const addButton = screen.getByTestId('add-columns-button');
|
||||||
await userEvent.click(addButton);
|
await user.click(addButton);
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(addButton);
|
await user.click(addButton);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Plus, Trash2 } from '@signozhq/icons';
|
|||||||
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
|
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { getBaseUrl } from 'utils/basePath';
|
import { getBaseUrl } from 'utils/basePath';
|
||||||
|
|
||||||
import VariablesDropdown from './VariablesDropdown';
|
import VariablesPopover from './VariablesPopover';
|
||||||
|
|
||||||
import './UpdateContextLinks.styles.scss';
|
import './UpdateContextLinks.styles.scss';
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ function UpdateContextLinks({
|
|||||||
customVariables: fieldVariables,
|
customVariables: fieldVariables,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transform variables into the format expected by VariablesDropdown
|
// Transform variables into the format expected by VariablesPopover
|
||||||
const transformedVariables = useMemo(
|
const transformedVariables = useMemo(
|
||||||
() => transformContextVariables(variables),
|
() => transformContextVariables(variables),
|
||||||
[variables],
|
[variables],
|
||||||
@@ -224,7 +224,9 @@ function UpdateContextLinks({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<VariablesDropdown
|
{/* TODO: replace with AutoComplete with options for variables and
|
||||||
|
previously used URLs for better UX */}
|
||||||
|
<VariablesPopover
|
||||||
onVariableSelect={handleVariableSelect}
|
onVariableSelect={handleVariableSelect}
|
||||||
variables={transformedVariables}
|
variables={transformedVariables}
|
||||||
>
|
>
|
||||||
@@ -252,7 +254,7 @@ function UpdateContextLinks({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</VariablesDropdown>
|
</VariablesPopover>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Remove the separate variables section */}
|
{/* Remove the separate variables section */}
|
||||||
@@ -282,7 +284,7 @@ function UpdateContextLinks({
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<VariablesDropdown
|
<VariablesPopover
|
||||||
onVariableSelect={(variableName, cursorPosition): void =>
|
onVariableSelect={(variableName, cursorPosition): void =>
|
||||||
handleParamVariableSelect(index, variableName, cursorPosition)
|
handleParamVariableSelect(index, variableName, cursorPosition)
|
||||||
}
|
}
|
||||||
@@ -311,7 +313,7 @@ function UpdateContextLinks({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</VariablesDropdown>
|
</VariablesPopover>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
.variables-dropdown-container {
|
|
||||||
.url-input-trigger {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.url-input-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override Ant Design dropdown styles
|
|
||||||
.ant-dropdown-menu {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.variable-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.variable-source {
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import { Typography } from '@signozhq/ui/typography';
|
|
||||||
|
|
||||||
import './VariablesDropdown.styles.scss';
|
|
||||||
|
|
||||||
interface VariablesDropdownProps {
|
|
||||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
|
||||||
variables: VariableItem[];
|
|
||||||
children: (props: {
|
|
||||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (open: boolean) => void;
|
|
||||||
cursorPosition: number | null;
|
|
||||||
setCursorPosition: (position: number | null) => void;
|
|
||||||
}) => ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VariableItem {
|
|
||||||
name: string;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function VariablesDropdown({
|
|
||||||
onVariableSelect,
|
|
||||||
variables,
|
|
||||||
children,
|
|
||||||
}: VariablesDropdownProps): JSX.Element {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Click outside handler
|
|
||||||
useEffect(() => {
|
|
||||||
function handleClickOutside(event: MouseEvent): void {
|
|
||||||
if (
|
|
||||||
wrapperRef.current &&
|
|
||||||
!wrapperRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
return (): void => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
|
||||||
() =>
|
|
||||||
variables.map((v) => ({
|
|
||||||
key: v.name,
|
|
||||||
label: (
|
|
||||||
<div className="variable-row">
|
|
||||||
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
|
|
||||||
<Typography.Text className="variable-source">{v.source}</Typography.Text>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
[variables],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="variables-dropdown-container" ref={wrapperRef}>
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: dropdownItems,
|
|
||||||
onClick: ({ key }): void => {
|
|
||||||
const variableName = key as string;
|
|
||||||
onVariableSelect(`{{${variableName}}}`, cursorPosition || undefined);
|
|
||||||
setIsOpen(false);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
open={isOpen}
|
|
||||||
placement="bottomLeft"
|
|
||||||
trigger={['click']}
|
|
||||||
getPopupContainer={(): HTMLElement => wrapperRef.current || document.body}
|
|
||||||
>
|
|
||||||
{children({
|
|
||||||
onVariableSelect,
|
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
cursorPosition,
|
|
||||||
setCursorPosition,
|
|
||||||
})}
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VariablesDropdown;
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.variables-popover-container {
|
||||||
|
.url-input-trigger {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.url-input-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-popover-anchor-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-popover-content {
|
||||||
|
// antd Modal uses z-index ~1000; popover must sit above it.
|
||||||
|
z-index: 1100;
|
||||||
|
padding: 4px 0;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-width: var(--radix-popover-trigger-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-popover-empty {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--l3-foreground, #999);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-popover-item {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--l1-foreground);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--l1-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.variable-name,
|
||||||
|
.variable-source {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-source {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Uses Popover (not DropdownMenu like the rest of the antd-dropdown migration):
|
||||||
|
// DropdownMenuTrigger preventDefaults pointerdown, breaking input focus and
|
||||||
|
// dismissing on every keystroke. PopoverAnchor is a passive positioning element.
|
||||||
|
import { ReactNode, useRef, useState } from 'react';
|
||||||
|
import { Popover, PopoverAnchor, PopoverContent } from '@signozhq/ui/popover';
|
||||||
|
import { Typography } from '@signozhq/ui/typography';
|
||||||
|
|
||||||
|
import './VariablesPopover.styles.scss';
|
||||||
|
|
||||||
|
interface VariablesPopoverProps {
|
||||||
|
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||||
|
variables: VariableItem[];
|
||||||
|
children: (props: {
|
||||||
|
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
cursorPosition: number | null;
|
||||||
|
setCursorPosition: (position: number | null) => void;
|
||||||
|
}) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VariableItem {
|
||||||
|
name: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariablesPopover({
|
||||||
|
onVariableSelect,
|
||||||
|
variables,
|
||||||
|
children,
|
||||||
|
}: VariablesPopoverProps): JSX.Element {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
||||||
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean): void => {
|
||||||
|
// Accept "close" events from the popover (outside-click, Esc) but ignore
|
||||||
|
// opens — opening is driven by the input's onFocus in the consumer.
|
||||||
|
if (!open) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="variables-popover-container">
|
||||||
|
<Popover open={isOpen} onOpenChange={handleOpenChange} modal={false}>
|
||||||
|
<PopoverAnchor asChild>
|
||||||
|
<div className="variables-popover-anchor-wrap" ref={anchorRef}>
|
||||||
|
{children({
|
||||||
|
onVariableSelect,
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
cursorPosition,
|
||||||
|
setCursorPosition,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
sideOffset={4}
|
||||||
|
className="variables-popover-content"
|
||||||
|
onOpenAutoFocus={(e): void => e.preventDefault()}
|
||||||
|
onCloseAutoFocus={(e): void => e.preventDefault()}
|
||||||
|
onInteractOutside={(e): void => {
|
||||||
|
// Keep the popover open while interacting with the anchor (the input),
|
||||||
|
// otherwise typing/clicking the input would close it immediately.
|
||||||
|
const target = e.target as Node | null;
|
||||||
|
if (target && anchorRef.current?.contains(target)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocusOutside={(e): void => {
|
||||||
|
const target = e.target as Node | null;
|
||||||
|
if (target && anchorRef.current?.contains(target)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{variables.length === 0 ? (
|
||||||
|
<div className="variables-popover-empty">No variables available</div>
|
||||||
|
) : (
|
||||||
|
variables.map((v) => (
|
||||||
|
<button
|
||||||
|
key={v.name}
|
||||||
|
type="button"
|
||||||
|
className="variables-popover-item"
|
||||||
|
onMouseDown={(e): void => {
|
||||||
|
// Prevent the input from losing focus when clicking an item.
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={(): void => {
|
||||||
|
onVariableSelect(`{{${v.name}}}`, cursorPosition || undefined);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="variable-row">
|
||||||
|
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
|
||||||
|
<Typography.Text className="variable-source">
|
||||||
|
{v.source}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VariablesPopover;
|
||||||
@@ -204,7 +204,7 @@ const processContextLinks = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms context variables into the format expected by VariablesDropdown
|
* Transforms context variables into the format expected by VariablesPopover
|
||||||
* @param variables - Array of context variables from useContextVariables
|
* @param variables - Array of context variables from useContextVariables
|
||||||
* @returns Array of transformed variables with proper source descriptions
|
* @returns Array of transformed variables with proper source descriptions
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
import { ChevronDown } from '@signozhq/icons';
|
import { ChevronDown } from '@signozhq/icons';
|
||||||
import { Button, ColorPicker, Dropdown, MenuProps, Space } from 'antd';
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
|
import { Button, ColorPicker, Space } from 'antd';
|
||||||
import type { Color } from 'antd/es/color-picker';
|
import type { Color } from 'antd/es/color-picker';
|
||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ function ColorSelector({
|
|||||||
setColorFromPicker(hex);
|
setColorFromPicker(hex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: MenuProps['items'] = [
|
const items: MenuItem[] = [
|
||||||
{
|
{
|
||||||
key: 'Red',
|
key: 'Red',
|
||||||
label: <CustomColor color="Red" />,
|
label: <CustomColor color="Red" />,
|
||||||
@@ -62,7 +63,7 @@ function ColorSelector({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={{ items }} trigger={['click']}>
|
<DropdownMenuSimple menu={{ items }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={(e): void => e.preventDefault()}
|
onClick={(e): void => e.preventDefault()}
|
||||||
className="color-selector-button"
|
className="color-selector-button"
|
||||||
@@ -72,7 +73,7 @@ function ColorSelector({
|
|||||||
<ChevronDown size="md" />
|
<ChevronDown size="md" />
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||||
import { Button } from '@signozhq/ui/button';
|
import { Button } from '@signozhq/ui/button';
|
||||||
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
import { Input } from '@signozhq/ui/input';
|
import { Input } from '@signozhq/ui/input';
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||||
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
||||||
@@ -134,7 +133,7 @@ function ServiceAccountsSettings(): JSX.Element {
|
|||||||
|
|
||||||
const totalCount = allAccounts.length;
|
const totalCount = allAccounts.length;
|
||||||
|
|
||||||
const filterMenuItems: MenuProps['items'] = [
|
const filterMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
key: FilterMode.All,
|
key: FilterMode.All,
|
||||||
label: (
|
label: (
|
||||||
@@ -231,10 +230,9 @@ function ServiceAccountsSettings(): JSX.Element {
|
|||||||
) : (
|
) : (
|
||||||
<div className="sa-settings__list-section">
|
<div className="sa-settings__list-section">
|
||||||
<div className="sa-settings__controls">
|
<div className="sa-settings__controls">
|
||||||
<Dropdown
|
<DropdownMenuSimple
|
||||||
menu={{ items: filterMenuItems }}
|
menu={{ items: filterMenuItems }}
|
||||||
trigger={['click']}
|
className="sa-settings-filter-dropdown"
|
||||||
overlayClassName="sa-settings-filter-dropdown"
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -247,7 +245,7 @@ function ServiceAccountsSettings(): JSX.Element {
|
|||||||
className="sa-settings-filter-trigger__chevron"
|
className="sa-settings-filter-trigger__chevron"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuSimple>
|
||||||
|
|
||||||
<div className="sa-settings__search">
|
<div className="sa-settings__search">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||||
import { rest, server } from 'mocks-server/server';
|
import { rest, server } from 'mocks-server/server';
|
||||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||||
@@ -129,6 +130,7 @@ describe('ServiceAccountsSettings (integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
|
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
render(
|
render(
|
||||||
<NuqsTestingAdapter>
|
<NuqsTestingAdapter>
|
||||||
<ServiceAccountsSettings />
|
<ServiceAccountsSettings />
|
||||||
@@ -137,10 +139,10 @@ describe('ServiceAccountsSettings (integration)', () => {
|
|||||||
|
|
||||||
await screen.findByText('CI Bot');
|
await screen.findByText('CI Bot');
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /All accounts/i }));
|
await user.click(screen.getByRole('button', { name: /All accounts/i }));
|
||||||
|
|
||||||
const activeOption = await screen.findByText(/Active ⎯/i);
|
const activeOption = await screen.findByText(/Active ⎯/i);
|
||||||
fireEvent.click(activeOption);
|
await user.click(activeOption);
|
||||||
|
|
||||||
await screen.findByText('CI Bot');
|
await screen.findByText('CI Bot');
|
||||||
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();
|
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -662,7 +662,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.pinned):hover,
|
&:not(.pinned).is-hovered,
|
||||||
&.dropdown-open {
|
&.dropdown-open {
|
||||||
flex: 0 0 240px;
|
flex: 0 0 240px;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -25,7 +26,14 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Button, Dropdown, MenuProps, Modal, Tooltip } from 'antd';
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@signozhq/ui/dropdown-menu';
|
||||||
|
import { Button, MenuProps, Modal, Tooltip } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { Logout } from 'api/utils';
|
import { Logout } from 'api/utils';
|
||||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||||
@@ -162,7 +170,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
|||||||
|
|
||||||
const [hasScroll, setHasScroll] = useState(false);
|
const [hasScroll, setHasScroll] = useState(false);
|
||||||
const navTopSectionRef = useRef<HTMLDivElement>(null);
|
const navTopSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sidenavRef = useRef<HTMLDivElement>(null);
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const isDropdownOpenRef = useRef(false);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
|
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
|
||||||
@@ -175,9 +185,27 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
// When the dropdown is open its content renders in a portal outside
|
||||||
|
// the sidenav, which causes the browser to fire mouseleave on the
|
||||||
|
// sidenav. Keep the sidenav expanded in that case.
|
||||||
|
if (isDropdownOpenRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDropdownOpenChange = useCallback((open: boolean): void => {
|
||||||
|
isDropdownOpenRef.current = open;
|
||||||
|
setIsDropdownOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
// Re-sync hover state on close: the cursor may have moved to the
|
||||||
|
// portal content (outside .sideNav), so mouseleave never fired.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsHovered(sidenavRef.current?.matches(':hover') ?? false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const checkScroll = useCallback((): void => {
|
const checkScroll = useCallback((): void => {
|
||||||
if (navTopSectionRef.current) {
|
if (navTopSectionRef.current) {
|
||||||
const { scrollHeight, clientHeight, scrollTop } = navTopSectionRef.current;
|
const { scrollHeight, clientHeight, scrollTop } = navTopSectionRef.current;
|
||||||
@@ -959,9 +987,11 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className={cx('sidenav-container', isPinned && 'pinned')}>
|
<div className={cx('sidenav-container', isPinned && 'pinned')}>
|
||||||
<div
|
<div
|
||||||
|
ref={sidenavRef}
|
||||||
className={cx(
|
className={cx(
|
||||||
'sideNav',
|
'sideNav',
|
||||||
isPinned && 'pinned',
|
isPinned && 'pinned',
|
||||||
|
isHovered && 'is-hovered',
|
||||||
isDropdownOpen && 'dropdown-open',
|
isDropdownOpen && 'dropdown-open',
|
||||||
)}
|
)}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
@@ -1182,46 +1212,95 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
|||||||
{isAIAssistantEnabled && renderNavItems([aiAssistantMenuItem], false)}
|
{isAIAssistantEnabled && renderNavItems([aiAssistantMenuItem], false)}
|
||||||
|
|
||||||
<div className="nav-dropdown-item">
|
<div className="nav-dropdown-item">
|
||||||
<Dropdown
|
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
|
||||||
menu={{
|
<DropdownMenuTrigger asChild>
|
||||||
items: helpSupportDropdownMenuItems,
|
<div className="nav-item">
|
||||||
onClick: handleHelpSupportMenuItemClick,
|
<div className="nav-item-data" data-testid="help-support-nav-item">
|
||||||
}}
|
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
|
||||||
placement="topLeft"
|
|
||||||
overlayClassName="nav-dropdown-overlay help-support-dropdown"
|
|
||||||
trigger={['click']}
|
|
||||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
|
||||||
>
|
|
||||||
<div className="nav-item">
|
|
||||||
<div className="nav-item-data" data-testid="help-support-nav-item">
|
|
||||||
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
|
|
||||||
|
|
||||||
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
|
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DropdownMenuTrigger>
|
||||||
</Dropdown>
|
<DropdownMenuContent
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
className="nav-dropdown-overlay help-support-dropdown"
|
||||||
|
>
|
||||||
|
{helpSupportDropdownMenuItems.map((item, idx) => {
|
||||||
|
if ('type' in item) {
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
return <DropdownMenuSeparator key={`help-sep-${idx}`} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={String(item.key)}
|
||||||
|
leftIcon={item.icon}
|
||||||
|
onClick={(e): void =>
|
||||||
|
handleHelpSupportMenuItemClick({
|
||||||
|
...item,
|
||||||
|
key: String(item.key),
|
||||||
|
domEvent: e.nativeEvent,
|
||||||
|
} as unknown as SidebarItem)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nav-dropdown-item">
|
<div className="nav-dropdown-item">
|
||||||
<Dropdown
|
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
|
||||||
menu={{
|
<DropdownMenuTrigger asChild>
|
||||||
items: userSettingsDropdownMenuItems,
|
<div className={cx('nav-item', isSettingsPage && 'active')}>
|
||||||
onClick: handleSettingsMenuItemClick,
|
<div className="nav-item-active-marker" />
|
||||||
}}
|
<div className="nav-item-data" data-testid="settings-nav-item">
|
||||||
placement="topLeft"
|
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
|
||||||
overlayClassName="nav-dropdown-overlay settings-dropdown"
|
|
||||||
trigger={['click']}
|
|
||||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
|
||||||
>
|
|
||||||
<div className={cx('nav-item', isSettingsPage && 'active')}>
|
|
||||||
<div className="nav-item-active-marker" />
|
|
||||||
<div className="nav-item-data" data-testid="settings-nav-item">
|
|
||||||
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
|
|
||||||
|
|
||||||
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
|
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DropdownMenuTrigger>
|
||||||
</Dropdown>
|
<DropdownMenuContent
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
className="nav-dropdown-overlay settings-dropdown"
|
||||||
|
>
|
||||||
|
{(userSettingsDropdownMenuItems ?? []).map((item, idx) => {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ('type' in item && item.type === 'divider') {
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
return <DropdownMenuSeparator key={`settings-sep-${idx}`} />;
|
||||||
|
}
|
||||||
|
const settingsItem = item as {
|
||||||
|
key?: string | number;
|
||||||
|
label?: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={String(settingsItem.key)}
|
||||||
|
leftIcon={settingsItem.icon}
|
||||||
|
disabled={settingsItem.disabled}
|
||||||
|
onClick={(e): void =>
|
||||||
|
handleSettingsMenuItemClick({
|
||||||
|
key: String(settingsItem.key),
|
||||||
|
domEvent: e.nativeEvent,
|
||||||
|
} as unknown as SidebarItem)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{settingsItem.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
border-color: var(--l1-border);
|
border-color: var(--l1-border);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.dropdown-icon {
|
.dropdown-trigger-wrapper {
|
||||||
margin-right: 4px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
import { Button, Divider, Tooltip } from 'antd';
|
||||||
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
import { Switch } from '@signozhq/ui/switch';
|
import { Switch } from '@signozhq/ui/switch';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
|
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
} from 'pages/AlertDetails/hooks';
|
} from 'pages/AlertDetails/hooks';
|
||||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||||
import { useAlertRule } from 'providers/Alert';
|
import { useAlertRule } from 'providers/Alert';
|
||||||
import { CSSProperties } from 'styled-components';
|
|
||||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||||
import { AlertDef } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
|
||||||
@@ -21,16 +21,6 @@ import RenameModal from './RenameModal';
|
|||||||
|
|
||||||
import './ActionButtons.styles.scss';
|
import './ActionButtons.styles.scss';
|
||||||
|
|
||||||
const menuItemStyle: CSSProperties = {
|
|
||||||
fontSize: '14px',
|
|
||||||
letterSpacing: '0.14px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuItemStyleV2: CSSProperties = {
|
|
||||||
fontSize: '13px',
|
|
||||||
letterSpacing: '0.13px',
|
|
||||||
};
|
|
||||||
|
|
||||||
function AlertActionButtons({
|
function AlertActionButtons({
|
||||||
ruleId,
|
ruleId,
|
||||||
alertDetails,
|
alertDetails,
|
||||||
@@ -68,9 +58,7 @@ function AlertActionButtons({
|
|||||||
|
|
||||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||||
|
|
||||||
const finalMenuItemStyle = isV2Alert ? menuItemStyleV2 : menuItemStyle;
|
const menuItems: MenuItem[] = [
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = [
|
|
||||||
...(!isV2Alert
|
...(!isV2Alert
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -78,7 +66,6 @@ function AlertActionButtons({
|
|||||||
label: 'Rename',
|
label: 'Rename',
|
||||||
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
|
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
|
||||||
onClick: handleRename,
|
onClick: handleRename,
|
||||||
style: finalMenuItemStyle,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -87,17 +74,13 @@ function AlertActionButtons({
|
|||||||
label: 'Duplicate',
|
label: 'Duplicate',
|
||||||
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
|
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
|
||||||
onClick: handleAlertDuplicate,
|
onClick: handleAlertDuplicate,
|
||||||
style: finalMenuItemStyle,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete-rule',
|
key: 'delete-rule',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
|
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
|
||||||
onClick: handleAlertDelete,
|
onClick: handleAlertDelete,
|
||||||
style: {
|
danger: true,
|
||||||
...finalMenuItemStyle,
|
|
||||||
color: Color.BG_CHERRY_400,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -140,16 +123,21 @@ function AlertActionButtons({
|
|||||||
|
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
|
|
||||||
<Dropdown trigger={['click']} menu={{ items: menuItems }}>
|
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||||
<Tooltip title="More options">
|
<span className="dropdown-trigger-wrapper">
|
||||||
<Ellipsis
|
<Tooltip title="More options">
|
||||||
size={16}
|
<Button
|
||||||
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
|
type="text"
|
||||||
cursor="pointer"
|
icon={
|
||||||
className="dropdown-icon"
|
<Ellipsis
|
||||||
/>
|
size={16}
|
||||||
</Tooltip>
|
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
|
||||||
</Dropdown>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuSimple>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RenameModal
|
<RenameModal
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import {
|
import { Button, Divider, Form, Space, Tooltip } from 'antd';
|
||||||
Button,
|
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Form,
|
|
||||||
MenuProps,
|
|
||||||
Space,
|
|
||||||
Tooltip,
|
|
||||||
} from 'antd';
|
|
||||||
import { Switch } from '@signozhq/ui/switch';
|
import { Switch } from '@signozhq/ui/switch';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
||||||
@@ -44,16 +37,22 @@ function FunnelStep({
|
|||||||
const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] =
|
const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
|
||||||
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
|
const latencyPointerItems: MenuItem[] = [
|
||||||
(option) => ({
|
{
|
||||||
key: option.value,
|
type: 'radio-group',
|
||||||
label: option.key,
|
value: stepData.latency_pointer,
|
||||||
style:
|
onChange: (value): void =>
|
||||||
option.value === stepData.latency_pointer
|
onStepChange(index, {
|
||||||
? { backgroundColor: 'var(--bg-slate-100)' }
|
latency_pointer: value as FunnelStepData['latency_pointer'],
|
||||||
: {},
|
}),
|
||||||
}),
|
children: LatencyPointers.map((option) => ({
|
||||||
);
|
type: 'radio',
|
||||||
|
key: option.value,
|
||||||
|
label: option.key,
|
||||||
|
value: option.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const updatedCurrentQuery = useMemo(
|
const updatedCurrentQuery = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -211,17 +210,18 @@ function FunnelStep({
|
|||||||
</div>
|
</div>
|
||||||
<div className="latency-pointer">
|
<div className="latency-pointer">
|
||||||
<div className="latency-pointer__label">Latency pointer</div>
|
<div className="latency-pointer__label">Latency pointer</div>
|
||||||
<Dropdown
|
{hasEditPermission ? (
|
||||||
menu={{
|
<DropdownMenuSimple menu={{ items: latencyPointerItems }}>
|
||||||
items: latencyPointerItems,
|
<Space>
|
||||||
onClick: ({ key }): void =>
|
{
|
||||||
onStepChange(index, {
|
LatencyPointers.find(
|
||||||
latency_pointer: key as FunnelStepData['latency_pointer'],
|
(option) => option.value === stepData.latency_pointer,
|
||||||
}),
|
)?.key
|
||||||
}}
|
}
|
||||||
trigger={['click']}
|
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
||||||
disabled={!hasEditPermission}
|
</Space>
|
||||||
>
|
</DropdownMenuSimple>
|
||||||
|
) : (
|
||||||
<Space>
|
<Space>
|
||||||
{
|
{
|
||||||
LatencyPointers.find(
|
LatencyPointers.find(
|
||||||
@@ -230,7 +230,7 @@ function FunnelStep({
|
|||||||
}
|
}
|
||||||
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
||||||
</Space>
|
</Space>
|
||||||
</Dropdown>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
Reference in New Issue
Block a user