Compare commits

..

12 Commits

Author SHA1 Message Date
Gaurav Tewari
04c58a1572 chore: save antd file 2026-05-22 01:58:11 +05:30
Gaurav Tewari
9bbdd00858 chore: sync with main 2026-05-22 01:56:59 +05:30
Gaurav Tewari
316e9c7361 chore: remove internal docs 2026-05-22 01:50:54 +05:30
Gaurav Tewari
634166860b fix: add dropdown to list 2026-05-22 01:40:15 +05:30
Gaurav Tewari
af8f2fa95a chore: self review comments 2026-05-22 01:32:54 +05:30
Gaurav Tewari
f81fd78ff6 fix: lint 2026-05-21 22:09:24 +05:30
Gaurav Tewari
b589a7b2e9 fix: failing test 2026-05-21 20:24:03 +05:30
Gaurav Tewari
716dbc7847 chore: migration from antd 2026-05-21 18:05:21 +05:30
Gaurav Tewari
3a92c7577f fix: another bug 2026-05-21 16:00:12 +05:30
Gaurav Tewari
ba043a5741 fix: side nav issue 2026-05-21 14:59:23 +05:30
Gaurav Tewari
6d2b99eb8d fix: self review changes 2026-05-21 14:11:13 +05:30
Gaurav Tewari
3765ca3d42 chore: migrate dropdown 2026-05-20 17:39:33 +05:30
74 changed files with 833 additions and 666 deletions

View File

@@ -49,7 +49,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.21",
"@signozhq/ui": "0.0.19",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -15,8 +15,9 @@
const BANNED_COMPONENTS = {
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
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.',
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
};
export default {

View File

@@ -77,8 +77,8 @@ importers:
specifier: 0.0.2
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.21
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
specifier: 0.0.19
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -3269,8 +3269,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.21':
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
'@signozhq/ui@0.0.19':
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -3851,6 +3851,27 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
'@webassemblyjs/floating-point-hex-parser@1.13.2':
resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
'@webassemblyjs/helper-api-error@1.13.2':
resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
'@webassemblyjs/helper-buffer@1.14.1':
resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
'@webassemblyjs/helper-numbers@1.13.2':
resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
'@webassemblyjs/helper-wasm-bytecode@1.13.2':
resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
'@webassemblyjs/helper-wasm-section@1.14.1':
resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
@@ -12013,7 +12034,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)

View File

@@ -51,6 +51,13 @@
background: var(--l1-background);
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}

View File

@@ -9,13 +9,13 @@ import {
Flex,
Input,
InputRef,
Progress,
Space,
Spin,
TableColumnsType,
TableColumnType,
Tooltip,
} from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
@@ -59,7 +59,7 @@ function ProgressRender(item: string | number): JSX.Element {
<Progress
percent={percent}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const cpuPercent = percent;
if (cpuPercent >= 90) {

View File

@@ -137,11 +137,12 @@ function CreateServiceAccountModal(): JSX.Element {
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
{...({ form: 'create-sa-form' } as Record<string, string>)}
>
Create Service Account
</Button>

View File

@@ -1,7 +0,0 @@
.dropdown-button {
color: var(--l1-foreground);
}
.dropdown-icon {
font-size: 1.2rem;
}

View File

@@ -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;

View File

@@ -1,15 +1,7 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -241,9 +233,9 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<Ellipsis size="md" />
</Dropdown>
<DropdownMenuSimple menu={moreOptionMenu}>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</DropdownMenuSimple>
)}
</Space>
</OffSetCol>

View File

@@ -11,6 +11,9 @@ import { GuardAuthZ } from './GuardAuthZ';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const ErrorFallback = (error: Error): ReactElement => (
<div>Error occurred: {error.message}</div>
);
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
@@ -87,28 +90,40 @@ describe('GuardAuthZ', () => {
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
it('should render fallbackOnError when API error occurs', async () => {
const errorMessage = 'Internal Server Error';
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
it('should pass error object to fallbackOnError function', async () => {
const errorMessage = 'Network request failed';
let receivedError: Error | null = null;
const errorFallbackWithCapture = (error: Error): ReactElement => {
receivedError = error;
return <div>Captured error: {error.message}</div>;
};
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
@@ -116,14 +131,35 @@ describe('GuardAuthZ', () => {
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnError={<div>Custom error fallback</div>}
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
expect(receivedError).not.toBeNull();
});
expect(receivedError).toBeInstanceOf(Error);
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
});
it('should render null when error occurs and no fallbackOnError provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();

View File

@@ -12,7 +12,7 @@ export type GuardAuthZProps<R extends AuthZRelation> = {
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: JSX.Element;
fallbackOnError?: (error: Error) => JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
@@ -35,7 +35,7 @@ export function GuardAuthZ<R extends AuthZRelation>({
}
if (error) {
return fallbackOnError ?? children;
return fallbackOnError?.(error) ?? null;
}
if (!permissions?.[permission]?.isGranted) {

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { Dropdown } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
)}
{isMultiQueryAllowed && (
<Dropdown
<DropdownMenuSimple
className="query-actions-dropdown"
menu={{
items: [
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
: []),
],
}}
placement="bottomRight"
align="end"
>
<Ellipsis size={16} />
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</div>

View File

@@ -4,13 +4,13 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { Button, Flex, Switch } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from '@signozhq/icons';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
@@ -85,8 +85,9 @@ function DynamicColumnTable({
);
};
const items: MenuProps['items'] =
const items: MenuItem[] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div className="dynamicColumnsTable-items">
<div>{column.title?.toString()}</div>
@@ -96,8 +97,6 @@ function DynamicColumnTable({
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -126,18 +125,14 @@ function DynamicColumnTable({
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<DropdownMenuSimple menu={{ items }}>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
</DropdownMenuSimple>
)}
</Flex>

View File

@@ -127,11 +127,12 @@ function KeyFormPhase({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
{...({ form: FORM_ID } as Record<string, string>)}
>
Create Key
</Button>

View File

@@ -190,11 +190,12 @@ function EditKeyForm({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
{...({ form: FORM_ID } as Record<string, string>)}
>
Save Changes
</Button>

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
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 TimeItems, {
timePreferance,
@@ -27,20 +28,17 @@ function TimePreference({
const menu = useMemo(
() => ({
items: menuItems,
onClick: timeMenuItemOnChangeHandler,
items: menuItems.map((item) => ({
...item,
onClick: timeMenuItemOnChangeHandler,
})),
}),
[timeMenuItemOnChangeHandler],
);
return (
<Dropdown
menu={menu}
rootClassName="time-selection-menu"
className="time-selection-target"
trigger={['click']}
>
<Button>
<DropdownMenuSimple menu={menu} className="time-selection-menu">
<Button className="time-selection-target">
<div className="button-selected-text">
<Globe size={14} />
<Typography.Text className="selected-value">
@@ -49,7 +47,7 @@ function TimePreference({
</div>
<ChevronDown size="md" />
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -204,7 +204,7 @@ describe('createGuardedRoute', () => {
).not.toBeInTheDocument();
});
it('should render the component when API error occurs (fail open)', async () => {
it('should render error fallback when API error occurs', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
@@ -230,8 +230,12 @@ describe('createGuardedRoute', () => {
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
});
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render no permissions fallback when permission is denied', async () => {

View File

@@ -9,11 +9,14 @@ import { parsePermission } from 'hooks/useAuthZ/utils';
import noDataUrl from '@/assets/Icons/no-data.svg';
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import AppLoading from '../AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
@@ -60,6 +63,7 @@ export function createGuardedRoute<P extends object, R extends AuthZRelation>(
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnError={onErrorFallback}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}

View File

@@ -45,10 +45,6 @@
.contributors-row {
height: 80px;
}
.top-contributors-progress {
--progress-background: transparent;
}
&__content {
.ant-table {
&-cell {

View File

@@ -1,7 +1,6 @@
import { HTMLAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Table, TableColumnsType as ColumnsType } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Table, TableColumnsType as ColumnsType } from 'antd';
import logEvent from 'api/common/logEvent';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
@@ -52,8 +51,8 @@ function TopContributorsRows({
<Progress
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
trailColor="rgba(255, 255, 255, 0)"
strokeColor={Color.BG_ROBIN_500}
className="top-contributors-progress"
/>
</ConditionalAlertPopover>
),

View File

@@ -141,9 +141,12 @@
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
span {
font-weight: 600;
.ant-progress-text {
font-weight: 600;
}
}
}

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Skeleton, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Skeleton, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -142,7 +141,7 @@ function DomainMetrics({
Number(formattedDomainMetricsData.errorRate).toFixed(2),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
Number(formattedDomainMetricsData.errorRate).toFixed(2),

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Skeleton, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Skeleton, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
getDisplayValue,
@@ -84,7 +83,7 @@ function EndPointMetrics({
status="active"
percent={Number(Number(metricsData?.errorRate ?? 0).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
Number(metricsData?.errorRate ?? 0).toFixed(2),

View File

@@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { Color } from '@signozhq/design-tokens';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import {
FiltersType,
@@ -261,7 +260,7 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
status="active"
percent={Number((errorRateValue as number).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRateValue as number).toFixed(2));
if (errorRatePercent >= 90) {
@@ -1030,7 +1029,7 @@ export const getEndPointsColumnsConfig = (
).toFixed(1),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRate as number).toFixed(1));
if (errorRatePercent >= 90) {
@@ -2518,7 +2517,7 @@ export const dependentServicesColumns: ColumnType<DependentServicesData>[] = [
status="active"
percent={Number((errorPercentage as number).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorPercentagePercent = Number(
(errorPercentage as number).toFixed(2),
@@ -3030,7 +3029,7 @@ export const getAllEndpointsWidgetData = (
).toFixed(2),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
(

View File

@@ -11,8 +11,13 @@ import {
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import { Dropdown, Skeleton } from 'antd';
import { Skeleton } from 'antd';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
@@ -200,10 +205,19 @@ export default function CustomDomainSettings(): JSX.Element {
!workspaceName ? 'workspace-name-hidden' : ''
}`}
>
<Dropdown
trigger={['click']}
disabled={isFetchingHosts}
dropdownRender={(): JSX.Element => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="link"
color="none"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
@@ -236,14 +250,8 @@ export default function CustomDomainSettings(): JSX.Element {
);
})}
</div>
)}
>
<Button variant="link" color="none">
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}

View File

@@ -1,4 +1,5 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
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 () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
fireEvent.click(
await user.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
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 { DownloadProps } from './Download.types';
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
};
return (
<Dropdown menu={menu} trigger={['click']}>
<DropdownMenuSimple menu={menu}>
<Button
className="download-button"
loading={isLoading || isDownloading}
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
Download
</Flex>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -1,8 +1,4 @@
import {
Col,
Dropdown as DropDownComponent,
Input as InputComponent,
} from 'antd';
import { Col, Input as InputComponent } from 'antd';
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
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`
&&& {
min-width: 100px;

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
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 ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
@@ -176,6 +177,7 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
describe('WidgetGraphComponent', () => {
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(
<MockQueryClientProvider>
<ErrorModalProvider>
@@ -208,7 +210,7 @@ describe('WidgetGraphComponent', () => {
expect(skeleton).toBeInTheDocument();
const moreOptionsButton = getByTestId('widget-header-options');
fireEvent.mouseEnter(moreOptionsButton);
await user.click(moreOptionsButton);
const menu = await findByRole('menu');
expect(menu).toBeInTheDocument();

View File

@@ -54,6 +54,17 @@
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 {
padding-right: 0.25rem;
}

View File

@@ -467,6 +467,7 @@ describe('WidgetHeader', () => {
describe('Create Alerts Menu Item', () => {
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<WidgetHeader
title={TEST_WIDGET_TITLE}
@@ -483,7 +484,7 @@ describe('WidgetHeader', () => {
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
expect(moreOptionsIcon).toBeInTheDocument();
await userEvent.hover(moreOptionsIcon);
await user.click(moreOptionsIcon);
await screen.findByText(CREATE_ALERTS_TEXT);
@@ -494,6 +495,7 @@ describe('WidgetHeader', () => {
});
it('Create Alerts menu item is enabled and clickable', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockCreateAlertsHandler = jest.fn();
const useCreateAlerts = jest.requireMock(
'hooks/queryBuilder/useCreateAlerts',
@@ -517,12 +519,12 @@ describe('WidgetHeader', () => {
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
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);
// Verify the menu item is clickable by actually clicking it
await userEvent.click(createAlertsMenuItem);
await user.click(createAlertsMenuItem);
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
});
});

View File

@@ -15,7 +15,8 @@ import {
X,
} from '@signozhq/icons';
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 ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
@@ -128,7 +129,7 @@ function WidgetHeader({
],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
if (isTWidgetOptions(key)) {
const functionToCall = keyMethodMapping[key];
@@ -188,18 +189,8 @@ function WidgetHeader({
{
key: MenuItemKeys.CreateAlerts,
icon: <Bell size="md" />,
label: (
<span
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
}}
>
{MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts]}
<SquareArrowOutUpRight size={10} />
</span>
),
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
rightIcon: <SquareArrowOutUpRight size="lg" />,
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
disabled: false,
},
@@ -221,8 +212,10 @@ function WidgetHeader({
const menu = useMemo(
() => ({
items: updatedMenuList,
onClick: onMenuItemSelectHandler,
items: updatedMenuList.map((item) => ({
...item,
onClick: onMenuItemSelectHandler,
})),
}),
[updatedMenuList, onMenuItemSelectHandler],
);
@@ -321,7 +314,12 @@ function WidgetHeader({
/>
)}
{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
data-testid="widget-header-options"
className={`widget-header-more-options ${
@@ -329,7 +327,7 @@ function WidgetHeader({
}`}
icon={<EllipsisVertical size="md" />}
/>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</>

View File

@@ -6,6 +6,7 @@ export interface MenuItem {
key: MenuItemKeys;
icon: ReactNode;
label: ReactNode;
rightIcon?: ReactNode;
isVisible: boolean;
disabled: boolean;
danger?: boolean;

View File

@@ -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 { MenuItem } from './types';
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
actions
.filter((action: MenuItem) => action.isVisible)
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({

View File

@@ -39,5 +39,7 @@
width: 100% !important;
--progress-width: 100%;
.ant-progress-steps-outer {
width: 100% !important;
}
}

View File

@@ -1,4 +1,4 @@
import { Progress } from '@signozhq/ui/progress';
import { Progress } from 'antd';
import { ChecklistItem } from '../HomeChecklist/HomeChecklist';
@@ -15,7 +15,9 @@ function StepsProgress({
const totalChecklistItems = checklistItems.length;
const progress = (completedChecklistItems.length / totalChecklistItems) * 100;
const progress = Math.round(
(completedChecklistItems.length / totalChecklistItems) * 100,
);
return (
<div className="steps-progress-container">

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tag } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
getHostLists,
@@ -80,8 +79,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
size="small"
strokeColor={getProgressColor(Number(value))}
showInfo
/>
),
},
@@ -91,8 +90,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
size="small"
strokeColor={getMemoryProgressColor(Number(value))}
showInfo
/>
),
},

View File

@@ -60,6 +60,11 @@
& > div {
width: 100%;
}
:global(.ant-progress-bg) {
height: 8px !important;
border-radius: 4px;
}
}
.progressBar {

View File

@@ -103,8 +103,12 @@
.progress-container {
width: 158px;
span {
font-weight: 600;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
@@ -288,6 +292,10 @@
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {

View File

@@ -1,4 +1,4 @@
import { Progress } from '@signozhq/ui/progress';
import { Progress } from 'antd';
import TanStackTable from 'components/TanStackTableView';
import {
getMemoryProgressColor,
@@ -53,6 +53,7 @@ export function EntityProgressBar({
<Progress
percent={percentage}
strokeLinecap="butt"
size="small"
status="normal"
strokeColor={getStrokeColor(type, value)}
className={styles.progressBar}

View File

@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { Button, Flex, Input } from 'antd';
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 logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -15,7 +16,6 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
TableDataSource,
@@ -323,55 +323,67 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'id',
key: 'action',
width: 10,
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
<div data-testid="alert-actions">
<DropDown
onDropDownItemClick={(item): void =>
alertActionLogEvent(item.key, record)
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => {
const actionItems = [
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
element={[
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
type="link"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
key="3-new-tab"
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>
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"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
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>
),
style={{ color: 'var(--l1-foreground)' }}
icon={<Ellipsis size={16} />}
/>
</DropdownMenuSimple>
</div>
);
},
});
}

View File

@@ -12,12 +12,11 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Modal,
Popover,
Skeleton,
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
];
const getCreateDashboardItems = useMemo(() => {
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
{
label: (
<div
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
{createNewDashboard && (
<section className="actions">
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="text"
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
>
New Dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
<Button
type="text"
className="learn-more"
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
onChange={handleSearch}
/>
{createNewDashboard && (
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="primary"
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
>
New dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
)}
</div>

View File

@@ -2,7 +2,13 @@ import { useCallback } from 'react';
import { useCopyToClipboard } from 'react-use';
import { orange } from '@ant-design/colors';
import { Settings } from '@signozhq/icons';
import { Dropdown, MenuProps } from 'antd';
import {
type BaseMenuItem,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import {
negateOperator,
OPERATORS,
@@ -135,41 +141,38 @@ function BodyTitleRenderer({
viewName,
]);
const onClickHandler: MenuProps['onClick'] = (props): void => {
const onClickHandler = (key: string): void => {
const mapper = {
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
};
const handler = mapper[props.key];
const handler = mapper[key];
if (handler) {
handler();
}
};
const menu: MenuProps = {
items: [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
],
onClick: onClickHandler,
};
const menuItems: BaseMenuItem[] = [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
];
const handleNodeClick = useCallback(
(e: React.MouseEvent): void => {
@@ -218,15 +221,23 @@ function BodyTitleRenderer({
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<div data-log-detail-ignore="true">
{menuItems.map((item) => (
<DropdownMenuItem
key={item.key}
onSelect={(): void => onClickHandler(item.key as string)}
>
{item.label}
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</span>
)}
{title.toString()}{' '}

View File

@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Check, ChevronDown, Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Input } from '@signozhq/ui/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListUsers } from 'api/generated/services/users';
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
@@ -95,7 +94,7 @@ function MembersSettings(): JSX.Element {
).length;
const totalCount = allMembers.length;
const filterMenuItems: MenuProps['items'] = [
const filterMenuItems: MenuItem[] = [
{
key: FilterMode.All,
label: (
@@ -171,10 +170,9 @@ function MembersSettings(): JSX.Element {
</div>
<div className="members-settings__controls">
<Dropdown
<DropdownMenuSimple
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="members-filter-dropdown"
className="members-filter-dropdown"
>
<Button
variant="solid"
@@ -184,7 +182,7 @@ function MembersSettings(): JSX.Element {
<span>{filterLabel}</span>
<ChevronDown size={12} className="members-filter-trigger__chevron" />
</Button>
</Dropdown>
</DropdownMenuSimple>
<div className="members-settings__search">
<Input

View File

@@ -1,4 +1,5 @@
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
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 () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
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);
fireEvent.click(pendingOption);
await user.click(pendingOption);
await screen.findByText('charlie@signoz.io');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();

View File

@@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { generatePath } from 'react-router-dom';
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 {
useGetMetricAlerts,
@@ -126,12 +127,11 @@ function DashboardsAndAlertsPopover({
return (
<div className="dashboards-and-alerts-popover-container">
{dashboardsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: dashboardsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover dashboards-popover"
@@ -142,15 +142,14 @@ function DashboardsAndAlertsPopover({
{pluralize(dashboards.length, 'dashboard')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
{alertsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: alertsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover alerts-popover"
@@ -161,7 +160,7 @@ function DashboardsAndAlertsPopover({
{pluralize(alerts.length, 'alert rule')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
);

View File

@@ -142,6 +142,13 @@
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}

View File

@@ -7,7 +7,12 @@ import {
DropResult,
} from 'react-beautiful-dnd';
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 { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -159,34 +164,12 @@ function ExplorerColumnsRenderer({
debouncedSetQuerySearchText(e.target.value);
};
const items: MenuProps['items'] = [
{
key: 'search',
label: (
<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 handleOpenChange = (nextOpen: boolean): void => {
setOpen(nextOpen);
if (nextOpen) {
setSearchText('');
}
};
const removeSelectedLogField = (name: string): void => {
if (
@@ -238,13 +221,6 @@ function ExplorerColumnsRenderer({
}
};
const toggleDropdown = (): void => {
setOpen(!open);
if (!open) {
setSearchText('');
}
};
const isDarkMode = useIsDarkMode();
return (
@@ -327,25 +303,38 @@ function ExplorerColumnsRenderer({
</Droppable>
</DragDropContext>
<div>
<Dropdown
menu={{ items }}
arrow
placement="top"
open={open}
overlayClassName="explorer-columns-dropdown"
>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
onClick={toggleDropdown}
/>
</Dropdown>
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
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>
)}

View File

@@ -146,6 +146,7 @@ describe('ExplorerColumnsRenderer', () => {
});
it('opens and closes the dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Wrapper>
<ExplorerColumnsRenderer
@@ -158,12 +159,12 @@ describe('ExplorerColumnsRenderer', () => {
);
const addButton = screen.getByTestId('add-columns-button');
await userEvent.click(addButton);
await user.click(addButton);
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
expect(screen.getByText('attribute1')).toBeInTheDocument();
await userEvent.click(addButton);
await user.click(addButton);
await waitFor(() => {
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

View File

@@ -13,7 +13,7 @@ import { Plus, Trash2 } from '@signozhq/icons';
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
import { getBaseUrl } from 'utils/basePath';
import VariablesDropdown from './VariablesDropdown';
import VariablesPopover from './VariablesPopover';
import './UpdateContextLinks.styles.scss';
@@ -71,7 +71,7 @@ function UpdateContextLinks({
customVariables: fieldVariables,
});
// Transform variables into the format expected by VariablesDropdown
// Transform variables into the format expected by VariablesPopover
const transformedVariables = useMemo(
() => transformContextVariables(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}
variables={transformedVariables}
>
@@ -252,7 +254,7 @@ function UpdateContextLinks({
/>
</div>
)}
</VariablesDropdown>
</VariablesPopover>
</Form.Item>
{/* Remove the separate variables section */}
@@ -282,7 +284,7 @@ function UpdateContextLinks({
/>
</Col>
<Col span={16}>
<VariablesDropdown
<VariablesPopover
onVariableSelect={(variableName, cursorPosition): void =>
handleParamVariableSelect(index, variableName, cursorPosition)
}
@@ -311,7 +313,7 @@ function UpdateContextLinks({
}
/>
)}
</VariablesDropdown>
</VariablesPopover>
</Col>
<Col span={2}>
<Button

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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
* @returns Array of transformed variables with proper source descriptions
*/

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
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 useDebounce from 'hooks/useDebounce';
@@ -26,7 +27,7 @@ function ColorSelector({
setColorFromPicker(hex);
};
const items: MenuProps['items'] = [
const items: MenuItem[] = [
{
key: 'Red',
label: <CustomColor color="Red" />,
@@ -62,7 +63,7 @@ function ColorSelector({
];
return (
<Dropdown menu={{ items }} trigger={['click']}>
<DropdownMenuSimple menu={{ items }}>
<Button
onClick={(e): void => e.preventDefault()}
className="color-selector-button"
@@ -72,7 +73,7 @@ function ColorSelector({
<ChevronDown size="md" />
</Space>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -87,7 +87,12 @@
.service-progress-indicator {
width: fit-content;
--progress-width: 30px;
margin-inline-end: 0px !important;
margin-bottom: 0px !important;
.ant-progress-inner {
width: 30px;
}
}
.percent-value {

View File

@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Skeleton, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Skeleton, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { AxiosError } from 'axios';
import Spinner from 'components/Spinner';

View File

@@ -1,9 +1,8 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Check, ChevronDown, Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Input } from '@signozhq/ui/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
@@ -134,7 +133,7 @@ function ServiceAccountsSettings(): JSX.Element {
const totalCount = allAccounts.length;
const filterMenuItems: MenuProps['items'] = [
const filterMenuItems: MenuItem[] = [
{
key: FilterMode.All,
label: (
@@ -231,10 +230,9 @@ function ServiceAccountsSettings(): JSX.Element {
) : (
<div className="sa-settings__list-section">
<div className="sa-settings__controls">
<Dropdown
<DropdownMenuSimple
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="sa-settings-filter-dropdown"
className="sa-settings-filter-dropdown"
>
<Button
variant="solid"
@@ -247,7 +245,7 @@ function ServiceAccountsSettings(): JSX.Element {
className="sa-settings-filter-trigger__chevron"
/>
</Button>
</Dropdown>
</DropdownMenuSimple>
<div className="sa-settings__search">
<Input

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from 'react';
import userEvent from '@testing-library/user-event';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -129,6 +130,7 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter>
<ServiceAccountsSettings />
@@ -137,10 +139,10 @@ describe('ServiceAccountsSettings (integration)', () => {
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);
fireEvent.click(activeOption);
await user.click(activeOption);
await screen.findByText('CI Bot');
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();

View File

@@ -662,7 +662,7 @@
}
}
&:not(.pinned):hover,
&:not(.pinned).is-hovered,
&.dropdown-open {
flex: 0 0 240px;
max-width: 240px;

View File

@@ -1,5 +1,6 @@
import {
MouseEvent,
ReactNode,
useCallback,
useEffect,
useMemo,
@@ -25,7 +26,14 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
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 { Logout } from 'api/utils';
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 navTopSectionRef = useRef<HTMLDivElement>(null);
const sidenavRef = useRef<HTMLDivElement>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isDropdownOpenRef = useRef(false);
const [isHovered, setIsHovered] = useState(false);
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
@@ -175,9 +185,27 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}, []);
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);
}, []);
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 => {
if (navTopSectionRef.current) {
const { scrollHeight, clientHeight, scrollTop } = navTopSectionRef.current;
@@ -959,9 +987,11 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
return (
<div className={cx('sidenav-container', isPinned && 'pinned')}>
<div
ref={sidenavRef}
className={cx(
'sideNav',
isPinned && 'pinned',
isHovered && 'is-hovered',
isDropdownOpen && 'dropdown-open',
)}
onMouseEnter={handleMouseEnter}
@@ -1182,46 +1212,95 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{isAIAssistantEnabled && renderNavItems([aiAssistantMenuItem], false)}
<div className="nav-dropdown-item">
<Dropdown
menu={{
items: helpSupportDropdownMenuItems,
onClick: handleHelpSupportMenuItemClick,
}}
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>
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
<DropdownMenuTrigger asChild>
<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>
</Dropdown>
</DropdownMenuTrigger>
<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 className="nav-dropdown-item">
<Dropdown
menu={{
items: userSettingsDropdownMenuItems,
onClick: handleSettingsMenuItemClick,
}}
placement="topLeft"
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>
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
<DropdownMenuTrigger asChild>
<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>
</Dropdown>
</DropdownMenuTrigger>
<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>

View File

@@ -48,10 +48,11 @@ describe('useAuthZ', () => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).toBeNull();
expect(result.current.permissions).toStrictEqual(expectedResponse);
});
it('should return error and null permissions when API errors', async () => {
it('should handle API errors', async () => {
const permission = buildPermission('read', 'role:*');
server.use(
@@ -190,6 +191,7 @@ describe('useAuthZ', () => {
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.permissions).toStrictEqual({});
});

View File

@@ -8,8 +8,10 @@
border-color: var(--l1-border);
margin: 0;
}
.dropdown-icon {
margin-right: 4px;
.dropdown-trigger-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
}
.dropdown-menu {

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
import { Button, Divider, Switch, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
import {
@@ -11,7 +12,6 @@ import {
} from 'pages/AlertDetails/hooks';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useAlertRule } from 'providers/Alert';
import { CSSProperties } from 'styled-components';
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
import { AlertDef } from 'types/api/alerts/def';
@@ -20,16 +20,6 @@ import RenameModal from './RenameModal';
import './ActionButtons.styles.scss';
const menuItemStyle: CSSProperties = {
fontSize: '14px',
letterSpacing: '0.14px',
};
const menuItemStyleV2: CSSProperties = {
fontSize: '13px',
letterSpacing: '0.13px',
};
function AlertActionButtons({
ruleId,
alertDetails,
@@ -67,9 +57,7 @@ function AlertActionButtons({
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
const finalMenuItemStyle = isV2Alert ? menuItemStyleV2 : menuItemStyle;
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
...(!isV2Alert
? [
{
@@ -77,7 +65,6 @@ function AlertActionButtons({
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: handleRename,
style: finalMenuItemStyle,
},
]
: []),
@@ -86,17 +73,13 @@ function AlertActionButtons({
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: handleAlertDuplicate,
style: finalMenuItemStyle,
},
{
key: 'delete-rule',
label: 'Delete',
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
onClick: handleAlertDelete,
style: {
...finalMenuItemStyle,
color: Color.BG_CHERRY_400,
},
danger: true,
},
];
@@ -143,16 +126,21 @@ function AlertActionButtons({
<Divider type="vertical" />
<Dropdown trigger={['click']} menu={{ items: menuItems }}>
<Tooltip title="More options">
<Ellipsis
size={16}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
cursor="pointer"
className="dropdown-icon"
/>
</Tooltip>
</Dropdown>
<DropdownMenuSimple menu={{ items: menuItems }}>
<span className="dropdown-trigger-wrapper">
<Tooltip title="More options">
<Button
type="text"
icon={
<Ellipsis
size={16}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
/>
}
/>
</Tooltip>
</span>
</DropdownMenuSimple>
</div>
<RenameModal

View File

@@ -1,14 +1,6 @@
import { useMemo, useState } from 'react';
import {
Button,
Divider,
Dropdown,
Form,
MenuProps,
Space,
Switch,
Tooltip,
} from 'antd';
import { Button, Divider, Form, Space, Switch, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import cx from 'classnames';
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
import { QueryParams } from 'constants/query';
@@ -44,16 +36,22 @@ function FunnelStep({
const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] =
useState<boolean>(false);
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
(option) => ({
key: option.value,
label: option.key,
style:
option.value === stepData.latency_pointer
? { backgroundColor: 'var(--bg-slate-100)' }
: {},
}),
);
const latencyPointerItems: MenuItem[] = [
{
type: 'radio-group',
value: stepData.latency_pointer,
onChange: (value): void =>
onStepChange(index, {
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(
() => ({
@@ -212,17 +210,18 @@ function FunnelStep({
</div>
<div className="latency-pointer">
<div className="latency-pointer__label">Latency pointer</div>
<Dropdown
menu={{
items: latencyPointerItems,
onClick: ({ key }): void =>
onStepChange(index, {
latency_pointer: key as FunnelStepData['latency_pointer'],
}),
}}
trigger={['click']}
disabled={!hasEditPermission}
>
{hasEditPermission ? (
<DropdownMenuSimple menu={{ items: latencyPointerItems }}>
<Space>
{
LatencyPointers.find(
(option) => option.value === stepData.latency_pointer,
)?.key
}
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</DropdownMenuSimple>
) : (
<Space>
{
LatencyPointers.find(
@@ -231,7 +230,7 @@ function FunnelStep({
}
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</Dropdown>
)}
</div>
</div>
</Form>

View File

@@ -223,23 +223,25 @@ function PrettyView({
return (
<span className="pretty-view__value-row">
<span>{content}</span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<span onClick={(e: React.MouseEvent): void => e.stopPropagation()}>
<Dropdown
menu={{ items: menuItems }}
align="start"
className="pretty-view-actions-dropdown"
<Dropdown
menu={{ items: menuItems }}
align="start"
className="pretty-view-actions-dropdown"
// onClick on the dropdown content is forwarded to the underlying div via ...props
// but is not in the public type. Stop click bubbling so item clicks don't reach
// clickable ancestors of the trigger through the React tree.
// @ts-expect-error see comment above
onClick={(e: React.MouseEvent): void => e.stopPropagation()}
>
<span
className="pretty-view__actions"
onClick={(e): void => e.stopPropagation()}
role="button"
tabIndex={0}
>
<span
className="pretty-view__actions"
onClick={(e): void => e.stopPropagation()}
role="button"
tabIndex={0}
>
<Ellipsis size={12} />
</span>
</Dropdown>
</span>
<Ellipsis size={12} />
</span>
</Dropdown>
</span>
);
},

View File

@@ -129,13 +129,14 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const {
permissions: permissionsResult,
isFetching: isFetchingPermissions,
error: errorOnPermissions,
refetchPermissions,
} = useAuthZ([IsAdminPermission, IsEditorPermission, IsViewerPermission], {
enabled: isLoggedIn,
});
const isFetchingUser = isFetchingUserData || isFetchingPermissions;
const userFetchError = userFetchDataError;
const userFetchError = userFetchDataError || errorOnPermissions;
const userRole = useMemo(() => {
if (permissionsResult?.[IsAdminPermission]?.isGranted) {

View File

@@ -298,32 +298,9 @@ describe('AppProvider when authz/check fails', () => {
beforeEach(() => {
queryClient.clear();
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
server.use(
rest.get(MY_USER_URL, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
id: 'u-1',
displayName: 'Test User',
email: 'test@signoz.io',
orgId: 'org-1',
isRoot: false,
status: 'active',
},
}),
),
),
rest.get(MY_ORG_URL, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ data: { id: 'org-1', displayName: 'Org' } }),
),
),
);
});
it('does not set userFetchError when authz/check returns 500 (authz errors are ignored)', async () => {
it('sets userFetchError when authz/check returns 500 (same as user fetch error)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_, res, ctx) =>
res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })),
@@ -337,13 +314,13 @@ describe('AppProvider when authz/check fails', () => {
await waitFor(
() => {
expect(result.current.userFetchError).toBeFalsy();
expect(result.current.userFetchError).toBeTruthy();
},
{ timeout: 2000 },
);
});
it('does not set userFetchError when authz/check fails with network error (authz errors are ignored)', async () => {
it('sets userFetchError when authz/check fails with network error (same as user fetch error)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_, res) => res.networkError('Network error')),
);
@@ -355,7 +332,7 @@ describe('AppProvider when authz/check fails', () => {
await waitFor(
() => {
expect(result.current.userFetchError).toBeFalsy();
expect(result.current.userFetchError).toBeTruthy();
},
{ timeout: 2000 },
);