Compare commits

..

16 Commits

Author SHA1 Message Date
Gaurav Tewari
c1e81a57a3 chore: remove ox lint 2026-05-22 20:54:16 +05:30
Gaurav Tewari
c30994f966 fix: lint 2026-05-22 20:51:38 +05:30
Gaurav Tewari
9aef69a186 fix: lint 2026-05-22 20:48:05 +05:30
Gaurav Tewari
66bc4b4398 Merge remote-tracking branch 'origin/main' into refactor/dropdown-migration
# Conflicts:
#	frontend/plugins/rules/no-antd-components.mjs
#	frontend/src/components/ResizeTable/DynamicColumnTable.tsx
#	frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx
#	frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.tsx
2026-05-22 19:03:05 +05:30
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
83 changed files with 1033 additions and 982 deletions

7
.github/CODEOWNERS vendored
View File

@@ -118,9 +118,6 @@ go.mod @therealpandey
/tests/integration/ @therealpandey
# e2e tests
/tests/e2e/ @AshwinBhatkal
# Flagger Owners
/pkg/flagger/ @therealpandey
@@ -165,7 +162,3 @@ go.mod @therealpandey
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend

View File

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

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

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

@@ -69,8 +69,6 @@ export function useLogsTableColumns({
id: 'timestamp',
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
canBeHidden: false,
enableRemove: false,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
@@ -94,7 +92,6 @@ export function useLogsTableColumns({
header: 'Body',
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
enableRemove: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text

View File

@@ -62,7 +62,6 @@ describe('LogsFormatOptionsMenu (unit)', () => {
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
onReorder: jest.fn(),
},
}}
/>,

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,14 +4,14 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} 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 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';
@@ -84,8 +84,9 @@ function DynamicColumnTable({
);
};
const items: MenuProps['items'] =
const items: MenuItem[] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div
className="dynamicColumnsTable-items"
@@ -99,8 +100,6 @@ function DynamicColumnTable({
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -129,18 +128,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

@@ -308,7 +308,9 @@ function TanStackTableInner<TData>(
});
const hasSingleColumn = useMemo(
() => effectiveColumns.filter((c) => !c.pin).length <= 1,
() =>
effectiveColumns.filter((c) => !c.pin && c.enableRemove !== false).length <=
1,
[effectiveColumns],
);

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

@@ -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';
@@ -137,11 +136,12 @@ function DomainMetrics({
<Tooltip title={formattedDomainMetricsData.errorRate}>
{formattedDomainMetricsData.errorRate !== '-' ? (
<Progress
status="active"
percent={Number(
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,
@@ -81,9 +80,10 @@ function EndPointMetrics({
<Tooltip title={metricsData?.errorRate}>
{metricsData?.errorRate !== '-' ? (
<Progress
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,
@@ -258,9 +257,10 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate;
return (
<Progress
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) {
@@ -1022,13 +1022,14 @@ export const getEndPointsColumnsConfig = (
className: `column`,
render: (errorRate: number | string): React.ReactNode => (
<Progress
status="active"
percent={Number(
(
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
).toFixed(1),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRate as number).toFixed(1));
if (errorRatePercent >= 90) {
@@ -2513,9 +2514,10 @@ export const dependentServicesColumns: ColumnType<DependentServicesData>[] = [
render: (errorPercentage: number | string): React.ReactNode =>
errorPercentage !== '-' ? (
<Progress
status="active"
percent={Number((errorPercentage as number).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorPercentagePercent = Number(
(errorPercentage as number).toFixed(2),
@@ -3020,13 +3022,14 @@ export const getAllEndpointsWidgetData = (
),
F1: (errorRate: any): ReactNode => (
<Progress
status="active"
percent={Number(
(
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
).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,15 @@ 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 +246,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

@@ -16,6 +16,7 @@ import { useLogsTableColumns } from 'components/Logs/TableView/useLogsTableColum
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import type { TanStackTableHandle } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { useHiddenColumnIds } from 'components/TanStackTableView/useColumnStore';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
@@ -23,11 +24,13 @@ import { QueryParams } from 'constants/query';
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEventSource } from 'providers/EventSource';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
// interfaces
import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -51,6 +54,9 @@ function LiveLogsList({
const { isConnectionLoading } = useEventSource();
const { activeLogId } = useCopyLogLink();
const { logs: logsPreferences } = usePreferenceContext();
const hiddenColumnIds = useHiddenColumnIds(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const hasReconciledHiddenColumnsRef = useRef(false);
const {
activeLog,
@@ -66,7 +72,7 @@ function LiveLogsList({
[logs],
);
const { options, config } = useOptionsMenu({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
aggregateOperator: StringOperators.NOOP,
@@ -77,7 +83,16 @@ function LiveLogsList({
[formattedLogs, activeLogId],
);
const selectedFields = convertKeysToColumnFields(options.selectColumns);
const selectedFields = convertKeysToColumnFields([
...defaultLogsSelectedColumns,
...options.selectColumns,
]);
const syncedSelectedColumns = useMemo(
() =>
options.selectColumns.filter(({ name }) => !hiddenColumnIds.includes(name)),
[options.selectColumns, hiddenColumnIds],
);
const logsColumns = useLogsTableColumns({
fields: selectedFields,
@@ -85,6 +100,30 @@ function LiveLogsList({
appendTo: 'end',
});
useEffect(() => {
if (hasReconciledHiddenColumnsRef.current) {
return;
}
hasReconciledHiddenColumnsRef.current = true;
if (syncedSelectedColumns.length === options.selectColumns.length) {
return;
}
logsPreferences.updateColumns(syncedSelectedColumns);
}, [logsPreferences, options.selectColumns.length, syncedSelectedColumns]);
const handleColumnRemove = useCallback(
(columnId: string) => {
const updatedColumns = options.selectColumns.filter(
({ name }) => name !== columnId,
);
logsPreferences.updateColumns(updatedColumns);
},
[options.selectColumns, logsPreferences],
);
const makeOnLogCopy = useCallback(
(log: ILog) =>
(event: MouseEvent<HTMLElement>): void => {
@@ -198,7 +237,7 @@ function LiveLogsList({
ref={ref as React.Ref<TanStackTableHandle>}
columns={logsColumns}
columnStorageKey={LOCALSTORAGE.LOGS_LIST_COLUMNS}
onColumnRemove={config?.addColumn?.onRemove}
onColumnRemove={handleColumnRemove}
plainTextCellLineClamp={options.maxLines}
cellTypographySize={options.fontSize}
data={formattedLogs}

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

@@ -18,19 +18,21 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import type { TanStackTableHandle } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import type { TableColumnDef } from 'components/TanStackTableView/types';
import { useHiddenColumnIds } from 'components/TanStackTableView/useColumnStore';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import APIError from 'types/api/error';
// interfaces
import { ILog } from 'types/api/logs/log';
@@ -67,6 +69,10 @@ function LogsExplorerList({
const [, setCopy] = useCopyToClipboard();
const isDarkMode = useIsDarkMode();
const { activeLogId } = useCopyLogLink();
const { logs: logsPreferences } = usePreferenceContext();
const hiddenColumnIds = useHiddenColumnIds(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const hasReconciledHiddenColumnsRef = useRef(false);
const {
activeLog,
onAddToQuery,
@@ -75,7 +81,7 @@ function LogsExplorerList({
handleCloseLogDetail,
} = useLogDetailHandlers();
const { options, config } = useOptionsMenu({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
aggregateOperator:
@@ -91,15 +97,28 @@ function LogsExplorerList({
);
const selectedFields = useMemo(
() => convertKeysToColumnFields(options.selectColumns),
[options.selectColumns],
() =>
convertKeysToColumnFields([
...defaultLogsSelectedColumns,
...options.selectColumns,
]),
[options],
);
const handleColumnOrderChange = useCallback(
(cols: TableColumnDef<ILog>[]): void => {
config?.addColumn?.onReorder(cols.map((c) => c.id));
const syncedSelectedColumns = useMemo(
() =>
options.selectColumns.filter(({ name }) => !hiddenColumnIds.includes(name)),
[options.selectColumns, hiddenColumnIds],
);
const handleColumnRemove = useCallback(
(columnId: string) => {
const updatedColumns = options.selectColumns.filter(
({ name }) => name !== columnId,
);
logsPreferences.updateColumns(updatedColumns);
},
[config],
[options.selectColumns, logsPreferences],
);
const logsColumns = useLogsTableColumns({
@@ -142,6 +161,20 @@ function LogsExplorerList({
}
}, [isLoading, isFetching, isError, logs.length]);
useEffect(() => {
if (hasReconciledHiddenColumnsRef.current) {
return;
}
hasReconciledHiddenColumnsRef.current = true;
if (syncedSelectedColumns.length === options.selectColumns.length) {
return;
}
logsPreferences.updateColumns(syncedSelectedColumns);
}, [logsPreferences, options.selectColumns.length, syncedSelectedColumns]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
@@ -204,8 +237,7 @@ function LogsExplorerList({
ref={ref as React.Ref<TanStackTableHandle>}
columns={logsColumns}
columnStorageKey={LOCALSTORAGE.LOGS_LIST_COLUMNS}
onColumnRemove={config?.addColumn?.onRemove}
onColumnOrderChange={handleColumnOrderChange}
onColumnRemove={handleColumnRemove}
plainTextCellLineClamp={options.maxLines}
cellTypographySize={options.fontSize}
data={logs}

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

@@ -1,79 +0,0 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultLogsSelectedColumns,
ensureLogsRequiredColumns,
} from '../constants';
const TIMESTAMP = defaultLogsSelectedColumns.find(
(c) => c.name === 'timestamp',
);
const BODY = defaultLogsSelectedColumns.find((c) => c.name === 'body');
if (!TIMESTAMP || !BODY) {
throw new Error('defaults missing timestamp/body — test fixture invalid');
}
const ATTR_A: TelemetryFieldKey = {
name: 'service.name',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
};
const ATTR_B: TelemetryFieldKey = {
name: 'severity_text',
signal: 'logs',
fieldContext: 'log',
fieldDataType: 'string',
};
describe('ensureLogsRequiredColumns', () => {
it('prepends both timestamp + body to an empty list', () => {
expect(ensureLogsRequiredColumns([])).toStrictEqual([TIMESTAMP, BODY]);
});
it('prepends only `body` when `timestamp` is already present', () => {
expect(ensureLogsRequiredColumns([TIMESTAMP, ATTR_A])).toStrictEqual([
BODY,
TIMESTAMP,
ATTR_A,
]);
});
it('prepends only `timestamp` when `body` is already present', () => {
expect(ensureLogsRequiredColumns([BODY, ATTR_A])).toStrictEqual([
TIMESTAMP,
BODY,
ATTR_A,
]);
});
it('returns the same array when both are present (no duplicates, original order preserved)', () => {
const input = [TIMESTAMP, BODY, ATTR_A, ATTR_B];
expect(ensureLogsRequiredColumns(input)).toBe(input);
});
it('preserves a non-default order when both are present', () => {
const input = [ATTR_A, BODY, ATTR_B, TIMESTAMP];
expect(ensureLogsRequiredColumns(input)).toStrictEqual(input);
});
it('prepends both when neither is present in a list of user attributes', () => {
expect(ensureLogsRequiredColumns([ATTR_A, ATTR_B])).toStrictEqual([
TIMESTAMP,
BODY,
ATTR_A,
ATTR_B,
]);
});
it('does not duplicate if a required column appears twice in the input', () => {
// Tolerant of malformed input — invariant only adds *missing* required
// columns; it does not deduplicate existing entries (that's a separate
// concern, not its job).
const input = [BODY, BODY, ATTR_A];
const result = ensureLogsRequiredColumns(input);
expect(result.filter((c) => c.name === 'timestamp')).toHaveLength(1);
expect(result[0]).toStrictEqual(TIMESTAMP);
});
});

View File

@@ -35,32 +35,6 @@ export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
},
];
const LOGS_REQUIRED_COLUMNS = ['timestamp', 'body'] as const;
/**
* Always-on invariant: every logs selectColumns array must contain `body` and
* `timestamp`. Applied at both loader and writer boundaries so the picker, the
* table, and persisted state can never diverge into a "missing required
* column" state.
*/
export function ensureLogsRequiredColumns(
columns: TelemetryFieldKey[],
): TelemetryFieldKey[] {
const missing = LOGS_REQUIRED_COLUMNS.filter(
(name) => !columns.some((c) => c.name === name),
);
if (missing.length === 0) {
return columns;
}
const defaultsByName = new Map(
defaultLogsSelectedColumns.map((c) => [c.name, c]),
);
const prepended = missing
.map((name) => defaultsByName.get(name))
.filter((c): c is TelemetryFieldKey => c !== undefined);
return [...prepended, ...columns];
}
export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
{
name: 'service.name',

View File

@@ -40,6 +40,5 @@ export type OptionsMenuConfig = {
isFetching: boolean;
value: TelemetryFieldKey[];
onRemove: (key: string) => void;
onReorder: (orderedIds: string[]) => void;
};
};

View File

@@ -187,6 +187,30 @@ const useOptionsMenu = ({
searchedAttributesDataV5?.data.data.keys || {},
).flat();
if (searchedAttributesDataList.length) {
if (dataSource === DataSource.LOGS) {
const logsSelectedColumns: TelemetryFieldKey[] =
defaultLogsSelectedColumns.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}));
return [
...logsSelectedColumns,
...searchedAttributesDataList
.filter((attribute) => attribute.name !== 'body')
// eslint-disable-next-line sonarjs/no-identical-functions
.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
})),
];
}
// eslint-disable-next-line sonarjs/no-identical-functions
return searchedAttributesDataList.map((e) => ({
...e,
name: e.name,
@@ -273,9 +297,24 @@ const useOptionsMenu = ({
return [...acc, column];
}, [] as TelemetryFieldKey[]);
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: newSelectedColumns,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateColumns(newSelectedColumns);
handleRedirectWithOptionsData(optionsData);
},
[searchedAttributeKeys, selectedColumnKeys, preferences, updateColumns],
[
searchedAttributeKeys,
selectedColumnKeys,
preferences,
handleRedirectWithOptionsData,
updateColumns,
],
);
const handleRemoveSelectedColumn = useCallback(
@@ -288,12 +327,27 @@ const useOptionsMenu = ({
notifications.error({
message: 'There must be at least one selected column',
});
return;
} else {
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: newSelectedColumns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines:
preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize:
preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateColumns(newSelectedColumns || []);
handleRedirectWithOptionsData(optionsData);
}
updateColumns(newSelectedColumns || []);
},
[dataSource, notifications, preferences, updateColumns],
[
dataSource,
notifications,
preferences,
handleRedirectWithOptionsData,
updateColumns,
],
);
const handleFormatChange = useCallback(
@@ -360,18 +414,6 @@ const useOptionsMenu = ({
setSearchText(value);
}, []);
const reorderSelectColumns = useCallback(
(orderedIds: string[]): void => {
const current = preferences?.columns ?? [];
const byName = new Map(current.map((f) => [f.name, f]));
const reordered = orderedIds
.map((id) => byName.get(id))
.filter((f): f is TelemetryFieldKey => f !== undefined);
updateColumns(reordered);
},
[preferences, updateColumns],
);
const handleFocus = (): void => {
setIsFocused(true);
};
@@ -394,7 +436,6 @@ const useOptionsMenu = ({
onSelect: handleSelectColumns,
onRemove: handleRemoveSelectedColumn,
onSearch: handleSearchAttribute,
onReorder: reorderSelectColumns,
},
format: {
value: preferences?.formatting?.format || defaultOptionsQuery.format,
@@ -416,7 +457,6 @@ const useOptionsMenu = ({
handleSelectColumns,
handleRemoveSelectedColumn,
handleSearchAttribute,
reorderSelectColumns,
handleFormatChange,
handleMaxLinesChange,
handleFontSizeChange,

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

@@ -30,7 +30,10 @@ import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ArrowUp10, Minus } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import { AppState } from 'store/reducers';
@@ -82,6 +85,10 @@ function ListView({
},
});
const { draggedColumns, onDragColumns } = useDragColumns<RowData>(
LOCALSTORAGE.TRACES_LIST_COLUMNS,
);
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
QueryParams.pagination,
);
@@ -93,19 +100,6 @@ function ListView({
[stagedQuery, orderBy],
);
// TEMP — remove after traces moves to TanStack table.
// - Drag updates selectColumns; raw queryKey would churn on reorder.
// - Trace API fetches only listed columns → add/remove must refetch.
// - Sorted-name signature: stable on reorder, changes on add/remove.
const selectColumnsSignature = useMemo(
() =>
(options?.selectColumns ?? [])
.map((c) => c.name)
.sort()
.join(','),
[options?.selectColumns],
);
const queryKey = useMemo(
() => [
REACT_QUERY_KEY.GET_QUERY_RANGE,
@@ -115,7 +109,7 @@ function ListView({
stagedQuery,
panelType,
paginationConfig,
selectColumnsSignature,
options?.selectColumns,
orderBy,
],
[
@@ -123,7 +117,7 @@ function ListView({
panelType,
globalSelectedTime,
paginationConfig,
selectColumnsSignature,
options?.selectColumns,
maxTime,
minTime,
orderBy,
@@ -188,14 +182,13 @@ function ListView({
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = useMemo(
() =>
getListColumns(
options?.selectColumns || [],
formatTimezoneAdjustedTimestamp,
),
[options?.selectColumns, formatTimezoneAdjustedTimestamp],
);
const columns = useMemo(() => {
const updatedColumns = getListColumns(
options?.selectColumns || [],
formatTimezoneAdjustedTimestamp,
);
return getDraggedColumns(updatedColumns, draggedColumns);
}, [options?.selectColumns, formatTimezoneAdjustedTimestamp, draggedColumns]);
const transformedQueryTableData = useMemo(
() => transformDataWithDate(queryTableData) || [],
@@ -203,16 +196,9 @@ function ListView({
);
const handleDragColumn = useCallback(
(fromIndex: number, toIndex: number): void => {
const reordered = [...columns];
const [moved] = reordered.splice(fromIndex, 1);
reordered.splice(toIndex, 0, moved);
const orderedIds = reordered
.map((c) => String(('dataIndex' in c && c.dataIndex) || c.key || ''))
.filter(Boolean);
config?.addColumn?.onReorder(orderedIds);
},
[columns, config],
(fromIndex: number, toIndex: number) =>
onDragColumns(columns, fromIndex, toIndex),
[columns, onDragColumns],
);
const handleOrderChange = useCallback((value: string) => {

View File

@@ -1,15 +1,4 @@
import { createBrowserHistory } from 'history';
import { getBasePath } from 'utils/basePath';
const history = createBrowserHistory({ basename: getBasePath() });
let inAppPushCount = 0;
history.listen((_, action) => {
if (action === 'PUSH') {
inAppPushCount += 1;
}
});
export const hasInAppHistory = (): boolean => inAppPushCount > 0;
export default history;
export default createBrowserHistory({ basename: getBasePath() });

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, 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 { useIsDarkMode } from 'hooks/useDarkMode';
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
@@ -12,7 +13,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';
@@ -21,16 +21,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,
@@ -68,9 +58,7 @@ function AlertActionButtons({
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
const finalMenuItemStyle = isV2Alert ? menuItemStyleV2 : menuItemStyle;
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
...(!isV2Alert
? [
{
@@ -78,7 +66,6 @@ function AlertActionButtons({
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: handleRename,
style: finalMenuItemStyle,
},
]
: []),
@@ -87,17 +74,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,
},
];
@@ -140,16 +123,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,8 +0,0 @@
function DashboardPageV2(): JSX.Element {
return (
<div>
<h1>Dashboard Page V2</h1>
</div>
);
}
export default DashboardPageV2;

View File

@@ -1,9 +0,0 @@
function DashboardsListPageV2(): JSX.Element {
return (
<div>
<h1>Dashboards List Page V2</h1>
</div>
);
}
export default DashboardsListPageV2;

View File

@@ -3,6 +3,7 @@ import { useQueryClient } from 'react-query';
import * as Sentry from '@sentry/react';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import { TelemetryFieldKey } from 'api/v5/v5';
import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
@@ -14,6 +15,12 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
import LogsExplorerViewsContainer from 'container/LogsExplorerViews';
import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
URL_OPTIONS,
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import Toolbar from 'container/Toolbar/Toolbar';
@@ -24,9 +31,11 @@ import {
useHandleExplorerTabChange,
} from 'hooks/useHandleExplorerTabChange';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { defaultTo, isEmpty, isNull } from 'lodash-es';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { defaultTo, isEmpty, isEqual, isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { EventSourceProvider } from 'providers/EventSource';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { Warning } from 'types/api';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -53,6 +62,8 @@ function LogsExplorer(): JSX.Element {
const [selectedView, setSelectedView] = useState<ExplorerViews>(
() => panelTypeToExplorerView[panelTypesFromUrl],
);
const { logs } = usePreferenceContext();
const { preferences } = logs;
const [showFilters, setShowFilters] = useState<boolean>(() => {
const localStorageValue = getLocalStorageKey(
@@ -171,6 +182,116 @@ function LogsExplorer(): JSX.Element {
setShowFilters((prev) => !prev);
};
const { redirectWithQuery: redirectWithOptionsData } =
useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
// Get and parse stored columns from localStorage
const logListOptionsFromLocalStorage = useMemo(() => {
const data = getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS);
if (!data) {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
}, []);
// Check if the columns have the required columns (timestamp, body)
const hasRequiredColumns = useCallback(
(columns?: TelemetryFieldKey[] | null): boolean => {
if (!columns?.length) {
return false;
}
const hasTimestamp = columns.some((col) => col.name === 'timestamp');
const hasBody = columns.some((col) => col.name === 'body');
return hasTimestamp && hasBody;
},
[],
);
// Merge the columns with the required columns (timestamp, body) if missing
const mergeWithRequiredColumns = useCallback(
(columns: TelemetryFieldKey[]): TelemetryFieldKey[] => [
// Add required columns (timestamp, body) if missing
...(!hasRequiredColumns(columns) ? defaultLogsSelectedColumns : []),
...columns,
],
[hasRequiredColumns],
);
// Migrate the options query to the new format
const migrateOptionsQuery = useCallback(
(query: OptionsQuery): OptionsQuery => {
// Skip if already migrated
if (query.version) {
return query;
}
if (logListOptionsFromLocalStorage?.version) {
return logListOptionsFromLocalStorage;
}
// Case 1: we have localStorage columns
if (logListOptionsFromLocalStorage?.selectColumns?.length > 0) {
return {
...query,
version: 1,
selectColumns: mergeWithRequiredColumns(
logListOptionsFromLocalStorage.selectColumns,
),
};
}
// Case 2: No query columns in localStorage in but query has columns
if (query.selectColumns.length > 0) {
return {
...query,
version: 1,
selectColumns: mergeWithRequiredColumns(query.selectColumns),
};
}
// Case 3: No columns anywhere, use defaults
return {
...query,
version: 1,
selectColumns: defaultLogsSelectedColumns,
};
},
[mergeWithRequiredColumns, logListOptionsFromLocalStorage],
);
useEffect(() => {
if (!preferences) {
return;
}
const migratedQuery = migrateOptionsQuery({
selectColumns: preferences.columns || defaultLogsSelectedColumns,
maxLines: preferences.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: preferences.formatting?.format || defaultOptionsQuery.format,
fontSize: preferences.formatting?.fontSize || defaultOptionsQuery.fontSize,
version: preferences.formatting?.version,
});
// Only redirect if the query was actually modified
if (
!isEqual(migratedQuery, {
selectColumns: preferences?.columns,
maxLines: preferences?.formatting?.maxLines,
format: preferences?.formatting?.format,
fontSize: preferences?.formatting?.fontSize,
version: preferences?.formatting?.version,
})
) {
redirectWithOptionsData(migratedQuery);
}
}, [migrateOptionsQuery, preferences, redirectWithOptionsData]);
const toolbarViews = useMemo(
() => ({
list: {

View File

@@ -16,7 +16,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import history, { hasInAppHistory } from 'lib/history';
import history from 'lib/history';
import {
ArrowLeft,
CalendarClock,
@@ -96,7 +96,13 @@ function TraceDetailsHeader({
}, [traceID]);
const handlePreviousBtnClick = useCallback((): void => {
if (hasInAppHistory()) {
const isSpaNavigate =
document.referrer &&
// oxlint-disable-next-line signoz/no-raw-absolute-path
new URL(document.referrer).origin === window.location.origin;
const hasBackHistory = window.history.length > 1;
if (isSpaNavigate && hasBackHistory) {
history.goBack();
} else {
history.push(ROUTES.TRACES_EXPLORER);
@@ -124,7 +130,6 @@ function TraceDetailsHeader({
size="md"
className={styles.backBtn}
onClick={handlePreviousBtnClick}
aria-label="Back"
>
<ArrowLeft size={14} />
</Button>

View File

@@ -1,60 +0,0 @@
import { fireEvent, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { render } from 'tests/test-utils';
import TraceDetailsHeader from '../TraceDetailsHeader';
const mockGoBack = jest.fn();
const mockPush = jest.fn();
const mockHasInAppHistory = jest.fn();
jest.mock('lib/history', () => ({
__esModule: true,
default: {
goBack: (): void => mockGoBack(),
push: (path: string): void => mockPush(path),
replace: jest.fn(),
location: { pathname: '/', search: '' },
listen: (): (() => void) => (): void => undefined,
},
hasInAppHistory: (): boolean => mockHasInAppHistory(),
}));
const baseProps = {
filterMetadata: {
startTime: 0,
endTime: 1,
traceId: 'trace-123',
},
onFilteredSpansChange: jest.fn(),
isDataLoaded: false,
};
describe('TraceDetailsHeader back button', () => {
beforeEach(() => {
mockGoBack.mockClear();
mockPush.mockClear();
mockHasInAppHistory.mockReset();
});
it('calls history.goBack() when there is in-app SPA history', () => {
mockHasInAppHistory.mockReturnValue(true);
render(<TraceDetailsHeader {...baseProps} />);
fireEvent.click(screen.getByRole('button', { name: /back/i }));
expect(mockGoBack).toHaveBeenCalledTimes(1);
expect(mockPush).not.toHaveBeenCalled();
});
it('pushes to the traces explorer route when there is no in-app SPA history', () => {
mockHasInAppHistory.mockReturnValue(false);
render(<TraceDetailsHeader {...baseProps} />);
fireEvent.click(screen.getByRole('button', { name: /back/i }));
expect(mockPush).toHaveBeenCalledTimes(1);
expect(mockPush).toHaveBeenCalledWith(ROUTES.TRACES_EXPLORER);
expect(mockGoBack).not.toHaveBeenCalled();
});
});

View File

@@ -1,13 +1,6 @@
import { useMemo, useState } from 'react';
import {
Button,
Divider,
Dropdown,
Form,
MenuProps,
Space,
Tooltip,
} from 'antd';
import { Button, Divider, Form, Space, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Switch } from '@signozhq/ui/switch';
import cx from 'classnames';
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
@@ -44,16 +37,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(
() => ({
@@ -211,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(
@@ -230,7 +230,7 @@ function FunnelStep({
}
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</Dropdown>
)}
</div>
</div>
</Form>

View File

@@ -108,9 +108,7 @@ describe('PreferencesProvider integration', () => {
},
);
// Loader's ensureLogsRequiredColumns prepends timestamp + body, so the
// 1 column in localStorage becomes 3 in preferences.
expect(Number(screen.getByTestId('logs-columns-len').textContent)).toBe(3);
expect(Number(screen.getByTestId('logs-columns-len').textContent)).toBe(1);
});
it('direct mode updateColumns persists to localStorage', async () => {
@@ -128,11 +126,8 @@ describe('PreferencesProvider integration', () => {
const stored = getLocalStorageJSON<LogsLocalOptions>(
LOCALSTORAGE.LOGS_LIST_OPTIONS,
);
// Writer's ensureLogsRequiredColumns prepends `body` when only
// `timestamp` was passed in (defaults.slice(0,1) is just timestamp).
expect(stored?.selectColumns).toStrictEqual([
defaultLogsSelectedColumns[1] as TelemetryFieldKey, // body
defaultLogsSelectedColumns[0] as TelemetryFieldKey, // timestamp
defaultLogsSelectedColumns[0] as TelemetryFieldKey,
]);
});
@@ -188,9 +183,7 @@ describe('PreferencesProvider integration', () => {
value: originalLocation,
});
// Loader's ensureLogsRequiredColumns prepends timestamp + body, so the
// URL's 1 column becomes 3 in preferences.
expect(Number(screen.getByTestId('logs-columns-len').textContent)).toBe(3);
expect(Number(screen.getByTestId('logs-columns-len').textContent)).toBe(1);
});
it('updateFormatting persists to localStorage in direct mode', async () => {

View File

@@ -1,10 +1,7 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode } from 'container/LogsTable';
import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
} from 'container/OptionsMenu/constants';
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
import { FontSize } from 'container/OptionsMenu/types';
import {
FormattingOptions,
@@ -88,21 +85,18 @@ describe('logsUpdaterConfig', () => {
logsUpdater.updateColumns(newColumns, PreferenceMode.DIRECT);
// Writer guards body+timestamp via ensureLogsRequiredColumns invariant
const guardedColumns = [...defaultLogsSelectedColumns, ...newColumns];
// Should update URL
expect(redirectWithOptionsData).toHaveBeenCalledWith({
...defaultOptionsQuery,
...mockPreferences.formatting,
selectColumns: guardedColumns,
selectColumns: newColumns,
});
// Should update localStorage with the guarded shape
// Should update localStorage
const storedData = JSON.parse(
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS],
);
expect(storedData.selectColumns).toStrictEqual(guardedColumns);
expect(storedData.selectColumns).toStrictEqual(newColumns);
expect(storedData.maxLines).toBe(1); // Should preserve other fields
// Should not update saved view preferences

View File

@@ -1,5 +1,4 @@
import { renderHook, waitFor } from '@testing-library/react';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { DataSource } from 'types/common/queryBuilder';
import logsLoaderConfig from '../configs/logsLoaderConfig';
@@ -61,9 +60,9 @@ describe('usePreferenceLoader', () => {
expect(result.current.loading).toBe(false);
});
// Loader wraps with ensureLogsRequiredColumns — body+timestamp always prepended
// Should have loaded from local storage (highest priority)
expect(result.current.preferences).toStrictEqual({
columns: [...defaultLogsSelectedColumns, { name: 'local-column' }],
columns: [{ name: 'local-column' }],
formatting: { maxLines: 5, format: 'table', fontSize: 'medium', version: 1 },
});
expect(result.current.error).toBeNull();

View File

@@ -3,10 +3,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
import {
defaultOptionsQuery,
ensureLogsRequiredColumns,
} from 'container/OptionsMenu/constants';
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
@@ -21,12 +18,11 @@ const getLogsUpdaterConfig = (
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
} => ({
updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => {
const guardedColumns = ensureLogsRequiredColumns(newColumns);
if (mode === PreferenceMode.SAVED_VIEW) {
setSavedViewPreferences((prev) => {
if (!prev) {
return {
columns: guardedColumns,
columns: newColumns,
formatting: {
maxLines: 1,
format: 'table',
@@ -38,7 +34,7 @@ const getLogsUpdaterConfig = (
return {
...prev,
columns: guardedColumns,
columns: newColumns,
};
});
}
@@ -48,14 +44,14 @@ const getLogsUpdaterConfig = (
redirectWithOptionsData({
...defaultOptionsQuery,
...preferences?.formatting,
selectColumns: guardedColumns,
selectColumns: newColumns,
});
// Also update local storage
const local = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
);
local.selectColumns = guardedColumns;
local.selectColumns = newColumns;
setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local));
}
},

View File

@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { TelemetryFieldKey } from 'api/v5/v5';
import { ensureLogsRequiredColumns } from 'container/OptionsMenu/constants';
import { has } from 'lodash-es';
import { DataSource } from 'types/common/queryBuilder';
@@ -52,11 +51,7 @@ function logsPreferencesLoader(): {
columns: TelemetryFieldKey[];
formatting: FormattingOptions;
} {
const result = preferencesLoader<{
columns: TelemetryFieldKey[];
formatting: FormattingOptions;
}>(logsLoaderConfig);
return { ...result, columns: ensureLogsRequiredColumns(result.columns) };
return preferencesLoader(logsLoaderConfig);
}
function tracesPreferencesLoader(): {

View File

@@ -1,10 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useEffect, useState } from 'react';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultLogsSelectedColumns,
ensureLogsRequiredColumns,
} from 'container/OptionsMenu/constants';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { DataSource } from 'types/common/queryBuilder';
@@ -57,10 +54,9 @@ export function usePreferenceSync({
let columns: TelemetryFieldKey[] = [];
let formatting: FormattingOptions | undefined;
if (dataSource === DataSource.LOGS) {
columns = ensureLogsRequiredColumns(
columns =
updateExtraDataSelectColumns(parsedExtraData?.selectColumns) ||
defaultLogsSelectedColumns,
);
defaultLogsSelectedColumns;
formatting = {
maxLines: parsedExtraData?.maxLines ?? 1,
format: parsedExtraData?.format ?? 'table',

View File

@@ -1,50 +0,0 @@
package impllogspipeline
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/logspipeline"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/valuer"
)
const elementType = "log_pipelines"
type module struct {
sqlStore sqlstore.SQLStore
}
func NewModule(sqlStore sqlstore.SQLStore) logspipeline.Module {
return &module{sqlStore: sqlStore}
}
func (m *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
subq := m.sqlStore.BunDB().NewSelect().
TableExpr("agent_config_version").
ColumnExpr("MAX(version)").
Where("org_id = ?", orgID).
Where("element_type = ?", elementType)
var result struct {
Total int `bun:"total"`
Enabled int `bun:"enabled_count"`
}
err := m.sqlStore.BunDB().NewSelect().
TableExpr("agent_config_element AS e").
Join("JOIN agent_config_version AS v ON v.id = e.version_id").
Join("JOIN pipelines AS p ON p.id = e.element_id").
Where("v.org_id = ?", orgID).
Where("v.element_type = ?", elementType).
Where("v.version = (?)", subq).
ColumnExpr("COUNT(*) AS total").
ColumnExpr("SUM(CASE WHEN p.enabled THEN 1 ELSE 0 END) AS enabled_count").
Scan(ctx, &result)
if err != nil {
return nil, err
}
return map[string]any{
"logs_pipeline.total.count": result.Total,
"logs_pipeline.enabled.count": result.Enabled,
}, nil
}

View File

@@ -1,7 +0,0 @@
package logspipeline
import "github.com/SigNoz/signoz/pkg/statsreporter"
type Module interface {
statsreporter.StatsCollector
}

View File

@@ -19,8 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/logspipeline"
"github.com/SigNoz/signoz/pkg/modules/logspipeline/impllogspipeline"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -87,7 +85,6 @@ type Modules struct {
Promote promote.Module
ServiceAccount serviceaccount.Module
CloudIntegration cloudintegration.Module
LogsPipeline logspipeline.Module
RuleStateHistory rulestatehistory.Module
TraceDetail tracedetail.Module
SpanMapper spanmapper.Module
@@ -146,7 +143,6 @@ func NewModules(
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, providerSettings, config.InfraMonitoring),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: serviceAccount,
LogsPipeline: impllogspipeline.NewModule(sqlstore),
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),

View File

@@ -497,7 +497,6 @@ func New(
modules.AuthDomain,
serviceAccount,
cloudIntegrationModule,
modules.LogsPipeline,
}
// Initialize stats reporter from the available stats reporter provider factories